Three.js Scene Graph — Parent-Child Hierarchy and Object Management
In this tutorial, you will learn about Three.js Scene Graph. We cover key concepts, practical examples, and best practices to help you master this topic.
Three.js scene graph is a tree structure where objects inherit transforms from their parent, enabling hierarchical grouping and coordinate management in 3D scenes.
What You'll Learn
By the end of this guide, you will understand how the Three.js scene graph works, how parent-child relationships affect position, rotation, and scale, how to traverse the scene tree, how to use groups and layers, and how to manage object lifecycles without memory leaks.
Why the Scene Graph Matters
Every object in a Three.js scene is a node in a tree. The root is the Scene itself. Child nodes inherit the transform of their parent. When you rotate a parent, all children rotate with it. This is how a solar system works — planets orbit the sun because they are children of a group centered on the sun. The same pattern powers character skeletons, vehicle assemblies, and nested UI in 3D space.
In Durga Antivirus Pro, the scene graph organizes the threat visualization dashboard: the main threat group contains node groups, each containing individual mesh objects. Moving the threat group moves everything, making it trivial to animate entire sections of the dashboard.
flowchart TD
A[Scene Root] --> B[Group: Environment]
B --> C[Mesh: Ground]
B --> D[Mesh: Sky]
A --> E[Group: Player]
E --> F[Mesh: Body]
E --> G[Mesh: Head]
E --> H[Mesh: Arms]
A --> I[Group: Enemies]
I --> J[Mesh: Enemy1]
I --> K[Mesh: Enemy2]
A --> L[Lights]
A --> M[Camera]
Parent-Child Transforms — How Inheritance Works
Every Object3D in Three.js has a position, rotation, and scale. When an object is a child of another, its transform is relative to the parent's coordinate system.
var parent = new THREE.Group();
parent.position.set(5, 0, 0);
scene.add(parent);
var child = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshStandardMaterial({ color: 0xff6600 })
);
child.position.set(2, 0, 0);
parent.add(child);
// The child appears at world position (7, 0, 0)
// because its local position (2,0,0) is offset by the parent's position (5,0,0)
Expected output: A cube rendered at x=7 in world coordinates, because the parent group is at x=5 and the child is offset by x=2 from the parent.
The Matrix Chain
Every frame, Three.js multiplies each object's local matrix by its parent's world matrix. This is called matrix concatenation.
// These three things happen internally:
child.matrixAutoUpdate = true;
child.updateMatrix();
child.matrixWorld.multiplyMatrices(parent.matrixWorld, child.matrix);
Why this matters: You never need to calculate world positions manually. Three.js does it for you.
Groups — The Building Block of Hierarchies
A Group is an empty Object3D that serves as a container. It has no geometry or material — it exists only to hold children.
var solarSystem = new THREE.Group();
scene.add(solarSystem);
var sun = new THREE.Mesh(
new THREE.SphereGeometry(2, 32, 32),
new THREE.MeshStandardMaterial({ color: 0xffaa00, emissive: 0xff5500 })
);
solarSystem.add(sun);
var earthOrbit = new THREE.Group();
earthOrbit.position.x = 8;
solarSystem.add(earthOrbit);
var earth = new THREE.Mesh(
new THREE.SphereGeometry(0.8, 32, 32),
new THREE.MeshStandardMaterial({ color: 0x4488ff })
);
earth.position.x = 1.5;
earthOrbit.add(earth);
// Rotating the solar system rotates everything inside it
solarSystem.rotation.y += 0.01;
// Rotating only the earth orbit rotates the moon around earth
earthOrbit.rotation.y += 0.05;
Expected output: A sun at the center of the solar system group, with an earth orbiting around it, and a moon orbiting the earth. Rotating the solar system group rotates the entire system.
Traversing the Scene Graph
You can walk through every node in the scene tree using traverse.
scene.traverse(function(object) {
if (object.isMesh) {
object.material.color.setHex(0x44aa88);
}
if (object.isLight) {
object.intensity = 1.5;
}
});
Why traverse: It applies an operation to every node without you needing to track references manually.
Finding Specific Objects
var cube = scene.getObjectByName('myCube');
var allMeshes = [];
scene.traverse(function(obj) {
if (obj.isMesh) allMeshes.push(obj);
});
Adding and Removing Children
Managing object lifecycles is critical for performance.
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// Later, remove it
scene.remove(mesh);
// Proper disposal prevents GPU memory leaks
geometry.dispose();
material.dispose();
Why disposal matters: Every geometry and material allocates memory on the GPU. If you remove a mesh without disposing its resources, the GPU memory is never freed. In a dynamic scene that continuously spawns objects, this causes the GPU memory to fill up and eventually crash.
Layers — Visibility Groups
Layers let you control which objects are visible to which cameras.
var mesh = new THREE.Mesh(geometry, material);
mesh.layers.set(1); // Assign to layer 1
camera.layers.set(1); // Camera only sees layer 1
// OR
camera.layers.enable(1); // Camera sees default + layer 1
Real-world use: A minimap camera in a game sees only Layer 2 objects (enemy dots, markers). The main camera sees layers 0 and 1 but not layer 2.
Common Mistakes
1. Adding the Same Object to Multiple Parents
// Wrong — the mesh will only appear under the last parent
group1.add(mesh);
group2.add(mesh); // mesh is removed from group1
// Right — clone or create a second instance
group1.add(mesh);
group2.add(mesh.clone());
2. Forgetting That Children Move With Parent
When you rotate or move a parent, everything inside it moves. If you want an object to stay in world space, attach it directly to the scene, not to a moving group.
3. Memory Leaks From Orphaned Objects
Removing a mesh without disposing its geometry and material leaves GPU memory allocated. Always call dispose on removed geometries and materials.
4. Assuming Child Position is in World Coordinates
child.position.set(0, 0, 0); // THIS is relative to parent, not world!
Use child.getWorldPosition(new THREE.Vector3()) to get the actual world position.
5. Modifying matrixWorld Directly
Three.js computes matrixWorld automatically from the parent chain. Writing to it directly causes unpredictable transforms.
6. Deep Nesting Hurting Performance
Every level of nesting adds matrix multiplications. For scenes with thousands of objects, flatten the tree where possible.
Practice Questions
Q1: What happens to a child's world position when its parent is moved? A: The child moves with the parent. The child's local position stays the same, but its world position changes because it's added to the parent's transform.
Q2: What is the difference between scene.add() and group.add()?
A: Both add children to the parent object. scene.add() attaches to the root scene. group.add() attaches to the group, making the child's transform relative to the group.
Q3: Why must you call dispose() on geometries and materials when removing objects?
A: To free GPU memory. Three.js does not automatically release GPU resources when an object is removed from the scene.
Q4: How do you find an object by name in the scene graph?
A: Use scene.getObjectByName('name') or traverse the scene with scene.traverse().
Q5: What are layers used for? A: Layers control visibility between cameras and objects. Each Object3D and Camera has a layers property. A camera only sees objects on its active layers.
Challenge: Create a scene with a car made from grouped meshes (body, wheels). The car group moves along a path. Each wheel group rotates relative to the car. When the car turns, the wheels should still rotate in the correct direction.
FAQ
Try It Yourself — Interactive Scene Graph Explorer
Build a complete HTML page that creates a nested hierarchy of colored cubes. Click buttons to add child cubes, rotate parent groups, and toggle visibility through layers. Use OrbitControls to inspect the scene from any angle.
<!DOCTYPE html>
<html>
<head>
<title>Scene Graph Explorer</title>
<style>
body { margin: 0; overflow: hidden; background: #1a1a2e; font-family: sans-serif; }
#controls { position: absolute; top: 10px; left: 10px; color: white; background: rgba(0,0,0,0.7); padding: 15px; border-radius: 8px; }
#controls button { padding: 6px 12px; margin: 4px; cursor: pointer; }
#controls .info { margin-top: 8px; font-size: 13px; color: #aaa; }
</style>
</head>
<body>
<div id="controls">
<button onclick="addChild()">Add Child</button>
<button onclick="rotateParent()">Rotate Parent 45deg</button>
<button onclick="removeLast()">Remove Last</button>
<div class="info">Objects: <span id="count">0</span></div>
</div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
var scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e);
var camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(8, 6, 10);
var renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
var controls = new OrbitControls(camera, renderer.domElement);
var ambient = new THREE.AmbientLight(0x404060);
scene.add(ambient);
var dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(5, 10, 7);
scene.add(dirLight);
var colors = [0xff6b35, 0x4ecdc4, 0x45b7d1, 0xf9ca24, 0xa29bfe, 0xfd79a8];
var parentGroup = new THREE.Group();
parentGroup.position.y = 2;
scene.add(parentGroup);
var baseCube = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshStandardMaterial({ color: 0xff6b35, emissive: 0x331100 })
);
parentGroup.add(baseCube);
function addChild() {
var color = colors[Math.floor(Math.random() * colors.length)];
var child = new THREE.Mesh(
new THREE.BoxGeometry(0.6, 0.6, 0.6),
new THREE.MeshStandardMaterial({ color: color })
);
child.position.set(
(Math.random() - 0.5) * 4,
(Math.random() - 0.5) * 4 + 1,
(Math.random() - 0.5) * 4
);
parentGroup.add(child);
document.getElementById("count").textContent = parentGroup.children.length;
}
function rotateParent() {
parentGroup.rotation.y += Math.PI / 4;
}
function removeLast() {
var children = parentGroup.children;
if (children.length > 1) {
var removed = children[children.length - 1];
parentGroup.remove(removed);
if (removed.geometry) removed.geometry.dispose();
if (removed.material) removed.material.dispose();
document.getElementById("count").textContent = parentGroup.children.length;
}
}
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
window.addEventListener("resize", function() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
What's Next
Now that you understand scene graph hierarchies, explore advanced materials and shaders in the next lesson.
Materials Advanced — PBR, textures, environment maps and custom materials. Shaders GLSL — Writing custom shader programs for unique visual effects.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro