## 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> loadGLTF(std::string_view nameOrPath)` — cached by canonical absolute path - `std::optional> 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 createMesh(const MeshCreateInfo &info)` - `std::shared_ptr createMesh(const std::string &name, std::span v, std::span i, std::shared_ptr material = {}, bool build_bvh = true)` - `std::shared_ptr getMesh(const std::string &name) const` - `std::shared_ptr 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 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 vertices{}; // when Provided std::span 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; // 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 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 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(v.data(), v.size()); ti.geometry.indices = std::span(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.