Model Loading & Rendering

Microsoft::Xna::Framework::Graphics — Model, ModelMesh, ModelMeshPart, runtime glTF, the gltf_to_cnj and cna-content pipelines, and CNAEXT skeletal animation

ⓘ

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.

TopicBehaviour at this snapshot
Mesh groupsEvery 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 scaleFixed 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.
SceneOnly nodes reachable from the selected scene become model content.
Rigid (node) animationRetained 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.
MaterialsFactor-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).
TopologyAll seven primitive modes are handled; strips, fans and loops are converted. With Draco only TRIANGLES and TRIANGLE_STRIP are accepted.
TangentsLegacy _TANGENT and _BINORMAL VEC3 attributes are imported as a tangent VEC4.
Cameras, variants, reportThe 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.
extensionsRequiredEnforced. 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:

RouteCommandWhat 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>
}