骨骼动画
骨骼动画是模型动画中的一种,通过骨骼关节的旋转、平移,变换 Mesh 顶点位置,达到驱动模型动画的目的。
TIP
1、目前引擎只支持模型内置的骨骼动画,需要用户提前通过3D建模软件制作对应的骨骼动画素材
2、从 v0.8 开始,骨骼动画和Morph动画通过 AnimatorComponent 统一驱动
简介
Mesh 上的每个顶点数据,都包含该点受影响的骨骼索引编号,以及受该骨骼影响的权重,这类数据信息统称为蒙皮信息,顶点受影响的骨骼一般限制为4根,更多的骨骼数只会增加计算量,对动画质量没有显著提高。
在 AnimatorComponent 组件中,PrefabBoneData 包含骨骼关节相关的数据,拥有名称,旋转、平移、父骨骼等信息,多个 PrefabBoneData 组成一套完整的骨架 PrefabAvatarData
PropertyAnimationClip 是一系列骨骼姿势变换的曲线数据集,存储了每根骨骼节点的缩放、旋转、平移的变换数据。
PropertyAnimationClipState 为动画播放状态,它与 PropertyAnimationClip 关联,用于维护播放状态,以及插值权重等相关数据。
AnimatorComponent 是整个动画的驱动组件,它与多个 PropertyAnimationClipState 关联,用来在多个动画状态之间切换,融合,驱动整个骨骼动画的最终变换姿势。
加载动画模型
当加载带有骨骼动画数据的模型文件后,引擎会自动为模型添加一个 AnimatorComponent 组件,并将模型中的动画数据加入其中。可以直接在模型的根实体上获取 AnimatorComponent 组件,并播放指定动画。
// load test model
let soldier = await engine.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 ,用以区分不同动画状态。
let clips = animation.clips;
for (var i = 0; i < clips.length; i++) {
console.log("Name:", clips[i].clipName)
}播放指定动画
AnimatorComponent 组件提供 playAnim 方法来播放指定动画:
// 播放 Walk 名称的动画
animator.playAnim('Walk');
// 播放列表中首个动画
let clips = animation.clips;
animator.playAnim(clips[0].clipName);调整播放速度
playAnim 方法播放指定动画时,默认为正常速度播放(1.0),如需加速播放通过参数 speed 设置,数值越大播放速度越快,数值越小播放速度越慢,当该值为负时将进行倒播。
// 正常播放
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 相同,数值越大播放速度越快,数值越小播放速度越慢,当该值为负时将进行倒播。
// 正常播放
animator.timeScale = 1.0;
// 2倍减速
animator.timeScale = 0.5;
// 2倍加速
animator.timeScale = 2.0;
// 2倍加速倒播
animator.timeScale = -2.0;import { Engine3D, Scene3D, Object3D, AtmosphericComponent, View3D, DirectLight, HoverCameraController, Color, CameraUtil, SkeletonAnimationComponent, Vector3, AnimatorComponent } from '@orillusion/core';
import * as dat from 'dat.gui';
// Init Engine3D
let engine = await Engine3D.init();
// Create Scene3D
let scene = new Scene3D();
// add a camera object with Camera3D
let mainCamera = CameraUtil.createCamera3DObject(scene);
mainCamera.perspective(60, engine.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 engine.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
engine.startRenderView(view);动画过渡
旧版写法(用于融合时已弃用)
通过 crossFade / 手动调整 clipsState[].weight 来实现多动画融合是旧版写法。新项目的动画融合请改用下文的 动画分层与融合(addLayer)。crossFade 本身仍可用于基础层(base layer)的单轨过渡。
可以使用 crossFade 方法来使当前动画过渡到指定状态。第一个参数为要过渡到的动画状态名称,第二个参数为过渡时间(秒)。
// 播放走路动画
animation.playAnim('Walk');
// 从走路状态历时1秒过度到跑步状态
animation.crossFade('Run', 1.0);import { Engine3D, Scene3D, Object3D, AtmosphericComponent, View3D, DirectLight, HoverCameraController, Color, CameraUtil, SkeletonAnimationComponent, Vector3, AnimatorComponent } from '@orillusion/core';
import * as dat from 'dat.gui';
// Init Engine3D
let engine = 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, engine.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 engine.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
engine.startRenderView(view);动画分层与融合
版本说明
动画分层(AnimationLayer / AnimatorComponent.addLayer)在 @orillusion/core 0.9.0 引入,是多动画融合的推荐方式,取代了旧版手动调整 clipsState 权重的做法。
新版本通过**分层(Layer)**机制融合动画:**基础层(layer 0)**由 playAnim / crossFade 驱动;在其之上可以叠加任意多个 AnimationLayer,每层独立播放一个动画、用 weight 控制混入程度,并可用 BoneMask 限制只影响部分骨骼(如只动上半身)。
创建并添加分层
new AnimationLayer(name, weight, blendMode, mask):
| 参数 | 类型 | 说明 |
|---|---|---|
name | string | 层名称(唯一) |
weight | number | 混合权重 0~1 |
blendMode | LayerBlendMode | 混合模式:Override(向该层姿势插值)或 Additive(在基础姿势上叠加差量) |
mask | BoneMask | null | 骨骼遮罩,null 表示全身 |
import { AnimatorComponent, AnimationLayer, LayerBlendMode } from '@orillusion/core';
const animator = model.getComponentsInChild(AnimatorComponent)[0];
// 基础层:正常播放行走
animator.playAnim('Walk');
// 叠加一个 Additive 层(例如“受击抖动”),权重 0.6,全身
const layer = new AnimationLayer('hit', 0.6, LayerBlendMode.Additive, null);
layer.clipName = 'HitReact'; // 该层播放的动画
animator.addLayer(layer);
// 运行时调整该层权重(淡入/淡出)
animator.setLayerWeight('hit', 0.3);混合模式
| 模式 | 公式 | 用途 |
|---|---|---|
LayerBlendMode.Override | lerp(base, layer, weight) | 用该层姿势覆盖基础姿势(按权重插值),如切换上半身动作 |
LayerBlendMode.Additive | base + (layer - rest) * weight | 在基础姿势上叠加差量动作,如呼吸、瞄准偏移、受击 |
骨骼遮罩(BoneMask)
用 BoneMask 限制分层只作用于部分骨骼,例如让上半身播放“挥手”、下半身继续“行走”:
import { BoneMask } from '@orillusion/core';
const upperBody = new BoneMask();
upperBody.add('Spine').add('Chest').add('LeftArm').add('RightArm');
// 或按子树批量添加:upperBody.addSubtree(avatar, 'Spine');
const waveLayer = new AnimationLayer('wave', 1.0, LayerBlendMode.Override, upperBody);
waveLayer.clipName = 'Wave';
animator.addLayer(waveLayer);分层管理 API
| 方法 | 说明 |
|---|---|
addLayer(layer) | 添加一个分层,返回该层 |
getLayer(name) | 按名称获取分层 |
removeLayer(name) | 移除分层 |
setLayerWeight(name, weight) | 设置某层权重 |
setLayerClip(name, clipName, time?, timeScale?) | 设置某层播放的动画 |
layers | 只读,当前所有分层 |

