6 Aug 2026

Introducing the Scene API in APS Viewer

If you've ever added custom 3D content to APS Viewer, you've probably run into this. You start with overlays because it's the simplest option, then watch your frame rate drop as you add more content. So you switch to the Scene Builder extension, which renders progressively, but now you're dealing with a different set of types and no object tree. And neither API was ever a documented contract, so filling the gaps meant reverse-engineering our internals.

We're excited to announce the new Scene API, which replaces both. It's one documented, typed API for creating and manipulating 3D content, built on the same code paths the Viewer already uses for models loaded from our platform.

The Mental Model

Let's start with the most important Concepts of the API:

  • Model is a container. It can hold content loaded from a design file or content you construct at runtime (aka dynamic model), and the renderer treats both identically.

  • An instance is the unit that actually gets drawn: geometry + material + transform + visibility state. There's no Instance class to construct; instances are owned and addressed by numeric IDs through InstanceCollection3D, which you get from model.getInstances().

  • An object is a logical component (a door, an engine) in an object tree, identified by dbId, and it's what isolatehide, and other common Viewer methods operate on. One object can own several instances, for example, a door object might contain two instances: one for a wooden door frame, and one for a brass door knob.

On top of that sit two optional extras: a scene graph of Node3D transform groups you build yourself when instances need to move together, and render layers for content that has to draw before, after, or on top of the main scene.

Adding Custom Content

Here's an example that touches most of the authoring side of the API: creating the model, building geometry, assigning materials, and grouping instances under a shared transform. The full runnable versions of each step live in the CodePens embedded in Hello TriangleGeometry Factory, and Building a Hierarchy.

const av = Autodesk.Viewing;
const avs = Autodesk.Viewing.Scene;
const avm = Autodesk.Viewing.Math;

function addCustomContent(viewer) {
    // A dynamic model is just a Model you construct yourself.
    // Register it with the renderer *before* you touch its instances.
    const model = new av.Model();
    viewer.showModel(model);
    const instances = model.getInstances();

    // Geometry 1: raw vertex data, for anything the factory can't give you.
    const positions = new Float32Array([
        -50.0, -50.0, 0.0,   // bottom-left
         50.0, -50.0, 0.0,   // bottom-right
         0.0,   50.0, 0.0    // top-center
    ]);
    const normals = new Float32Array([
        0.0, 0.0, 1.0,
        0.0, 0.0, 1.0,
        0.0, 0.0, 1.0
    ]);
    const triangleGeometry = new avs.BufferGeometry();
    triangleGeometry.setAttribute(
        'position', new avs.BufferAttribute(positions, 3)
    );
    triangleGeometry.setAttribute(
        'normal', new avs.BufferAttribute(normals, 3)
    );
    triangleGeometry.setIndices(new Uint16Array([0, 1, 2]));

    // Geometry 2: the same result for a common primitive, minus the typing.
    const sphereGeometry = avs.GeometryFactory.createSphere(50.0, 32, 16);

    const shadedMaterial = new avs.StandardMaterial({ color: 0x4488ff, specularColor: 0xeeeeee, specularPower: 128, side: avs.Side.Double });
    const unlitMaterial = new avs.UnlitMaterial({ color: 0xffaa00 });

    // geometry + material + transform = one instance, addressed by id.
    const triangleId = instances.add(triangleGeometry, shadedMaterial, new avm.Matrix4().makeTranslation(-1.5, 0, 100.0));
    const sphereId = instances.add(sphereGeometry, unlitMaterial, new avm.Matrix4().makeTranslation(1.5, 0, 100.0));

    // Every instance stays mutable afterwards.
    instances.setMaterial(triangleId, new avs.StandardMaterial({ color: 0xff5522 }));
    instances.setVisibilityState(sphereId, avs.VisibilityState.Ghosted);

    // Group both instances under one transform node and lift them together.
    const root = instances.getSceneGraph(true);
    const group = new avs.Node3D();
    root.add(group);
    avs.InstanceNode3D.fromInstance(triangleId, group);
    avs.InstanceNode3D.fromInstance(sphereId, group);

    group.position.y = 2;
    group.markDirty();
    root.updateTransformWorld();

    viewer.refresh(true);
    return model;
}

Custom model added to existing scene

Here's what the code does:

  • new av.Model() allocates an empty, programmatically-owned model. No upload, no translation job, no URN.
  • avs.BufferGeometry holds your shape as typed-array attribute buffers. position and normal are the minimum a lit material needs, both with an item size of 3. setIndices() accepts a Uint16Array or a Uint32Array; prefer Uint16Array where the vertex count allows it, to save memory.
  • avs.GeometryFactory is a convenience layer over the same BufferGeometry, not a separate path. It covers spheres, boxes, cylinders, cones, and planes, and takes an optional trailing boolean for edge indices, which you need if you want wireframe rendering via viewer.setDisplayEdges(true).
  • instances.add(geometry, material, transform) returns a numeric instance ID, or -1 if the instance couldn't be added. That ID is your handle for every later update.
  • avs.VisibilityState is the same three-state model (VisibleGhostedHidden) the Viewer uses for its own isolate and ghost operations, so your content behaves consistently with loaded content. See Visibility States.
  • getSceneGraph(true) creates the scene graph on demand, since a model doesn't have one by default. markDirty() flags a subtree, and root.updateTransformWorld() recomputes only the dirty subtrees, which is what makes per-frame animation of deep hierarchies cheap.
  • viewer.refresh(true) schedules a frame. Nothing you changed appears until you call it.

Manipulating a Loaded Model

