--- name: nodetool-3d-scene description: "Build, edit, validate or render a NodeTool 3D model: glTF objects, transforms, lights, materials, and headless Blender renders." featured: true --- # Work a NodeTool 3D model A 3D model is a `.glb` or `.gltf` **asset**, addressed by its asset id. The capabilities edit the stored glTF in place and save it back over the same asset, so the id stays valid and an open editor reloads it. For a 2D game, use `native-game`. The built-in game engine will add 3D scene support later. For a 3D clip inside a cut, build the model here and place it on a timeline. ## The loop 1. **Find it.** `list_model3ds` returns asset id, name, content type and size. Nothing to start from: `create_model3d {name, ops?}` makes an empty glTF scene and can apply the whole scene in the same call. 2. **Read it.** `get_model3d {model_id}` lists every object with its uuid, name, type, visibility, transform and material color, plus the scene's world-space bounds. Call this before editing, for the uuids. 3. **Edit.** `edit_model3d {model_id, ops: [...]}`. Operations run in order. Anything they do not name — meshes, textures, skins, animations — is kept, so an imported model survives an edit. 4. **Validate.** `validate_model3d {model_id | document}`. 5. **Render**, when a picture is asked for. `render_model3d` goes through headless Blender (EEVEE or Cycles), honors scene cameras and lights, and stores a PNG asset. It needs Blender on the server. ## `edit_model3d` ops | Op | Fields | |---|---| | `add_object` | `kind`: `box`, `sphere`, `plane`, `cylinder`, `torus`, `directionalLight`, `pointLight`. Optional `name` | | `delete_object` | `target` | | `set_transform` | `position`, `rotation` (Euler **degrees**), `scale`, each `[x, y, z]` | | `set_visibility` | `visible` | | `rename_object` | `target`, `name` | | `set_material_color` | `color` as a CSS hex string, meshes only | | `select_object` | `target`, or `null` to clear | `target` is a uuid or a name. Object ids are stamped into `node.extras.nodetool_id`, because glTF addresses nodes by array index and a delete renumbers them. Duplicate names make addressing ambiguous, which `validate_model3d` warns about. `delete_object` keeps the file valid: an animation left with no channels is removed, and so is a skin that lost a joint, which leaves its mesh in the bind pose. `set_material_color` recolors only the named object, copying a material or mesh it shares with another object first. These are the `ui_3d_*` verbs with no editor open. The operations, the units and the addressing live in `@nodetool-ai/model3d`, shared with the browser editor, so a model built headlessly opens there unchanged. ## No camera headlessly `ui_3d_frame_scene` and `ui_3d_capture_view` need a WebGL context. Without one, `get_model3d`'s world-space bounds are what answer "how big is this and where is it". Use `render_model3d` when an actual picture is required. ## What `validate_model3d` catches The glTF version, references that resolve to nothing (node, mesh, accessor, material, buffer, skin, camera, skin joint, animation target and sampler), a node with two parents, a cycle in the node hierarchy, a node carrying both a matrix and TRS fields, an accessor with an unknown type or reading past its buffer view, a buffer view reading past its buffer, an undeclared light, and an extension this build cannot honor. It warns on an empty scene, geometry with no light, and duplicate names. It only reads the document, and it handles hierarchies of any depth. ## On a timeline A `model3d` layer is the one kind `preview_timeline_frame` cannot draw on its own canvas. The preview pass collects every 3D layer across the requested instants, groups them by glTF plus the options that fix a renderer, and hands each group to one headless Chromium page. Two fields on the layer's report say what the pixels do not: `camera` (the pose it was drawn with, authored camera folded with the animated channels) and `animation_time_sec` (the glTF animation's own clock, not timeline time). With no Chrome on the host the group is left out and reported as a `model3d_unavailable` degradation. ## Reference In a NodeTool checkout, these repository sources go further: - `docs/harnesses.md` § 3D scene tools - `docs/harnesses.md` § 3D clips in preview_timeline_frame