Scene Graph
How three.js organizes 3D objects in a parent–child hierarchy and computes world-space transforms through the tree.
The Tree Structure
Three.js uses a scene graph: a tree of Object3D nodes. Every visible thing — meshes, lights, cameras — is an Object3D. The scene itself is the root node.
This means moving the "car" Group automatically moves all its children in world space. This is how articulated objects, vehicle wheels, character limbs etc. work.
Object3D — The Base Class Object3D.js
Every object in the scene graph extends Object3D. It provides transforms, hierarchy, visibility, and lifecycle events.
Identity & Hierarchy Properties
| Property | Line | Description |
|---|---|---|
| id | L79 | Auto-incrementing integer ID unique across the session |
| uuid | L87 | UUID for serialization (stays stable across export/import) |
| name | L95 | Optional string name for getObjectByName() lookup |
| parent | L121 | Reference to the parent Object3D (null for scene root) |
| children | L128 | Array of child Object3Ds — this forms the tree |
Transform Properties
| Property | Line | Description |
|---|---|---|
| position | L168 | Vector3 — local translation relative to parent |
| rotation | L180 | Euler — local rotation in radians (XYZ order by default) |
| quaternion | L191 | Quaternion — same rotation as rotation, kept in sync automatically |
| scale | L203 | Vector3 — local scale (1, 1, 1) = no scaling |
| matrix | L233 | Matrix4 — composed local transform: position × rotation × scale |
| matrixWorld | L241 | Matrix4 — absolute world transform: parent.matrixWorld × matrix |
| matrixAutoUpdate | L253 | If true, matrix is recomputed each frame from position/rotation/scale |
| matrixWorldAutoUpdate | L265 | If true, matrixWorld is recomputed when the scene graph updates |
Visibility & Rendering Properties
| Property | Line | Description |
|---|---|---|
| visible | L291 | If false, object and all children are skipped during render traversal |
| layers | L283 | Bitmask — object is rendered only if camera.layers matches any bit |
| renderOrder | L327 | Integer — overrides Z-sort for this object. Higher = rendered later |
| castShadow | — | Whether this object contributes to shadow maps |
| receiveShadow | — | Whether this object receives shadows from the shadow map |
Building the Tree: add() and remove() L746
// scene.add(mesh) calls Object3D.add() on the scene node:
add( object ) {
// Prevent adding self
if ( object === this ) throw new Error( ... );
// If object has an existing parent, remove it first
if ( object.parent !== null ) object.removeFromParent();
// Set bi-directional links
object.parent = this; // child knows its parent
this.children.push( object ); // parent knows its children
// Fire events so listeners can react
object.dispatchEvent( _addedEvent );
this.dispatchEvent( _childaddedEvent );
return this; // chainable
}
// Removal is the mirror image:
remove( object ) {
const index = this.children.indexOf( object );
if ( index !== -1 ) {
object.parent = null;
this.children.splice( index, 1 );
object.dispatchEvent( _removedEvent );
this.dispatchEvent( _childremovedEvent );
}
return this;
}
Transform Composition: Local → World Space
Each node stores a local transform (relative to its parent) and a world transform (absolute in scene space). The renderer needs world transforms to place vertices correctly.
// In Object3D.js — the actual implementation
updateMatrixWorld( force ) {
if ( this.matrixAutoUpdate ) this.updateMatrix();
if ( this.matrixWorldNeedsUpdate || force ) {
if ( this.matrixWorldAutoUpdate ) {
if ( this.parent === null ) {
this.matrixWorld.copy( this.matrix );
} else {
// Multiply parent's world matrix by local matrix
this.matrixWorld.multiplyMatrices(
this.parent.matrixWorld,
this.matrix
);
}
}
this.matrixWorldNeedsUpdate = false;
force = true; // propagate to children
}
const children = this.children;
for ( let i = 0, l = children.length; i < l; i++ ) {
children[ i ].updateMatrixWorld( force );
}
}
The key insight: matrixWorld is a product of all ancestor transforms. Moving a parent automatically repositions all descendants, because their matrixWorld is recomputed from the new parent.matrixWorld.
Why Quaternions, Not Euler Angles?
Euler angles (rotation.x/y/z) are easy to use but suffer from gimbal lock — certain rotations can lose a degree of freedom. Quaternions represent rotation as a 4D unit vector and compose without gimbal lock. Three.js keeps both in sync: setting rotation updates quaternion and vice versa, but internally uses quaternions for matrix composition.
Scene — The Root Node Scene.js
Scene extends Object3D and adds renderer hints. It is the tree root passed to renderer.render(scene, camera).
| Property | Line | Description |
|---|---|---|
| isScene | L26 | Type discriminator — renderer checks this to validate input |
| background | L40 | Color, Texture, or CubeTexture drawn behind everything else |
| environment | L50 | CubeTexture/Texture used for PBR environment lighting (IBL) |
| fog | L59 | Fog or FogExp2 — if set, fog uniforms are sent to every shader that supports it |
| overrideMaterial | L113 | If set, ALL objects use this material instead of their own (useful for depth/normal passes) |
How overrideMaterial Works
In renderObjects() in WebGLRenderer, before calling renderObject(), the renderer checks:
// WebGLRenderer.js — inside renderObjects()
const overrideMaterial = scene.isScene
? scene.overrideMaterial
: null;
// Each object renders with override if set
const material = overrideMaterial !== null
? overrideMaterial
: renderItem.material;
renderObject( object, scene, camera, geometry, material, group );
This is how shadow map rendering works — an override MeshDepthMaterial replaces every object's real material for the shadow pass.
Traversal Utilities
// Walk the tree depth-first
scene.traverse( object => {
console.log( object.name );
});
// Find by name (stops at first match)
const wheel = scene.getObjectByName( 'wheel_FL' );
// Find all objects matching a condition
const meshes = [];
scene.traverseVisible( obj => {
if ( obj.isMesh ) meshes.push( obj );
});
// Walk ancestors up to root
mesh.traverseAncestors( ancestor => {
console.log( ancestor.name );
});
traverseVisible() if you only care about rendered objects — it skips subtrees where visible === false.External References
- Wikipedia — Scene Graph — the general pattern, not three.js-specific
- Three.js Docs — Object3D
- Three.js Manual — Scene Graph — visual diagrams of the hierarchy
- Euclidean Space — Quaternion to Matrix
- OpenGL Tutorial — Model/View/Projection Matrices