# 3D scene format

> How a Nodaro 3D scene plan is built, from version 1 primitives and keyframes to version 2 GLB assets, semantic entities, camera tracks, shots and edit overlays.

Source: https://nodaro.ai/docs/developers/embed/scene3d-format

A **3D scene plan** is the data Nodaro stores for an editable 3D scene: a composition with `planType: "3d-scene"` and an explicit `schemaVersion`. [Generate 3D Scene](https://nodaro.ai/docs/nodes/video/generate-3d-scene) and [Edit 3D Scene](https://nodaro.ai/docs/nodes/video/edit-3d-scene) jobs return one in `output_data.scenePlan`, [Render Video](https://nodaro.ai/docs/nodes/video/render-video) turns one into an MP4, and the [3D scene viewport](https://nodaro.ai/docs/developers/embed/scene3d) draws one. This page explains how a plan is organized, so your code can store, validate, show and edit plans.

## Rules for every version

- **Units and axes.** Positions are in meters, in a right-handed world with Y pointing up. Euler rotations are in radians.
- **Time.** Frames are numbered from zero. A scene lasts from 1 to 60 seconds, at 15 to 60 frames per second; the default is 4 seconds at 24 fps.
- **Revisions.** Every plan has a `revisionId`. An edit never changes a plan: it creates a new revision whose `parentRevisionId` is the revision it came from. Each rendered MP4 uses one exact revision.
- **Versions.** Read `schemaVersion` before you read any field that belongs to one version. Version 1 describes primitive geometry; version 2 adds baked geometry and shots.
- **Discovery.** `GET /v1/3d-scene/capabilities` tells you which authoring engines, and so which scene versions, a deployment supports. Optional engines depend on the deployment's configuration. The default engine, Basic, produces version 1.

## Types and schemas

The `@nodaro/shared` package exports the types and the validators:

| Export | What it is |
| --- | --- |
| `Scene3DPlanV1`, `Scene3DPlanV2` | The type of each version. |
| `Scene3DPlan` | The union of both versions. |
| `scene3DAnyPlanSchema` | A validator that accepts both versions. |
| `scene3DPlanSchema` | A validator for version 1 only. |
| `scene3DPlanV2Schema` | A validator for version 2 only. |

The validators check the structure and the rules across fields: parent cycles, missing parents, keyframes after the last frame and the duration ceiling. A plan that fails is refused whole.

## Version 1: primitives and keyframes

A version 1 scene is self-contained. It describes bounded primitive shapes and groups, including simple character stand-ins, with sparse keyframes for each object and for the camera. It does not reconstruct detailed textured meshes or physical simulations.

A version 1 plan stores:

- object ids, transforms and dimensions;
- the camera's position, target and lens;
- the lighting and the background;
- the keyframes of each object and of the camera.

| Limit | Value |
| --- | --- |
| Objects per scene | 100 |
| Keyframes per object or per camera track | 240 |
| Length of an object id | 64 characters |
| Frame width and height | 100 to 2,560 px on each axis |

### Edit a version 1 scene

`POST /v1/3d-scene/edit` takes a `scenePlan` and an `expectedRevisionId`, plus either an instruction `prompt` or a list of `operations`. A revision mismatch is refused. Operations run without calling a model:

| Operation | Fields |
| --- | --- |
| `set-object` | `objectId`, and `changes` to any object field other than the id |
| `add-object` | `object` |
| `remove-object` | `objectId` |
| `set-camera` | `changes` |
| `set-lighting` | `changes` |
| `set-background` | `color` |

An operation changes exactly the fields you supply. To change a pose that has keyframes, include the keyframe changes. The whole edited scene is validated again, so an edit cannot leave orphaned parents or references. Use `lockedObjectIds` to keep objects unchanged during an instruction edit. See [Edit 3D Scene](https://nodaro.ai/docs/nodes/video/edit-3d-scene).

## Version 2: baked geometry

A version 2 scene adds four things to the concepts of version 1:

- **Retained GLB geometry,** stored as immutable assets.
- **Semantic entities,** the named parts of the scene that users select and edit.
- **A dense camera track,** with a camera pose for every frame.
- **Contiguous shots,** ranges of frames with exact cuts between them.

### Assets

A version 2 asset reference holds an opaque `assetId`, a kind, a role, a byte length and a SHA-256 digest. It holds no storage credential and no download URL.

- A reader needs exactly the referenced bytes. It refuses a missing, oversized or changed asset before it draws anything.
- Playback loads the geometry and the camera data. The native source files of a scene are a separate download, with their own permission.
- Each retained revision pins all its assets, including bytes it reuses from an earlier revision.

### Entities

Semantic entities name roots in the GLB geometry and the material roles of those roots, for selection and editing.

- **Materials.** Every editable material role names a material inside the entity's own geometry. Clay shading keeps each material's base color, so changing a vehicle's body paint leaves its tires unchanged.
- **Parents.** An entity may declare a parent. Its exported root is then nested inside the parent's root, and its node transform is relative to the parent. The parent's placement and baked animation reach the child through the file itself.
- **Agreement.** The plan and the GLB file must describe the same parentage. A reader refuses a scene where they disagree.
- **Ownership.** Geometry, materials and selection belong to the entity that declares them. Recoloring a parent never reaches an entity nested inside it.
- **Organizational roots.** A parent entity may own no geometry at all, and only carry a baked, possibly animated transform for the entities nested inside it. A reader accepts it as long as something is nested there.

### Anchors

An entity **anchor** is a named point: a stable `name` and a `position` in the entity's local space.

- For an asset entity, an optional `nodeName` binds the anchor to a raw GLB node inside that entity's own root. The position and the optional rotation then use that node's local coordinates, and follow the node's animation, its ancestors and manual edits of the entity.
- For example, `{ "name": "door.tip", "nodeName": "car/door.hinge", "position": [1, 0, 0] }` places a point one meter along the X axis of the hinge.
- A missing node, or a binding into a nested child entity, is refused before playback. Anchors of primitives and groups stay in the entity's local space.
- Whether an authoring engine can create these bindings depends on the engine.

### Camera and animation

The dense camera track holds a position, a rotation and a projection for every frame. A reader keeps those values exactly, including roll and exact cuts between shots. Animation is sampled from the requested frame, so scrubbing backward and rendering any single frame give the same pose.

### Visibility

An entity's optional `visible` flag records its baked visibility; when it is missing, the entity is visible.

- Hidden geometry stays loaded and its animation keeps running, so an overlay can show it again at once.
- An overlay takes precedence over the baked value, and removing the overlay restores the baked value.
- A hidden parent also hides everything nested inside it.
- Hidden objects do not catch clicks in the preview. Use the entity list to select them and show them.
- The baked visibility is part of the revision's content digest.

## Edit a version 2 scene with overlays

Version 2 edits are immutable **overlays** above the baked scene. There are four kinds: transform, material color, visibility, and shot camera offset. An overlay never touches the base geometry or the camera bytes.

- **A transform overlay** applies above the baked placement of the entity and of its ancestors. Its `space` says in which frame of reference its values are read:
  - `local` means the entity's own parent frame, baked placement included. The values stay constant there, and the edit travels with a moving parent.
  - `world` means the scene's axes. The values are the entity's world position, rotation and scale; a rotated, scaled or animated ancestor changes where the entity ends up, never what the numbers mean. The entity stays parented, keeps its own baked animation, and only the channels the edit names are held.
  - A reader refuses the one case neither space can express: an ancestor with non-uniform scale under a rotation, which leaves a transform that is not a position, a rotation and a scale.
- **A visibility overlay** hides the entity and everything nested inside it. Each entity keeps its own visibility, so showing the ancestor again restores exactly the descendants that were not hidden in their own right.
- **Every edit creates a new revision,** with a parent revision and a new content digest. Locks on entities are enforced, and an edit made against an outdated revision or digest is refused.
- **Derived files are not carried over.** Posters, validation reports and native downloads are made again for the edited revision before they are attached to it.

To save overlays without a generation job and without an LLM charge, call `POST /v1/3d-scene/revisions/:revisionId/edits` with a `newRevisionId`, the base `expectedContentHash`, the `operations` and optional `lockedObjectIds`, or use `client.scene3d.applyEdits()` in the SDK. For OAuth apps, it needs the `workflows:write` scope. The [3D scene viewport](https://nodaro.ai/docs/developers/embed/scene3d#edit-a-baked-scene) produces these operations for you.

## Rendering

The browser preview and the MP4 render use the same reader, so a scene looks the same in both. Version 2 accepts clay geometry and rigid animation; textured, skinned and morph-target assets are refused.

| Version 2 limit | Value |
| --- | --- |
| Semantic entities | 100 |
| Mesh nodes | 2,000 |
| Triangles | 200,000 |
| Shots | 32 |
| Assets | 64 |
| Overrides | 200 |
| Playback asset bytes | 64 MiB |

The schema also bounds the timing, the dimensions, the depth of the hierarchy and the size of the manifest. A render's price depends on the frame size of the plan; see [Render Video](https://nodaro.ai/docs/nodes/video/render-video).

## Imported GLBs

A new scene can start from GLB files that already exist in retained revisions. On `POST /v1/3d-scene/generate`, `inputAssets` selects up to eight of them, each as `{ id, revisionId, assetId, label? }`. Image and video `references` stay a separate field.

- Imports need an authoring engine with import support. Basic, and engines without imports, refuse them before charging.
- You send only ids. Nodaro checks your access to the exact revision, and supplies the digest and byte length itself. A URL or a receipt from the caller is refused.
- When the engine reads an imported GLB, Nodaro checks access again and issues a short-lived download grant. Grants are transport credentials: they are never part of the scene plan.
- A saved revision can keep private copies of the files it was built from. They belong to the revision's owner, stay pinned through manual edits, and never appear among the playback assets or the public downloads.

## Read the binary files over the API

A version 2 manifest names its assets, but the bytes come from the authenticated API, with ordinary bearer authentication:

| Method | Path | What it returns |
| --- | --- | --- |
| `GET` | `/v1/3d-scene/revisions/:revisionId` | The scene manifest and its asset descriptors. |
| `GET` | `/v1/3d-scene/revisions/:revisionId/assets/:assetId` | One playback asset, such as a GLB or the camera track. |
| `GET` | `/v1/3d-scene/revisions/:revisionId/source` | The editable native source, when the revision retains one. |

- Playback assets need permission to view the revision's workflow. The native source needs permission to edit it. Personal revisions are readable by their owner only.
- A deleted or inaccessible revision answers `404`, and every response carries `Cache-Control: no-store`.
- In the SDK, `client.scene3d.assetBytes(revisionId, asset)` and `client.scene3d.sourceBytes(revisionId)` return an `ArrayBuffer`, and check the declared length.

See the [3D scenes API](https://nodaro.ai/docs/developers/api/3d-scenes) for the generation, edit and render endpoints.

## Frequently asked questions

### What units and axes does a Nodaro 3D scene use?

Meters, in a right-handed world with Y pointing up. Frames are numbered from zero, and Euler rotations are in radians.

### What is the difference between version 1 and version 2 scenes?

A version 1 scene describes primitive shapes and sparse keyframes for its objects and camera, all inside the plan. A version 2 scene adds retained GLB geometry, semantic entities, a dense camera track and contiguous shots, and its binary files stay behind the authenticated API.

### How do I validate a scene plan in my code?

Use the schemas of the @nodaro/shared package. scene3DAnyPlanSchema accepts both versions, and scene3DPlanSchema validates version 1 only.

### Does editing a scene change the original plan?

No. Every edit creates a new, immutable revision with its own revision id and a link to its parent revision. The plan you edited stays unchanged, and a version 2 edit keeps the base geometry and camera files as they are.

### Which GLB files can a version 2 scene use?

Clay geometry with rigid animation. Textured, skinned and morph-target assets are refused. A scene holds at most 100 entities, 2,000 mesh nodes, 200,000 triangles, 32 shots and 64 MiB of playback assets.
