Skip to content

骨骼动画

骨骼动画是模型动画中的一种,通过骨骼关节的旋转、平移,变换 Mesh 顶点位置,达到驱动模型动画的目的。

TIP

1、目前引擎只支持模型内置的骨骼动画,需要用户提前通过3D建模软件制作对应的骨骼动画素材
2、从 v0.8 开始,骨骼动画和Morph动画通过 AnimatorComponent 统一驱动

简介

Mesh 上的每个顶点数据,都包含该点受影响的骨骼索引编号,以及受该骨骼影响的权重,这类数据信息统称为蒙皮信息,顶点受影响的骨骼一般限制为4根,更多的骨骼数只会增加计算量,对动画质量没有显著提高。

AnimatorComponent 组件中,PrefabBoneData 包含骨骼关节相关的数据,拥有名称,旋转、平移、父骨骼等信息,多个 PrefabBoneData 组成一套完整的骨架 PrefabAvatarData

PropertyAnimationClip 是一系列骨骼姿势变换的曲线数据集,存储了每根骨骼节点的缩放、旋转、平移的变换数据。

PropertyAnimationClipState 为动画播放状态,它与 PropertyAnimationClip 关联,用于维护播放状态,以及插值权重等相关数据。

AnimatorComponent 是整个动画的驱动组件,它与多个 PropertyAnimationClipState 关联,用来在多个动画状态之间切换,融合,驱动整个骨骼动画的最终变换姿势。

加载动画模型

当加载带有骨骼动画数据的模型文件后,引擎会自动为模型添加一个 AnimatorComponent 组件,并将模型中的动画数据加入其中。可以直接在模型的根实体上获取 AnimatorComponent 组件,并播放指定动画。

ts
// load test model
let soldier = await Engine3D.res.loadGltf('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('Walk');

获取动画名称

组件提供 clips 属性,用来获取所有动画剪辑数据对象,该对象都有唯一的 clipName ,用以区分不同动画状态。

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

播放指定动画

AnimatorComponent 组件提供 playAnim 方法来播放指定动画:

ts
// 播放 Walk 名称的动画
animator.playAnim('Walk');

// 播放列表中首个动画
let clips = animation.clips;
animator.playAnim(clips[0].clipName);

调整播放速度

playAnim 方法播放指定动画时,默认为正常速度播放(1.0),如需加速播放通过参数 speed 设置,数值越大播放速度越快,数值越小播放速度越慢,当该值为负时将进行倒播。

ts
// 正常播放
animator.playAnim('Walk', 1);

// 2倍减速
animator.playAnim('Walk', 0.5);

// 3倍加速
animator.playAnim('Walk', 3.0);

// 正常倒播
animator.playAnim('Walk', -1.0);

// 3倍加速倒播
animator.playAnim('Walk', -3.0);

也可通过 AnimatorComponent 上的 timeScale 属性设置全局时间线缩放,与 speed 相同,数值越大播放速度越快,数值越小播放速度越慢,当该值为负时将进行倒播。

ts
// 正常播放
animator.timeScale = 1.0;

// 2倍减速
animator.timeScale = 0.5;

// 2倍加速
animator.timeScale = 2.0;

// 2倍加速倒播
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);

动画过渡

可以使用 crossFade 方法来使当前动画过渡到指定状态。第一个参数为要过渡到的动画状态名称,第二个参数为过渡时间(秒)

ts
// 播放走路动画
animation.playAnim('Walk');
// 从走路状态历时1秒过度到跑步状态
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);