How Pyrite64’s Object Hierarchy and Scene Graph Structure Drive World Transforms and Parent-Child Relationships
Pyrite64 represents every entity as a Project::Object containing local transform data and bidirectional parent-child pointers, calculating world transforms by recursively multiplying local matrices up the hierarchy chain.
The object hierarchy and scene graph structure in Pyrite64 form the spatial backbone of the engine, determining how entities relate to one another and how local transformations convert into world-space coordinates. By storing raw parent pointers and shared pointer collections in every Object, the engine creates a directed acyclic graph where ancestor modifications automatically propagate to all descendants. This architecture, implemented across src/project/scene/object.h and src/project/scene/scene.h, ensures consistent spatial calculations for rendering, physics, and editor tooling.
Core Architecture: The Object Class and Bidirectional Links
Every element appearing in a Pyrite64 level is stored as a Project::Object. The class definition in src/project/scene/object.h declares three essential local transform properties—position, rotation, and scale—alongside the structural links that build the scene graph:
- A raw pointer to its parent (
Object* parent{nullptr};) - A container of its children (
std::vector<std::shared_ptr<Object>> children{})
These members appear at lines 24–35 of src/project/scene/object.h and establish the bidirectional relationships necessary for transform propagation.
Local Transform Storage
Each Object maintains its spatial data in local coordinate space relative to its parent. The pos, rot, and scale properties describe where the object sits in relation to its immediate ancestor rather than the world origin. This design allows artists and developers to move parent objects without manually recalculating every child position, as the hierarchy handles the mathematical conversion automatically.
World Transform Calculation Through the Scene Graph
The scene graph determines world transforms through recursive matrix multiplication. When the engine requires a world-space transformation—for rendering, collision detection, or script queries—it walks up the parent chain defined by the parent pointers.
Recursive Matrix Multiplication
The calculation follows this deterministic rule:
WorldMatrix(obj) = WorldMatrix(obj.parent) * LocalMatrix(obj)
If obj.parent is nullptr, the recursion terminates and the world matrix equals the object's local matrix. This multiplication order ensures that child objects inherit all transformations (translation, rotation, and scaling) from their ancestors. Because the hierarchy is strictly a directed acyclic graph, the engine guarantees no circular dependencies exist that would cause infinite recursion during traversal.
Automatic Propagation Behavior
Any modification to an ancestor's local transform immediately affects all descendants without requiring manual updates. When the editor adjusts an object's pos, rot, or scale—such as through the gizmo interface in viewport3D.cpp—the next render pass queries the world matrix via computeWorldMatrix. This routine walks the parent chain and multiplies each local matrix, producing updated world-space coordinates for the entire subtree.
Scene Graph Management via the Scene Class
The Project::Scene class, defined in src/project/scene/scene.h (lines 30–48), serves as the root container and primary API for object manipulation. It maintains the top-level objects—those whose parent pointer remains nullptr—and provides the interface that the editor, build system, and runtime scripts use to modify the hierarchy.
Root Object Container
Scene tracks all objects within a level, distinguishing between root nodes and nested children. Root objects represent the entry points for graph traversal, while their descendants form subtrees that move as cohesive units. This container relationship ensures that serialization and memory management operations occur at the scene level while preserving the hierarchical integrity of individual object chains.
Object Lifecycle and UUID Assignment
When creating entities, Scene::addObject generates a new Object instance, assigns it a unique UUID, and optionally links it to a parent. The implementation sets newObj.parent = &parent and pushes the new object into the parent's children vector using std::shared_ptr ownership. This dual-link approach—raw pointer upward, shared pointer downward—enables efficient upward traversal for transform calculations while ensuring proper memory management for child collections.
Practical Impact on Editor and Runtime Systems
The object hierarchy influences multiple subsystems beyond pure mathematics, affecting how data persists and how components receive spatial information.
Serialization and Persistence
During scene saving operations (Scene::serialize), Pyrite64 writes the object tree using depth-first traversal, preserving parent UUID references for each child. On load (Scene::deserialize), the system reconstructs objects and re-establishes parent-child links by resolving these UUIDs before any world-matrix calculations occur. This serialization strategy ensures that hierarchical relationships survive between editor sessions and runtime deployments.
Component System Integration
Components such as compTransform, compCamera, and compLight receive world transform data through the scene graph API rather than maintaining independent spatial calculations. When a render pass queries a camera's view matrix or a light's position, it accesses the owning Object's computed world transform, guaranteeing that all subsystems operate on a consistent spatial representation derived from the same parent-child hierarchy.
Code Example: Building a Hierarchical Object Tree
The following implementation demonstrates constructing a parent-child relationship and querying the resulting world transforms:
// 1️⃣ Access the active scene
Project::Scene *scene = project->getScenes().getLoadedScene();
// 2️⃣ Create a root object (a "ship")
auto ship = scene->addObject(/*json descriptor*/, /*parent UUID = 0*/);
// 3️⃣ Attach a child object (a "cannon")
auto cannon = scene->addObject(/*json descriptor*/, ship->uuid);
cannon->parent = ship.get(); // establish upward link
ship->children.emplace_back(cannon); // establish downward link
// 4️⃣ Configure local transforms
ship->pos = {0.0f, 0.0f, 0.0f};
ship->rot = glm::quat{1,0,0,0};
ship->scale = {1.0f, 1.0f, 1.0f};
cannon->pos = {2.0f, 0.0f, 0.0f}; // 2 units forward from ship center
cannon->rot = glm::quat{1,0,0,0};
cannon->scale = {0.5f, 0.5f, 0.5f};
// 5️⃣ Query world matrices for rendering
glm::mat4 shipWorld = scene->computeWorldMatrix(*ship);
glm::mat4 cannonWorld = scene->computeWorldMatrix(*cannon);
// cannonWorld == shipWorld * cannonLocalMatrix
The computeWorldMatrix function recursively traverses the parent pointer chain, multiplying each local matrix to produce the final world-space transformation. This mechanism powers both the visual representation in the editor viewport and the physics simulation calculations at runtime.
Summary
- Bidirectional linking:
Project::Objectstores a rawparentpointer and astd::vector<std::shared_ptr<Object>>of children insrc/project/scene/object.h, enabling both upward and downward graph traversal. - Recursive world transforms: World matrices calculate as
WorldMatrix(parent) * LocalMatrix(object), automatically propagating ancestor transformations to all descendants. - Scene-level management:
Project::Sceneinsrc/project/scene/scene.hmaintains root objects and providesaddObject,serialize, anddeserializeAPIs that preserve hierarchical integrity. - Subsystem consistency: Components receive world transforms through the scene graph API, ensuring rendering, physics, and editor tools share identical spatial data.
Frequently Asked Questions
How does Pyrite64 calculate world transforms from local coordinates?
Pyrite64 uses recursive matrix multiplication defined in the scene graph traversal logic. The engine multiplies an object's local matrix by its parent's world matrix, continuing up the chain until reaching a root node with no parent. This calculation occurs in methods like computeWorldMatrix, which walks the parent pointer chain defined in src/project/scene/object.h.
What data structure does Pyrite64 use to store parent-child relationships?
Each Object maintains a raw pointer to its parent (Object* parent) and a std::vector<std::shared_ptr<Object>> containing its children. This dual-link structure appears in src/project/scene/object.h and supports both efficient upward traversal for transform calculations and safe memory management for child collections through shared ownership.
How does the Scene class manage object hierarchies?
Project::Scene, defined in src/project/scene/scene.h, tracks top-level objects (those with null parent pointers) and provides the addObject method for creating new entities. When adding objects, the scene handles UUID generation and establishes parent-child links by setting raw pointers and populating the children vector, ensuring the directed acyclic graph structure remains valid.
How does serialization preserve the scene graph structure?
During Scene::serialize, Pyrite64 writes objects using depth-first traversal while recording parent UUID references. The Scene::deserialize method reconstructs objects and resolves these UUIDs to re-establish parent-child links before any world-matrix calculations occur. This approach ensures that hierarchical relationships persist across save and load operations.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →