Skip to content

Skeleton Animation

SkeletonAnimation is a type of model animation, which drives the model animation by rotating and translating the Joint of the skeleton, transforming the position of the Mesh vertex.

Our Engine only supports the built-in skeleton animation of the model, which needs to be pre-made by 3D modeling software.

Introduction

Every vertex data on the Mesh contains the index number of the skeleton affected by the point, as well as the weight affected by the skeleton. This type of data information is called skinning information. The vertex affected by the skeleton is generally limited to 4 bones, and more bones will only increase the calculation. The quality of the animation has no significant improvement.

Joint is the skeleton joint, which has the name, rotation, translation, parent skeleton, etc. Multiple Joint form a complete Skeleton: Skeleton

JointPose is used to store the skeleton joint pose data, which has the matrix data of the current skeleton transformation to this pose. Multiple JointPose form a complete SkeletonPose: SkeletonPose

SkeletonAnimationClip is a time sequence of a series of skeleton poses (SkeletonPose), also called keyframe sequence, which only stores animation pose data through SkeletonAnimationClipState:

SkeletonAnimationClipState is the animation playback state, which is associated with SkeletonAnimationClip to maintain the playback state and calculate the transition pose SkeletonPose between frames.

SkeletonAnimation is the overall skeleton animation control component, which is associated with multiple SkeletonAnimationClipState to switch, blend, and drive the final transformation pose of the entire skeleton animation between animation states.

Load Animation Model

When loading a model file with skeleton animation data, the engine will automatically add a SkeletonAnimation component to the model, and add the animation clip data in the model to it. You can directly get the SkeletonAnimation component on the root entity of the model, and play the specified animation.

ts
// Load external model;
let soldier = await Engine3D.res.loadGltf('gltfs/glb/Soldier.glb');
soldier.rotationY = -90;
soldier.localScale.set(2, 2, 2);
scene.addChild(soldier);

// Get animation controller;
let animator = soldier.getComponentsInChild(SkeletonAnimation)[0];
animator.play('Walk');

Get Animation Name

This component provides the getAnimationClips method to get all animation clip data objects, which all have a unique name property to distinguish different animation states.

ts
let clips = animation.getAnimationClips();
for (var i = 0; i < clips.length; i++) {
    console.log("Name:", clips[i].name)
}

Play Specified Animation

SkeletonAnimation provides the play method to play the specified animation:

ts
//Play animation with name 'Walk'
animator.play('Walk');

// Play the first animation in the list
let clips = animation.getAnimationClips();
animator.play(clips[0].name);

Adjust Playback Speed

Play method plays the specified animation at the default normal speed (1.0), if you need to accelerate the playback, set the parameter speed, the larger the number, the faster the playback speed, the smaller the number, the slower the playback speed, when the value is negative, it will be reversed.

ts
// Normal speed
animator.play('Walk', 1);

// 2 times slower
animator.play('Walk', 0.5);

// 3 times faster
animator.play('Walk', 3.0);

// Reverse playback
animator.play('Walk', -1.0);

// 3 times faster reverse playback
animator.play('Walk', -3.0);

You can also set the global timeline scaling through the timeScale property on SkeletonAnimation, which is the same as speed. The larger the number, the faster the playback speed, the smaller the number, the slower the playback speed, and when the value is negative, it will be reversed.

ts
// Normal speed
animator.timeScale = 1.0;

// 2 times slower
animator.timeScale = 0.5;

// 2 times faster
animator.timeScale = 2.0;

// 2 times faster reverse playback
animator.timeScale = -2.0;

WebGPU is not supported in your browser
Please upgrade to latest Chrome/Edge

<
ts
import { Engine3D, Scene3D, Object3D, AtmosphericComponent, View3D, DirectLight, HoverCameraController, Color, CameraUtil, SkeletonAnimationComponent, Vector3 } from '@orillusion/core';
import * as dat from 'dat.gui';

// Init Engine3D
await Engine3D.init();

// Create Scene3D
let scene = new Scene3D();

// add a camera object with Camera3D
let mainCamera = CameraUtil.createCamera3DObject(scene);
mainCamera.perspective(60, Engine3D.aspect, 0.1, 10000.0);
let hc = mainCamera.object3D.addComponent(HoverCameraController);
hc.setCamera(0, -15, 5, new Vector3(0, 1, 0));

// add a dir light
{
    let ligthObj = new Object3D();
    ligthObj.rotationY = 135;
    ligthObj.rotationX = 45;
    let dl = ligthObj.addComponent(DirectLight);
    dl.lightColor = new Color(1.0, 0.95, 0.84, 1.0);
    scene.addChild(ligthObj);
    dl.castShadow = true;
    dl.intensity = 10;
}

// load test model
let soldier = await Engine3D.res.loadGltf('https://cdn.orillusion.com/gltfs/glb/Soldier.glb');
soldier.rotationY = -90;
soldier.localScale.set(2, 2, 2);
scene.addChild(soldier);

