Tutorial 110: Loading glTF 2.0 Models at Runtime
What you’ll learn
- Loading a
.gltfor.glbfile with oneContent.Load<Model>()call — no tooling step. - The order
ContentManagersearches, and why.xnbalways wins. - What the runtime importer supports, and the two limits it deliberately keeps.
- Which glTF features are silently dropped, so you can spot them before shipping.
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
Model* house = Content->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 | The literal path, if a file of exactly that name exists. |
| 3 | models/house.cnj | CNA's own JSON asset format. |
| 4 | .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.
Two deliberate limits on the runtime path
The runtime importer reads only the first mesh group in the file, and hardcodes a unit scale of 1.0.
Neither is a bug; they are the price of a zero-configuration path. They matter in two common cases:
- A file containing several independent objects — three characters exported into one
.glbwill yield only the first. - A file authored in centimetres — Blender and Maya scenes frequently are. At scale 1.0 your model arrives a hundred times too large.
For either case, use the offline converter, which takes an explicit unit scale and emits one asset 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.
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 | Optional — needs libdraco at build time |
| Rigid (unskinned) node animation | Silently dropped |
| Factor-only PBR materials (no texture maps) | Material properties lost |
TRIANGLE_STRIP, TRIANGLE_FAN, LINES, POINTS | Refused with a diagnostic |
Three pitfalls worth knowing
Rigid animation disappears without warning. Animation channels are resolved against the skin's joint set, so a channel that targets an ordinary mesh node — a rotating door, an orbiting moon, a spinning fan — is discarded and no clip is emitted. There is no diagnostic. If an animation you exported simply is not there, this is the first thing to check.
Factor-only materials lose everything. A material that sets base colour, metallic and roughness as plain numbers, with no texture maps, is downgraded to a BasicEffect, and its base colour factor, alpha mode, alpha cutoff and double-sidedness do not survive. Assign even a small texture and the PBR path is taken instead. See Tutorial 114.
Required extensions are not checked. CNA does not inspect extensionsRequired. A file that depends on an extension CNA does not implement will load and render incorrectly rather than refuse. Four KHR_ extensions are recognised; treat anything beyond them as unverified, and eyeball the result.
glTF correctness is an active campaign, not a finished feature. The CNA project is explicit that its own glTF viewer still displays some assets incorrectly. Runtime loading is genuinely useful today — but check each asset visually rather than assuming fidelity.