A scene is a tree. Scene is the root, Meshes, cameras, Lights and plain Object3Ds are the nodes, and every node's transform is relative to its parent. The whole point of the tree is that "relative to the parent" composes: move the parent and every descendant moves with it.
position, quaternion, rotation, scale: the object's transform
relative to its parent. The rotation is stored twice: quaternion is a
Quaternion, the representation the local matrix is built from, and
rotation is the same rotation as Euler angles in radians, XYZ order (see
Matrices). Write to whichever is
convenient; the tree walk keeps them in sync (see below).localMatrix: position, quaternion and scale composed into one matrix,
T × R × S.worldMatrix: parent.worldMatrix × localMatrix, the transform relative
to the scene root. This is what the renderer uploads as modelMatrix.parent and children, maintained by add(), remove() and
removeFromParent(). An object has one parent; adding it somewhere else
moves it. add() refuses to create a cycle.visible: an invisible object and its whole subtree are skipped by
prepareScene().updateWorldMatrix() is the one function that turns the tree into matrices.
The renderer calls it on the scene at the start of every frame, and it
recurses depth-first:
matrixAutoUpdate is true (the default), bring quaternion and
rotation in sync and recompose localMatrix from position, quaternion
and scale. The sync compares both rotation fields against their values at
the last update: if the quaternion changed, rotation is derived from
it; if only the Euler angles changed, quaternion is derived from them;
if both changed, the quaternion wins. There is no change detection on
position and scale, so the recompose always happens; composing is cheap.worldMatrix from the parent's, clear the flag, call the
onWorldMatrixChanged() hook (cameras use it to update their view
matrix), and force all descendants to recompute too.force flag.A camera that is not part of the scene gets its own updateWorldMatrix()
call from prepareScene(), so scene.add(camera) is optional. Adding it is
useful when the camera should follow an object: parent it to the object.
Set matrixAutoUpdate = false to stop the recompose in step 1, write to
localMatrix directly and set worldMatrixNeedsUpdate = true so step 2
runs. This is for cases where a matrix is the natural input: a transform
loaded from a file, a physics engine, an instance matrix. The three vectors
then no longer describe the object.
Because a child inherits its parent's rotation, planet.add(moon) with the
moon at (2, 0, 0) makes the moon orbit when planet.rotation.y changes.
But that also spins the planet. To rotate the moon around the planet without
rotating the planet, insert an empty Object3D as a pivot:
const pivot = new Object3D();
planet.add(pivot);
pivot.add(moon);
moon.position.set(2, 0, 0);
pivot.rotation.y += dt;
The pivot sits at the planet's origin, so rotating it swings everything attached to it around that origin. Empty nodes as pivots, hinges and joints are the standard tool in any scene graph; a glTF file is full of them.
object.lookAt(target) rotates the object so that its +Z axis points at
target, a point in the parent's coordinate system (the same system as
position). A camera's viewing direction is -Z, so Camera overrides
lookAt to point -Z at the target, and DirectionalLight, which
shines along -Z, does the same; all share Mat4.lookAt, which builds a
rotation from a forward vector and an up hint by two cross products.
The resulting matrix goes into setRotationFromMatrix(), which sets
quaternion from the rotation part of the matrix and derives rotation
from the quaternion, so both fields are current right after the call. The
conversions are explained on the Quaternions
page.
object.traverse(callback) visits the object and every descendant,
depth-first, in the order the renderer draws them. It is the tool for things
like "hide every mesh with this material" or "count the triangles".
add() / parent / children API.setRotationFromMatrix and the
rotation sync do), and why quaternions exist.