animation_playback - ryzom/ryzomcore GitHub Wiki


title: Playing animations description: Load animations, bind a skeleton to a mesh, and play back animations with blending published: true date: 2026-03-14T00:00:00.000Z tags: editor: markdown dateCreated: 2026-03-14T00:00:00.000Z

In the previous tutorial we lit a scene with point lights. Now we'll bring shapes to life by loading skeleton and animation files, binding them to mesh instances, and playing back animations using NeL's playlist system.

This tutorial covers:

  • Creating a UAnimationSet and loading .anim files
  • Creating a USkeleton and binding a mesh to it with bindSkin
  • Using UPlayListManager and UPlayList to play animations
  • Looping vs. one-shot animations
  • Blending between animations with weight transitions

Concepts

NeL's animation system works with four key objects:

  • UAnimationSet - A container holding all loaded animation clips. Created once, then finalized with build().
  • USkeleton - An instance of a skeleton file (.skel). Mesh instances are bound to skeletons with bindSkin() so the skeleton's bones deform the mesh.
  • UPlayListManager - Evaluates all active playlists each frame. One per scene is typical.
  • UPlayList - A mixer with multiple slots (channels). Each slot can play one animation with its own time origin, speed, weight, and wrap mode. Blending multiple slots produces smooth transitions.

Setting up the animation system

Create the animation set and playlist manager during initialization. The animation set is created on the driver, while the playlist manager lives on the scene:

#include <nel/3d/u_animation_set.h>
#include <nel/3d/u_animation.h>
#include <nel/3d/u_play_list_manager.h>
#include <nel/3d/u_play_list.h>
#include <nel/3d/u_skeleton.h>
	// Create the animation set and load clips
	m_AnimationSet = m_Driver->createAnimationSet();

	m_IdWalk = m_AnimationSet->addAnimation("marche.anim", "Walk");
	m_IdIdle = m_AnimationSet->addAnimation("idle.anim", "Idle");

	// Finalize - must be called after all animations are added
	m_AnimationSet->build();

	// Create the playlist manager
	m_PlayListManager = m_Scene->createPlayListManager();

The addAnimation() parameters are the filename and a logical name. It returns an animation ID used later to select which clip to play.

Creating a skeleton and binding a mesh

A skinned mesh needs a skeleton to deform its vertices. Create both, then bind:

	// Create the mesh instance and skeleton
	m_Instance = m_Scene->createInstance("gnu.shape");
	m_Skeleton = m_Scene->createSkeleton("gnu.skel");

	// Bind the mesh to the skeleton (skinning)
	m_Skeleton.bindSkin(m_Instance);

	// Position the skeleton - the bound mesh follows automatically
	m_Skeleton.setPos(NLMISC::CVector(0.f, 0.f, 0.f));

Creating a playlist and playing an animation

Each entity that plays animations gets its own UPlayList. Register both the skeleton and the instance with the playlist so it can animate bone transforms and material properties:

	m_PlayList = m_PlayListManager->createPlayList(m_AnimationSet);
	m_PlayList->registerTransform(m_Instance);
	m_PlayList->registerTransform(m_Skeleton);

To play an animation, assign it to a slot with a time origin, wrap mode, and weight:

	double currentTime = NLMISC::CTime::getLocalTime() / 1000.0;

	m_PlayList->setAnimation(0, m_IdIdle);
	m_PlayList->setTimeOrigin(0, currentTime);
	m_PlayList->setWrapMode(0, NL3D::UPlayList::Repeat); // loop
	m_PlayList->setWeight(0, 1.0f);

Wrap modes

Each slot has a wrap mode that controls what happens when the animation reaches its end:

Mode Behavior
Repeat Loop the animation continuously
Clamp Stop at the last frame and hold
Disable Slot has no effect (like removing the animation)

Updating animations each frame

Call PlayListManager::animate() once per frame with the current time. This evaluates all registered playlists and updates their transforms:

	m_PlayListManager->animate(NLMISC::CTime::getLocalTime() / 1000.0);

This must be called before Scene::animate() and Scene::render().

Blending between animations

The power of the playlist system is smooth transitions between animations. Use two slots and crossfade their weights:

