The scene graph is the hierarchical structure that organizes all 3D objects in a Materia scene.
A scene graph is a tree structure where each node represents an object in 3D space. Parent-child relationships determine how transformations (position, rotation, scale) are inherited.
Scene (root)
├── Camera
├── DirectionalLight
├── Group "Environment"
│ ├── Mesh "Ground"
│ └── Mesh "Skybox"
└── Group "Characters"
├── Group "Player"
│ ├── SkinnedMesh "Body"
│ └── Mesh "Weapon"
└── Group "Enemy"
└── SkinnedMesh "Body"
The base class for all scene graph nodes.
val object3d = Object3D()
object3d.position.set(1f, 2f, 3f)
object3d.rotation.y = PI / 2
object3d.scale.set(2f, 2f, 2f)
The root of the scene graph. There's typically one per render.
val scene = Scene()
scene.background = Color(0x1a1a2e)
scene.add(camera)
scene.add(light)
scene.add(mesh)
Empty container for organizing objects.
val robot = Group()
robot.name = "robot"
val body = Mesh(bodyGeometry, material)
val head = Mesh(headGeometry, material)
head.position.y = 1.5f
robot.add(body)
robot.add(head)
scene.add(robot)
// Transform the whole robot
robot.rotation.y = PI / 4
parent.add(child)
parent.add(child1, child2, child3) // Add multiple
parent.remove(child)
child.removeFromParent()
parent.clear() // Remove all children
// Move to new parent, keeping world transform
newParent.attach(child)
Child transforms are relative to their parent:
val parent = Group()
parent.position.set(10f, 0f, 0f)
val child = Mesh(geometry, material)
child.position.set(5f, 0f, 0f) // Local position
parent.add(child)
scene.add(parent)
// Child world position is (15, 0, 0)
val worldPos = child.getWorldPosition() // Vector3(15, 0, 0)
// Local (relative to parent)
object.position // Local position
object.rotation // Local rotation
object.scale // Local scale
object.matrix // Local transform matrix
// World (absolute)
object.getWorldPosition(target) // World position
object.getWorldQuaternion(target) // World rotation
object.getWorldScale(target) // World scale
object.matrixWorld // World transform matrix
// Conversion
object.localToWorld(vector) // Convert local point to world
object.worldToLocal(vector) // Convert world point to local
scene.traverse { object3d ->
println(object3d.name)
}
scene.traverseVisible { object3d ->
// Skip invisible objects
}
child.traverseAncestors { ancestor ->
println(ancestor.name)
}
val player = scene.getObjectByName("Player")
val object3d = scene.getObjectById(42)
val meshes = mutableListOf<Mesh>()
scene.traverse { obj ->
if (obj is Mesh) {
meshes.add(obj)
}
}
Transforms are stored as matrices for GPU efficiency. Materia handles updates automatically, but you can control this:
// Automatic (default)
object3d.matrixAutoUpdate = true // Update matrix from position/rotation/scale
// Manual
object3d.matrixAutoUpdate = false
object3d.matrix.compose(position, quaternion, scale)
object3d.matrixWorldNeedsUpdate = true
// Force update
object3d.updateMatrix() // Update local matrix
object3d.updateMatrixWorld(true) // Update world matrix (and descendants)
// Hide object (and all descendants)
object3d.visible = false
// Conditional visibility
object3d.layers.set(1) // Only render on layer 1
camera.layers.enable(1) // Camera sees layer 1
Layers control which objects cameras can see:
// Set object to layer 2
mesh.layers.set(2)
// Enable multiple layers
mesh.layers.enable(0)
mesh.layers.enable(1)
// Camera configuration
camera.layers.enableAll() // See all layers
camera.layers.set(0) // Only layer 0
camera.layers.enable(1) // Also layer 1
camera.layers.disable(2) // Not layer 2
camera.layers.toggle(3) // Toggle layer 3
// Good: Organized hierarchy
val scene = Scene()
val environment = Group().apply { name = "environment" }
val characters = Group().apply { name = "characters" }
val ui = Group().apply { name = "ui" }
scene.add(environment, characters, ui)
Deep hierarchies have more matrix multiplications:
// Avoid: Too deep
grandparent.add(parent)
parent.add(child)
child.add(grandchild)
grandchild.add(greatGrandchild)
// Better: Flatter when possible
parent.add(child1, child2, child3)
// Share geometry and material
val geometry = BoxGeometry(1f, 1f, 1f)
val material = MeshStandardMaterial().apply { color = Color.RED }
val meshes = (0 until 100).map { i ->
Mesh(geometry, material).apply {
position.x = i.toFloat()
}
}
// For 1000+ similar objects
val instancedMesh = InstancedMesh(geometry, material, count = 1000)
for (i in 0 until 1000) {
val matrix = Matrix4()
matrix.setPosition(positions[i])
instancedMesh.setMatrixAt(i, matrix)
}
scene.add(instancedMesh)