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