void switchAnimation(uint newAnimId, bool loop)
{
	double currentTime = NLMISC::CTime::getLocalTime() / 1000.0;
	float transitionTime = 0.25f;

	// Swap active slots (toggle between 0 and 1)
	uint newSlot = m_NextSlot;
	uint oldSlot = 1 - m_NextSlot;
	m_NextSlot = 1 - m_NextSlot;

	// Set up the new animation
	m_PlayList->setAnimation(newSlot, newAnimId);
	m_PlayList->setTimeOrigin(newSlot, currentTime);
	m_PlayList->setWrapMode(newSlot, loop ? UPlayList::Repeat : UPlayList::Clamp);
	m_PlayList->setWeightSmoothness(newSlot, 1.0f);

	// Crossfade: fade out old, fade in new
	m_PlayList->setStartWeight(oldSlot, 1.0f, currentTime);
	m_PlayList->setEndWeight(oldSlot, 0.0f, currentTime + transitionTime);

	m_PlayList->setStartWeight(newSlot, 0.0f, currentTime);
	m_PlayList->setEndWeight(newSlot, 1.0f, currentTime + transitionTime);
}

setStartWeight and setEndWeight define a time range over which the weight interpolates. Setting setWeightSmoothness to 1.0 makes the interpolation use a smooth curve instead of linear.

Cleaning up

Delete in reverse order of creation:

	m_PlayListManager->deletePlayList(m_PlayList);
	m_Scene->deletePlayListManager(m_PlayListManager);
	m_Skeleton.detachSkeletonSon(m_Instance);
	m_Scene->deleteInstance(m_Instance);
	m_Scene->deleteSkeleton(m_Skeleton);

Complete source code

This example loads the Snowballs gnu model with its skeleton and two animations (idle and walk), toggling between them with the space bar. The gnu data files (gnu.shape, gnu.skel, idle.anim, marche.anim) are part of the Snowballs data.

#include <nel/misc/types_nl.h>
#include <nel/misc/app_context.h>
#include <nel/misc/event_listener.h>
#include <nel/misc/debug.h>
#include <nel/misc/time_nl.h>
#include <nel/misc/path.h>
#include <nel/3d/u_driver.h>
#include <nel/3d/u_scene.h>
#include <nel/3d/u_camera.h>
#include <nel/3d/u_instance.h>
#include <nel/3d/u_skeleton.h>
#include <nel/3d/u_animation_set.h>
#include <nel/3d/u_animation.h>
#include <nel/3d/u_play_list_manager.h>
#include <nel/3d/u_play_list.h>

using namespace std;
using namespace NLMISC;

class CMyGame : public IEventListener
{
public:
	CMyGame();
	~CMyGame();
	void run();
	virtual void operator()(const CEvent &event) NL_OVERRIDE;

private:
	void switchAnimation(uint animId, bool loop);

	NL3D::UDriver *m_Driver;
	NL3D::UScene *m_Scene;
	NL3D::UInstance m_Instance;
	NL3D::USkeleton m_Skeleton;
	NL3D::UAnimationSet *m_AnimationSet;
	NL3D::UPlayListManager *m_PlayListManager;
	NL3D::UPlayList *m_PlayList;
	uint m_IdIdle;
	uint m_IdWalk;
	uint m_NextSlot;
	bool m_Walking;
	bool m_CloseWindow;
	double m_LastTime;
	float m_CamAngle;
};

CMyGame::CMyGame()
	: m_CloseWindow(false)
	, m_Scene(NULL)
	, m_AnimationSet(NULL)
	, m_PlayListManager(NULL)
	, m_PlayList(NULL)
	, m_NextSlot(0)
	, m_Walking(false)
	, m_CamAngle(0.f)
{
	m_Driver = NL3D::UDriver::createDriver(0, NL3D::UDriver::OpenGl3);
	if (!m_Driver) { nlerror("Failed to create driver"); return; }

	m_Driver->EventServer.addListener(EventCloseWindowId, this);
	m_Driver->EventServer.addListener(EventKeyDownId, this);
	m_Driver->setDisplay(NL3D::UDriver::CMode(800, 600, 32));
	m_Driver->setWindowTitle("Animation Playback");

	// Set up search paths for data files
	CPath::addSearchPath("data", true, false);
	CPath::remapExtension("dds", "tga", true);

	// Create scene
	m_Scene = m_Driver->createScene(true);
	m_Scene->enableLightingSystem(true);
	m_Scene->setSunDiffuse(CRGBA(255, 255, 240));
	m_Scene->setSunDirection(CVector(1.f, 1.f, -2.f).normed());

	// Camera
	NL3D::UCamera cam = m_Scene->getCam();
	cam.setTransformMode(NL3D::UTransformable::DirectMatrix);
	cam.setPerspective(float(Pi / 3.0), 800.f / 600.f, 0.1f, 1000.f);
	cam.lookAt(CVector(0.f, -4.f, 2.f), CVector(0.f, 0.f, 1.f));

	// Load animations into an animation set
	m_AnimationSet = m_Driver->createAnimationSet();
	m_IdIdle = m_AnimationSet->addAnimation("idle.anim", "Idle");
	m_IdWalk = m_AnimationSet->addAnimation("marche.anim", "Walk");
	m_AnimationSet->build();

	// Create playlist manager
	m_PlayListManager = m_Scene->createPlayListManager();

	// Create skeleton and bind mesh
	m_Instance = m_Scene->createInstance("gnu.shape");
	m_Skeleton = m_Scene->createSkeleton("gnu.skel");
	m_Skeleton.bindSkin(m_Instance);
	m_Skeleton.setPos(CVector(0.f, 0.f, 0.f));

	// Create a playlist for this entity
	m_PlayList = m_PlayListManager->createPlayList(m_AnimationSet);
	m_PlayList->registerTransform(m_Instance);
	m_PlayList->registerTransform(m_Skeleton);

	// Start with idle animation
	switchAnimation(m_IdIdle, true);

	m_LastTime = CTime::ticksToSecond(CTime::getPerformanceTime());
}

