Model Loading & Rendering
Implementation status: Model loading and Draw are implemented. CNA accepts direct glTF 2.0, converter-generated .cnj, older loose model descriptors and XNB models built by an XNA-compatible content pipeline. Skeletal and rigid animation, multiple skins, morph targets, material variants, cameras and import reports have CNAEXT carriers. 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 carries its own VertexBuffer, IndexBuffer, and Effect, mirroring the XNA 4.0 design exactly.
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 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.
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 alpha.1 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.
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 .vb.bin/.ib.bin/.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.
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 loose file when both are present. Remember that the XNB reader registry is empty until you call CNA::Internal::Xnb::RegisterAllBuiltInXnbReaders() at startup. See XNB Content Pipeline 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. |
Meshes |
ModelMeshCollection |
Ordered collection of all ModelMesh objects in this model. |
Bones |
ModelBoneCollection |
Flat list of all bones. Each ModelBone exposes a local Transform matrix and an Index. |
Root |
ModelBone* |
The root bone of the skeleton hierarchy. |
CopyAbsoluteBoneTransformsTo(Matrix[]) |
void |
Walks the bone hierarchy and fills the caller-supplied array with world-space (absolute) bone matrices. The array must have at least Bones.size() entries. |
getTagProperty() |
System::Object* |
Compatibility carrier used, for example, by the first imported SkinningData or by ModelAnimationsEXT on an unskinned animated model. |
ModelMesh
| Member | Type | Description |
|---|---|---|
Name |
std::string |
The mesh name carried through from the source asset. |
MeshParts |
ModelMeshPartCollection |
Collection of ModelMeshPart entries that together define this mesh's geometry. |
Effects |
EffectCollection |
All effects referenced by parts in this mesh. Useful for bulk parameter updates. |
Draw() |
void |
Draws all MeshParts using their currently assigned effects. |
ParentBone |
ModelBone* |
The bone that controls this mesh's transform in the skeleton hierarchy. |
BoundingSphere |
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. |
PrimitiveCount |
int |
Number of primitives in this part’s own topology; not necessarily triangles. |
StartIndex |
int |
First index in the IndexBuffer for this part. |
VertexOffset |
int |
Offset added to each index when reading from the VertexBuffer. |
NumVertices |
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 and otherwise reading the .cnj, and returns a fully initialised Model.
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.
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. Skeletal animation with bone transforms
Call CopyAbsoluteBoneTransformsTo to obtain world-space matrices for 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 rendering skinned models exported with a bone hierarchy.
std::vector<Matrix> transforms(model.getBonesProperty().getCountProperty());
model.CopyAbsoluteBoneTransformsTo(transforms);
for (ModelMesh* mesh : model.getMeshesProperty()) {
Matrix meshWorld = transforms[mesh->getParentBoneProperty()->getIndexProperty()] * world;
for (ModelMeshPart* part : mesh->getMeshPartsProperty()) {
if (auto* effect = dynamic_cast<BasicEffect*>(part->getEffectProperty())) {
effect->setWorldProperty(meshWorld);
effect->setViewProperty(view);
effect->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. The gltf_to_cnj converter emits the clip data these types consume, so an animated glTF asset is playable end to end without writing a sampler.
| Type | Role |
|---|---|
SkinnedModelEXT |
A model carrying a skeleton and its animation clips, rather than mesh data alone. |
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. |
MorphTargetEXT |
Blend shapes (morph targets) — vertex-level deformation for facial animation and similar effects, entirely outside XNA's model. |
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>
}