13 KiB
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 containassets/and/orshaders/). - Upward search: If unset, searches upward from the current directory for folders named
assetsandshaders. - Fallbacks: Tries
./assets,../assetsand./shaders,../shaders. - Methods:
shaderPath(name),assetPath(name), andmodelPath(name)(alias ofassetPath). Relative or absolute input is returned if already valid; otherwise resolution is attempted as above.
Access the manager anywhere via EngineContext:
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 pathstd::optional<std::shared_ptr<LoadedGLTF>> loadGLTF(std::string_view nameOrPath, const GLTFLoadCallbacks *cb)— with custom callbackssize_t prefetchGLTFTextures(std::string_view nameOrPath)— schedule texture loads ahead of timeGLTFTexturePrefetchResult 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) conststd::shared_ptr<MeshAsset> getPrimitive(std::string_view name) const— returns existing default primitives if createdbool removeMesh(const std::string &name)bool removeMeshDeferred(const std::string &name, DeletionQueue &dq)— deferred cleanup via deletion queuevoid 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.
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 viastb_imageand uploaded; per-mesh UBO is allocated and filled fromconstants. - Ownership: Material buffers and any images created by the AssetManager are tracked and destroyed on
removeMesh(name)orcleanup(). - 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):
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:
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):
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:
// 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:
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.
// 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.
// 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
AssetManagerand registers them as dynamic scene instances. - Reuse by name:
createMesh("name", ...)returns the cached mesh if it already exists. Use a unique name or callremoveMesh(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
normalPathis empty, a flat normal is used. - Occlusion & Emissive: Supported via
occlusionPathandemissivePathinMaterialOptions. - 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.