Tutorial 110: Loading glTF 2.0 Models at Runtime

CNA Tutorials  ·  3D Content

What you’ll learn

  • Loading a .gltf or .glb file with one Content.Load<Model>() call — no tooling step.
  • The order ContentManager searches, and why .xnb always 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 startTutorial 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:

#CandidateNotes
1models/house.xnbAlways wins. A leftover .xnb will shadow your .glb.
2models/houseThe literal path, if a file of exactly that name exists.
3models/house.cnjCNA's own JSON asset format.
4.cnj, .gltf, .glbReader 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 .glb will 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

FeatureStatus
Node hierarchy → ModelBone with composed transformsYes
Skeletal animation (joints, inverse bind matrices)Yes
Morph targetsYes
LINEAR, STEP and CUBICSPLINE interpolationYes
PBR metallic-roughness materialsYes
Draco mesh compressionOptional — needs libdraco at build time
Rigid (unskinned) node animationSilently dropped
Factor-only PBR materials (no texture maps)Material properties lost
TRIANGLE_STRIP, TRIANGLE_FAN, LINES, POINTSRefused 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.

Where to go next