10 KiB
Scene System: Cameras, DrawContext, and Instances
Thin scene layer that produces RenderObjects for the renderer. It gathers opaque/transparent surfaces, maintains the main camera, and exposes simple runtime instance APIs.
Components
-
SceneManager(src/scene/vk_scene.h/.cpp)- Owns the main
Camera,GPUSceneData, andDrawContext. - Loads GLTF scenes via
AssetManager/LoadedGLTFand creates dynamic mesh/GLTF instances. - Updates per‑frame transforms, camera, and
GPUSceneData(view/proj/viewproj, sun/ambient).
- Owns the main
-
DrawContext- Two lists:
OpaqueSurfacesandTransparentSurfacesofRenderObject. - Populated by scene graph traversal and dynamic instances each frame.
- Two lists:
-
RenderObject- Geometry:
indexBuffer,vertexBuffer(for RG tracking),vertexBufferAddress(device address used by shaders). - Material:
MaterialInstance* materialwith bound set and pipeline. - Transform and bounds for optional culling.
- Geometry:
Frame Flow
SceneManager::update_scene()clears the draw lists and rebuilds them by drawing all active scene/instance nodes.- Renderer consumes the lists:
- Geometry pass sorts opaque by material and index buffer to improve locality.
- Transparent pass sorts back‑to‑front against camera and blends to the HDR target.
- Uniforms: Passes allocate a small per‑frame UBO (
GPUSceneData) and bind it via a shared layout.
Sorting / Culling
- Opaque (geometry): stable sort by
materialthenindexBuffer(seesrc/render/passes/geometry.cpp). - Transparent: sort by camera‑space depth far→near (see
src/render/passes/transparent.cpp). - An example frustum test exists in
passes/geometry.cpp(is_visible) and can be enabled to cull meshes.
Dynamic Instances
-
Mesh instances
addMeshInstance(name, mesh, transform),removeMeshInstance(name),clearMeshInstances().- Useful for spawning primitives or asset meshes at runtime.
-
GLTF instances
addGLTFInstance(name, LoadedGLTF, transform),removeGLTFInstance(name),clearGLTFInstances().
GLTF Animation / “Actions”
GLTF files can contain one or more animation clips (e.g. Idle, Walk, Run). The loader (LoadedGLTF) parses these into LoadedGLTF::Animation objects. Animation state (which clip, time, loop flag) is stored outside the glTF asset:
- One
AnimationStateper named static scene (forloadScene). - One
AnimationStateper runtime glTF instance (SceneManager::GLTFInstance).
This means that animation is independent per scene and per instance, even if they share the same underlying LoadedGLTF asset and meshes.
Static scenes (loaded via loadScene)
Example: engine default scene in VulkanEngine::init():
structureis loaded and registered via:sceneManager->loadScene("structure", structureFile);
To control its animation state:
- By index (per‑scene state):
scene->setSceneAnimation("structure", 0); // first clipscene->setSceneAnimation("structure", 1, true); // second clip, reset time
- By name (per‑scene state; matches glTF animation name):
scene->setSceneAnimation("structure", "Idle");scene->setSceneAnimation("structure", "Run");
- Looping (per‑scene state):
scene->setSceneAnimationLoop("structure", true); // enable loopscene->setSceneAnimationLoop("structure", false); // play once and stop at end
All functions return bool to indicate whether the scene name was found. A negative index (e.g. -1) disables animation for that scene (pose stays at the last evaluated frame).
Runtime GLTF instances
GLTF instances are created via:
scene->addGLTFInstance("player", playerGltf, playerTransform);
You can treat each instance as an “actor” and drive its current action from your game state. Each instance has its own AnimationState, even if multiple instances share the same LoadedGLTF.
- By index (per‑instance state):
scene->setGLTFInstanceAnimation("player", 0);scene->setGLTFInstanceAnimation("player", -1); // disable animation for this actor
- By name (per‑instance state):
scene->setGLTFInstanceAnimation("player", "Idle");scene->setGLTFInstanceAnimation("player", "Run");
- Looping (per‑instance state):
scene->setGLTFInstanceAnimationLoop("player", true);
These helpers update the instance’s AnimationState. SceneManager::update_scene() advances each instance’s state every frame using a per‑frame dt before drawing, so once you select an action, it will keep playing automatically until you change it or disable looping for that instance.
Per‑Instance Node / Joint Overrides
For non‑skinned models (rigid parts), you can apply local‑space pose offsets to specific glTF nodes on a per‑instance basis. This is useful for things like control surfaces, doors, or turrets layered on top of an existing animation.
- API (on
SceneManager):bool setGLTFInstanceNodeOffset(const std::string &instanceName, const std::string &nodeName, const glm::mat4 &offset);bool clearGLTFInstanceNodeOffset(const std::string &instanceName, const std::string &nodeName);void clearGLTFInstanceNodeOffsets(const std::string &instanceName);
Notes:
- Offsets are local‑space post‑multipliers:
- Effective local transform =
node.localTransform * offset.
- Effective local transform =
- Offsets are per instance:
- Different instances of the same glTF can have different joint poses at the same animation time.
- Overrides are applied during draw via
DrawContext::gltfNodeLocalOverridesandMeshNode::Draw, without modifying the shared glTF asset.
GPU Scene Data
GPUSceneDatacarries camera matrices and lighting constants for the frame.- Passes map and fill it into a per-frame UBO, bindable with
DescriptorManager::gpuSceneDataLayout().
Point Lights
SceneManager::PointLightposition– world‑space position.radius– approximate influence radius (used for falloff).color– RGB color.intensity– scalar brightness.
- API
addPointLight(const PointLight &light)clearPointLights()getPointLightCount(),getPointLight(index, outLight),setPointLight(index, light),removePointLight(index)
- Usage pattern
- On level load: call
addPointLightfor each baked/runtime point light. - At runtime (e.g. gameplay): read/modify lights via the indexed helpers.
- On level load: call
Picking & Selection (Game‑Facing)
The scene system exposes CPU ray‑based picking and rectangle selection that the engine uses for editor tools, but you can also call them directly from game code.
-
Single‑object ray pick
- Function:
bool SceneManager::pick(const glm::vec2 &mousePosPixels, RenderObject &outObject, glm::vec3 &outWorldPos) - Input:
mousePosPixelsin window coordinates (SDL style), origin at top‑left. - Output:
outObject– the closest hitRenderObject(opaque or transparent) along the camera ray.outWorldPos– world‑space hit position (mesh BVH‑refined when available).
- Returns
truewhen something was hit,falseotherwise.
- Function:
-
ID‑buffer picking
- In addition to CPU ray picks, the engine can render an object‑ID buffer and read back a single pixel.
- Core API:
SceneManager::resolveObjectID(uint32_t id, RenderObject &outObject) const- Takes an ID from the ID buffer and resolves it back to the
RenderObjectin the latestDrawContext.
- Takes an ID from the ID buffer and resolves it back to the
- Engine integration:
RenderObject::objectIDis assigned inMeshNode::Drawand consumed by the geometry pass to write the ID buffer.VulkanEnginewires a smallPickReadbackrender‑graph pass that copies one pixel from the ID buffer into a CPU readback buffer when ID‑buffer picking is enabled.
-
Rectangle selection
- Function:
void SceneManager::selectRect(const glm::vec2 &p0, const glm::vec2 &p1, std::vector<RenderObject> &outObjects) const - Input:
p0,p1– opposite corners of a screen‑space rectangle in window coordinates (top‑left origin). - Output:
outObjects– allRenderObjects whose projected bounds intersect the rectangle. - Internals:
- Uses
sceneData.viewprojandbox_overlaps_ndc_rectto test each object’s bounds in clip space.
- Uses
- Function:
Engine‑Level Picking Helpers
VulkanEngine wraps the scene picking functions and keeps a small amount of per‑frame state that game/editor code can query:
-
Structures on
VulkanEnginestruct PickInfoMeshAsset *mesh,LoadedGLTF *scene,Node *nodeRenderObject::OwnerType ownerType,std::string ownerNameglm::vec3 worldPos,glm::mat4 worldTransformuint32_t indexCount,firstIndex,surfaceIndexbool valid
- Fields:
_lastPick– result of the last click selection (via CPU ray or ID‑buffer)._hoverPick– result of the last per‑frame hover raycast (under the mouse cursor)._dragSelection– list ofPickInfofilled bySceneManager::selectRectwhen a drag‑select completes._useIdBufferPicking– toggles between CPU ray picking and ID‑buffer picking.
-
How the engine fills them (for reference)
- Mouse hover:
- The frame loop tracks
_mousePosPixelsfrom SDL mouse motion events. - Each frame,
VulkanEngine::draw()callsSceneManager::pick(_mousePosPixels, ...)and stores the result in_hoverPick.
- The frame loop tracks
- Click selection:
- On mouse button release, the engine either:
- Queues a pick request for the ID buffer (if
_useIdBufferPickingis true), or - Calls
SceneManager::pick(...)directly (CPU ray).
- Queues a pick request for the ID buffer (if
- The resolved result is stored in
_lastPick.
- On mouse button release, the engine either:
- Drag selection:
- The engine tracks a drag rectangle in screen space and, on release, calls
SceneManager::selectRect(...)and converts eachRenderObjectinto aPickInfostored in_dragSelection.
- The engine tracks a drag rectangle in screen space and, on release, calls
- Mouse hover:
-
Typical game‑side usage
- Hover tooltips:
- Read
engine->_hoverPickeach frame; ifvalid, useownerName,worldPos, orownerTypeto drive UI.
- Read
- Click selection:
- After input handling, inspect
engine->_lastPickfor the most recent clicked object.
- After input handling, inspect
- Multi‑select:
- After a drag, iterate
engine->_dragSelectionfor group actions.
- After a drag, iterate
- Hover tooltips:
Tips
- Treat
DrawContextas immutable during rendering; build it fully inupdate_scene(). - Keep
RenderObjectsmall; use device addresses for vertex data to avoid per-draw vertex buffer binds. - For custom sorting/culling, modify only the scene layer; render passes stay simple.