Tutorial 167: Drawing and animating avatars

CNA Tutorials  ·  CNA snapshot b0e97bb1

ℹ

What you’ll learn: how to create, animate and draw avatars with CNA's own avatar art, which way they face, how loading works, how to read a player's avatar and share one over a session.

ℹ

Status: avatars work offline, with no server. CNA draws them with its own avatar art — original catalogs generated by CNA’s tools, not Microsoft’s Xbox avatars — so they look and move differently from the console’s. The code follows CNA’s headers and avatar demos at snapshot b0e97bb1; it was not compiled or run for this page. It assumes the setup from Tutorial 162.

The three classes

  • AvatarDescription: what an avatar looks like, as XNA’s opaque buffer of exactly 1,021 bytes. CNA stores its own versioned encoding inside (body type, height, build, colours, clothing items, face shape and a checksum); it is not Xbox avatar data.
  • AvatarAnimation: one of XNA’s 31 AvatarAnimationPreset clips — eight standing idles, Clap, Wave, Celebrate and ten female and ten male idles and emotions — as 71 bone transforms and a facial expression at the current position.
  • AvatarRenderer: draws a description with XNA’s 71-bone skeleton (AvatarRenderer::BoneCount), its own lights and the given world, view and projection.

The renderer takes its graphics device from the IGraphicsDeviceService in the service provider gamer services were initialized with, as in XNA. So the game needs a GraphicsDeviceManager and a GamerServicesComponent; drawing before gamer services are initialized throws InvalidOperationException.

1. Draw a random avatar

#include "Microsoft/Xna/Framework/GamerServices/AvatarAnimation.hpp"
#include "Microsoft/Xna/Framework/GamerServices/AvatarAnimationPreset.hpp"
#include "Microsoft/Xna/Framework/GamerServices/AvatarDescription.hpp"
#include "Microsoft/Xna/Framework/GamerServices/AvatarRenderer.hpp"
#include "Microsoft/Xna/Framework/MathHelper.hpp"
#include "Microsoft/Xna/Framework/Matrix.hpp"
#include "Microsoft/Xna/Framework/Vector3.hpp"
#include <memory>

using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::GamerServices;

// members
std::unique_ptr<AvatarDescription> description_;
std::unique_ptr<AvatarRenderer> renderer_;
std::unique_ptr<AvatarAnimation> animation_;

void MyGame::LoadContent()
{
    description_ = std::make_unique<AvatarDescription>(AvatarDescription::CreateRandom());
    renderer_ = std::make_unique<AvatarRenderer>(description_.get(), true);   // loading effect while it loads
    animation_ = std::make_unique<AvatarAnimation>(AvatarAnimationPreset::Wave);
}

void MyGame::Update(GameTime& gameTime)
{
    Game::Update(gameTime);
    animation_->Update(gameTime.getElapsedGameTimeProperty(), true);   // true: loop
}

void MyGame::Draw(const GameTime& gameTime)
{
    auto& device = getGraphicsDeviceProperty();
    device.Clear(Color::CornflowerBlue);
    const float aspect = device.getViewportProperty().getAspectRatioProperty();
    renderer_->setWorldProperty(Matrix::CreateRotationY(MathHelper::Pi));   // turn it to face the camera on +Z
    renderer_->setViewProperty(Matrix::CreateLookAt(Vector3(0.0f, 1.0f, 3.0f), Vector3(0.0f, 0.9f, 0.0f), Vector3::Up));
    renderer_->setProjectionProperty(Matrix::CreatePerspectiveFieldOfView(MathHelper::PiOver4, aspect, 0.1f, 100.0f));
    renderer_->Draw(animation_.get());
    Game::Draw(gameTime);
}

CreateRandom() picks a body type, face, colours and clothes from the newest catalog compiled in; CreateRandom(AvatarBodyType::Female) fixes the body type. The renderer copies the description’s bytes, so the description does not have to outlive it. The model always renders solid with clockwise front faces, as in XNA, and the renderer restores the blend, depth, rasterizer and first sampler states it changes.

2. Which way the avatar faces

XNA’s avatars face −Z, with their left on −X, and CNA follows that space in everything the API exposes. A camera on +Z looking at the origin therefore sees the avatar’s back unless the world matrix turns it half a turn about Y, as above — the arrangement XNA’s AvatarAnimationBlending sample uses. Bone transforms and the bind pose are in the same space, so the XNA way of attaching an object to a hand works unchanged: the joint of bone i is BoneTransforms[i] * BindPose[i] * parentWorld. AvatarAnimation::getBoneTransformsProperty() holds 71 local animation deltas, with zero translation on every bone except the root, because the renderer’s getBindPoseProperty() supplies the joint offsets scaled to the avatar’s height; that is why one animation fits every height and build. getParentBonesProperty() is XNA’s 71-entry parent table, -1 for the root.

A game that blends or builds its own poses calls Draw(bones, expression) with exactly 71 matrices and an AvatarExpression (eyes, eyebrows and mouth); every bone must be decomposable, or Draw throws InvalidOperationException.

3. Loading states

getStateProperty() is Loading while the body, clothes and face are assembled from the catalog, then Ready. A description that holds no avatar CNA can read — an all-zero one, or bytes in another format — is Unavailable and draws nothing. While it loads, a renderer built with useLoadingEffect (the one-argument constructor’s default) draws a softly pulsing silhouette; with false it draws nothing until ready. getBindPoseProperty() throws until the state is Ready, so test the state before computing attachments.

