Skip to content

Skeleton Animation

Skeleton animation 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.

TIP

  1. Currently, the engine only supports bone animations that are built into the model, requiring users to prepare the corresponding skeletal animation assets in advance using 3D modeling software.
  2. From v0.8, both skeletal animations and Morph animations are driven uniformly through the AnimatorComponent.

Introduction

Each vertex data on a Mesh contains the index numbers of the bones that affect that vertex, as well as the weights of those influences. This type of data is collectively referred to as skinning information. The number of bones that can influence a vertex is generally limited to 4; more bones only increase the computational load without significantly improving animation quality.

In the AnimatorComponent, PrefabBoneData includes data related to bone joints, such as names, rotation, translation, and parent bones. Multiple PrefabBoneData entries together form a complete skeletal structure called PrefabAvatarData.

PropertyAnimationClip is a dataset of curves representing a series of transformations for skeletal poses, storing transformation data for scaling, rotation, and translation for each bone node.

PropertyAnimationClipState represents the animation playback state, which is associated with PropertyAnimationClip and is used to maintain playback status, interpolation weights, and other related data.

The AnimatorComponent is the driving component for the entire animation. It is associated with multiple PropertyAnimationClipState instances to switch and blend between various animation states, driving the final transformation posture of the entire skeletal animation.

Load Animation Model

When loading a model file with skeleton animation data, the engine will automatically add a AnimatorComponent component to the model, and add the animation clip data in the model to it. You can directly get the AnimatorComponent 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(AnimatorComponent)[0];
animator.playAnim('Walk');

Get Animation Name

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

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

Play Specified Animation

AnimatorComponent provides the playAnim method to play the specified animation:

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

// Play the first animation in the list
let clips = animation.clips;
animator.playAnim(clips[0].clipName);

Adjust Playback Speed

playAnim 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.playAnim('Walk', 1);

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

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

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

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

You can also set the global timeline scaling through the timeScale property on AnimatorComponent, 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, AnimatorComponent } 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 = 5.0;
}

// 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 animator component
let soldierAnimation = soldier.getComponentsInChild(AnimatorComponent)[0];
soldierAnimation.playAnim('Idle');

const GUIHelp = new dat.GUI();
GUIHelp.addFolder('Animation');
GUIHelp.add(soldierAnimation, 'timeScale', -6, 6, 0.01);
GUIHelp.add({ Idle: () => soldierAnimation.playAnim('Idle') }, 'Idle');
GUIHelp.add({ Walk: () => soldierAnimation.playAnim('Walk') }, 'Walk');
GUIHelp.add({ Run: () => soldierAnimation.playAnim('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);

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.playAnim('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, AnimatorComponent } 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 = 5.0;
}

// 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 animator component
let animator = soldier.getComponentsInChild(AnimatorComponent)[0];
animator.playAnim('Idle');

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

f = GUIHelp.addFolder('Animation-play');
animator.clipsState.forEach((clipState, _) => {
    f.add({ click: () => animator.playAnim(clipState.clip.clipName) }, 'click').name(clipState.clip.clipName);
});
f.open();

f = GUIHelp.addFolder('Animation-crossFade');
animator.clipsState.forEach((clipState, _) => {
    f.add({ click: () => animator.crossFade(clipState.clip.clipName, 0.3) }, 'click').name('crossFade(' + clipState.clip.clipName + ')');
});
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);

Released under the MIT License