// get animation controller
let soldierAnimation = soldier.getComponentsInChild(SkeletonAnimationComponent)[0];
soldierAnimation.play('Idle');

const GUIHelp = new dat.GUI();
GUIHelp.addFolder('Animation');
GUIHelp.add(soldierAnimation, 'timeScale', -6, 6, 0.01);
GUIHelp.add({ Idle: () => soldierAnimation.play('Idle') }, 'Idle');
GUIHelp.add({ Walk: () => soldierAnimation.play('Walk') }, 'Walk');
GUIHelp.add({ Run: () => soldierAnimation.play('Run') }, 'Run');

// set skybox
scene.addComponent(AtmosphericComponent).sunY = 0.6;
// create a view with target scene and camera
let view = new View3D();
view.scene = scene;
view.camera = mainCamera;
// start render
Engine3D.startRenderView(view);

Add Animation Clip

Normally, the animator will make a separate animation clip for each model, and each animation clip has a unique name. But there are also cases where all animation states are in the same animation clip. For example, an animation clip containing Idle, Walk, Run and other states, which are differentiated by different time periods, such as 0~1s for idle, 1~3s for walking, and 3~6s for running. In order to facilitate state switching, you can create multiple substates in the engine by specifying the animation clip, and add them to the controller through the addAnimationClip method, for example:

ts
// Get the first animation clip data object
let clip = animator.getAnimationClips()[0];

// Cut and create a new animation clip data object (cut idle animation)
animator.addAnimationClip(clip.createSubClip('Idel', 0, 1.0))

// Cut and create a new animation clip data object (cut walking animation)
animator.addAnimationClip(clip.createSubClip('Walk', 1.0, 3.0))

// Cut and create a new animation clip data object (cut running animation)
animator.addAnimationClip(clip.createSubClip('Run', 3.0, 6.0))

Get Animation State

You can use the currName property to get the name of the animation currently being played. If you need more detailed playback status information, you can use the getAnimationClipState method to get it:

ts
// Get the name of the animation currently being played
var currentPlayName = animator.currName;

// Get the current animation clip state
const currentState = animator.getAnimationClipState(animator.currName);

Set Animation Loop

The loaded animation clip is in a default loop state, which can be set to loop or not through the setAnimIsLoop method:

ts
// Set the death animation to play once (not loop)
animation.setAnimIsLoop('death', false);
// Play death animation
animation.play('death');

Also, you can directly modify the loop property of SkeletonAnimationClipState:

ts
// Get the SkeletonAnimationClipState object of the death animation
var deathClipState = animation.getAnimationClipState('death');
// Set the death animation to play once (not loop)
deathClipState.loop = false;
// Play death animation
animation.play('death');

Animation Transition

You can use the crossFade method to transition the current animation to the specified state. The first parameter is the name of the animation state to transition to, and the second parameter is the transition time (seconds).

ts
// Play walk animation
animation.play('Walk');
// Transition from walk state to run state in 1 second
animation.crossFade('Run', 1.0);

WebGPU is not supported in your browser
Please upgrade to latest Chrome/Edge

<
ts
import { Engine3D, Scene3D, Object3D, AtmosphericComponent, View3D, DirectLight, HoverCameraController, Color, CameraUtil, SkeletonAnimationComponent, Vector3 } from '@orillusion/core';
import * as dat from 'dat.gui';

// Init Engine3D
await Engine3D.init();

// Create Scene3D
let scene = new Scene3D();
scene.exposure = 0.3;

// add a camera object with Camera3D
let mainCamera = CameraUtil.createCamera3DObject(scene);
mainCamera.perspective(60, Engine3D.aspect, 0.1, 10000.0);
let hc = mainCamera.object3D.addComponent(HoverCameraController);
hc.setCamera(0, -15, 5, new Vector3(0, 1, 0));

// set light
{
    let ligthObj = new Object3D();
    ligthObj.rotationY = 135;
    ligthObj.rotationX = 45;
    let dl = ligthObj.addComponent(DirectLight);
    dl.lightColor = new Color(1.0, 0.95, 0.84, 1.0);
    scene.addChild(ligthObj);
    dl.castShadow = true;
    dl.intensity = 15;
}

// load test model
let soldier = await Engine3D.res.loadGltf('https://cdn.orillusion.com/gltfs/glb/Soldier.glb');
soldier.rotationY = -90;
soldier.localScale.set(2, 2, 2);
scene.addChild(soldier);

// get animation controller
let animation = soldier.getComponentsInChild(SkeletonAnimationComponent)[0];
animation.play('Idle');

const GUIHelp = new dat.GUI();
let f = GUIHelp.addFolder('Animation-weight');
animation.getAnimationClipStates().forEach((clipState, _) => {
    f.add(clipState, 'weight', 0, 1.0, 0.01).name(clipState.name);
});
f.open();

