Files
QuaternionEngine/docs/GameAPI.md

1508 lines
47 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## GameFacing API Overview
This document summarizes the main engine APIs that are directly useful when building a game (spawn actors, control lights/animations, and interact via picking).
For details on the underlying systems, see also:
- `docs/Scene.md` cameras, draw context, instances, picking.
- `docs/EngineContext.md` access to managers and perframe state.
- `docs/RenderGraph.md` rendergraph API for custom passes.
- `docs/InputSystem.md` keyboard, mouse, and cursor mode handling.
- `docs/Picking.md` object selection, hover detection, and drag-box multi-select.
- `docs/ImGuiSystem.md` immediate-mode UI integration and debug widgets.
---
## `GameAPI::Engine` (HighLevel Game Wrapper)
Header: `src/core/game_api.h`
Implementation: `src/core/game_api.cpp`
`GameAPI::Engine` is a thin, gamefriendly wrapper around `VulkanEngine`. It exposes stable, snake_case methods grouped by responsibility:
- Texture streaming and VRAM budget.
- Shadows and reflections.
- IBL volumes.
- Instances and animation.
- Postprocessing (tonemap, bloom, FXAA).
- Camera control.
- Lighting (directional, point, spot).
- Volumetrics (clouds, smoke, flame).
- Particle systems.
- Debug drawing.
- Picking and rendergraph pass toggles.
- Time and statistics.
Typical creation:
```cpp
#include "core/engine.h"
#include "core/game_api.h"
VulkanEngine engine;
engine.init();
GameAPI::Engine api(&engine); // nonowning
```
You then call `api.*` from your game loop to spawn content and tweak settings.
### Texture Streaming & VRAM Budget
Relevant methods:
- `size_t get_texture_budget() const;`
- `void set_texture_loads_per_frame(int count);`
- `int get_texture_loads_per_frame() const;`
- `void set_texture_upload_budget(size_t bytes);`
- `size_t get_texture_upload_budget() const;`
- `void set_cpu_source_budget(size_t bytes);`
- `size_t get_cpu_source_budget() const;`
- `void set_max_upload_dimension(uint32_t dim);`
- `uint32_t get_max_upload_dimension() const;`
- `void set_keep_source_bytes(bool keep);`
- `bool get_keep_source_bytes() const;`
- `void evict_textures_to_budget();`
At a lower level, `VulkanEngine::query_texture_budget_bytes()` computes a conservative perframe texture budget using VMA heap info and constants in `src/core/config.h`:
- `kTextureBudgetFraction` fraction of total devicelocal VRAM reserved for streamed textures (default `0.35`).
- `kTextureBudgetFallbackBytes` fallback budget when memory properties are unavailable (default `512 MiB`).
- `kTextureBudgetMinBytes` minimum budget clamp (default `128 MiB`).
To globally change how aggressive streaming can be, edit these constants in `config.h` and rebuild. Use the `GameAPI::Engine` setters for perscene tuning (e.g. reducing upload bandwidth on lowend machines).
#### Texture Loading
```cpp
// Load from file (relative to assets/textures/ or absolute path)
TextureHandle load_texture(const std::string& path, const TextureLoadParams& params = {});
// Load from memory (compressed image data: PNG, JPG, KTX2, etc.)
TextureHandle load_texture_from_memory(const std::vector<uint8_t>& data, const TextureLoadParams& params = {});
// Check if texture is loaded and resident in VRAM
bool is_texture_loaded(TextureHandle handle) const;
// Get internal Vulkan image view (VkImageView) for advanced use
void* get_texture_image_view(TextureHandle handle) const;
// Pin texture to prevent automatic eviction (for UI, critical assets)
void pin_texture(TextureHandle handle);
void unpin_texture(TextureHandle handle);
bool is_texture_pinned(TextureHandle handle) const;
// Unload texture and free VRAM (optional - cache auto-manages)
void unload_texture(TextureHandle handle);
// Create ImGui descriptor set for use with ImGui::Image()
void* create_imgui_texture(TextureHandle handle, void* sampler = nullptr);
void free_imgui_texture(void* imgui_texture_id);
```
**TextureLoadParams:**
```cpp
struct TextureLoadParams
{
bool srgb{false}; // Use sRGB color space (true for albedo/emissive)
bool mipmapped{true}; // Generate mipmap chain
TextureChannels channels{Auto}; // Channel hint (Auto, R, RG, RGBA)
uint32_t mipLevels{0}; // 0 = full chain, otherwise limit to N levels
};
```
**Usage Example:**
```cpp
GameAPI::Engine api(&engine);
// Load UI texture and pin it to prevent eviction
GameAPI::TextureLoadParams params;
params.srgb = true;
params.mipmapped = false; // UI textures don't need mipmaps
TextureHandle uiTex = api.load_texture("ui/button.png", params);
api.pin_texture(uiTex);
// Create ImGui descriptor for rendering
void* imguiId = api.create_imgui_texture(uiTex);
ImGui::Image(imguiId, ImVec2(128, 64));
// Later: cleanup
api.free_imgui_texture(imguiId);
api.unpin_texture(uiTex);
```
### Shadows: Resolution, Quality, and RT Modes
Shadows are controlled by a combination of:
- Global settings in `EngineContext::shadowSettings`.
- Config constants in `src/core/config.h`.
- The `ShadowPass` render pass and lighting shader (`shadow.vert`, `deferred_lighting.frag`).
Highlevel gameside controls:
- `void set_shadows_enabled(bool enabled);`
- `void set_shadow_mode(ShadowMode mode);`
- `ClipmapOnly` cascaded shadow maps only.
- `ClipmapPlusRT` cascades + optional rayquery assist.
- `RTOnly` raytraced shadows only (no raster maps).
- `void set_hybrid_ray_cascade_mask(uint32_t mask);`
- `void set_hybrid_ray_threshold(float threshold);`
These map directly onto `EngineContext::shadowSettings` and are also visualized in the ImGui “Shadows / Ray Query” tab.
#### Shadow Map Resolution (`kShadowMapResolution`)
The shadow map resolution is driven by `kShadowMapResolution` in `src/core/config.h`:
- Used for:
- The actual depth image size created for each cascaded shadow map in `VulkanEngine::draw()` (`shadowExtent`).
- Texel snapping for cascade stabilization in `SceneManager::update_scene()`:
- `texel = (2.0f * cover) / float(kShadowMapResolution);`
- Default: `2048.0f`, which gives a good compromise between quality and VRAM usage on midrange GPUs.
Increasing `kShadowMapResolution` has two important effects:
- **VRAM cost grows quadratically.**
- Depth D32F, per cascade:
- 2048 → ~16 MB.
- 4096 → ~64 MB.
- With 4 cascades, 4096×4096 can consume ~256 MB just for shadow depth, on top of swapchain, HDR, Gbuffers, IBL, and other images.
- **Allocation failures can effectively “kill” shadows.**
- All shadow maps are created as transient RenderGraph images each frame run.
- If VMA runs out of suitable devicelocal memory, `vmaCreateImage` will fail, and the engine will assert via `VK_CHECK`. In practice (especially in a release build), this often manifests as:
- No shadow rendering, or
- The app aborting when the first frame tries to allocate these images.
Practical guidance:
- Prefer 2048 or 3072 on consumer hardware unless you have headroom and have profiled memory.
- If you push to 4096 and shadows “disappear”, suspect VRAM pressure:
- Try reducing `kTextureBudgetFraction` so textures use less VRAM.
- Or bring `kShadowMapResolution` back down and retest.
The following qualityrelated shadow constants also live in `config.h`:
- `kShadowCascadeCount`, `kShadowCSMFar`, `kShadowCascadeRadiusScale`, `kShadowCascadeRadiusMargin`.
- `kShadowBorderSmoothNDC`, `kShadowPCFBaseRadius`, `kShadowPCFCascadeGain`.
- `kShadowDepthBiasConstant`, `kShadowDepthBiasSlope`.
These affect how cascades are distributed and how soft/filtered the resulting shadows are. Changing them is safe but should be tested against your content and FOV ranges.
### IBL (Image-Based Lighting)
**API:**
```cpp
// Load global IBL asynchronously
bool load_global_ibl(const IBLPaths& paths);
// Get/set global IBL paths (does not trigger reload)
IBLPaths get_global_ibl_paths() const;
void set_global_ibl_paths(const IBLPaths& paths);
// Add/remove local IBL volumes
size_t add_ibl_volume(const IBLVolume& volume);
size_t add_ibl_volume(const IBLVolumeD& volume); // double-precision
bool remove_ibl_volume(size_t index);
// Get/set IBL volume properties
bool get_ibl_volume(size_t index, IBLVolume& out) const;
bool set_ibl_volume(size_t index, const IBLVolume& volume);
bool get_ibl_volume(size_t index, IBLVolumeD& out) const; // double-precision
bool set_ibl_volume(size_t index, const IBLVolumeD& volume);
// Query active volume
int get_active_ibl_volume() const; // -1 = global
size_t get_ibl_volume_count() const;
void clear_ibl_volumes();
```
**Structures:**
```cpp
struct IBLPaths
{
std::string specularCube; // .ktx2 specular cubemap
std::string diffuseCube; // .ktx2 diffuse cubemap
std::string brdfLut; // .ktx2 BRDF lookup table
std::string background; // .ktx2 background (optional, falls back to specular)
};
struct IBLVolume
{
glm::vec3 center{0.0f};
glm::vec3 halfExtents{10.0f};
IBLPaths paths;
bool enabled{true};
};
struct IBLVolumeD // double-precision variant
{
glm::dvec3 center{0.0};
glm::vec3 halfExtents{10.0f};
IBLPaths paths;
bool enabled{true};
};
```
**Usage Example:**
```cpp
GameAPI::Engine api(&engine);
// Load global IBL (outdoor environment)
GameAPI::IBLPaths globalIBL;
globalIBL.specularCube = "ibl/outdoor_spec.ktx2";
globalIBL.diffuseCube = "ibl/outdoor_diff.ktx2";
globalIBL.brdfLut = "ibl/brdf_lut.ktx2";
api.load_global_ibl(globalIBL);
// Create local IBL volume for interior (overrides global when camera inside)
GameAPI::IBLVolume interior;
interior.center = glm::vec3(10.0f, 2.0f, -5.0f);
interior.halfExtents = glm::vec3(5.0f, 3.0f, 5.0f);
interior.paths.specularCube = "ibl/indoor_spec.ktx2";
interior.paths.diffuseCube = "ibl/indoor_diff.ktx2";
interior.paths.brdfLut = "ibl/brdf_lut.ktx2";
interior.enabled = true;
size_t idx = api.add_ibl_volume(interior);
// Query which IBL is active
int activeVol = api.get_active_ibl_volume();
if (activeVol == -1)
{
// Using global IBL
}
else
{
// Using local volume at index activeVol
}
```
### Reflections and PostProcessing
Gameside reflection controls:
- `void set_ssr_enabled(bool enabled);`
- `void set_reflection_mode(ReflectionMode mode);`
(`SSROnly`, `SSRPlusRT`, `RTOnly`)
Tone mapping and bloom:
- `void set_exposure(float exposure);`
- `void set_tonemap_operator(TonemapOperator op);` (`Reinhard`, `ACES`)
- `void set_bloom_enabled(bool enabled);`
- `void set_bloom_threshold(float threshold);`
- `void set_bloom_intensity(float intensity);`
These wrap `TonemapPass` parameters and are equivalent to flipping the corresponding ImGui controls at runtime.
FXAA:
- `void set_fxaa_enabled(bool enabled);`
- `void set_fxaa_edge_threshold(float threshold);`
- `void set_fxaa_edge_threshold_min(float threshold);`
### Camera and Render Scale
Camera:
- `void set_camera_position(const glm::vec3 &position);`
- `void set_camera_position(const glm::dvec3 &position);` // double-precision
- `glm::vec3 get_camera_position() const;`
- `glm::dvec3 get_camera_position_d() const;` // double-precision
- `void set_camera_rotation(float pitchDeg, float yawDeg);`
- `void get_camera_rotation(float &pitchDeg, float &yawDeg) const;`
- `void set_camera_fov(float fovDegrees);`
- `float get_camera_fov() const;`
- `void camera_look_at(const glm::vec3 &target);`
- `void camera_look_at(const glm::dvec3 &target);` // double-precision
These functions internally manipulate the quaternionbased `Camera::orientation` and `position` in `SceneManager`. They respect the engine's `-Z` forward convention. Double-precision variants allow precise camera positioning in large worlds.
Render resolution scaling:
- `void set_render_scale(float scale); // 0.31.0`
- `float get_render_scale() const;`
This scales the internal draw extent relative to the swapchain and main HDR image sizes, trading resolution for performance.
### Picking & Pass Toggles
Picking:
- `Engine::PickResult get_last_pick() const;`
- `Engine::PickResultD get_last_pick_d() const;` // double-precision
- `void set_use_id_buffer_picking(bool use);`
- `bool get_use_id_buffer_picking() const;`
**PickResult structure:**
```cpp
struct PickResult
{
bool valid{false};
std::string ownerName;
glm::vec3 worldPosition{0.0f};
};
struct PickResultD // double-precision variant
{
bool valid{false};
std::string ownerName;
glm::dvec3 worldPosition{0.0};
};
```
These mirror `VulkanEngine::get_last_pick()` and `_useIdBufferPicking`, letting you choose between:
- CPU raycast picking (immediate, cheaper VRAM).
- IDbuffer based picking (async, 1frame latency, robust for dense scenes).
Rendergraph pass toggles:
- `void set_pass_enabled(const std::string &passName, bool enabled);`
- `bool get_pass_enabled(const std::string &passName) const;`
This writes into `VulkanEngine::_rgPassToggles` and is applied during RenderGraph compilation. It allows you to permanently disable or enable named passes (e.g. `"ShadowMap[0]"`, `"FXAA"`, `"SSR"`) from game code, not just via the debug UI.
---
## Input System
Header: `src/core/input/input_system.h`
Docs: `docs/InputSystem.md`
The engine provides a unified input abstraction layer that wraps SDL2 events. Access it via `VulkanEngine::input()` or store a reference during initialization.
### Polled State (Recommended for Games)
Query current keyboard/mouse state each frame:
```cpp
void Game::update(VulkanEngine& engine)
{
const InputState& input = engine.input().state();
// Movement (held keys)
glm::vec3 move{0.0f};
if (input.key_down(Key::W)) move.z -= 1.0f;
if (input.key_down(Key::S)) move.z += 1.0f;
if (input.key_down(Key::A)) move.x -= 1.0f;
if (input.key_down(Key::D)) move.x += 1.0f;
// Sprint modifier
float speed = input.modifiers().shift ? 10.0f : 5.0f;
player.move(move * speed * dt);
// Fire on left click (just pressed this frame)
if (input.mouse_pressed(MouseButton::Left))
{
player.fire();
}
// Toggle menu on Escape (just pressed)
if (input.key_pressed(Key::Escape))
{
ui.toggle_menu();
}
// Mouse look (right button held)
if (input.mouse_down(MouseButton::Right))
{
engine.input().set_cursor_mode(CursorMode::Relative);
glm::vec2 delta = input.mouse_delta();
camera.rotate(delta.x * 0.1f, delta.y * 0.1f);
}
else
{
engine.input().set_cursor_mode(CursorMode::Normal);
}
// Scroll wheel for zoom
float scroll = input.wheel_delta().y;
if (scroll != 0.0f)
{
camera.zoom(scroll * 0.5f);
}
}
```
### Key State Types
- `key_down(Key)` / `mouse_down(MouseButton)` — true while held (continuous actions)
- `key_pressed(Key)` / `mouse_pressed(MouseButton)` — true only on the frame it was pressed (one-shot)
- `key_released(Key)` / `mouse_released(MouseButton)` — true only on the frame it was released
### Available Keys
Letters: `Key::A` through `Key::Z`
Numbers: `Key::Num0` through `Key::Num9`
Special: `Key::Enter`, `Key::Escape`, `Key::Space`, `Key::Tab`, `Key::Backspace`
Modifiers: `Key::LeftShift`, `Key::LeftCtrl`, `Key::LeftAlt`, `Key::LeftSuper` (and Right variants)
### Mouse Buttons
`MouseButton::Left`, `MouseButton::Middle`, `MouseButton::Right`, `MouseButton::X1`, `MouseButton::X2`
### Cursor Modes
```cpp
enum class CursorMode : uint8_t
{
Normal = 0, // Default visible cursor
Hidden = 1, // Hidden but not captured
Relative = 2, // FPS-style: hidden + captured, only delta matters
};
// Set via:
engine.input().set_cursor_mode(CursorMode::Relative);
```
### Mouse Position and Motion
```cpp
glm::vec2 pos = input.mouse_position(); // Screen coordinates (pixels)
glm::vec2 delta = input.mouse_delta(); // Frame motion delta
glm::vec2 wheel = input.wheel_delta(); // Scroll wheel delta
```
### Modifier Keys
```cpp
InputModifiers mods = input.modifiers();
if (mods.ctrl && input.key_pressed(Key::S))
{
save_game();
}
```
### Event-Driven Access (For UI)
For text input or other event-driven needs:
```cpp
for (const InputEvent& ev : engine.input().events())
{
if (ev.type == InputEvent::Type::KeyDown)
{
if (ev.key == Key::Backspace)
{
text_field.delete_char();
}
}
else if (ev.type == InputEvent::Type::MouseWheel)
{
scroll_view.scroll(ev.wheel_delta.y);
}
}
```
### Window State
```cpp
if (engine.input().quit_requested())
{
// Handle window close
}
if (engine.input().window_minimized())
{
// Skip heavy rendering
}
```
---
## VulkanEngine Helpers
Header: `src/core/engine.h`
- Lifecycle
- `void init()` initialize SDL, device, managers, scene, render graph.
- `void run()` main loop: handles events, updates scene, builds Render Graph, submits frames.
- `void cleanup()` destroy managers and GPU resources.
- GLTF spawn helper
- `bool addGLTFInstance(const std::string &instanceName, const std::string &modelRelativePath, const glm::mat4 &transform = glm::mat4(1.f));`
- Loads a glTF from `assets/models/...` (via `AssetManager`) and registers it as a runtime scene instance.
- `instanceName` becomes the logical name used by `SceneManager` (e.g. for picking and animation control).
- Picking access
- `struct PickInfo` (nested in `VulkanEngine`)
- `MeshAsset *mesh`, `LoadedGLTF *scene`, `Node *node`
- `RenderObject::OwnerType ownerType` (e.g. `StaticGLTF`, `GLTFInstance`, `MeshInstance`)
- `std::string ownerName`
- `glm::vec3 worldPos`
- `glm::mat4 worldTransform`
- `uint32_t indexCount`, `firstIndex`, `surfaceIndex`
- `bool valid`
- `const PickInfo &get_last_pick() const`
- Returns the last click selection result.
- Filled by the engine from either CPU ray picking or IDbuffer picking depending on `_useIdBufferPicking`.
- Typical usage:
- On mouseup in your game layer, read `engine->get_last_pick()` and, if `valid`, use `ownerName`/`worldPos` to drive selection logic.
> Note: hover picks and drag selections are also available as internal fields on `VulkanEngine` (`_hoverPick`, `_dragSelection`) and are documented in `docs/Scene.md`. You can expose additional getters if you want to rely on them from game code.
---
## Scene & Instances (Actors, Lights, Animations)
Header: `src/scene/vk_scene.h`
Docs: `docs/Scene.md`
### Transform Structures
The GameAPI provides both single and double-precision transform representations:
```cpp
struct Transform
{
glm::vec3 position{0.0f};
glm::quat rotation{1.0f, 0.0f, 0.0f, 0.0f};
glm::vec3 scale{1.0f};
glm::mat4 to_matrix() const;
static Transform from_matrix(const glm::mat4& m);
};
struct TransformD // double-precision variant for large worlds
{
glm::dvec3 position{0.0};
glm::quat rotation{1.0f, 0.0f, 0.0f, 0.0f};
glm::vec3 scale{1.0f};
glm::mat4 to_matrix() const;
static TransformD from_matrix(const glm::mat4& m);
};
```
Use `TransformD` for positioning objects in large worlds (e.g., space games, flight sims) where single-precision floating point loses sub-meter precision at large coordinates.
### GLTF Instances
**API:**
```cpp
// Add glTF model instance (path relative to assets/models/)
bool add_gltf_instance(const std::string& name,
const std::string& modelPath,
const Transform& transform = {},
bool preloadTextures = true);
bool add_gltf_instance(const std::string& name,
const std::string& modelPath,
const TransformD& transform,
bool preloadTextures = true);
// Add glTF model asynchronously (returns job ID, 0 on failure)
uint32_t add_gltf_instance_async(const std::string& name,
const std::string& modelPath,
const Transform& transform = {},
bool preloadTextures = true);
uint32_t add_gltf_instance_async(const std::string& name,
const std::string& modelPath,
const TransformD& transform,
bool preloadTextures = true);
// Remove glTF instance
bool remove_gltf_instance(const std::string& name);
// Get/set glTF instance transform
bool get_gltf_instance_transform(const std::string& name, Transform& out) const;
bool set_gltf_instance_transform(const std::string& name, const Transform& transform);
bool get_gltf_instance_transform(const std::string& name, TransformD& out) const;
bool set_gltf_instance_transform(const std::string& name, const TransformD& transform);
// Preload textures for an instance
void preload_instance_textures(const std::string& name);
// Clear all dynamic instances
void clear_all_instances();
```
### Primitive Mesh Instances
**API:**
```cpp
// Add primitive mesh instance
bool add_primitive_instance(const std::string& name,
PrimitiveType type,
const Transform& transform = {});
bool add_primitive_instance(const std::string& name,
PrimitiveType type,
const TransformD& transform);
// Remove mesh instance
bool remove_mesh_instance(const std::string& name);
// Get/set mesh instance transform
bool get_mesh_instance_transform(const std::string& name, Transform& out) const;
bool set_mesh_instance_transform(const std::string& name, const Transform& transform);
bool get_mesh_instance_transform(const std::string& name, TransformD& out) const;
bool set_mesh_instance_transform(const std::string& name, const TransformD& transform);
```
**Typical usage:**
- Spawn primitives or dynamic meshes at runtime (e.g. projectiles, props).
- Use `set_mesh_instance_transform` every frame to move them based on game logic.
### Textured Primitives
Spawn primitive meshes (cube, sphere, plane, capsule) with custom PBR textures at runtime.
#### PrimitiveMaterial Structure
```cpp
struct PrimitiveMaterial
{
std::string albedoPath; // Color/diffuse texture (relative to assets/)
std::string metalRoughPath; // Metallic (R) + Roughness (G) texture
std::string normalPath; // Tangent-space normal map
std::string occlusionPath; // Ambient occlusion (R channel)
std::string emissivePath; // Emissive map
glm::vec4 colorFactor{1.0f}; // Base color multiplier (RGBA)
float metallic{0.0f}; // Metallic factor (0-1)
float roughness{0.5f}; // Roughness factor (0-1)
};
```
#### PrimitiveType Enum
```cpp
enum class PrimitiveType
{
Cube,
Sphere,
Plane,
Capsule
};
```
#### API Functions
```cpp
bool add_textured_primitive(const std::string& name,
PrimitiveType type,
const PrimitiveMaterial& material,
const Transform& transform = {});
bool add_textured_primitive(const std::string& name,
PrimitiveType type,
const PrimitiveMaterial& material,
const TransformD& transform); // double-precision
```
#### Usage Example
```cpp
GameAPI::Engine api(&engine);
// Create material with textures
GameAPI::PrimitiveMaterial mat;
mat.albedoPath = "textures/brick_albedo.png";
mat.normalPath = "textures/brick_normal.png";
mat.metalRoughPath = "textures/brick_mro.png"; // Metallic-Roughness-Occlusion packed
mat.roughness = 0.7f;
mat.metallic = 0.0f;
// Spawn a textured cube
GameAPI::Transform transform;
transform.position = glm::vec3(0.0f, 1.0f, -5.0f);
transform.scale = glm::vec3(2.0f);
api.add_textured_primitive("my_brick_cube", GameAPI::PrimitiveType::Cube, mat, transform);
// Spawn a textured sphere with different material
GameAPI::PrimitiveMaterial metalMat;
metalMat.albedoPath = "textures/metal_albedo.png";
metalMat.normalPath = "textures/metal_normal.png";
metalMat.metallic = 1.0f;
metalMat.roughness = 0.3f;
metalMat.colorFactor = glm::vec4(0.9f, 0.9f, 1.0f, 1.0f); // Slight blue tint
GameAPI::Transform sphereT;
sphereT.position = glm::vec3(3.0f, 1.0f, -5.0f);
api.add_textured_primitive("chrome_sphere", GameAPI::PrimitiveType::Sphere, metalMat, sphereT);
```
#### Notes
- Texture paths are relative to the `assets/` directory.
- If a texture path is empty, the engine uses default placeholder textures:
- Albedo: error checkerboard (magenta/black)
- Normal: flat normal (0.5, 0.5, 1.0)
- MetalRough: white (default values from `metallic`/`roughness` factors)
- Occlusion: white (no occlusion)
- Emissive: black (no emission)
- Textures are loaded asynchronously via `TextureCache`; placeholders appear until upload completes.
- For non-textured primitives with solid colors, use `add_primitive_instance()` instead.
- GLTF instances (actors)
- `void addGLTFInstance(const std::string &name, std::shared_ptr<LoadedGLTF> scene, const glm::mat4 &transform = glm::mat4(1.f));`
- `bool getGLTFInstanceTransform(const std::string &name, glm::mat4 &outTransform);`
- `bool setGLTFInstanceTransform(const std::string &name, const glm::mat4 &transform);`
- `bool removeGLTFInstance(const std::string &name);`
- `void clearGLTFInstances();`
- Usage pattern:
- Treat each GLTF instance as an “actor” with a name; use transforms to place characters, doors, props, etc.
### Animations (GLTF)
**API:**
```cpp
// Set animation by index for a glTF instance (-1 to disable)
bool set_instance_animation(const std::string& instanceName, int animationIndex, bool resetTime = true);
// Set animation by name for a glTF instance
bool set_instance_animation(const std::string& instanceName, const std::string& animationName, bool resetTime = true);
// Set animation looping for a glTF instance
bool set_instance_animation_loop(const std::string& instanceName, bool loop);
```
**Notes:**
- All functions return `bool` indicating whether the named instance exists.
- Animation state is **independent per instance**:
- Each glTF instance has its own `AnimationState`, even when sharing the same `LoadedGLTF`.
- An index `< 0` (e.g. `-1`) disables animation for that instance (pose is frozen at the last evaluated state).
- `SceneManager::update_scene()` advances each active animation state every frame using engine delta time.
**Usage Example:**
```cpp
GameAPI::Engine api(&engine);
// Play walk animation by index
api.set_instance_animation("player", 0, true); // Reset to start
api.set_instance_animation_loop("player", true);
// Switch to run animation by name
api.set_instance_animation("player", "run", true);
// Stop animation (freeze pose)
api.set_instance_animation("player", -1);
```
### PerInstance Node / Joint Control (NonSkinned)
For rigid models and simple "joints" (e.g. flaps, doors, turrets), you can apply localspace pose offsets to individual glTF nodes per instance:
**API:**
```cpp
bool set_instance_node_offset(const std::string& instanceName, const std::string& nodeName, const glm::mat4& offset);
bool clear_instance_node_offset(const std::string& instanceName, const std::string& nodeName);
void clear_all_instance_node_offsets(const std::string& instanceName);
```
Typical usage:
- Use glTF animation for the base motion (e.g. gear deployment).
- Layer gamedriven offsets on top for perinstance control:
```cpp
// Rotate a control surface on one aircraft instance
glm::mat4 offset =
glm::rotate(glm::mat4(1.f),
glm::radians(aileronDegrees),
glm::vec3(1.f, 0.f, 0.f));
sceneMgr->setGLTFInstanceNodeOffset("plane01", "LeftAileron", offset);
```
### Lighting - Directional (Sunlight)
- `void set_sunlight_direction(const glm::vec3& dir);`
- `glm::vec3 get_sunlight_direction() const;`
- `void set_sunlight_color(const glm::vec3& color, float intensity);`
- `glm::vec3 get_sunlight_color() const;`
- `float get_sunlight_intensity() const;`
### Lighting - Point Lights
**Structs:**
```cpp
struct PointLight
{
glm::vec3 position{0.0f};
float radius{10.0f};
glm::vec3 color{1.0f};
float intensity{1.0f};
};
struct PointLightD // double-precision variant
{
glm::dvec3 position{0.0};
float radius{10.0f};
glm::vec3 color{1.0f};
float intensity{1.0f};
};
```
**API:**
- `size_t add_point_light(const PointLight &light);`
- `size_t add_point_light(const PointLightD &light);`
- `bool remove_point_light(size_t index);`
- `bool get_point_light(size_t index, PointLight &out) const;`
- `bool get_point_light(size_t index, PointLightD &out) const;`
- `bool set_point_light(size_t index, const PointLight &light);`
- `bool set_point_light(size_t index, const PointLightD &light);`
- `size_t get_point_light_count() const;`
- `void clear_point_lights();`
**Typical usage:**
- On level load, add all static lights.
- At runtime, animate or toggle lights based on gameplay events (e.g. explosions, flickering lamps).
### Lighting - Spot Lights
**Structs:**
```cpp
struct SpotLight
{
glm::vec3 position{0.0f};
glm::vec3 direction{0.0f, -1.0f, 0.0f};
float radius{10.0f};
glm::vec3 color{1.0f};
float intensity{1.0f};
float inner_angle_deg{15.0f};
float outer_angle_deg{25.0f};
};
struct SpotLightD // double-precision variant
{
glm::dvec3 position{0.0};
glm::vec3 direction{0.0f, -1.0f, 0.0f};
float radius{10.0f};
glm::vec3 color{1.0f};
float intensity{1.0f};
float inner_angle_deg{15.0f};
float outer_angle_deg{25.0f};
};
```
**API:**
- `size_t add_spot_light(const SpotLight &light);`
- `size_t add_spot_light(const SpotLightD &light);`
- `bool remove_spot_light(size_t index);`
- `bool get_spot_light(size_t index, SpotLight &out) const;`
- `bool get_spot_light(size_t index, SpotLightD &out) const;`
- `bool set_spot_light(size_t index, const SpotLight &light);`
- `bool set_spot_light(size_t index, const SpotLightD &light);`
- `size_t get_spot_light_count() const;`
- `void clear_spot_lights();`
**Usage Example:**
```cpp
GameAPI::Engine api(&engine);
// Create a flashlight
GameAPI::SpotLight flashlight;
flashlight.position = glm::vec3(0.0f, 1.5f, 0.0f);
flashlight.direction = glm::vec3(0.0f, 0.0f, -1.0f);
flashlight.radius = 20.0f;
flashlight.color = glm::vec3(1.0f, 0.95f, 0.8f); // Warm white
flashlight.intensity = 50.0f;
flashlight.inner_angle_deg = 10.0f;
flashlight.outer_angle_deg = 25.0f;
size_t idx = api.add_spot_light(flashlight);
// Later: update flashlight direction to follow camera
flashlight.direction = camera_forward;
api.set_spot_light(idx, flashlight);
```
---
## Picking System
Header: `src/core/picking/picking_system.h`
Docs: `docs/Picking.md`
The engine provides a unified picking system that handles click selection, hover detection, and drag-box multi-select. Access it via `VulkanEngine::picking()`.
### Accessing Pick Results
```cpp
void Game::handle_interaction(VulkanEngine& engine)
{
PickingSystem& picking = engine.picking();
// Last click selection
const PickingSystem::PickInfo& pick = picking.last_pick();
if (pick.valid)
{
fmt::println("Selected: {}", pick.ownerName);
interact_with(pick.ownerName, pick.worldPos);
}
// Current hover (for tooltips)
const PickingSystem::PickInfo& hover = picking.hover_pick();
if (hover.valid)
{
show_tooltip(hover.ownerName);
}
// Drag-box multi-select
for (const auto& sel : picking.drag_selection())
{
if (sel.valid)
{
add_to_selection(sel.ownerName);
}
}
}
```
### PickInfo Structure
```cpp
struct PickInfo
{
MeshAsset *mesh; // Source mesh
LoadedGLTF *scene; // Source glTF
Node *node; // glTF node
RenderObject::OwnerType ownerType; // StaticGLTF, GLTFInstance, MeshInstance
std::string ownerName; // Logical name (e.g., "player")
WorldVec3 worldPos; // Hit position (double-precision)
glm::mat4 worldTransform; // Object transform
uint32_t indexCount, firstIndex; // Picked surface indices
uint32_t surfaceIndex; // Surface index in mesh
bool valid; // True if hit something
};
```
### Picking Modes
```cpp
// CPU ray picking (default) - immediate, BVH-accelerated
picking.set_use_id_buffer_picking(false);
// ID-buffer picking - pixel-perfect, 1-frame latency
picking.set_use_id_buffer_picking(true);
```
### Owner Types
```cpp
enum class OwnerType
{
None,
StaticGLTF, // Loaded via loadScene()
GLTFInstance, // Runtime glTF instance
MeshInstance, // Runtime mesh instance
};
```
---
## ImGui System
Header: `src/core/ui/imgui_system.h`
Docs: `docs/ImGuiSystem.md`
The engine integrates Dear ImGui for debug UI and editor tools. Access it via `VulkanEngine::imgui()`.
### Adding Custom UI
```cpp
void Game::init(VulkanEngine& engine)
{
// Register your UI callback
engine.imgui().add_draw_callback([this]() {
draw_game_ui();
});
}
void Game::draw_game_ui()
{
if (ImGui::Begin("Game HUD"))
{
ImGui::Text("Score: %d", _score);
ImGui::Text("Health: %.0f%%", _health * 100.0f);
if (ImGui::Button("Pause"))
{
toggle_pause();
}
}
ImGui::End();
}
```
### Input Capture
Always check ImGui input capture before processing game input:
```cpp
void Game::update(VulkanEngine& engine)
{
// Skip game mouse input when ImGui wants it
if (!engine.imgui().want_capture_mouse())
{
handle_mouse_input();
}
// Skip game keyboard input when ImGui wants it
if (!engine.imgui().want_capture_keyboard())
{
handle_keyboard_input();
}
}
```
### Built-in Debug UI
The engine provides comprehensive debug widgets in `src/core/engine_ui.cpp`:
- **Window Tab**: Monitor selection, fullscreen modes, HiDPI info
- **Stats Tab**: Frame time, FPS, draw calls, triangle count
- **Scene Tab**: Instance spawning, point light editor, ImGuizmo gizmos
- **Render Graph Tab**: Pass toggles, resource tracking
- **Texture Streaming Tab**: VRAM budget, cache stats
- **Shadows Tab**: Shadow mode, cascade visualization
- **Post Processing Tab**: Tonemapping, bloom, FXAA, SSR settings
### ImGuizmo Integration
For 3D transform gizmos on selected objects:
```cpp
#include "ImGuizmo.h"
void draw_gizmo(const PickingSystem::PickInfo& pick,
const glm::mat4& view, const glm::mat4& proj)
{
if (!pick.valid) return;
glm::mat4 transform = pick.worldTransform;
ImGuizmo::SetOrthographic(false);
ImGuizmo::SetDrawlist();
ImGuiIO& io = ImGui::GetIO();
ImGuizmo::SetRect(0, 0, io.DisplaySize.x, io.DisplaySize.y);
if (ImGuizmo::Manipulate(glm::value_ptr(view),
glm::value_ptr(proj),
ImGuizmo::TRANSLATE,
ImGuizmo::WORLD,
glm::value_ptr(transform)))
{
// Apply transform to the picked object
scene->setGLTFInstanceTransform(pick.ownerName, transform);
}
}
```
---
## Time and Statistics
Header: `src/core/game_api.h`
### Delta Time
```cpp
// Get delta time in seconds for the current frame (clamped to 0.0-0.1)
float get_delta_time() const;
```
Use this for frame-rate independent movement and animation.
### Engine Statistics
```cpp
struct Stats
{
float frametime{0.0f}; // ms
float drawTime{0.0f}; // ms
float sceneUpdateTime{0.0f}; // ms
int triangleCount{0};
int drawCallCount{0};
};
Stats get_stats() const;
```
**Usage Example:**
```cpp
GameAPI::Engine api(&engine);
// Frame-rate independent movement
float dt = api.get_delta_time();
player_position += velocity * dt;
// Display performance stats
GameAPI::Stats stats = api.get_stats();
fmt::println("FPS: {:.1f} | Tris: {} | Draws: {}",
1000.0f / stats.frametime,
stats.triangleCount,
stats.drawCallCount);
```
---
## Volumetrics (Clouds, Smoke, Flame)
Header: `src/core/game_api.h`
The engine supports GPU-based voxel volumetric rendering for clouds, smoke, and flame effects. Up to 4 independent volumes can be active simultaneously.
### API
```cpp
// Enable/disable volumetrics system
void set_volumetrics_enabled(bool enabled);
bool get_volumetrics_enabled() const;
// Get/set voxel volume settings by index (0-3)
bool get_voxel_volume(size_t index, VoxelVolumeSettings& out) const;
bool set_voxel_volume(size_t index, const VoxelVolumeSettings& settings);
// Get maximum number of voxel volumes
size_t get_max_voxel_volumes() const; // Returns 4
```
### VoxelVolumeSettings Structure
```cpp
struct VoxelVolumeSettings
{
bool enabled{false};
VoxelVolumeType type{VoxelVolumeType::Clouds}; // Clouds, Smoke, Flame
// Volume positioning
bool followCameraXZ{false}; // Follow camera in XZ, offset in Y
bool animateVoxels{true}; // Run voxel advection/update compute
glm::vec3 volumeCenterLocal{0.0f, 2.0f, 0.0f};
glm::vec3 volumeHalfExtents{8.0f, 8.0f, 8.0f};
glm::vec3 volumeVelocityLocal{0.0f, 0.0f, 0.0f}; // Drift when not following camera
// Raymarch/composite controls
float densityScale{1.0f};
float coverage{0.0f}; // 0..1 threshold (higher = emptier)
float extinction{1.0f}; // Absorption/extinction scale
int stepCount{48}; // Raymarch steps
// Voxel grid resolution (cubic)
uint32_t gridResolution{48};
// Voxel animation (advection + injection) parameters
glm::vec3 windVelocityLocal{0.0f, 2.0f, 0.0f}; // Local units/sec (buoyancy)
float dissipation{1.25f}; // Density decay rate (1/sec)
float noiseStrength{1.0f}; // Injection rate
float noiseScale{8.0f}; // Noise frequency in UVW space
float noiseSpeed{1.0f}; // Time scale for injection noise
// Smoke/flame source in normalized volume UVW space
glm::vec3 emitterUVW{0.5f, 0.05f, 0.5f};
float emitterRadius{0.18f}; // Normalized (0..1)
// Shading
glm::vec3 albedo{1.0f, 1.0f, 1.0f}; // Scattering tint (cloud/smoke)
float scatterStrength{1.0f};
glm::vec3 emissionColor{1.0f, 0.6f, 0.25f}; // Flame emissive tint
float emissionStrength{0.0f};
};
```
### Usage Example
```cpp
GameAPI::Engine api(&engine);
// Enable volumetrics
api.set_volumetrics_enabled(true);
// Create a flame effect
GameAPI::VoxelVolumeSettings flame;
flame.enabled = true;
flame.type = GameAPI::VoxelVolumeType::Flame;
flame.volumeCenterLocal = glm::vec3(0.0f, 1.0f, -5.0f);
flame.volumeHalfExtents = glm::vec3(2.0f, 3.0f, 2.0f);
flame.gridResolution = 64;
flame.densityScale = 1.5f;
flame.coverage = 0.3f;
flame.windVelocityLocal = glm::vec3(0.0f, 5.0f, 0.0f); // Upward
flame.emitterUVW = glm::vec3(0.5f, 0.1f, 0.5f);
flame.emitterRadius = 0.2f;
flame.emissionStrength = 2.0f;
flame.emissionColor = glm::vec3(1.0f, 0.5f, 0.1f);
api.set_voxel_volume(0, flame);
// Create cloud layer that follows camera
GameAPI::VoxelVolumeSettings clouds;
clouds.enabled = true;
clouds.type = GameAPI::VoxelVolumeType::Clouds;
clouds.followCameraXZ = true;
clouds.volumeCenterLocal = glm::vec3(0.0f, 50.0f, 0.0f); // Offset in Y
clouds.volumeHalfExtents = glm::vec3(100.0f, 20.0f, 100.0f);
clouds.gridResolution = 128;
clouds.densityScale = 0.8f;
clouds.coverage = 0.5f;
clouds.albedo = glm::vec3(0.9f, 0.95f, 1.0f); // Bluish tint
api.set_voxel_volume(1, clouds);
```
---
## Particle Systems
Header: `src/core/game_api.h`
GPU-accelerated particle systems with flipbook animation, soft particles, and flexible spawning.
### API
```cpp
// Create/destroy particle systems
uint32_t create_particle_system(uint32_t particle_count);
bool destroy_particle_system(uint32_t id);
bool resize_particle_system(uint32_t id, uint32_t new_count);
// Get/set particle system settings
bool get_particle_system(uint32_t id, ParticleSystem& out) const;
bool set_particle_system(uint32_t id, const ParticleSystem& system);
// Query particle systems
std::vector<uint32_t> get_particle_system_ids() const;
uint32_t get_allocated_particles() const;
uint32_t get_free_particles() const;
uint32_t get_max_particles() const;
// Preload VFX textures (e.g., "vfx/flame.ktx2")
void preload_particle_texture(const std::string& assetPath);
```
### ParticleSystem Structure
```cpp
struct ParticleSystem
{
uint32_t id{0};
uint32_t particleCount{0};
bool enabled{true};
bool reset{true};
ParticleBlendMode blendMode{ParticleBlendMode::Additive}; // Additive or Alpha
ParticleParams params{};
// Asset-relative texture paths (e.g., "vfx/flame.ktx2")
std::string flipbookTexture{"vfx/flame.ktx2"};
std::string noiseTexture{"vfx/simplex.ktx2"};
};
struct ParticleParams
{
glm::vec3 emitterPosLocal{0.0f, 0.0f, 0.0f};
float spawnRadius{0.1f};
glm::vec3 emitterDirLocal{0.0f, 1.0f, 0.0f};
float coneAngleDegrees{20.0f};
float minSpeed{2.0f};
float maxSpeed{8.0f};
float minLife{0.5f};
float maxLife{1.5f};
float minSize{0.05f};
float maxSize{0.15f};
float drag{1.0f};
float gravity{0.0f}; // Positive pulls down -Y in local space
glm::vec4 color{1.0f, 0.5f, 0.1f, 1.0f};
// Soft particles (fade near opaque geometry)
float softDepthDistance{0.15f};
// Flipbook animation (atlas layout)
uint32_t flipbookCols{16};
uint32_t flipbookRows{4};
float flipbookFps{30.0f};
float flipbookIntensity{1.0f};
// Noise UV distortion
float noiseScale{6.0f};
float noiseStrength{0.05f};
glm::vec2 noiseScroll{0.0f, 0.0f};
};
```
### Usage Example
```cpp
GameAPI::Engine api(&engine);
// Create fire particle system
uint32_t fireId = api.create_particle_system(4096);
GameAPI::ParticleSystem fire;
fire.id = fireId;
fire.particleCount = 4096;
fire.enabled = true;
fire.blendMode = GameAPI::ParticleBlendMode::Additive;
fire.flipbookTexture = "vfx/flame.ktx2";
fire.noiseTexture = "vfx/simplex.ktx2";
fire.params.emitterPosLocal = glm::vec3(0.0f, 0.0f, -5.0f);
fire.params.spawnRadius = 0.5f;
fire.params.emitterDirLocal = glm::vec3(0.0f, 1.0f, 0.0f);
fire.params.coneAngleDegrees = 15.0f;
fire.params.minSpeed = 2.0f;
fire.params.maxSpeed = 4.0f;
fire.params.minLife = 0.8f;
fire.params.maxLife = 1.5f;
fire.params.minSize = 0.3f;
fire.params.maxSize = 0.6f;
fire.params.gravity = -2.0f; // Upward buoyancy
fire.params.color = glm::vec4(1.0f, 0.7f, 0.3f, 1.0f);
fire.params.flipbookCols = 16;
fire.params.flipbookRows = 4;
fire.params.flipbookFps = 24.0f;
api.set_particle_system(fireId, fire);
// Later: move emitter to follow player
fire.params.emitterPosLocal = player_position;
api.set_particle_system(fireId, fire);
// Reset particles (trigger burst)
fire.reset = true;
api.set_particle_system(fireId, fire);
```
---
## Debug Drawing
Header: `src/core/game_api.h`
Runtime debug visualization for primitives (lines, spheres, boxes, etc.) with optional depth testing and duration.
### Settings API
```cpp
// Enable/disable debug drawing system
void set_debug_draw_enabled(bool enabled);
bool get_debug_draw_enabled() const;
// Control which debug layers are visible (bitmask)
void set_debug_layer_mask(uint32_t mask);
uint32_t get_debug_layer_mask() const;
// Show/hide depth-tested primitives
void set_debug_show_depth_tested(bool show);
bool get_debug_show_depth_tested() const;
// Show/hide overlay (always-on-top) primitives
void set_debug_show_overlay(bool show);
bool get_debug_show_overlay() const;
// Set tessellation quality (segments for circles/spheres)
void set_debug_segments(int segments);
int get_debug_segments() const;
// Clear all debug draw commands
void debug_draw_clear();
```
### Drawing API
All drawing functions support both single and double-precision variants:
```cpp
// Line
void debug_draw_line(const glm::vec3& a, const glm::vec3& b,
const glm::vec4& color = glm::vec4(1.0f),
float duration_seconds = 0.0f,
bool depth_tested = true);
void debug_draw_line(const glm::dvec3& a, const glm::dvec3& b, ...);
// Ray (origin + direction + length)
void debug_draw_ray(const glm::vec3& origin, const glm::vec3& direction, float length,
const glm::vec4& color = glm::vec4(1.0f),
float duration_seconds = 0.0f,
bool depth_tested = true);
void debug_draw_ray(const glm::dvec3& origin, const glm::dvec3& direction, double length, ...);
// AABB (axis-aligned bounding box)
void debug_draw_aabb(const glm::vec3& center, const glm::vec3& half_extents,
const glm::vec4& color = glm::vec4(1.0f),
float duration_seconds = 0.0f,
bool depth_tested = true);
void debug_draw_aabb(const glm::dvec3& center, const glm::vec3& half_extents, ...);
// Sphere
void debug_draw_sphere(const glm::vec3& center, float radius,
const glm::vec4& color = glm::vec4(1.0f),
float duration_seconds = 0.0f,
bool depth_tested = true);
void debug_draw_sphere(const glm::dvec3& center, float radius, ...);
// Capsule (line segment + radius)
void debug_draw_capsule(const glm::vec3& p0, const glm::vec3& p1, float radius,
const glm::vec4& color = glm::vec4(1.0f),
float duration_seconds = 0.0f,
bool depth_tested = true);
void debug_draw_capsule(const glm::dvec3& p0, const glm::dvec3& p1, float radius, ...);
// Circle (center + normal + radius)
void debug_draw_circle(const glm::vec3& center, const glm::vec3& normal, float radius,
const glm::vec4& color = glm::vec4(1.0f),
float duration_seconds = 0.0f,
bool depth_tested = true);
void debug_draw_circle(const glm::dvec3& center, const glm::dvec3& normal, float radius, ...);
// Cone (apex + direction + length + angle)
void debug_draw_cone(const glm::vec3& apex, const glm::vec3& direction,
float length, float angle_degrees,
const glm::vec4& color = glm::vec4(1.0f),
float duration_seconds = 0.0f,
bool depth_tested = true);
void debug_draw_cone(const glm::dvec3& apex, const glm::dvec3& direction,
float length, float angle_degrees, ...);
```
### Usage Example
```cpp
GameAPI::Engine api(&engine);
// Enable debug drawing
api.set_debug_draw_enabled(true);
api.set_debug_segments(32); // Smooth circles/spheres
// Visualize player bounds (persistent, depth-tested)
api.debug_draw_aabb(player_pos, glm::vec3(0.5f, 1.0f, 0.5f),
glm::vec4(0.0f, 1.0f, 0.0f, 1.0f),
0.0f, // Duration 0 = single frame
true); // Depth tested
// Visualize raycast (red ray, always on top, 2 seconds)
api.debug_draw_ray(ray_origin, ray_dir, 100.0f,
glm::vec4(1.0f, 0.0f, 0.0f, 1.0f),
2.0f, // Show for 2 seconds
false); // Always on top
// Visualize trigger volume (transparent sphere)
api.debug_draw_sphere(trigger_pos, trigger_radius,
glm::vec4(1.0f, 1.0f, 0.0f, 0.3f), // Yellow, 30% alpha
0.0f,
true);
// Visualize spot light cone
api.debug_draw_cone(light_pos, light_dir, light_radius, light_angle_deg,
glm::vec4(1.0f, 0.9f, 0.7f, 0.5f),
0.0f,
true);
// One-shot clear (useful for clearing persistent debug viz)
api.debug_draw_clear();
```
**Notes:**
- `duration_seconds = 0.0f`: Draw for a single frame (re-submit each frame for persistent viz).
- `duration_seconds > 0.0f`: Draw for N seconds, then automatically expire.
- `depth_tested = true`: Primitive is occluded by scene geometry.
- `depth_tested = false`: Always on top (overlay mode).
- All primitives support alpha blending via the color's alpha channel.