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.

Scene (root) │ ├── Group "car" │ ├── Mesh "body" ← geometry + material │ ├── Mesh "wheel_FL" │ └── Mesh "wheel_FR" │ ├── DirectionalLight └── PerspectiveCamera Each node has: position / rotation / scale → local transform matrix → composed local transform (4×4) matrixWorld → absolute world transform (4×4)

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

PropertyLineDescription
idL79Auto-incrementing integer ID unique across the session
uuidL87UUID for serialization (stays stable across export/import)
nameL95Optional string name for getObjectByName() lookup
parentL121Reference to the parent Object3D (null for scene root)
childrenL128Array of child Object3Ds — this forms the tree

Transform Properties

PropertyLineDescription
positionL168Vector3 — local translation relative to parent
rotationL180Euler — local rotation in radians (XYZ order by default)
quaternionL191Quaternion — same rotation as rotation, kept in sync automatically
scaleL203Vector3 — local scale (1, 1, 1) = no scaling
matrixL233Matrix4 — composed local transform: position × rotation × scale
matrixWorldL241Matrix4 — absolute world transform: parent.matrixWorld × matrix
matrixAutoUpdateL253If true, matrix is recomputed each frame from position/rotation/scale
matrixWorldAutoUpdateL265If true, matrixWorld is recomputed when the scene graph updates

Visibility & Rendering Properties

PropertyLineDescription
visibleL291If false, object and all children are skipped during render traversal
layersL283Bitmask — object is rendered only if camera.layers matches any bit
renderOrderL327Integer — overrides Z-sort for this object. Higher = rendered later
castShadowWhether this object contributes to shadow maps
receiveShadowWhether 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;
}
No GPU work happens on add/remove. The tree is pure JavaScript — GPU resources are allocated lazily on first render.
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.

updateMatrix() — runs when matrixAutoUpdate = true matrix = compose( position, quaternion, scale ) = T × R × S (4×4 matrix multiply) updateMatrixWorld( force ) — recursive, called once per render if (parent exists): matrixWorld = parent.matrixWorld × matrix ← chain of transforms else: matrixWorld = matrix ← root node for each child: child.updateMatrixWorld() ← depth-first traversal
// 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).

PropertyLineDescription
isSceneL26Type discriminator — renderer checks this to validate input
backgroundL40Color, Texture, or CubeTexture drawn behind everything else
environmentL50CubeTexture/Texture used for PBR environment lighting (IBL)
fogL59Fog or FogExp2 — if set, fog uniforms are sent to every shader that supports it
overrideMaterialL113If 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 );
});
traverse() visits ALL nodes including invisible ones. Use traverseVisible() if you only care about rendered objects — it skips subtrees where visible === false.
External References