Batching glTF Primitives into a Single Draw Call
Mesh batching optimizes real-time 3D rendering by consolidating multiple static glTF primitives into unified vertex and index buffers, drastically reducing CPU-to-GPU communication overhead. This article explains the technical mechanics of mesh batching in glTF environments, detailing how spatial transforms, attribute buffers, index arrays, and material states are combined to execute the rendering of multiple static geometries using a single GPU draw call.
1. Baking World-Space Transforms
In a standard glTF hierarchy, individual static meshes exist under specific nodes that define their local translation, rotation, and scale (TRS). To combine these distinct objects into one coordinate space, their local transforms must be pre-multiplied. The batching process calculates the world matrix for each node and applies it directly to the vertex data:
- Positions: Multiplied by the node's full \(4 \times 4\) world transform matrix.
- Normals and Tangents: Multiplied by the inverse-transpose of the world matrix to maintain accurate lighting directions under non-uniform scaling.
By baking transforms directly into the vertex attributes, the GPU no longer needs separate uniform matrices or per-instance transformation pipelines during the draw command.
2. Merging Vertex Attribute Buffers
A glTF primitive references vertex data through
accessors that point to bufferViews. Batching
requires merging identical attributes—such as POSITION,
NORMAL, TEXCOORD_0, and
TANGENT—into unified memory allocations:
- Concatenation: The vertex arrays of identical types across all candidate primitives are appended together into a single contiguous array (or interleaved into one structured vertex format).
- Format Normalization: All source primitives must
share the same data types (e.g.,
FLOATfor positions, normalizedSHORTorFLOATfor texture coordinates). If data layouts differ, they must be converted to a common format prior to concatenation.
3. Re-indexing and Offsetting Indices
glTF primitives generally define their geometry using indexed
triangle lists via an indices accessor. Simply
concatenating index buffers would cause subsequent meshes to erroneously
point to the vertices of the first mesh.
To stitch the index buffers together properly:
- The batching pipeline reads the index array of the first primitive and writes it to the new global index buffer.
- For each subsequent primitive, an offset equal to the cumulative number of vertices processed so far is added to each index value.
- The adjusted index values are appended sequentially.
This ensures every triangle points to its corresponding transformed vertices within the newly merged vertex buffer.
4. Harmonizing Material and Pipeline State
A single GPU draw call requires a uniform render state, including shaders, blending modes, rasterization states, and bound texture descriptors. Multiple glTF primitives can only be combined into a single draw call if they fulfill one of the following conditions:
- Identical Material References: The primitives
already reference the exact same glTF
materialindex. - Texture Atlasing: Distinct textures from different materials are packed into a single texture atlas. The primitives' UV coordinates are then re-scaled and offset to map to their respective regions within the atlas.
- Texture Arrays or Bindless Textures: Textures are combined into a 2D texture array, and an additional vertex attribute (such as a material index or layer ID) is appended to each vertex to allow the fragment shader to sample the appropriate texture dynamically.
5. Executing the Unified Draw Command
Once the vertex buffers, index buffers, and material configurations
are harmonized, the original glTF node hierarchy for those static meshes
is bypassed. The graphics engine binds the single merged vertex buffer,
binds the combined index buffer, sets the shared pipeline state, and
issues a single indexed draw call (such as glDrawElements
in WebGL/OpenGL or drawIndexed in
WebGPU/DirectX/Vulkan).
This eliminates the CPU overhead of state changes, buffer bindings, and multiple draw API invocations, maximizing hardware utilization.