The asset loading system handles asynchronous loading of glTF models, meshes, materials, and textures from disk to GPU memory.
Architecture
Components
- AssetManager: Central coordinator, owns all sub-managers and pipelines
- GltfLoader: Parses glTF/GLB files using tinygltf
- Asset Pipelines: Multi-stage async loading (TextureAssetPipeline, ModelAssetPipeline)
- Asset Managers: Store and track loading state (MeshAssetManager, MaterialAssetManager, TextureAssetManager, ModelAssetManager)
- RenderingDataManager: Receives load notifications, creates Vulkan resources, uploads to GPU
Asset Types
| Type | Class | Storage |
| Mesh geometry | MeshAsset | ECS component Ecs::MeshPrimitiveData |
| Material data | MaterialAsset | ECS component (type-specific, e.g., DiffuseShaderMaterialData) |
| Texture image | TextureAsset | ECS component Ecs::ImageData |
| glTF file | ModelAsset | Bundles meshes/materials/textures from one file |
All asset classes inherit from Asset::AssetBase, which provides loading state tracking (UNLOADED, REQUESTED_LOAD, LOADING, LOADED).
Loading Flow
1. Request Asset
assetManager->loadEcsModel("assets/models/scene.gltf");
Creates a ModelAsset entry and submits to ModelAssetPipeline.
2. Pipeline Processing
The ModelAssetPipeline runs 4 stages per frame during Engine::processResourceLoadingPipelines():
assetManager->getMeshAssetPipeline().tick(true);
Stage 1: submitLoadModel
- Submits async tinygltf parse task to thread pool
- Returns std::future<tinygltf::Model>
Stage 2: processModel
- Checks if future is ready (non-blocking)
- Extracts meshes, materials, textures from glTF
- Creates MeshAsset, MaterialAsset, TextureAsset entries
- Submits texture paths to TextureAssetPipeline
- Pre-registers texture handles in TextureHandleRegistry
Stage 3: processMaterial
- Parses material extensions (VulkanSchnee custom data)
- Creates ECS material components with shader parameters
- Calls RenderingDataManager::onMaterialLoaded()
Stage 4: processMeshData
- Extracts vertex/index data from glTF buffers
- Generates meshlets using meshoptimizer
- Creates ECS mesh components
- Calls RenderingDataManager::onMeshLoaded()
3. Texture Loading
Textures follow a separate pipeline (TextureAssetPipeline):
For EXR files (lightmaps):
- requestExrHeaders() - Load header metadata
- loadImageData() - Load pixel data
- retrieveImageData() - Store in ECS, call onTextureLoaded()
For glTF images (PNG/JPG):
- Already loaded during processModel() (tinygltf loads eagerly)
- Extracted and stored in TextureAsset with image data
4. GPU Upload
RenderingDataManager hooks receive loaded assets:
void RenderingDataManager::onTextureLoaded(TextureAsset* asset, const std::filesystem::path& path)
{
Texture texture(context, renderer);
texture.createFromImage(image);
assetManager->registerTexture(path, texture);
renderer->getRenderingDataManager()->queueTextureForUpload(texturePtr);
textureHandleRegistry->updateHandle(path, descriptorIndex);
}
Similar flow for meshes:
void RenderingDataManager::onMeshLoaded(MeshAsset* asset)
{
}
Asset Managers
All managers use Asset::AssetManager<Key, AssetClass> template:
class TextureAssetManager :
public Asset::AssetManager<std::filesystem::path, TextureAsset> {};
A manager which is used to look up existing assets and their loading state.
Common Operations
Check if asset exists:
if (meshAssetManager->exists(assetPath)) { }
Get asset (returns std::optional):
auto asset = materialAssetManager->getAsset(path);
if (asset.has_value()) {
MaterialAsset* mat = asset.value();
}
Check loading state:
CpuLoadingState
State for assets in the asset loading process.
GltfLoader
Parses glTF files and extracts structured data.
Loading Models
GltfLoader loader(&threadPool);
std::optional<tinygltf::Model> model = loader.loadModel("scene.gltf");
Metadata-Only Load
Fast parse for node transforms without loading buffers:
tinygltf::Model metadata = loader.loadMetadataOnly("scene.gltf");
Extracting Data
GltfLoader::GltfMeshData meshData(model, mesh, node, transform);
std::vector<GltfLoader::GltfMeshPrimitiveData> primitives = meshData.getPrimitives();
for (auto& prim : primitives) {
std::vector<Vertex> vertices = prim.getVertices();
std::vector<uint32_t> indices = prim.getIndices();
GltfLoader::GltfMaterialData material = prim.getMaterialData();
}
Material Extensions
The engine uses custom glTF extensions:
VulkanSchnee Extension (extensions.VulkanSchnee):
GltfLoader::VulkanSchneeExtension ext(mesh.extensions);
if (ext.hasLightmapProperties()) {
std::filesystem::path lightmapPath = ext.lightmapProperties->lightmapPath;
uint32_t uvIndex = ext.lightmapProperties->lightmapUvIndex;
}
Material Extension (material.extensions.VulkanSchneeMaterial):
GltfLoader::VulkanSchneeMaterialExtension matExt(material.extensions);
PipelineNames shaderType = matExt.materialName;
std::map<std::string, ShaderParameter> params = matExt.shaderParameters;
Material Assets
Materials store shader type and texture references:
MaterialAsset* mat = materialAssetManager->getAsset(materialPath).value();
PipelineNames shaderType = mat->getType();
DiffuseShaderMaterialData& data = mat->getData<DiffuseShaderMaterialData>();
std::filesystem::path baseColor = mat->getBaseColorTexturePath();
std::filesystem::path normal = mat->getNormalTexturePath();
std::filesystem::path metallicRoughness = mat->getMetallicRoughnessTexturePath();
AlbedoTextureHandle albedo = mat->getBaseColorTextureHandle();
Texture Assets
TextureAsset* tex = textureAssetManager->getAsset(path).value();
entt::entity entity = tex->getEntity();
tinygltf::Image& image = imageData.
image;
Descriptor Index Lookup
Fast path for shader binding:
uint32_t index = assetManager->getTextureDescriptorIndex("textures/albedo.png");
if (index != 0xFFFFFFFF) {
}
Mesh Assets
MeshAsset* mesh = assetManager->getMeshAsset(assetPath);
if (mesh == nullptr) {
return;
}
Thread Safety
- Asset registration uses mutex-protected maps (AssetManager::registerTexture, registerMesh)
- Pipeline ticking runs on main thread, async tasks on worker threads
- ECS writes happen only on main thread after futures complete
Performance Notes
- Thread pools: 4 threads for asset loading, 10 for bounding sphere calculations
- Futures checked every frame with wait_for(0) (non-blocking)
- Meshlet generation is async and CPU-bound (uses meshoptimizer)
- Texture uploads use staging buffers for optimal transfer
Example: Full Load Cycle
assetManager->loadEcsModel("models/house.gltf");
Key Files