Model Loading & Rendering
Implementation status: In this snapshot, model loading and Draw are implemented. CNA accepts direct glTF 2.0, converter-generated .cnj, older loose model descriptors, .cnb models compiled by cna-content, and XNB models built by an XNA-compatible content pipeline (or by cna-content --format xnb). Skeletal and rigid animation, multiple skins, morph targets, material variants, cameras and import reports have CNAEXT carriers. The runtime glTF limits below were re-checked against this snapshot: all mesh groups are imported, extensionsRequired is enforced, and the unit scale is still fixed at 1.0. Implemented
Overview
The Model class lives in the Microsoft::Xna::Framework::Graphics namespace and represents a complete 3D mesh asset ready for rendering. A Model holds a hierarchy of ModelMesh objects; each mesh in turn contains one or more ModelMeshPart entries that map directly to a single draw call. Every part references a VertexBuffer, an IndexBuffer and an Effect (the parts of one model may share them), the same shape as XNA 4.0. The shape is XNA’s; the behaviour is not identical: CNA keeps deliberate C++ deviations, such as skipping a part whose effect is null, a copyable Model value over one shared graph and several XNA-internal setters exposed as CNAEXT members. The runtime graph and draw contract lists them.
Loading glTF 2.0 at runtime
The simplest path needs no tooling step at all: drop a .gltf or .glb into your content root and load it like any other asset. glTF support is compiled into every build — there is no CMake option to enable, and the loader (a vendored copy of cgltf) is always present.
// No conversion step. Content/models/house.glb on disk:
Model house = getContentProperty().Load<Model>("models/house");
Load<Model> resolves candidates in a fixed order: name.xnb always wins first, then name.cnb (CNA’s compiled container, new in this snapshot), then — only when you passed a name ending in .cnb — that literal file, then a literal name on disk, then name.cnj, then the reader extensions .cnj, .gltf and .glb. Both JSON .gltf and binary .glb are handled, including external .bin buffers, base64-embedded buffers and images, and PNG/JPEG textures embedded in a buffer view. Only glTF version 2.0 is accepted. Because .cnb outranks the loose sources it was compiled from, you can keep hero.glb next to a built hero.cnb and the compiled file wins.
The runtime path always uses a unit scale of 1.0. It now
imports every mesh group into one Model; every independent skin is exposed by
Model::getSkinsEXTProperty(), while the first skin also remains in
Tag for compatibility. Use the offline tool when an asset needs unit conversion
or when separate per-group files are more convenient. The importer validates
extensionsRequired against its source-owned extension registry and refuses a
file whose required semantics cannot be honoured.
Both this direct path and the converter-backed .cnj path attach structured
diagnostics to Model::getGltfImportReportEXTProperty(). Inspect its stable codes
when an optional extension, a third distinct UV set, or another deliberately reduced feature
needs your attention; do not rely on parsing warning text.
Runtime glTF: what is and is not imported
These limits were re-verified against the importer source at this snapshot. They are the ones people ask about most; the runtime path and the offline converter share one import core.
| Topic | Behaviour at this snapshot |
|---|---|
| Mesh groups | Every mesh group is imported into one Model. Groups are formed by skin plus one static group; each independent skin is exposed by getSkinsEXTProperty(), and the first skin also stays in Tag for compatibility. (One source comment above the importer still describes the old first-group-only behaviour; the code loops over every group.) |
| Unit scale | Fixed at 1.0 on the runtime path. Only the offline tools scale: cna_tool_gltf_to_cnj <in> <outDir> <base> [unitScale] and cna_tool_gltf_to_cnb --unit-scale <f>. cna-content has no unit-scale option. |
| Scene | Only nodes reachable from the selected scene become model content. |
| Rigid (node) animation | Retained through ModelAnimationsEXT for models without skins. It is dropped, with a named report diagnostic (GLTF-295) and a warning, in one case only: a file with skins whose extra rigid tracks no skin carries, because Model::Tag is already occupied by SkinningData. Skeletal animation supports LINEAR, STEP and CUBICSPLINE. |
| Materials | Factor-only metallic-roughness materials and vertex-coloured primitives stay on the PBR path; KHR_materials_pbrSpecularGlossiness is converted; a material with KHR_materials_unlit maps to an unlit effect. Clearcoat, sheen, transmission, volume and iridescence factor values are carried on the imported material, but no CNA effect renders them (the import report lists clearcoat, sheen and volume as parsed but ignored). |
| Topology | All seven primitive modes are handled; strips, fans and loops are converted. With Draco only TRIANGLES and TRIANGLE_STRIP are accepted. |
| Tangents | Legacy _TANGENT and _BINORMAL VEC3 attributes are imported as a tangent VEC4. |
| Cameras, variants, report | The direct path fills getCamerasEXTProperty(), the material-variant selection API (getMaterialVariantNamesEXTProperty() / setMaterialVariantEXTProperty(int)) and getGltfImportReportEXTProperty(). The .cnj path fills the report and variants; the .cnb and .xnb routes carry no cameras and return an empty report. |
extensionsRequired | Enforced. A file that requires an extension the importer does not claim is refused by name; an unknown entry in extensionsUsed only warns; EXT_meshopt_compression is refused outright; Draco is claimed only in a build with the Draco decoder (CNA_ENABLE_DRACO, on by default, off under Emscripten). |
The extension registry
One source-owned registry of 21 extension records decides what extensionsRequired may name. CNA’s own limitations document is generated from it and checked against it by a test, so it does not drift from the code.
| Claimed (accepted when required) | Not claimed (refused when required) |
|---|---|
KHR_texture_transform; KHR_mesh_quantization; KHR_materials_emissive_strength (PBR path); KHR_lights_punctual (approximated as up to three directional lights); KHR_draco_mesh_compression (build-dependent); KHR_materials_unlit; KHR_materials_variants; KHR_materials_ior |
KHR_materials_transmission (approximated as alpha); KHR_materials_pbrSpecularGlossiness (converted, not claimed); KHR_materials_specular (implemented with a named limit); KHR_materials_clearcoat, KHR_materials_sheen, KHR_materials_volume (parsed but ignored by the stock effects); KHR_texture_basisu and EXT_texture_webp (no decoder; a PNG/JPEG fallback is used when the file has one); EXT_meshopt_compression; EXT_mesh_gpu_instancing (one copy imported); KHR_materials_iridescence, KHR_materials_anisotropy, KHR_materials_dispersion (a recorded decision not to implement) |
The importer is built on cgltf 1.15. CNA’s conformance corpus for it is described by CNA as renderer-owned goldens rather than a comparison with a reference renderer; see Verification & Known Issues.
The gltf_to_cnj tool
For the cases the runtime path deliberately does not cover, the gltf_to_cnj tool (target cna_tool_gltf_to_cnj, under tools/gltf_to_cnj/, built unconditionally with the project) converts glTF 2.0 into CNA's own .cnj JSON asset format. It emits one Model .cnj per mesh group, plus <name>_meshN_verts.bin/<name>_meshN_idx.bin/<name>.skeleton.bin sidecars, one .cnj per animation clip, morph sidecars, and extracted PNG/JPEG textures.
# cna_tool_gltf_to_cnj <input.gltf|.glb> <outDir> <baseName> [unitScale]
cna_tool_gltf_to_cnj input/house.glb Content/models house 0.01
Use it when you want one output asset per mesh group or a unit scale other than 1.0. Otherwise the runtime path is simpler and costs you nothing at build time. (.cnj is also an input to the .cnb tools below.)
Compiled models: .cnb and cna-content
New in this snapshot: models can be compiled at build time into CNA’s binary .cnb container, which ContentManager loads in the tier just below .xnb and above every loose source. Three tools produce it, and each route keeps a different subset of what a glTF file can carry:
| Route | Command | What it keeps, and what it loses |
|---|---|---|
| Direct glTF at run time | Load<Model>("name") with name.glb |
Everything in the table above: all mesh groups, skins, animation, morph targets, lights, cameras, material variants and the import report. Unit scale 1.0. |
.cnb from the Content Pipeline |
cna-content build Models/robot.glb -o Content/Models/robot.cnb |
Model schema 1: skeleton, animation clips, morph data and lights are restored at load. One primary mesh group unless the generateChildAssets processor parameter is set (a multi-group file is otherwise refused with a message). Not carried: material variants (the .cnj-to-.cnb compiler refuses them), cameras, and the import report. No unit-scale option. |
.cnb with a unit scale |
cna_tool_gltf_to_cnb <in.gltf|glb> <outDir> <baseName> [--unit-scale <f>] [--keep-cnj <dir>] |
The same .cnb content, with the unit scale applied by the offline converter (--keep-cnj keeps the intermediate .cnj). |
.xnb from the Content Pipeline |
cna-content build Models/robot.glb -o Content/Models/robot.xnb --format xnb |
An XNA-compatible Model. Skeleton, animation clips, lights and morph targets are dropped with named warnings; PBR materials are downgraded to the corresponding stock effect; generated child assets and external-effect parts are refused. Use .cnb for animated glTF. |
XNA .x and .fbx sources |
cna-content build Models/ship.fbx -o Content/Models/ship.cnb |
Build time only. CNA still has no run-time FBX or .x importer; these two formats go through the XNA ModelProcessor in the pipeline and reach .cnb or .xnb. (These two legs are described by CNA’s source comments and its generated pipeline parity report; this page did not trace them end to end.) |
See Content Pipeline for the full option list and Tutorial 145 for an end-to-end walk-through. The command-line catalogue is on Command-Line Tools.
Loading pipeline-built models from .xnb
If you are porting an existing XNA title, its models are already compiled into .xnb. CNA's XNB reader registers the full model reader family, so those assets load without conversion — ContentManager prefers an .xnb over a .cnb and over any loose file when both are present. A Game subclass registers the built-in XNB readers itself; only a ContentManager used outside a Game needs an explicit CNA::Internal::Xnb::RegisterAllBuiltInXnbReaders() call. See XNB Loading & Interoperability for the full picture, including what will not load.
Model API
| Member | Type | Description |
|---|---|---|
Draw(world, view, projection) |
void |
One-call rendering. Binds matrices via IEffectMatrices on every part's effect, then draws all meshes. |
getMeshesProperty() (XNA Meshes) |
const ModelMeshCollection& |
Ordered collection of all ModelMesh objects in this model. |
getBonesProperty() (XNA Bones) |
const ModelBoneCollection& |
Flat list of all bones. Each ModelBone exposes a local transform (getTransformProperty()) and an index (getIndexProperty()). |
getRootProperty() (XNA Root) |
ModelBone* |
The root bone of the skeleton hierarchy. |
CopyAbsoluteBoneTransformsTo(std::vector<Matrix>&) |
void |
Walks the bone hierarchy and fills the caller-supplied vector with world-space (absolute) bone matrices. The vector should hold at least getBonesProperty().getCountProperty() entries. |
getTagProperty() |
System::Object* |
Compatibility carrier used, for example, by the first imported SkinningData or by ModelAnimationsEXT on an unskinned animated model. CNAEXT accessors beside it: getSkinsEXTProperty(), getCamerasEXTProperty(), getGltfImportReportEXTProperty() and the material-variant selection pair. |
ModelMesh
| Member | Type | Description |
|---|---|---|
getNameProperty() |
const std::string& |
The mesh name carried through from the source asset. |
getMeshPartsProperty() |
const ModelMeshPartCollection& |
Collection of ModelMeshPart entries that together define this mesh's geometry. |
getEffectsProperty() |
const ModelEffectCollection& |
All effects referenced by parts in this mesh. Useful for bulk parameter updates. |
Draw() |
void |
Draws all MeshParts using their currently assigned effects. |
getParentBoneProperty() |
ModelBone* (may be null) |
The bone that controls this mesh's transform in the skeleton hierarchy. |
getBoundingSphereProperty() |
BoundingSphere |
Bounding sphere in local mesh space. Used for frustum culling. |
getTagProperty() |
System::Object* |
Optional caller/content object attached to this mesh. |
ModelMeshPart
| Member | Type | Description |
|---|---|---|
getEffectProperty() |
Effect* |
The effect used to render this part. Assign a different effect here to override the default material. |
getVertexBufferProperty() |
VertexBuffer* |
GPU buffer holding the vertex data for this part. |
getIndexBufferProperty() |
IndexBuffer* |
GPU buffer holding the index data for this part. |
getPrimitiveCountProperty() |
int |
Number of primitives in this part’s own topology; not necessarily triangles. |
getStartIndexProperty() |
int |
First index in the IndexBuffer for this part. |
getVertexOffsetProperty() |
int |
Offset added to each index when reading from the VertexBuffer. |
getNumVerticesProperty() |
int |
Number of vertices referenced by this part. |
getTagProperty() |
System::Object* |
Optional content object, including imported MorphTargetDataEXT. |
Code Examples
1. Simple one-call draw
The simplest way to render a model. Model::Draw iterates every part, sets the world/view/projection matrices on each effect via IEffectMatrices, and issues the draw calls automatically.
model.Draw(world, view, projection);
2. Loading via ContentManager
Pass the asset name without extension. ContentManager resolves it against the content root, preferring an .xnb if one exists, then a .cnb, and otherwise reading the .cnj or glTF file, and returns a fully initialised Model by value.
Model model = getContentProperty().Load<Model>("models/house");
3. Per-mesh effect override
Override the default effect on each part to apply custom lighting or material properties per mesh. The BasicEffect cast means this loop only touches BasicEffect parts: a model loaded from direct glTF or a converted .cnj usually carries PbrEffect or SkinnedPbrEffect for its metallic-roughness materials, which the cast skips. Set the matrices through IEffectMatrices (example 4) and cast to a concrete effect type only for that type’s own properties.
for (ModelMesh* mesh : model.getMeshesProperty()) {
for (ModelMeshPart* part : mesh->getMeshPartsProperty()) {
if (auto* effect = dynamic_cast<BasicEffect*>(part->getEffectProperty())) {
effect->setWorldProperty(world);
effect->setViewProperty(view);
effect->setProjectionProperty(projection);
effect->setLightingEnabledProperty(true);
}
}
mesh->Draw();
}
4. A bone hierarchy without skinning (rigid parts)
Call CopyAbsoluteBoneTransformsTo to obtain the absolute (model-space) transform of every bone. Multiply each mesh's parent-bone transform by the scene world matrix before setting it on the effect. This is the standard pattern for a rigid multi-part model — a vehicle with turning wheels, an articulated arm — and it is not skinning: the array is indexed by scene node and carries no inverse bind pose, so it cannot feed SkinnedEffect or SkinnedPbrEffect. A skinned model takes its palette from AnimationPlayer::GetSkinTransforms() (see Skeletal animation below).
std::vector<Matrix> transforms(model.getBonesProperty().getCountProperty());
model.CopyAbsoluteBoneTransformsTo(transforms);
for (ModelMesh* mesh : model.getMeshesProperty()) {
ModelBone* parent = mesh->getParentBoneProperty(); // null only for an XNB mesh whose file names no parent
Matrix meshWorld = transforms[parent != nullptr ? parent->getIndexProperty() : 0] * world;
for (Effect* effect : mesh->getEffectsProperty()) {
// IEffectMatrices covers BasicEffect and the other stock effects, including the PbrEffect and
// SkinnedPbrEffect that glTF and converted .cnj models use; a dynamic_cast<BasicEffect*> would skip them.
if (auto* matrices = dynamic_cast<IEffectMatrices*>(effect)) {
matrices->setWorldProperty(meshWorld);
matrices->setViewProperty(view);
matrices->setProjectionProperty(projection);
}
}
mesh->Draw();
}
Skeletal animation (CNAEXT)
XNA 4.0 gave you bones and SkinnedEffect and left clip playback to your game code — the well-known "Skinned Model Sample" existed precisely to fill that hole. CNA closes it in the framework itself, through a set of types tagged CNAEXT because they have no XNA counterpart. Direct glTF loading, the gltf_to_cnj converter and the .cnb route all deliver the clip data these types consume: a skinned model carries a SkinningData object on Model::Tag that AnimationPlayer is constructed from, and an unskinned model with rigid node animation carries ModelAnimationsEXT there instead (posed with ApplyClipToBonesEXT), so an animated glTF asset is playable end to end without writing a sampler. SkinnedModelEXT is a separate, self-contained type that these routes do not produce (it is loaded from a .skinnedmodel.json manifest).
| Type | Role |
|---|---|
SkinnedModelEXT |
The separate, Avatar-oriented model type: its own skeleton arrays and animation clips, loaded from a .skinnedmodel.json manifest. It is not what glTF, .cnj or .cnb loading returns. |
SkinningData |
The skeleton (hierarchy, bind pose, inverse bind pose) and animation clips of a skinned Model. Direct glTF, .cnj and .cnb loading attach it to Model::Tag (every skin is also listed by getSkinsEXTProperty()); AnimationPlayer is constructed from it. |
ModelAnimationsEXT |
Rigid scene-node clips of a model without skins, attached to Model::Tag in place of SkinningData; pose the model with ApplyClipToBonesEXT. |
AnimationPlayer |
Drives clip playback over time and produces the bone pose to hand to the effect. |
AnimationClipEXT |
One named animation — a walk cycle, an idle, an attack. |
BoneTrackEXT |
The channel of keyframes belonging to a single bone within a clip. |
KeyframeEXT |
A single sampled bone transform at a point in time. |
MorphTargetDataEXT |
Blend shapes (morph targets) — vertex-level deformation for facial animation and similar effects, entirely outside XNA's model. Declared in the header MorphTargetEXT.hpp and attached to a ModelMeshPart’s tag. |
These types are outside the XNA 4.0 surface. If you are keeping a codebase strictly XNA-compatible, CNA's compile-time CNAEXT purity check will flag every use of them — which is the point: the boundary is visible rather than accidental.
Driving SkinnedEffect directly
The lower-level path remains available and is what the animation layer ultimately feeds. Set the bone palette with SetBoneTransforms(vector) and configure the number of skinning influences per vertex with setWeightsPerVertexProperty(n), where n is 1, 2 or 4.
auto* skinned = dynamic_cast<SkinnedEffect*>(part->getEffectProperty());
if (skinned != nullptr) {
skinned->setWorldProperty(meshWorld);
skinned->setViewProperty(view);
skinned->setProjectionProperty(projection);
skinned->EnableDefaultLighting();
skinned->setWeightsPerVertexProperty(4);
skinned->SetBoneTransforms(boneMatrices); // std::vector<Matrix>
}
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- glTF conformance: corpus, oracle ladder and evidence — The pinned glTF specification, the 148-asset generated corpus, the L0-L7 oracle ladder, renderer-owned pixel campaigns, the Khronos comparisons, the defect ledger and the milestone statuses.
- glTF feature matrix: importer, runtime and evidence — Every glTF 2.0 feature area with what CNA's import core does, what the runtime Model represents and which committed tests and layers provide evidence, at this snapshot.
- Model, ModelMesh and ModelMeshPart: the runtime graph and its draw contract — What Model::Draw and ModelMesh::Draw do, the invariants a loaded graph supplies, what copies share, the five model content routes and the collection rules, at this snapshot.
- Skinning, animation and morph targets on a Model — SkinningData and AnimationPlayer semantics, the three glTF skin index spaces, the D8 root prefix, clip resampling, rigid scene-node clips and CPU morph blending, at this snapshot.
- The CNJ model toolchain: gltf_to_cnj, the Model envelope and sidecars — What cna_tool_gltf_to_cnj writes, the per-type version-2 Model envelope, which descriptor and sidecar rules the .cnj reader enforces or trusts, route parity and the dual-texture occlusion remap.
- 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.
- Vertex packing: the glTF stride ABI, index widths and topology — The eleven canonical vertex strides, how a glTF primitive's layout is chosen, typed versus raw upload, index narrowing, the seven topologies, tangents, mirroring and the hard limits the bytes impose.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-128: Model::Draw indexes its bone scratch buffer with bone 0 even for a model with no bones — Model's three-argument constructor accepts an empty bone list, but Draw then reads sharedDrawBoneMatrices_[0] for every mesh without growing the thread-local vector, an unchecked out-of-range read or a stale matrix from
- CNA-BUG-129: BlendMorphTargetsEXT indexes NormalDeltas and every per-vertex delta array without checking their lengths — The function validates only the weight count; a hand-built MorphTargetDataEXT whose NormalDeltas has fewer entries than targets, or whose delta arrays are shorter than the vertex count, is read out of range.
- CNA-BUG-130: The .cnj Model reader truncates vertex and index sidecar sizes and never range-checks indices, where the SkinnedModel reader refuses the same input — ModelTypeReader and BuildModelMeshPartGeometryEXT derive vertex and index counts by integer division, discarding trailing bytes, and build the index buffer without checking indices against the vertex count; the SkinnedMo
- CNA-BUG-256: docs/model-content-pipeline-support.md still says CNA has no binary .xnb model reader and lists Model loader gaps that the code has closed — Below a short update note, the document says no .xnb Model can be loaded and that the .cnj Model loader has one bone, no ParentBone, no BoundingSphere and no tests. ModelReader, the bone hierarchy, parent bones, bounding
- CNA-BUG-277: The glTF importer still refuses a file that requires KHR_materials_specular and warns that renderer bindings are pending, although every PBR renderer samples both specular maps — GltfImportCore's registry still leaves KHR_materials_specular unclaimed with a 'renderer bindings are pending' note, so extensionsRequired refuses such a file, although since AM4-085 every PBR renderer samples both specu
- CNA-GAP-035: Only the direct glTF route fills Model::CamerasEXT; models loaded from .cnj or .cnb carry no cameras — ReadGltfModel extracts the file's cameras into Model::CamerasEXT, but the offline gltf_to_cnj/gltf_to_cnb tools only count camera nodes, so a converted model arrives without its authored framing.
- CNA-GAP-036: Runtime glTF import has no unit-scale option; only the offline converters can rescale a model — ContentManager's glTF reader always imports at unitScale 1.0, so a file not authored in metres must go through the offline converters: cna_tool_gltf_to_cnb --unit-scale, or the optional unitScale argument of cna_tool_glt
- CNA-GAP-037: EXT_mesh_gpu_instancing is not imported: each node keeps one placement and its per-instance transforms are dropped — A node declaring EXT_mesh_gpu_instancing is imported with its own single transform, so the file renders one copy where it describes many; the drop is reported per file (gpu-instancing-dropped).
- CNA-GAP-038: Mirrored glTF placements keep the shared winding, so under the default cull mode they render back-facing — A placement whose world transform has a negative determinant is detected and reported (mirrored-winding-unapplied) but CNA neither reverses its winding nor changes cull state, and exposes no per-mesh flag for the applica
- CNA-GAP-040: A skinned glTF model cannot also carry rigid scene-node animation: tracks not driven by its skins are dropped — Model::Tag holds SkinningData for a skinned model, so scene-node clips whose tracks are not carried by a skin palette are dropped with the rigid-animation-dropped-on-skinned-model diagnostic.