Node
The base 3D scene node — an empty transform. Mesh, Model,
Light and Camera all extend it, so everything here works on them. The native
engine owns the authoritative transform; the SDK reads it back on every access (physics can
rewrite it every frame), and every transform getter returns a fresh copy — see
value semantics.
At a glance
const scene = new Scene()
scene.add(Light.sun())
scene.open()
const pivot = new Node()
pivot.position = [0, 1, -2]
pivot.eulerAngles = [0, 45, 0] // degrees
pivot.add(Mesh.box({ material: Material.lit({ color: '#38f' }) }))
scene.add(pivot) // the box under it joins the scene too
pivot.animateTo({ eulerAngles: [0, 405, 0], duration: 4000, easing: 'linear', loop: true, loopMode: 'restart' }) // a turn every 4 snew Node() takes no options — create it, then set its transform.
Local transform
node.position = [x, y, z] // Vec3Like; world units ≈ meters
node.x = 3 // scalar accessors: x / y / z
node.quaternion = Quat.fromEuler(0, 90, 0)
node.eulerAngles = [0, 90, 0] // DEGREES (YXZ)
node.scale = 2 // uniform, or [sx, sy, sz]
node.matrix = Mat4.compose([0, 1, 0], Quat.identity, 1) // full local TRSGetters return fresh Vec3 / Quat / Mat4 copies — mutating one never touches the node. One
exception is compiled in: the direct spelling node.position.x = 3 is rewritten to node.x = 3 (the
chisel comp_write pass), so it works — but a copy you STORED first is still a copy:
node.position.x = 3 // ✓ compiled to node.x = 3 (direct spelling only)
node.x = 3 // ✓ single-axis setter
const p = node.position; p.x = 3 // ✗ silent no-op — a stored copy
const p = node.position; p.y += 1; node.position = p // ✓ mutate a local, assign backWorld space
node.worldMatrix // get/set: full world transform (setting composes through the parent)
node.worldPosition // Vec3 (read-only)
node.worldQuaternion // Quat (read-only)
node.worldEulerAngles // Vec3, degrees (read-only)
node.worldScale // Vec3 (read-only)
node.forward // Vec3: the node's forward (−Z) axis in world space (read-only)A parent's transform applies to its children: a child at local [1, 0, 0] under a parent at
[0, 1, -2] turned [0, 90, 0] sits at world [0, 1, -3] (a +90° yaw turns +X to −Z, and the
parent's forward from −Z to −X).
Name & visibility
node.name = 'door' // stored natively; bone() and Model child lookups match on it
node.visible = false // hides the node and everything under itvisible reads the hierarchy: a node under a hidden ancestor reads false. name is a display
name, unique only among siblings; scene files address nodes by path — /-joined names from
the scene root (see scene-files.md).
Hierarchy
node.add(...children: Node[]): this // parent children under this node
node.setParent(parent: Node | null, worldPositionStays = false): this
node.parent // Node | null
node.children // Node[] (snapshot array)
node.childCount
node.getChild(index: number): Node | null
node.traverse(callback: (node: Node) => void): void // this node, then all descendants
node.bone(name: string): Node | null // a descendant by name — a Model's bones includedWith worldPositionStays: true, setParent keeps the node where it is in the world (its local
transform is recomputed); with the default false the local transform is kept as-is — the node
jumps to the same offset from its new parent (setParent(null) included).
bone(name) finds a descendant by exact name, then by the part after the last : / |
(mixamorig:Hips answers bone('Hips')); a skinned joint wins over a plain node of the same name.
Cached per name; null when absent. Parent a weapon to a hand with
hero.bone('RightHand')?.add(sword).
scene membership follows the parent. scene.add(node) puts a node and its current
subtree in the draw set; a child added under a node that is already in a scene joins that scene,
so add a hierarchy's root and parent the rest. See Scene.
lookAt
node.lookAt(point: Vec3Like, mode = '-z', ortho = [0, 1, 0]): this
// mode: 'z' | '-z' | 'x' | '-x' | 'y' | '-y' — which local axis points at the targetOrients the node so the chosen local axis (default '-z', the forward axis) points at a
world-space point. ortho is the up-hint used to resolve roll. Scale is preserved.
only '-z' and 'z' build a proper rotation. The 'x' / '-x' / 'y' / '-y' modes
aim the requested axis correctly but return a mirrored basis (determinant −1 — the node is
reflected); aim with '-z' and turn a child instead.
Tweens
node.animateTo(props & { duration?, delay?, easing?, loop?, … }): Animation
node.animateFrom(props & { … }): Animation // from these values to the current ones
// props: position, scale, quaternion, eulerAngles (degrees) — an array of values is a keyframe listRuns natively on the game clock — it pauses with the game; clock: 'ui' runs on wall time — with no
JS per frame; duration is in milliseconds. eulerAngles interpolate per axis without
shortest-arc, so [0, 720, 0] spins twice. The props every target animates:
Timeline;
the handle it returns: animate. A node a physics body
or a character owns is not routed through it — see physics.
Materials of a renderable
node.material // slot 0 — get/set (null until one is set)
node.setMaterial(material: Material, index = 0): this
node.getMaterial(index = 0): Material | null // what was assigned through this APIAny renderable takes a Material per primitive slot — a Mesh, or an
internal node of a loaded Model (index = the primitive's order in the glTF mesh). A
GLB part's own glTF material stays native-side: material reads null until you replace it.
Aspects
Capabilities attach as aspects and chain (see aspects):
const ball = Mesh.sphere({ material: Material.lit() })
.aspect(Shape, { sphere: 0.5 }) // pick/collision shape → hittable, raycastable
.aspect(Physics, { motion: 'dynamic' }) // rigid body → node.physicsSee 3D physics & shapes for Shape, Physics, Trigger, CharacterController.
Events
node.addEventListener(channel, callback): void
node.removeEventListener(channel, callback): void| Channel | Fires | Needs |
|---|---|---|
click |
pointer-up over the node | a Shape aspect (pick body) |
touchstart |
pointer-down over the node; ev.track() starts a drag |
a Shape aspect |
enter / exit |
physics contact / trigger overlap began / ended ((other: Node)) |
Shape + Physics/Trigger |
Without a Shape aspect a node is invisible to taps — only scene-level listeners will see the
touch (with ev.target === null). The node's own listeners run before the scene's. Event objects
and ev.track() drags: Pointer events & gestures.
Lifecycle
node.destroy(): void // frees the node and its whole subtree nowA node lives while something holds it: your variable, a parent, or a scene. One in a scene or under
a parent stays when you drop your variable; one in neither is freed — with its subtree — once you
drop the last reference. destroy() is the early, explicit free: every aspect in the subtree
detaches first (its onDetach runs — bodies released, updates stopped), then the entity tree goes.
Every handle into the subtree is dead afterwards (id reads 0) — don't use it again. There is no
dispose().
Pitfalls
// ✗ passing options to the constructor — it takes none; an object type-checks but the node has no engine entity
const n = new Node({ position: [0, 1, 0] })
// ✓ create, then set
const n = new Node(); n.position = [0, 1, 0]
// ✗ mutating a STORED transform copy
const p = node.position; p.x += speed * dt // no-op: getters return copies
// ✓ (node.position.x += speed * dt written directly compiles to this)
node.x += speed * dt
// ✗ writing the transform of a dynamic physics body every frame (an aspect's update)
update() { this.node.position = target } // fights the physics step
// ✓ drive the body: node.physics.velocity / applyImpulse (see physics.md)See also
- Conventions — value semantics, degrees vs radians, units
- Mesh / Model — renderable node kinds
- 3D physics & shapes —
Shapepick bodies,enter/exit, rigid bodies - Pointer events & gestures —
click/touchstartpayloads, drags - ARScene — the
arplugin: anchors (Anchor extends Node) and theirtrack/untrack - Timeline — tweening nodes, lights and cameras together