Scene Loading (glTF)

The glTF scene loader is Himalaya's bridge between industry-standard 3D assets and the rendering pipeline. It parses glTF 2.0 files using the fastgltf library, transforms the hierarchical node structure into GPU-ready resources, and populates the scene data structures that the renderer consumes each frame. This page covers the loading pipeline architecture, resource transformation, and the relationship between glTF concepts and Himalaya's internal representations.

Sources: scene_loader.h, scene_loader.cpp

Architecture Overview

The SceneLoader class orchestrates a four-stage pipeline that converts glTF assets into renderable scene data. Each stage handles a distinct aspect of the transformation: parsing, mesh extraction, material processing, and scene graph flattening. The loader maintains ownership of all created GPU resources and provides a destroy() method for clean scene switching.

flowchart TD A[Load Request] --> B[Parse glTF with fastgltf] B --> C[Load Meshes
Vertex/Index Buffers] C --> D[Load Materials
Textures + Samplers] D --> E[Build Mesh Instances
Scene Graph Traversal] E --> F[Compute Scene Bounds] subgraph "Mesh Loading Stage" C1[Parse POSITION accessor] --> C2[Build Vertex Array] C2 --> C3[Generate Tangents
via MikkTSpace] C3 --> C4[Create GPU Buffers
with RT usage if enabled] end subgraph "Material Loading Stage" D1[Collect Unique
Texture References] --> D2[Hash + Cache Check] D2 --> D3[Parallel BC7 Compression
OpenMP] D3 --> D4[GPU Upload +
Bindless Registration] D4 --> D5[Build Material SSBO] end subgraph "Scene Graph Stage" E1[Traverse Nodes
fastgltf iterateSceneNodes] --> E2[Extract Directional Lights
KHR_lights_punctual] E2 --> E3[Create MeshInstances
with World Transforms] E3 --> E4[Transform AABBs
to World Space] end

Sources: scene_loader.cpp, scene_loader.h

The Loading Pipeline

Stage 1: Parsing with fastgltf

The loader uses fastgltf with the KHR_lights_punctual extension enabled to parse both the JSON structure and binary data. The parser loads external buffers and images automatically, handling both .gltf (with separate .bin files) and .glb (self-contained binary) formats. Error handling ensures that parsing failures result in a clean empty scene rather than partial state.

Sources: scene_loader.cpp

Stage 2: Mesh Loading

Each glTF primitive becomes an independent Mesh structure containing GPU vertex and index buffers. The loader processes vertex attributes systematically: POSITION is required by the glTF spec, while NORMAL, TEXCOORD_0, TANGENT, and TEXCOORD_1 are optional with sensible defaults. When tangents are missing but normals and UVs exist, the loader invokes MikkTSpace to generate proper tangent frames for normal mapping. For non-indexed primitives, the loader generates sequential indices automatically. The resulting buffers are created with ShaderDeviceAddress and AccelStructBuildInput usage when ray tracing is supported, enabling BLAS construction without resource recreation.

Sources: scene_loader.cpp, mesh.h

Stage 3: Material and Texture Processing

Material loading implements a sophisticated three-phase pipeline to maximize throughput and minimize redundant work. First, the loader collects unique (texture_index, role) pairs across all materials, ensuring that shared textures are processed only once. Second, it computes content hashes of the raw image bytes and checks the disk cache—cache hits skip decoding entirely. For cache misses, images are decoded and compressed in parallel using OpenMP, with BC7 compression format selected by role: Color data uses BC7_SRGB for gamma correctness, linear data (roughness, metallic, occlusion) uses BC7_UNORM, and normal maps use BC5_UNORM for optimal quality. Finally, compressed textures are uploaded to GPU and registered in the bindless descriptor array.

Sources: scene_loader.cpp, texture.h

Stage 4: Scene Graph Traversal

The loader uses fastgltf's iterateSceneNodes to traverse the scene graph, accumulating world transforms and creating MeshInstance structures. Each node with a mesh reference generates one MeshInstance per primitive, with the world transform matrix applied to both the instance and its local AABB. Directional lights from the KHR_lights_punctual extension are extracted during traversal, with their direction computed from the node's world transform (glTF lights point along -Z). The final scene bounds are computed as the union of all instance world AABBs, providing the renderer with spatial context for shadow distance calculations and camera positioning.

Sources: scene_loader.cpp, scene_data.h

Data Structure Mapping

The loader transforms glTF concepts into Himalaya's internal representations through well-defined conversion functions. Understanding these mappings is essential for debugging material behavior or extending the loader.

glTF Concept Himalaya Structure Key Mapping Details
mesh.primitives[] framework::Mesh One Mesh per primitive; group_id = glTF mesh index for BLAS grouping
material framework::MaterialInstance + GPUMaterialData SSBO index matches material instance order; texture indices are bindless array indices
texture + sampler rhi::ImageHandle + rhi::SamplerHandle Sampler deduplicated by glTF sampler index; texture deduplicated by (index, role)
node.transform MeshInstance.transform Column-major matrix converted to glm::mat4
KHR_lights_punctual framework::DirectionalLight Direction extracted from node transform; color and intensity passed through
alphaMode framework::AlphaMode Opaque=0, Mask=1, Blend=2; determines pass routing

Sources: scene_loader.cpp, material_system.h

Sampler Conversion

glTF samplers map directly to Vulkan samplers with careful handling of the minification filter modes. The conversion respects glTF defaults (linear filtering, repeat wrapping) and properly encodes the mipmapping configuration. When no mipmapping is specified (Nearest or Linear without MipMap suffix), max_lod is clamped to 0 to disable mip sampling. Anisotropic filtering is applied uniformly based on the RHI's reported maximum.

Sources: scene_loader.cpp

Resource Ownership and Cleanup

The SceneLoader maintains ownership of all GPU resources it creates, storing handles in internal vectors for deterministic cleanup. The destroy() method unregisters bindless textures before destroying images (preventing descriptor leaks), then destroys samplers and buffers in dependency order. This design supports scene switching: the application can call destroy() followed by load() with a new path, and the renderer will seamlessly transition to the new scene using the same material system and default resources.

Sources: scene_loader.cpp, scene_loader.h

Integration with the Application

Scene loading occurs during Application::init() within an immediate command buffer scope. The loader receives references to the resource manager, descriptor manager, and material system, along with the renderer's default textures and sampler for fallback resolution. After loading, the application queries scene_bounds() to configure shadow distances and camera positioning appropriately for the scene's spatial extent.

Sources: application.cpp