The same InstanceCollection3D also holds the instances of a model loaded from a design file, which is the part that used to require internals. The bridge between the two worlds is the object tree: dbId gets you to instance IDs, and from there everything above applies. The runnable version is in the CodePen on Wrapping Loaded Instances.

// Opt in before the Initializer runs. Required for loaded models.
Autodesk.Viewing.FeatureFlags.set(
    Autodesk.Viewing.PublicFeatureFlags.SceneAPI, true
);

// ... initialize the viewer and load a document ...

const avs = Autodesk.Viewing.Scene;

async function highlightAndLift(viewer, model, dbIds) {
    const instances = model.getInstances();
    const objectTree = await model.getObjectTreeAsync();

    // dbIds identify *objects*; resolve them down to instance ids.
    const instanceIds = [];
    for (const dbId of dbIds) {
        objectTree.enumNodeInstances(dbId, (instanceId) => {
            if (instances.getGeometry(instanceId)) {
                instanceIds.push(instanceId);
            }
        }, true);
    }

    // Recolour the loaded instances in place.
    const highlight = new avs.StandardMaterial({ color: 0xff5522 });
    for (const instanceId of instanceIds) {
        instances.setMaterial(instanceId, highlight);
    }

    // Or wrap them in scene graph nodes and move them as one group.
    const root = instances.getSceneGraph(true);
    const group = new avs.Node3D();
    root.add(group);
    for (const instanceId of instanceIds) {
        avs.InstanceNode3D.fromInstance(instanceId, group);
    }

    group.position.y += 100.0;
    group.markDirty();
    root.updateTransformWorld();

    viewer.refresh(true);
}

Loaded model with colored and moved elements

  • Autodesk.Viewing.PublicFeatureFlags.SceneAPI must be set before Autodesk.Viewing.Initializer. Dynamic models you build yourself work without it; using the Scene API on loaded models does not.
  • objectTree.enumNodeInstances(dbId, callback, recursive) walks from an object to its instances. The getGeometry check filters out instances that carry no geometry.
  • InstanceNode3D.fromInstance(instanceId, parent) wraps an existing instance, loaded or authored, in a scene graph node, giving you the full transform hierarchy over geometry you didn't create.

If you need a copy that outlives the original load state instead of an in-place change, read the geometry and material off the source collection and add them to a fresh dynamic model. Copy Instances shows the pattern. Note that geometries and materials are owned by their model and can't be shared across models, so you always clone first.

How It Compares

  Overlays Scene Builder Scene API
Types THREE.* (bundled three.js r71) THREE.*, restricted subset Documented Autodesk.Viewing.Scene types
Rendering Every frame, all at once Progressive, like a loaded model Progressive in the main scene; every frame on a render layer
Object tree None None Present on loaded models; optional on dynamic models
Materials Any three.js material that happens to work MeshPhong, MeshBasic, LineBasic, Prism StandardMaterialUnlitMaterialLineMaterialPointsMaterial
Runtime edits Add/remove meshes Add fragments, limited changes Full CRUD on geometry, material, transform, and visibility per instance
Loaded models Not addressable Not applicable Same API as your own content

The one thing overlays did that's worth calling out separately is draw ordering, and Render Layers is the unified replacement. viewer.layers.create(id, { renderTarget, renderOrder }) gives you explicit, named control: renderTarget: 'overlay' is where you land if you're coming from viewer.overlays, and renderTarget: 'main' with a negative or positive renderOrder covers drawing before or after the loaded model.

Overlays and Scene Builder aren't going anywhere in Viewer v7, and nothing you've shipped breaks. We do plan to deprecate both in Viewer v8, and we'll help you get across; more on that below.

Gotchas

Here's a few things to keep an eye on when using the new API:

  • Call viewer.showModel(model) before you do anything else with a dynamic model.
  • FeatureFlags.set(PublicFeatureFlags.SceneAPI, true) has to run before Autodesk.Viewing.Initializer if you want to use the Scene API with models loaded from designs.
  • Nothing renders until viewer.refresh(true). Every mutation (add, remove, setMaterialsetTransformWorld, a visibility change) needs a frame render scheduled after it.
  • Don't read typed arrays back out of a geometry after add(). The Viewer takes ownership and may re-optimize the buffer layout. Use geometry.getVertexCount() and geometry.getPosition(i, target) instead; Working with Buffers covers this in detail.
  • Geometries and materials belong to exactly one model. addGeometry() and addMaterial() return null if the object is already owned elsewhere. Clone before reusing across models.
  • setThemingColor(dbId, color) takes a dbId, not an instance ID, unlike every method sitting next to it in InstanceCollection3D. Theming is an object-level operation.
  • Setting node.position in a scene graph on its own does nothing visible. You need either markDirty() followed by root.updateTransformWorld(), or node.updateTransformWorld(true) to skip the dirty check entirely. And the scene graph doesn't exist until you ask for it with getSceneGraph(true).

See It at AU

The Scene API gets a full walkthrough and a live demo at Autodesk University 2026 in Las Vegas, September 15–17. If you can make it in person, come and bring the awkward questions. If you can't, the recording will be available afterwards.

AU class details

Wrapping Up

You can now build 3D content in code, place it in the same model and the same pipeline as your loaded designs, manipulate individual instances of a design file without touching Viewer internals, and group any of it under a transform hierarchy you control. Start with Concepts for the vocabulary, then work through the CodePens from Hello Triangle onward. Every page in the guide ships with a live, editable one.

A separate post is coming that walks through migrating existing overlay and Scene Builder code to the Scene API, endpoint by endpoint. In the meantime, if you hit something the docs don't cover, do let us know. That's exactly the kind of gap this API exists to close. Happy building! 🛠️

Resources

Related Article