CMyGame::~CMyGame()
{
	if (m_PlayList) m_PlayListManager->deletePlayList(m_PlayList);
	if (m_PlayListManager) m_Scene->deletePlayListManager(m_PlayListManager);
	if (!m_Instance.empty()) m_Scene->deleteInstance(m_Instance);
	if (!m_Skeleton.empty()) m_Scene->deleteSkeleton(m_Skeleton);
	if (m_Scene) m_Driver->deleteScene(m_Scene);
	m_Driver->release();
	delete m_Driver;
}

void CMyGame::switchAnimation(uint animId, bool loop)
{
	double currentTime = CTime::getLocalTime() / 1000.0;
	float transitionTime = 0.25f;

	uint newSlot = m_NextSlot;
	uint oldSlot = 1 - m_NextSlot;
	m_NextSlot = 1 - m_NextSlot;

	m_PlayList->setAnimation(newSlot, animId);
	m_PlayList->setTimeOrigin(newSlot, currentTime);
	m_PlayList->setWrapMode(newSlot, loop ? NL3D::UPlayList::Repeat : NL3D::UPlayList::Clamp);
	m_PlayList->setWeightSmoothness(newSlot, 1.0f);

	m_PlayList->setStartWeight(oldSlot, 1.0f, currentTime);
	m_PlayList->setEndWeight(oldSlot, 0.0f, currentTime + transitionTime);
	m_PlayList->setStartWeight(newSlot, 0.0f, currentTime);
	m_PlayList->setEndWeight(newSlot, 1.0f, currentTime + transitionTime);
}

void CMyGame::operator()(const CEvent &event)
{
	if (event == EventCloseWindowId)
		m_CloseWindow = true;
	else if (event == EventKeyDownId)
	{
		CEventKeyDown &kd = (CEventKeyDown &)event;
		if (kd.Key == KeySPACE && kd.FirstTime)
		{
			m_Walking = !m_Walking;
			switchAnimation(m_Walking ? m_IdWalk : m_IdIdle, true);
		}
	}
}

void CMyGame::run()
{
	while (m_Driver->isActive() && !m_CloseWindow)
	{
		m_Driver->EventServer.pump();

		double now = CTime::ticksToSecond(CTime::getPerformanceTime());
		float dt = float(now - m_LastTime);
		m_LastTime = now;
		m_CamAngle += dt * 0.3f;

		// Orbit camera around the model
		CVector eye(cosf(m_CamAngle) * 4.f, sinf(m_CamAngle) * 4.f, 2.f);
		m_Scene->getCam().lookAt(eye, CVector(0.f, 0.f, 1.f));

		m_Driver->clearBuffers(CRGBA(40, 40, 50));

		// Update animations then render the scene
		m_PlayListManager->animate(CTime::getLocalTime() / 1000.0);
		m_Scene->animate(CTime::getLocalTime() / 1000.0);
		m_Scene->render();

		m_Driver->swapBuffers();
	}
}

int main(int argc, char *argv[])
{
	CApplicationContext applicationContext;
	CMyGame myGame;
	myGame.run();
	return EXIT_SUCCESS;
}

Place the Snowballs gnu data files (gnu.shape, gnu.skel, idle.anim, marche.anim, and their textures) in a data directory next to the executable. Press Space to toggle between idle and walk animations.

What's next

In the next tutorial, you will learn how to load and control particle systems.

⚠️ **GitHub.com Fallback** ⚠️