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:
UAnimationSet and loading .anim filesUSkeleton and binding a mesh to it with bindSkinUPlayListManager and UPlayList to play animationsNeL'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.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.
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));
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);
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) |
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().
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.
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);
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.
In the next tutorial, you will learn how to load and control particle systems.