Lysa  0.0
Lysa 3D Engine
Scene node hierarchy

SceneInstance.ixx and SceneInstance.cpp

Lysa provides GPU-level primitives (lysa::MeshInstance, lysa::Scene, …) but does not prescribe a scene-graph or an ECS. This sample implements a thin wrapper that organises renderables into a parent-child tree.

Note
The Lysa Nodes library provides a ready-made Node-based scene graph if you prefer not to write your own.

SceneInstance

SceneInstance is the base tree node. It stores a localTransform (the transform relative to its parent) and a globalTransform (the world-space result). update() recomputes the global transform recursively:

void SceneInstance::update() {
const auto parentTransform =
parent ? parent->globalTransform : lysa::float4x4::identity();
globalTransform = lysa::mul(localTransform, parentTransform);
for (const auto& child : children) child->update();
}

Calling setTransform() on a node sets its localTransform and marks it dirty. The dirty flag is purely a hint; in this sample the full tree is always re-evaluated from the root each process tick by SceneTree::onProcess (see Scene tree). In a larger scene graph or an ECS you would skip the GPU upload for nodes whose dirty flag is clear.

lysa::MeshInstance vs SceneMeshInstance

Understanding the two-level split is important:

  • lysa::MeshInstance is the engine-side GPU handle. It stores the world transform, AABB, visibility, and shadow-cast flag that the renderer reads from GPU memory every frame. It is unaware of your application's scene-graph structure.
  • SceneMeshInstance is your application-side wrapper. It owns a lysa::MeshInstance and keeps it in sync with the scene-graph transform.

This separation lets you build any hierarchy you want while still satisfying the flat list of lysa::MeshInstance objects that the renderer expects.

SceneMeshInstance

SceneMeshInstance extends SceneInstance and owns a lysa::MeshInstance. Its update() override pushes the world transform and the world-space AABB to the engine after the base class has computed globalTransform:

void SceneMeshInstance::update() {
SceneInstance::update();
meshInstance.setTransform(globalTransform);
meshInstance.setAABB(
meshInstance.getMesh().getAABB().toGlobal(meshInstance.getTransform()));
meshInstance.setVisible(visible);
dirty = false;
}

The AABB is used by the GPU draw commands generation and frustum-culling compute shaders. It must cover the mesh in world space after the transform is applied. AABB::toGlobal re-computes the axis-aligned bounds from the mesh's local-space AABB and the world transform, accounting for rotation.

DummyAnimationPlayer

lysa::AssetsPack::load requires a third template parameter for an animation player node. DummyAnimationPlayer is a no-op stub that satisfies this interface when animation playback is not needed:

struct DummyAnimationPlayer : SceneInstance {
auto add(const std::shared_ptr<lysa::AnimationLibrary>&, const std::string& = "") const {}
std::shared_ptr<lysa::AnimationLibrary> getLibrary() const {
return std::make_shared<lysa::AnimationLibrary>();
}
void setCurrentAnimation(const std::string&) const {}
};

When a .assets file contains animation data the loader creates one DummyAnimationPlayer as a child of the root so the animation library is still accessible, but no playback logic runs.

Asset loading

The free function load() wraps lysa::AssetsPack::load with the three concrete node types used in this project:

std::shared_ptr<SceneInstance> load(const std::string& fileURI) {
SceneInstance,
SceneMeshInstance,
DummyAnimationPlayer
>(fileURI);
}

Internally AssetsPack::load:

  1. Opens the .assets file through the virtual filesystem.
  2. Reads the binary headers for images, textures, materials, meshes, and nodes.
  3. Uploads all BCn-compressed images to GPU VRAM through a single staging buffer using a transfer queue.
  4. Creates lysa::StandardMaterial objects from the material headers and upload them to GPU VRAM.
  5. Creates lysa::Mesh objects and uploads vertex / index data to the GPU VRAM.
  6. Instantiates one node per entry in the pack hierarchy using your template types.
  7. Reconstructs the parent-child tree and wraps all root nodes under a single synthetic root.
  8. Returns the synthetic root as a std::shared_ptr<T_OBJECT>.

The loader is designed to be called once per asset file. The resulting subtree is live: once addInstance has registered its mesh instances with the renderer, the subtree drives rendering every frame.

Cloning

clone() performs a deep copy of an existing subtree. It is useful for placing multiple independent instances of the same loaded asset in the scene without reloading from disk:

std::shared_ptr<SceneInstance> clone(const std::shared_ptr<SceneInstance>& orig) {
std::shared_ptr<SceneInstance> root;
if (const auto& mi = std::dynamic_pointer_cast<SceneMeshInstance>(orig)) {
root = std::make_shared<SceneMeshInstance>(*mi);
} else {
root = std::make_shared<SceneInstance>(*orig);
}
for (const auto& child : orig->children) root->addChild(clone(child));
return root;
}

SceneMeshInstance's copy constructor creates a new lysa::MeshInstance that shares the same underlying lysa::Mesh (GPU geometry is not duplicated). Only the per-instance transform, AABB, and visibility are independent. This makes instancing dozens of copies of the same model inexpensive in both VRAM and load time.

Next : Scene tree