Files
QuaternionEngine/docs/Scene.md
2025-12-20 23:43:34 +09:00

11 KiB
Raw Permalink Blame History

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, and DrawContext.
    • Loads GLTF scenes via AssetManager/LoadedGLTF and creates dynamic mesh/GLTF instances.
    • Updates perframe transforms, camera, and GPUSceneData (view/proj/viewproj, sun/ambient).
  • DrawContext

    • Two lists: OpaqueSurfaces and TransparentSurfaces of RenderObject.
    • Populated by scene graph traversal and dynamic instances each frame.
  • RenderObject

    • Geometry: indexBuffer, vertexBuffer (for RG tracking), vertexBufferAddress (device address used by shaders).
    • Material: MaterialInstance* material with bound set and pipeline.
    • Transform and bounds for optional culling.

Frame Flow

  1. SceneManager::update_scene() clears the draw lists and rebuilds them by drawing all active scene/instance nodes.
  2. Renderer consumes the lists:
    • Geometry pass sorts opaque by material and index buffer to improve locality.
    • Transparent pass sorts backtofront against camera and blends to the HDR target.
  3. Uniforms: Passes allocate a small perframe UBO (GPUSceneData) and bind it via a shared layout.

Sorting / Culling

  • Opaque (geometry): stable sort by material then indexBuffer (see src/render/passes/geometry.cpp).
  • Transparent: sort by cameraspace 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 AnimationState per named static scene (for loadScene).
  • One AnimationState per 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():

  • structure is loaded and registered via:
    • sceneManager->loadScene("structure", structureFile);

To control its animation state:

  • By index (perscene state):
    • scene->setSceneAnimation("structure", 0); // first clip
    • scene->setSceneAnimation("structure", 1, true); // second clip, reset time
  • By name (perscene state; matches glTF animation name):
    • scene->setSceneAnimation("structure", "Idle");
    • scene->setSceneAnimation("structure", "Run");
  • Looping (perscene state):
    • scene->setSceneAnimationLoop("structure", true); // enable loop
    • scene->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 (perinstance state):
    • scene->setGLTFInstanceAnimation("player", 0);
    • scene->setGLTFInstanceAnimation("player", -1); // disable animation for this actor
  • By name (perinstance state):
    • scene->setGLTFInstanceAnimation("player", "Idle");
    • scene->setGLTFInstanceAnimation("player", "Run");
  • Looping (perinstance state):
    • scene->setGLTFInstanceAnimationLoop("player", true);

These helpers update the instances AnimationState. SceneManager::update_scene() advances each instances state every frame using a perframe dt before drawing, so once you select an action, it will keep playing automatically until you change it or disable looping for that instance.

PerInstance Node / Joint Overrides

For nonskinned models (rigid parts), you can apply localspace pose offsets to specific glTF nodes on a perinstance 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 localspace postmultipliers:
    • Effective local transform = node.localTransform * offset.
  • 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::gltfNodeLocalOverrides and MeshNode::Draw, without modifying the shared glTF asset.

GPU Scene Data

  • GPUSceneData carries 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::PointLight
    • position worldspace 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 addPointLight for each baked/runtime point light.
    • At runtime (e.g. gameplay): read/modify lights via the indexed helpers.

Spot Lights

  • SceneManager::SpotLight
    • position_world worldspace position.
    • direction worldspace unit direction (cone axis).
    • radius approximate influence radius (used for falloff).
    • inner_angle_deg, outer_angle_deg cone halfangles in degrees (inner ≤ outer).
    • color RGB color.
    • intensity scalar brightness.
  • API
    • addSpotLight(const SpotLight &light)
    • clearSpotLights()
    • getSpotLightCount(), getSpotLight(index, outLight), setSpotLight(index, light), removeSpotLight(index)
  • Usage pattern
    • On level load: call addSpotLight for each flashlight/beam/cone light.
    • At runtime: read/modify lights via the indexed helpers.

Picking & Selection (GameFacing)

The scene system exposes CPU raybased picking and rectangle selection that the engine uses for editor tools, but you can also call them directly from game code.

  • Singleobject ray pick

    • Function: bool SceneManager::pick(const glm::vec2 &mousePosPixels, RenderObject &outObject, glm::vec3 &outWorldPos)
    • Input: mousePosPixels in window coordinates (SDL style), origin at topleft.
    • Output:
      • outObject the closest hit RenderObject (opaque or transparent) along the camera ray.
      • outWorldPos worldspace hit position (mesh BVHrefined when available).
    • Returns true when something was hit, false otherwise.
  • IDbuffer picking

    • In addition to CPU ray picks, the engine can render an objectID 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 RenderObject in the latest DrawContext.
    • Engine integration:
      • RenderObject::objectID is assigned in MeshNode::Draw and consumed by the geometry pass to write the ID buffer.
      • VulkanEngine wires a small PickReadback rendergraph pass that copies one pixel from the ID buffer into a CPU readback buffer when IDbuffer 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 screenspace rectangle in window coordinates (topleft origin).
    • Output: outObjects all RenderObjects whose projected bounds intersect the rectangle.
    • Internals:
      • Uses sceneData.viewproj and box_overlaps_ndc_rect to test each objects bounds in clip space.

EngineLevel Picking Helpers

VulkanEngine wraps the scene picking functions and keeps a small amount of perframe state that game/editor code can query:

  • Structures on VulkanEngine

    • struct PickInfo
      • MeshAsset *mesh, LoadedGLTF *scene, Node *node
      • RenderObject::OwnerType ownerType, std::string ownerName
      • glm::vec3 worldPos, glm::mat4 worldTransform
      • uint32_t indexCount, firstIndex, surfaceIndex
      • bool valid
    • Fields:
      • _lastPick result of the last click selection (via CPU ray or IDbuffer).
      • _hoverPick result of the last perframe hover raycast (under the mouse cursor).
      • _dragSelection list of PickInfo filled by SceneManager::selectRect when a dragselect completes.
      • _useIdBufferPicking toggles between CPU ray picking and IDbuffer picking.
  • How the engine fills them (for reference)

    • Mouse hover:
      • The frame loop tracks _mousePosPixels from SDL mouse motion events.
      • Each frame, VulkanEngine::draw() calls SceneManager::pick(_mousePosPixels, ...) and stores the result in _hoverPick.
    • Click selection:
      • On mouse button release, the engine either:
        • Queues a pick request for the ID buffer (if _useIdBufferPicking is true), or
        • Calls SceneManager::pick(...) directly (CPU ray).
      • The resolved result is stored in _lastPick.
    • Drag selection:
      • The engine tracks a drag rectangle in screen space and, on release, calls SceneManager::selectRect(...) and converts each RenderObject into a PickInfo stored in _dragSelection.
  • Typical gameside usage

    • Hover tooltips:
      • Read engine->_hoverPick each frame; if valid, use ownerName, worldPos, or ownerType to drive UI.
    • Click selection:
      • After input handling, inspect engine->_lastPick for the most recent clicked object.
    • Multiselect:
      • After a drag, iterate engine->_dragSelection for group actions.

Tips

  • Treat DrawContext as immutable during rendering; build it fully in update_scene().
  • Keep RenderObject small; 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.