骨骼动画
骨骼动画是模型动画中的一种,通过骨骼关节的旋转、平移,变换 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 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
,用以区分不同动画状态。
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
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 方法来使当前动画过渡到指定状态。第一个参数为要过渡到的动画状态名称,第二个参数为过渡时间(秒)
。
// 播放走路动画
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
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);