Chapter 42 The CNJ Model Toolchain
Direct runtime glTF loading is convenient and now retains all scene groups in one Model, but fixes unit scale at 1. The offline cna_tool_gltf_to_cnj path uses the same semantic import core and material packing, then writes each group as explicit CNJ and binary sidecars. It is the reproducible path for controlled scaling, one-output-per-group packaging, and inspectable build artifacts.
42.1 A renderer-free build tool
The tool is configured by cmake/ToolGltfToCnj.cmake, built unconditionally rather than behind CNA_BUILD_TESTS, and links CNA plus sharp-runtime. It does not initialize a window, graphics device, or renderer. The command line is
The input may be .gltf or .glb. For each group, the tool writes a model descriptor and sidecars:
-
•
<name>.cnj, a version-2 Model envelope;
-
•
<name>_meshN_verts.bin and _idx.bin;
-
•
optional _morph.bin files;
-
•
optional <name>.skeleton.bin;
-
•
one version-1 AnimationClip CNJ per clip; and
-
•
extracted base, occlusion, and other texture images with stable suffixes.
Multiple groups receive stable _static or skin-name suffixes. Runtime and offline routes now preserve the same group set but package it differently: one multi-skin runtime model versus separately named converter outputs.
42.2 The Model envelope is version 2
CNJ’s common envelope contains a version and a logical type. Most types accept version 1 only; Model has a per-type maximum of 2. Raising the Model ceiling did not globally bless version 2 for textures, effects, or animation clips. Validation receives the requested type and that type’s maximum, so a version accepted for one reader can still be rejected by another.
Version 2 adds a top-level bones array. Each record carries a name, parent index, and 16-float local transform, and records are parent-before-child with an identity root at index 0. Each mesh also receives a parentBone index. These fields preserve the glTF scene hierarchy and rigid instancing placement that a flat list of pre-transformed meshes could not.
The same version-2 envelope can also carry the alpha.1 import report, cameras, rigid clips, material-variant state, topology, multi-UV material metadata, and newer PBR fields. These are additive fields within the per-type version ceiling, not evidence that every CNJ type accepts a new global format version. The reader supplies empty/default carriers for older Model documents.
Those last two shape rules are producer invariants, not fully validated schema rules. The reader always constructs entry 0 as its root and does not apply that entry’s serialized transform or parent. For later entries it bounds-checks the parent against the complete array, but does not require parent < child; a forward parent therefore builds a pointer tree whose CopyAbsoluteBoneTransformsTo order is wrong. A transform array with any count other than 16 silently becomes identity. Generated files satisfy all three rules, but a hand-edited version-2 descriptor can load into an internally inconsistent graph rather than fail at the schema boundary.
Version-1 Model files remain loadable. When no general hierarchy exists, the reader constructs the older per-mesh fallback bone. This makes version 2 an additive fidelity improvement rather than a flag day.
42.3 Binary sidecars and bounded reads
Vertex and index bytes use the packed ABI from Chapter 40. The descriptor supplies strides and paths, from which the reader derives counts and primitive counts. Its validation is incomplete: a non-positive stride silently drops the mesh, an unknown positive stride can produce a vertex buffer without uploaded contents, and vertex/index byte lengths not divisible by their derived element size are truncated by integer division rather than rejected. Morph sidecars retain base bytes and target deltas used by MorphTargetDataEXT; their mandatory reads are bounds-checked, but the descriptor/sidecar pair is still trusted to agree semantically.
The skeleton sidecar historically contained two matrix blocks: bind poses and inverse binds. Version 2 can append a third block containing SkeletonRootPrefix. The reader checks whether at least boneCount * 64 bytes remain before reading it; if not, it supplies identity prefixes. This compatibility test does not distinguish a genuinely old file from a partially written new third block: any trailing prefix fragment shorter than the complete block is ignored rather than rejected, and extra bytes after a complete block are not required to be exhausted. The mandatory hierarchy/bind/inverse-bind reads remain bounds-checked. Appending an optional fixed-size block rather than changing the meaning of the old two is what keeps earlier sidecars valid, but producers should still publish each generated sidecar atomically.
The descriptor may reference separate clip CNJs. Those pass through the manager’s normal cached loading path, so repeated names can share a clip object. Mesh buffers, built-in effects, and most graph objects are still created per described part and retained by the model’s private ownership bundle.
42.4 Offline and runtime parity
Sharing GltfImportCore reduces drift, but does not prove parity by itself. The two front ends perform different work after extraction: one creates textures, effects, buffers and a live model; the other serializes bytes and descriptors that a later CNJ reader recreates. Tests therefore compare semantic and rendering-visible results across both routes, including reversed skin order, animation, PBR data, morph tracks, texture extraction, and hierarchy.
The important contract is not byte identity between a live object and JSON. It is that a group imported directly and the same group converted then loaded from CNJ produce the same scene-node placement, palette coordinate spaces, vertex/index bytes, material choice, and animation records. A fix made only in one front end is a regression even if the shared parser still builds.
42.5 Occlusion through DualTextureEffect
glTF occlusion uses 1.0 for fully visible light and 0.0 for full occlusion. XNA’s DualTextureEffect, however, multiplies its second texture contribution by 2; in that equation 0.5 is neutral. Passing glTF’s occlusion image unchanged would brighten rather than preserve an unoccluded surface.
For the non-PBR dual-texture approximation, CNA therefore decodes the occlusion image, halves its RGB channels, and re-encodes it. The resulting mapping is
The remapped image is cached separately from the original because the same source image may also be bound to PbrEffect, where glTF’s original convention is correct. The runtime and converter both use this shared extraction rule; tests cover successful remapping and decode failure.
42.6 Choosing a route
Use direct glTF for rapid iteration on a file known to have one relevant group and unit scale. Use the converter when the file contains multiple characters or static plus skinned content, when unit conversion is required, or when asset bytes must be reviewed and committed. Use XNB when compatibility with an existing XNA content pipeline is the governing requirement. The legacy .skinnedmodel.json path is for SkinnedModelEXT, not an alternate CNJ spelling.
Do not hand-edit counts or offsets in a generated Model CNJ without regenerating or validating the sidecars. The JSON is readable, but its numerical metadata and binary files form one asset contract.