1508 lines
47 KiB
Markdown
1508 lines
47 KiB
Markdown
## Game‑Facing 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 per‑frame state.
|
||
- `docs/RenderGraph.md` – render‑graph 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` (High‑Level Game Wrapper)
|
||
|
||
Header: `src/core/game_api.h`
|
||
Implementation: `src/core/game_api.cpp`
|
||
|
||
`GameAPI::Engine` is a thin, game‑friendly 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.
|
||
- Post‑processing (tonemap, bloom, FXAA).
|
||
- Camera control.
|
||
- Lighting (directional, point, spot).
|
||
- Volumetrics (clouds, smoke, flame).
|
||
- Particle systems.
|
||
- Debug drawing.
|
||
- Picking and render‑graph 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); // non‑owning
|
||
```
|
||
|
||
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 per‑frame texture budget using VMA heap info and constants in `src/core/config.h`:
|
||
|
||
- `kTextureBudgetFraction` – fraction of total device‑local 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 per‑scene tuning (e.g. reducing upload bandwidth on low‑end 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`).
|
||
|
||
High‑level game‑side controls:
|
||
|
||
- `void set_shadows_enabled(bool enabled);`
|
||
- `void set_shadow_mode(ShadowMode mode);`
|
||
- `ClipmapOnly` – cascaded shadow maps only.
|
||
- `ClipmapPlusRT` – cascades + optional ray‑query assist.
|
||
- `RTOnly` – ray‑traced 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 mid‑range 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, G‑buffers, 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 device‑local 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 re‑test.
|
||
|
||
The following quality‑related 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 Post‑Processing
|
||
|
||
Game‑side 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 quaternion‑based `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.3–1.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).
|
||
- ID‑buffer based picking (async, 1‑frame latency, robust for dense scenes).
|
||
|
||
Render‑graph 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 ID‑buffer picking depending on `_useIdBufferPicking`.
|
||
- Typical usage:
|
||
- On mouse‑up 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);
|
||
```
|
||
|
||
### Per‑Instance Node / Joint Control (Non‑Skinned)
|
||
|
||
For rigid models and simple "joints" (e.g. flaps, doors, turrets), you can apply local‑space 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 game‑driven offsets on top for per‑instance 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.
|