f = GUIHelp.addFolder('Animation-play');
animation.getAnimationClipStates().forEach((clipState, _) => {
    f.add({ click: () => animation.play(clipState.name) }, 'click').name(clipState.name);
});
f.open();

f = GUIHelp.addFolder('Animation-crossFade');
animation.getAnimationClipStates().forEach((clipState, _) => {
    f.add({ click: () => animation.crossFade(clipState.name, 0.3) }, 'click').name('crossFade(' + clipState.name + ')');
});
f.open();
// set skybox
scene.addComponent(AtmosphericComponent).sunY = 0.6;

// create a view with target scene and camera
let view = new View3D();
view.scene = scene;
view.camera = mainCamera;
// start render
Engine3D.startRenderView(view);

Animation Event

You can add an event point to the clip through the addEvent method on SkeletonAnimationClip, which takes two parameters, the first is the event name, and the second is the trigger time (seconds). When the clip animation plays to the specified time, the event will be triggered:

ts
// Get the clip with the specified name
const runClip = animation.getAnimationClip("Run");

// Add an event named BeginRun at 0.0 seconds
runClip.addEvent("BeginRun", 0);

// Add an event named EndRun at the end
runClip.addEvent("EndRun", runClip.totalTime);

// Add BeginRun event listener
animation.events.addEventListener("BeginRun", (e: AnimationEvent) => {
    console.log("Run-Begin", e.skeletonAnimation.getAnimationClipState('Run').time)
}, this);

// Add EndRun event listener
animation.events.addEventListener("EndRun", (e: AnimationEvent) => {
    console.log("Run-End:", e.skeletonAnimation.getAnimationClipState('Run').time)
    e.skeletonAnimation.crossFade("Idle", 0.5);
}, this);

WebGPU is not supported in your browser
Please upgrade to latest Chrome/Edge

<
ts
import { Engine3D, Scene3D, Object3D, AtmosphericComponent, View3D, DirectLight, HoverCameraController, Color, CameraUtil, SkeletonAnimationComponent, Vector3, OAnimationEvent } from '@orillusion/core';
import * as dat from 'dat.gui';

// Init Engine3D
await Engine3D.init();

// Create Scene3D
let scene = new Scene3D();
scene.exposure = 0.3;

// add a camera object with Camera3D
let mainCamera = CameraUtil.createCamera3DObject(scene);
mainCamera.perspective(60, Engine3D.aspect, 0.1, 10000.0);
let hc = mainCamera.object3D.addComponent(HoverCameraController);
hc.setCamera(0, -15, 5, new Vector3(0, 1, 0));

// add a dir light
{
    let ligthObj = new Object3D();
    ligthObj.rotationY = 135;
    ligthObj.rotationX = 45;
    let dl = ligthObj.addComponent(DirectLight);
    dl.lightColor = new Color(1.0, 0.95, 0.84, 1.0);
    scene.addChild(ligthObj);
    dl.castShadow = true;
    dl.intensity = 15;
}

// load test model
let soldier = await Engine3D.res.loadGltf('https://cdn.orillusion.com/gltfs/glb/Soldier.glb');
soldier.rotationY = -90;
soldier.localScale.set(2, 2, 2);
scene.addChild(soldier);

// get animation controller
let animation = soldier.getComponentsInChild(SkeletonAnimationComponent)[0];

const runClip = animation.getAnimationClip('Run');
runClip.addEvent('Begin', 0);
runClip.addEvent('Mid', runClip.totalTime / 2);
runClip.addEvent('End', runClip.totalTime);

animation.eventDispatcher.addEventListener(
    'Begin',
    (e: OAnimationEvent) => {
        console.log('Run-Begin', e.skeletonAnimation.getAnimationClipState('Run').time);
    },
    this
);
animation.eventDispatcher.addEventListener(
    'Mid',
    (e: OAnimationEvent) => {
        console.log('Run-Mid', e.skeletonAnimation.getAnimationClipState('Run').time);
    },
    this
);
animation.eventDispatcher.addEventListener(
    'End',
    (e: OAnimationEvent) => {
        console.log('Run-End:', e.skeletonAnimation.getAnimationClipState('Run').time);
        e.skeletonAnimation.crossFade('Idle', 0.5);
    },
    this
);

const GUIHelp = new dat.GUI();
GUIHelp.addFolder('Animation-play').open();
animation.getAnimationClipStates().forEach((clipState, _) => {
    GUIHelp.add({ click: () => animation.play(clipState.name) }, 'click').name(clipState.name);
});

// set skybox
scene.addComponent(AtmosphericComponent).sunY = 0.6;
// create a view with target scene and camera
let view = new View3D();
view.scene = scene;
view.camera = mainCamera;
// start render
Engine3D.startRenderView(view);

Released under the MIT License