Natively, one shared background thread assembles avatars, so creating a renderer never blocks the game thread, and identical descriptions share one assembled model. Browser builds have no thread to give it, so there the avatar is assembled where it is requested, on the calling thread. An XNA description that is valid by XNA’s rule (first byte non-zero) is not necessarily one CNA can render.

4. A player’s own avatar

Every local profile gets a random avatar when it is created, stored with the profile; a server account has the one stored for it on the server. BeginGetFromGamer reads either:

std::unique_ptr<System::IAsyncResult> avatarRead_;   // caller-owned

avatarRead_.reset(AvatarDescription::BeginGetFromGamer(player, {}, {}));

// in Update: a local profile completes at once, an account during the dispatcher update
if (avatarRead_ && avatarRead_->getIsCompletedProperty())
{
    description_ = std::make_unique<AvatarDescription>(AvatarDescription::EndGetFromGamer(avatarRead_.get()));
    avatarRead_.reset();
    renderer_ = std::make_unique<AvatarRenderer>(description_.get(), true);
}

A gamer without an avatar, a remote SystemLink gamer (no server identity) or an unreachable server yields an all-zero description, so the game takes its usual “no avatar” path. Players change their avatar with Edit avatar in the Guide (Home key), which works for accounts and local profiles alike. Every 10 seconds the dispatcher re-reads the avatar it returned for each signed-in player, and when one has changed it raises that description’s Changed event with the SignedInGamer as sender. AvatarDescription is a C++ value, so every copy of the description returned for that player shares the one event.

5. Share avatars in a session

As in XNA, a game can send the 1,021 bytes to other machines and rebuild the description there. This needs no server, so it works in SystemLink sessions with local profiles:

#include "Microsoft/Xna/Framework/Net/LocalNetworkGamer.hpp"
#include "Microsoft/Xna/Framework/Net/PacketReader.hpp"
#include "Microsoft/Xna/Framework/Net/PacketWriter.hpp"

// sender: now and then, reliably
const auto bytes = description_->getDescriptionProperty();
Net::PacketWriter writer;
writer.Write(bytes.data(), 0, static_cast<SharpRuntime::intcs>(bytes.size()));
me->SendData(writer, Net::SendDataOptions::ReliableInOrder);

// receiver
Net::PacketReader reader;
Net::NetworkGamer* sender = nullptr;
while (me->getIsDataAvailableProperty())
{
    me->ReceiveData(reader, sender);
    if (sender != nullptr && !sender->getIsLocalProperty())
        remote_ = std::make_unique<AvatarDescription>(reader.ReadBytes(1021));
}

6. Catalogs, the editor and licensing

  • Catalogs. All geometry, textures and motion come from CNA’s avatar catalogs v1, v2 and v3, generated by tools/avatar_builder/ in the CNA repository with no downloaded, third-party or Xbox content, and covered by the repository’s own licence. Every version is compiled into the library, so avatars render offline. Released catalogs never change; v3 is the current one. A description is drawn from the catalog version it names, never from another; if that version cannot be obtained, CNA draws a default avatar with the description’s body type, height, build and colours.
  • Catalog packs. With a server, an avatar may name a catalog a game’s build does not have. CNA then installs that whole catalog once, as a validated pack of at most 64 MiB by default; avatarCatalogUpdates or CNA_AVATAR_CATALOG_UPDATES=0 turns this off, and the server then sends a projection onto a catalog the game has. Packs are kept in CNA_GAMER_SERVICES_CATALOGS_DIR, else $XDG_DATA_HOME/cna/avatar-catalogs.
  • Editor. Besides the Guide’s Edit avatar screen, CNA’s examples build cna_avatar_editor, the same editor as a program of its own. No XNA API is added for either.
  • Lighting. setLightDirectionProperty, setLightColorProperty and setAmbientLightColorProperty are the whole lighting model. There is no baked ambient occlusion or self-shadowing; shadows a game draws itself, as in XNA’s AvatarShadows sample, work.

Evidence and qualification

  • CNA’s final audit run of this work (2 October 2026, Linux): the GamerServices suite 649 passed and the Net suite 524 passed, with no failures; the server’s 40 registered tests: 33 passed, 7 network-isolation tests skipped for lack of a test tool, none failed. The GamerServices suite includes the avatar API, catalog and coordinate-space tests. These are CNA-recorded runs; this site did not re-run them.
  • Avatar pixels: CNA’s 257-frame avatar review ran on OpenGL 3.3, Vulkan, SOFTWARE, SDL_GPU and WebGPU and agreed within 0.10/255 mean pixel difference of OpenGL 3.3; the AvatarShadows sample runs on OpenGL ES 3. Not measured: OpenGL ES 2, Direct3D, Metal, FNA3D and SDL_RENDERER.
  • The avatar sample ports play in the browser at samples.libcna.com; avatars run in browser builds, while the service, relay and voice do not.
  • Not qualified: Windows and macOS, a real wide-area network with NAT, voice on physical audio devices, and avatar pixels on Direct3D and Metal.

Next

Tutorial 168 operates the server, including importing avatar catalogs and giving accounts avatars. Back to Tutorial 166 for online sessions, or to the Gamer Services overview.