Files
QuaternionEngine/docs/asset_manager.md

274 lines
13 KiB
Markdown
Raw 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.
## Asset Manager
Centralized asset path resolution, glTF loading, and runtime mesh creation (including simple materials and primitives). Avoids scattered relative paths and duplicates by resolving roots at runtime and caching results.
### Path Resolution
- Environment root: Honors `VKG_ASSET_ROOT` (expected to contain `assets/` and/or `shaders/`).
- Upward search: If unset, searches upward from the current directory for folders named `assets` and `shaders`.
- Fallbacks: Tries `./assets`, `../assets` and `./shaders`, `../shaders`.
- Methods: `shaderPath(name)`, `assetPath(name)`, and `modelPath(name)` (alias of `assetPath`). Relative or absolute input is returned if already valid; otherwise resolution is attempted as above.
Access the manager anywhere via `EngineContext`:
```c++
auto *assets = context->getAssets();
auto spv = assets->shaderPath("mesh.vert.spv");
auto chairPath = assets->modelPath("models/chair.glb");
```
### API Summary
- Paths
- `std::string shaderPath(std::string_view)`
- `std::string assetPath(std::string_view)` / `modelPath(std::string_view)`
- `const AssetPaths& paths() const` / `void setPaths(const AssetPaths &p)` — get/set asset paths
- glTF
- `std::optional<std::shared_ptr<LoadedGLTF>> loadGLTF(std::string_view nameOrPath)` — cached by canonical absolute path
- `std::optional<std::shared_ptr<LoadedGLTF>> loadGLTF(std::string_view nameOrPath, const GLTFLoadCallbacks *cb)` — with custom callbacks
- `size_t prefetchGLTFTextures(std::string_view nameOrPath)` — schedule texture loads ahead of time
- `GLTFTexturePrefetchResult prefetchGLTFTexturesWithHandles(std::string_view nameOrPath)` — returns handles for tracking
- Meshes
- `std::shared_ptr<MeshAsset> createMesh(const MeshCreateInfo &info)`
- `std::shared_ptr<MeshAsset> createMesh(const std::string &name, std::span<Vertex> v, std::span<uint32_t> i, std::shared_ptr<GLTFMaterial> material = {}, bool build_bvh = true)`
- `std::shared_ptr<MeshAsset> getMesh(const std::string &name) const`
- `std::shared_ptr<MeshAsset> getPrimitive(std::string_view name) const` — returns existing default primitives if created
- `bool removeMesh(const std::string &name)`
- `bool removeMeshDeferred(const std::string &name, DeletionQueue &dq)` — deferred cleanup via deletion queue
- `void cleanup()` — releases meshes, material buffers, and any images owned by the manager
- Materials
- `std::shared_ptr<GLTFMaterial> createMaterialFromConstants(const std::string &name, const GLTFMetallic_Roughness::MaterialConstants &constants, MaterialPass pass = MaterialPass::MainColor)` — create PBR material from constants using engine default textures
### Mesh Creation Model
Use either the convenience descriptor (`MeshCreateInfo`) or the direct overload with vertex/index spans.
```c++
struct AssetManager::MaterialOptions {
std::string albedoPath; // resolved through AssetManager
std::string metalRoughPath; // resolved through AssetManager
std::string normalPath; // resolved through AssetManager (tangent-space normal)
std::string occlusionPath; // resolved through AssetManager (ambient occlusion)
std::string emissivePath; // resolved through AssetManager (emissive/glow)
bool albedoSRGB = true; // VK_FORMAT_R8G8B8A8_SRGB when true
bool metalRoughSRGB = false; // VK_FORMAT_R8G8B8A8_UNORM when false
bool normalSRGB = false; // normal maps should be UNORM
bool occlusionSRGB = false; // occlusion should be UNORM
bool emissiveSRGB = true; // emissive is typically sRGB
GLTFMetallic_Roughness::MaterialConstants constants{}; // extra[0].x as normalScale
MaterialPass pass = MaterialPass::MainColor; // or Transparent
};
struct AssetManager::MeshGeometryDesc {
enum class Type { Provided, Cube, Sphere, Plane, Capsule };
Type type = Type::Provided;
std::span<Vertex> vertices{}; // when Provided
std::span<uint32_t> indices{}; // when Provided
int sectors = 16; // for Sphere
int stacks = 16; // for Sphere
};
struct AssetManager::MeshMaterialDesc {
enum class Kind { Default, Textured };
Kind kind = Kind::Default;
MaterialOptions options{}; // used when Textured
};
struct AssetManager::MeshCreateInfo {
std::string name; // cache key; reused if already created
MeshGeometryDesc geometry; // Provided / Cube / Sphere / Plane / Capsule
MeshMaterialDesc material; // Default or Textured
std::optional<BoundsType> boundsType; // optional override for collision/picking bounds
};
```
Behavior and lifetime:
- Default material: If no material is given, a white material is created (2× white textures, per-mesh UBO with sane defaults).
- Textured material: When `MeshMaterialDesc::Textured`, images are loaded via `stb_image` and uploaded; per-mesh UBO is allocated and filled from `constants`.
- Ownership: Material buffers and any images created by the AssetManager are tracked and destroyed on `removeMesh(name)` or `cleanup()`.
- Caching: Meshes are cached by `name`. Re-creating with the same name returns the existing mesh (no new uploads).
### Examples
Create a simple plane and render it (default material):
```c++
std::vector<Vertex> v = {
{{-0.5f, 0.0f, -0.5f}, 0.0f, {0,1,0}, 0.0f, {1,1,1,1}},
{{ 0.5f, 0.0f, -0.5f}, 1.0f, {0,1,0}, 0.0f, {1,1,1,1}},
{{-0.5f, 0.0f, 0.5f}, 0.0f, {0,1,0}, 1.0f, {1,1,1,1}},
{{ 0.5f, 0.0f, 0.5f}, 1.0f, {0,1,0}, 1.0f, {1,1,1,1}},
};
std::vector<uint32_t> i = { 0,1,2, 2,1,3 };
auto plane = ctx->getAssets()->createMesh("plane", v, i); // default white material
glm::mat4 xform = glm::scale(glm::mat4(1.f), glm::vec3(10.f, 1.f, 10.f));
ctx->scene->addMeshInstance("ground", plane, xform);
```
Generate primitives via `MeshCreateInfo`:
```c++
AssetManager::MeshCreateInfo ci{};
ci.name = "cubeA";
ci.geometry.type = AssetManager::MeshGeometryDesc::Type::Cube;
ci.material.kind = AssetManager::MeshMaterialDesc::Kind::Default;
auto cube = ctx->getAssets()->createMesh(ci);
ctx->scene->addMeshInstance("cube.instance", cube,
glm::translate(glm::mat4(1.f), glm::vec3(-2.f, 0.f, -2.f)));
AssetManager::MeshCreateInfo si{};
si.name = "sphere48x24";
si.geometry.type = AssetManager::MeshGeometryDesc::Type::Sphere;
si.geometry.sectors = 48; si.geometry.stacks = 24;
si.material.kind = AssetManager::MeshMaterialDesc::Kind::Default;
auto sphere = ctx->getAssets()->createMesh(si);
ctx->scene->addMeshInstance("sphere.instance", sphere,
glm::translate(glm::mat4(1.f), glm::vec3(2.f, 0.f, -2.f)));
// Plane primitive
AssetManager::MeshCreateInfo pi{};
pi.name = "groundPlane";
pi.geometry.type = AssetManager::MeshGeometryDesc::Type::Plane;
pi.material.kind = AssetManager::MeshMaterialDesc::Kind::Default;
auto plane = ctx->getAssets()->createMesh(pi);
// Capsule primitive
AssetManager::MeshCreateInfo capi{};
capi.name = "capsuleA";
capi.geometry.type = AssetManager::MeshGeometryDesc::Type::Capsule;
capi.material.kind = AssetManager::MeshMaterialDesc::Kind::Default;
auto capsule = ctx->getAssets()->createMesh(capi);
```
Textured primitive (albedo + metal-rough + normal + occlusion + emissive):
```c++
AssetManager::MeshCreateInfo ti{};
ti.name = "ground.textured";
// provide vertices/indices for a plane (see first example)
ti.geometry.type = AssetManager::MeshGeometryDesc::Type::Provided;
ti.geometry.vertices = std::span<Vertex>(v.data(), v.size());
ti.geometry.indices = std::span<uint32_t>(i.data(), i.size());
ti.material.kind = AssetManager::MeshMaterialDesc::Kind::Textured;
ti.material.options.albedoPath = "textures/ground_albedo.png"; // sRGB
ti.material.options.metalRoughPath = "textures/ground_mr.png"; // UNORM, G=roughness, B=metallic
ti.material.options.normalPath = "textures/ground_n.png"; // UNORM
ti.material.options.occlusionPath = "textures/ground_ao.png"; // UNORM (optional)
ti.material.options.emissivePath = "textures/ground_emit.png"; // sRGB (optional)
ti.material.options.constants.extra[0].x = 1.0f; // normalScale
// ti.material.options.pass = MaterialPass::Transparent; // optional
auto texturedPlane = ctx->getAssets()->createMesh(ti);
glm::mat4 tx = glm::scale(glm::mat4(1.f), glm::vec3(10.f, 1.f, 10.f));
ctx->scene->addMeshInstance("ground.textured", texturedPlane, tx);
```
Textured cube/sphere/plane/capsule via options is analogous — set `geometry.type` to `Cube`, `Sphere`, `Plane`, or `Capsule` and fill `material.options`.
Using custom material from constants:
```c++
// Create a material with custom PBR values (using engine default textures)
GLTFMetallic_Roughness::MaterialConstants constants{};
constants.colorFactors = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f); // red
constants.metal_rough_factors = glm::vec4(0.0f, 0.8f, 0.0f, 0.0f); // non-metallic, rough
auto redMaterial = ctx->getAssets()->createMaterialFromConstants(
"red_rough_material",
constants,
MaterialPass::MainColor
);
// Use with custom mesh
auto mesh = ctx->getAssets()->createMesh("custom_mesh", vertices, indices, redMaterial);
```
Runtime glTF spawning:
```c++
auto chair = ctx->getAssets()->loadGLTF("models/chair.glb");
if (chair)
{
glm::mat4 t = glm::translate(glm::mat4(1.f), glm::vec3(0.f, 0.f, -3.f));
ctx->scene->addGLTFInstance("chair01", *chair, t);
}
// Move / overwrite
ctx->scene->addGLTFInstance("chair01", *chair,
glm::translate(glm::mat4(1.f), glm::vec3(0.f, 0.5f, -3.f)));
// Remove
ctx->scene->removeGLTFInstance("chair01");
```
### Texture Prefetching
Queue texture loads for a glTF file ahead of time. This parses the glTF, builds TextureCache keys for referenced images (both external URIs and embedded images in buffers), and issues `TextureCache::request()` calls. Actual uploads happen via the normal per-frame pump.
```c++
// Simple version: returns number of textures scheduled
size_t count = ctx->getAssets()->prefetchGLTFTextures("models/heavy_asset.glb");
// Advanced version: returns handles for tracking progress
auto result = ctx->getAssets()->prefetchGLTFTexturesWithHandles("models/heavy_asset.glb");
fmt::println("Scheduled {} textures", result.scheduled);
// Use result.handles with TextureCache to monitor loading state
```
Texture prefetching is particularly useful when combined with `AsyncAssetLoader` for loading large models in the background.
### Async Asset Loading
The `AsyncAssetLoader` class provides asynchronous glTF loading with worker threads for CPU-bound tasks (file I/O, parsing, mesh/BVH building). GPU uploads are still deferred through ResourceManager and the Render Graph.
```c++
// Access via EngineContext
auto *loader = ctx->async_loader;
// Queue a model to load in the background
auto jobID = loader->load_gltf_async(
"spaceship_01", // scene instance name
"models/spaceship.glb", // model path (resolved via AssetManager)
glm::translate(glm::mat4(1.f), glm::vec3(0, 5, -10)), // transform
true // preload textures
);
// Check progress in your update loop
JobState state;
float progress;
std::string error;
if (loader->get_job_status(jobID, state, progress, &error)) {
if (state == JobState::Completed) {
fmt::println("Model loaded successfully!");
} else if (state == JobState::Failed) {
fmt::println("Failed to load: {}", error);
}
}
// Commit completed jobs to the scene (call once per frame)
loader->pump_main_thread(*ctx->scene);
// Alternative: use WorldVec3 for large-world coordinates
auto jobID2 = loader->load_gltf_async(
"distant_building",
"models/building.glb",
WorldVec3{1000000.0, 0.0, 500000.0}, // world position
glm::quat(1.0f, 0.0f, 0.0f, 0.0f), // rotation
glm::vec3(1.0f), // scale
false // don't preload textures
);
```
The `AsyncAssetLoader` integrates with `TextureCache` to track texture streaming progress. When `preload_textures` is true, the loader will schedule all model textures for loading and track their residency state.
### Notes
- Default primitives: The engine creates default Cube/Sphere/Plane/Capsule meshes via `AssetManager` and registers them as dynamic scene instances.
- Reuse by name: `createMesh("name", ...)` returns the cached mesh if it already exists. Use a unique name or call `removeMesh(name)` to replace.
- sRGB/UNORM: Albedo and emissive are sRGB by default, metal-rough/normal/occlusion are UNORM by default. Adjust via `MaterialOptions`.
- Hot reload: Shaders are resolved via `shaderPath()`; pipeline hot reload is handled by the pipeline manager, not the AssetManager.
- Normal maps: Supported. If `normalPath` is empty, a flat normal is used.
- Occlusion & Emissive: Supported via `occlusionPath` and `emissivePath` in `MaterialOptions`.
- Tangents: Loaded from glTF when present; otherwise generated. Enable MikkTSpace at configure time with `-DENABLE_MIKKTS=ON`.
- BVH building: Enabled by default for meshes (`build_bvh = true`). Required for picking and ray-tracing.
- Deferred cleanup: Use `removeMeshDeferred()` when destroying meshes during rendering to avoid destroying resources that are in-flight on the GPU.