PART 1 — SCENE GRAPH
Each node is a CHRenderTarget. Children inherit parent world transforms. Auto-children update, render, and clean themselves up automatically.
WORLD view
UI view
auto-child (self-managing)
manual child
Actor
CHRenderTarget
WORLD view
Torso
CHActorPiece : CHRenderTarget
Arm Left
CHActorPiece
Weapon
CHRenderTarget
Arm Right
CHActorPiece
Head
CHActorPiece
UI Panel
CHRenderTarget
UI view
Health Bar
CHRenderTarget
Icon
CHRenderTarget
PART 2 — LAZY TRANSFORM PROPAGATION
Transforms are only recomputed when getWorld() is called. Mutations propagate dirty flags immediately; computation defers until needed.
Torso.setPosition({x, y}) called — what happens?
① setPosition() on Torso
localDirty = true
New render data stored.
Matrix not yet computed.
② notifyChildren()
Arm L: parentDirty=true
Arm R: parentDirty=true
No matrix math yet.
③ Cascades deeper
Weapon: parentDirty=true
Entire subtree flagged.
Still no matrix math.
④ Weapon.getWorld()
parentDirty == true
→ forceUpdateWorld()
Triggered by render()
⑤ forceUpdateWorld() resolves
world = parent.getWorld()
.combine(getLocal())
Traverses ancestors recursively
LAZY:
Dirty flags propagate instantly — O(children) — but no matrix math is done until getWorld() is called. This means
10,000 setPosition() calls on a node with no render pass in that frame cost only flag writes. The first getWorld() pays the compute cost.
CHRenderData3D — per-node transform data
glm::vec3 position // x, y used for 2D; all 3 for 3D objects
glm::vec3 scale // x, y used for 2D
glm::vec3 origin // rotation pivot point
glm::vec3 rotation // Euler angles (degrees); z-axis for 2D
→ forceUpdateLocal() builds a CHTransform (matrix) from this data
→ Rotation: x-axis, then y-axis, then z-axis (Euler Gimbal)
CHTransformGraphNode — two cached matrices
m_local (localDirty flag) — this node's position/scale/rotation only
m_world (parentDirty flag) — combined with all ancestors; what gets submitted to GPU
setPosition / setScale / setOrigin / setRotation → localDirty=true + notifyChildren
getWorld() → resolves dirty chain, returns cached m_world
getWorldPointer() → same, but returns pointer for direct GPU submission