Chapter 52 Avatar: Faithful Absence and CNAEXT Rendering
CNA has two independent avatar systems. The XNA-compatible classes reproduce the observable behavior of a reference assembly whose Xbox Live content service is gone; that path is inert. The SkinnedModelEXT path is a CNA extension that loads content and renders GPU-skinned meshes. Calling the extension does not change the base API’s state or semantics.
| Base Avatar API | SkinnedModelEXT (CNAEXT) | |
|---|---|---|
| Purpose | Preserve observed XNA surface without service content | Load and render CNA assets |
| Skeleton | Fixed 71-transform contract; zero transforms | Content-defined (bundled assets: 19 bones) |
| Draw | Validates, then performs no rendering | GraphicsDevice/SkinnedEffect draw |
| Activation | Normal API surface | Explicit opt-in |
| Evidence | Source translation and behavioral tests | Content, runtime, renderer, and pixel tests |
“Faithful” here is scoped to the translated behavior and tests. It does not mean that a C++ port is binary-identical to the managed assembly or that Xbox avatar services are available.
52.1 Base API contract
AvatarRenderer ignores both constructor arguments. Reading State sets and returns AvatarRendererState::Unavailable; no path reaches Ready or Loading. The IAvatarAnimation draw overload rejects null, reads its transforms and expression, and delegates. The transform-array overload requires exactly 71 entries and then does nothing. BindPose throws InvalidOperationException unless the unreachable Ready state is present.
AvatarDescription requires 1,021 bytes. CNA treats a correctly sized value as valid when byte zero is nonzero; the other bytes remain opaque. The description getter copies the bytes, height/body type default lazily, and the static Changed event is never raised. Both CreateRandom overloads return the same all-zero invalid description. The body-type overload validates its enum but does not use it when building the result.
BeginGetFromGamer/EndGetFromGamer complete synchronously and return that invalid description. The caller owns and must delete the heap-allocated IAsyncResult. AvatarAnimation supplies 71 zero transforms, zero duration, and a default expression for all 31 presets. The preset is retained only as the CNAEXT clip name returned by GetRealClipNameEXT().
These results are expected when the proprietary service-supplied mesh, texture, animation, and description formats are unavailable. FNA has no corresponding Avatar classes, so CNA’s source was translated from the XNA 4.0 reference assembly instead of inferred from FNA.
52.2 SkinnedModelEXT contract
SkinnedModelEXT is move-only because it owns GPU resources. It contains:
-
•
parent indices, local bind poses, and global inverse bind poses;
-
•
named clips with per-bone translation, scale, and quaternion-rotation tracks;
-
•
skinned mesh parts and their vertex, index, material, and texture resources.
Its skeleton is unrelated to AvatarRenderer’s 71-entry validation array. The bundled male and female assets use 19 bones; other assets may use different counts up to SkinnedEffect’s 72-bone draw limit.
AvatarRenderer::EnableRealRenderingEXT(device, model) creates the extension’s SkinnedEffect. DrawRealEXT(clip, position, loop) samples a clip and draws the model, or throws InvalidOperationException if opt-in has not occurred. It does not make State ready or give the base Draw() a renderer. Conventional roots are avatar/male/avatar and avatar/female/avatar.
The fragment is intended for a Game subclass with the Avatar, content, graphics, and <memory> headers already included.
52.3 Animation and palette computation
ComputeBoneTransformsEXT first initializes every local transform from the bind pose, then samples matching tracks. Translation and scale use linear interpolation; rotation uses Quaternion::Slerp. It composes children with already computed parents in stored topological order and finally multiplies each global transform by its global inverse bind pose. The result is the palette consumed by SkinnedEffect.
Current guards address four earlier failures:
-
•
looped time uses tick modulo instead of repeated subtraction, including negative time;
-
•
each non-root parent must precede its child, which also rejects cycles;
-
•
skeleton arrays must match BoneCount before indexing;
-
•
empty or out-of-range animation tracks are skipped without touching memory.
The time regression uses a position 1,000,000,000.5 clip durations from the origin and a 100-ms bound, specifically detecting the former duration-by-duration loop.
52.4 Lighting, appearance, and wardrobe
The extension uses one directional light plus ambient light. It disables the other two stock directional lights and zeroes specular and emissive terms before applying the caller’s values. Default ambient and key-light values keep an avatar visible without configuration.
AvatarAppearanceEXT is a CNA tint model, not a reconstruction of the proprietary appearance format. Case-sensitive part-name substrings select Hair, Shirt, Pants, or Shoes; unmatched names receive skin tint. It changes colors, not geometry or per-part textures.
AttachPartEXT requires a matching bone count. Reattaching a part with the same name first calls RemovePartEXT, which releases the old owned vertex/index buffers, mesh part, and texture before inserting the replacement. This closes both the former overlapping-part behavior and its GPU-resource leak.
Historical note. Lighting defects found through avatar rendering An early draw path applied ambient light before EnableDefaultLighting(), which then overwrote it. A separate EasyGL shader defect multiplied emissive color by diffuse color twice, turning the ambient term into . That defect affected all related skinned, vertex-lit, and environment-mapped variants. A pixel test’s near-black fraction fell from 4.1–6.0% to 0% after the shader formula was aligned with FNA’s lighting expression.
52.5 Content pipeline
The .skinnedmodel.json, .skeleton.bin, and .clip.bin pipeline loads the extension assets. Three corrected implementation traps are relevant to other importers:
-
•
manifest-relative paths must resolve from the manifest directory, not the content root;
-
•
chained reader.Read<float>() constructor arguments relied on unspecified C++ argument evaluation order, so components are now read into named locals;
-
•
hierarchy reordering must remap inverse bind matrices and per-vertex joint indices as well as parent indices.
Chapter 41 covers the corresponding glTF joint-space rules.
52.6 Open garment interpenetration
The procedural avatar still shows shirt and skin fragments around the raised shoulder and chest in the Wave clip. Measurement found the same 64 of 228 shirt vertices crossing the body in both neutral and Wave poses; the animation changes which crossings face the camera. This locates the remaining defect in the rest-pose geometry, not clip sampling.
The largest visible intersections are the collar and cuffs. The shirt’s Spine1 shell has radius 0.176 and spans to ; the head sphere has radius 0.15 and center , placing its bottom at . Their calculated overlap of about 0.126 m agrees with the measured 0.131 m collar penetration. A waist overlap is mutually hidden by garments.
Four attempted remedies did not remove the cause:
| Attempt | Observed result |
|---|---|
| Narrow garment blend radius | No clear visual improvement |
| Trim capsule boundary caps | Pants crossings rose from 21 to 39 |
| Flat cylinders plus joint spheres | More speckle, 28 new groin crossings, lower brightness |
| 32 instead of 12 segments | Smoother edges, same fragments, 4.2 vertex data |
The open choices are to size collar and cuff shells against the body radii along their full span, accept the limitation of convex per-bone placeholder shells, or explicitly accept the tessellation cost. The extension is therefore renderer-functional but not visually defect-free.
52.7 Renderer and evidence status
EasyGL and Vulkan run registered avatar integration programs with pixel readback. Their synthetic one-bone quads isolate palette/effect/draw wiring; they do not validate the entire asset converter. Demo programs add full humanoid content, clip changes, appearance, attachment, wardrobe replacement, API-boundary comparison, and two-process network synchronization.
BGFX contains relevant pipeline code but has no registered avatar integration result at the pinned revision; existing harnesses stop earlier at unimplemented 3D depth/blend setup. SDL_RENDERER rejects the required 3D resources. No conclusion for either renderer should be inferred from EasyGL or Vulkan pixels. Appendix G defines the evidence terms used here.