Tutorial 110: Loading glTF 2.0 Models at Runtime
What you’ll learn
- Loading a
.gltfor.glbfile with onegetContentProperty().Load<Model>()call — no tooling step. - The order
ContentManagersearches (.xnb, then.cnb, then loose files), and why.xnbalways wins. - How one runtime model retains every mesh group and exposes multiple skins.
- How unsupported extensions, approximations and unrepresentable data are refused or reported.
- When to compile the model to
.cnbinstead.
Before you start — Tutorial 35: Loading 3D Models covers the Model/ModelMesh/ModelMeshPart hierarchy this tutorial produces.
CNA can load glTF 2.0 models at runtime, with no conversion step. Support is compiled into every build: there is no CMake option to switch on, and the loader — a vendored copy of cgltf — is always present. This is a departure from XNA, where every asset had to pass through the offline content pipeline first.
Loading a model
Put house.glb in your content root and load it like any other asset. Note the extension is omitted, exactly as in XNA:
// Content/models/house.glb on disk. Load<T> returns the Model by value;
// a failed load throws ContentLoadException, so there is no null to check.
Model house = getContentProperty().Load<Model>("models/house");
// Draw it with the standard XNA Model API
house.Draw(world, view, projection);
Both container forms work: JSON .gltf with external buffers, and binary .glb. External .bin buffers, base64-embedded buffers and images, and PNG/JPEG textures embedded in a buffer view are all handled. Only glTF version 2.0 is accepted — anything else is rejected outright rather than half-loaded.
How ContentManager finds the file
Load<Model>("models/house") tries candidates in a fixed order, and stops at the first hit:
| # | Candidate | Notes |
|---|---|---|
| 1 | models/house.xnb | Always wins. A leftover .xnb will shadow your .glb. |
| 2 | models/house.cnb | CNA’s compiled container, new in this snapshot (see CNB Format). Ranks just below .xnb and above everything it can be compiled from. (If you pass a name that itself ends in .cnb, that literal file is tried next.) |
| 3 | models/house | The literal path, if a file of exactly that name exists. |
| 4 | models/house.cnj | CNA's own JSON asset format. |
| 5 | .cnj, .gltf, .glb | Reader extensions, tried in that order. |
The first row is the one that surprises people. If you are porting an XNA title and its old .xnb models are still in the content tree, dropping a .glb beside them changes nothing — the .xnb keeps winning. Delete it, or load under a different asset name. The same rule now applies to a stale .cnb built by cna-content from an older copy of the model: rebuild it or delete it, or the loose .glb beside it is never read.
Runtime shape and the remaining scale limit
Runtime loading imports every mesh group into one Model (as alpha.1 did; re-verified at this snapshot), but still hardcodes a unit scale of 1.0.
The direct path keeps skinned and unskinned groups together. Model::getSkinsEXTProperty() maps every independent skin to its exact mesh set and palette; the first skin also remains in Model::Tag for compatibility. The scale boundary matters when a file was not authored in glTF's metre units.
- Several independent objects or skins — all arrive in one runtime
Model; useSkinsEXTinstead of assumingTagdescribes every skin. - A file authored in centimetres — at scale 1.0 the model arrives a hundred times too large.
Use the offline converter when you need a scale multiplier or separately packaged assets per mesh group:
# cna_tool_gltf_to_cnj <input.gltf|.glb> <outDir> <baseName> [unitScale]
cna_tool_gltf_to_cnj assets/city.glb Content/models city 0.01
The tool is built unconditionally with the project, so it is already in your build tree. See Tutorial 111 for the full workflow and what the sidecar files contain. Two build-time alternatives also exist in this snapshot: cna_tool_gltf_to_cnb --unit-scale <f> writes a compiled .cnb with a scale applied, and cna-content build compiles a glTF to .cnb (or .xnb) with no unit-scale option. See Content Pipeline and Tutorial 145.
What is supported
| Feature | Status |
|---|---|
Node hierarchy → ModelBone with composed transforms | Yes |
| Skeletal animation (joints, inverse bind matrices) | Yes |
| Morph targets | Yes |
| LINEAR, STEP and CUBICSPLINE interpolation | Yes |
| PBR metallic-roughness materials | Yes |
| Draco mesh compression | Build-dependent — CNA_ENABLE_DRACO (on by default, off under Emscripten) |
| Rigid (unskinned) node animation | Yes — ModelAnimationsEXT |
| Factor-only and vertex-coloured PBR materials | Yes — keep PBR factors and material state |
TRIANGLE_STRIP, TRIANGLE_FAN | Converted to a winding-correct triangle list |
LINE_LOOP | Converted to a line strip that carries the closing segment |
LINES, LINE_STRIP, POINTS | Imported with matching CNA primitive types |
Three boundaries worth knowing
Rigid animation is retained only when Model::Tag is available for it. An unskinned model exposes scene-node clips as ModelAnimationsEXT, and ApplyClipToBonesEXT() poses the corresponding ModelBone hierarchy. In a file that mixes skins with additional rigid tracks, Tag is already occupied by the compatibility SkinningData pointer; those extra rigid tracks are dropped with a named import diagnostic rather than silently.
Two authored UV sets are the representable maximum. Each PBR map selects independently between the first two distinct sampled TEXCOORD sets and keeps its own KHR_texture_transform. A map asking for a third distinct set falls back to packed channel 0 and is named in the import report. See Tutorial 114.
extensionsRequired is enforced. CNA uses one source-owned extension registry for validation and diagnostics. A required extension it does not claim is refused by name; an unknown optional extensionsUsed entry becomes a warning. Some known extensions are deliberately approximated or only partly implemented, and their exact loss is recorded.
Read the public report. Model::getGltfImportReportEXTProperty() is populated by both direct glTF loading and converter-produced Model .cnj. Its stable diagnostic codes distinguish exact information, generated data, approximations, dropped data and unsupported optional features. This makes the importer substantially more robust, but not universally lossless; inspect the report and verify important assets on the renderer you ship. Note that the report is filled by the direct and .cnj paths only: a model loaded from a compiled .cnb or .xnb carries an empty report (and no imported cameras), so run your checks on the source file at import time.
Where to go next
- Tutorial 111: The CNJ format and the gltf_to_cnj tool
- Tutorial 112: Skeletal animation from glTF
- Tutorial 145: Build content with cna-content — compile a glTF to
.cnbahead of time - Tutorial 114: PBR materials
- Model Loading reference
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- The glTF import core: parser boundary, scene graph and extraction — How CNA's shared glTF import core parses, validates, flattens the scene, groups meshes, extracts materials and reports losses, and what its runtime and offline front ends share.