骨骼动画
骨骼动画 SkeletonAnimation 是模型动画中的一种,通过骨骼关节 Joint
的旋转、平移,变换 Mesh
顶点位置,达到驱动模型动画的目的。
目前引擎只支持模型内置的骨骼动画,需要用户提前通过3D建模软件制作对应的骨骼动画素材
简介
Mesh
上的每个顶点数据,都包含该点受影响的骨骼索引编号,以及受该骨骼影响的权重,这类数据信息统称为蒙皮信息,顶点受影响的骨骼一般限制为4根,更多的骨骼数只会增加计算量,对动画质量没有显著提高。
Joint
是骨骼关节,拥有名称,旋转、平移、父骨骼等信息,多个 Joint
组成一套完整的骨架 Skeleton
:
JointPose
用于存放骨骼关节姿势数据,拥有当前骨骼变换到该姿势时的矩阵数据,多个 JointPose
组成一套完整的骨架姿势 SkeletonPose
:
SkeletonAnimationClip
是一系列骨架姿势(SkeletonPose
)组成的时间序列,也叫关键帧序列,该对象只存储动画姿势数据,通过 SkeletonAnimationClipState
驱动。
SkeletonAnimationClipState
为动画播放状态,它与 SkeletonAnimationClip
关联,用于维护播放状态,计算帧之间的过渡姿势 SkeletonPose
。
SkeletonAnimation
是整个骨骼动画控制组件,它与多个 SkeletonAnimationClipState
关联,用来在多个动画状态之间切换,融合,驱动整个骨骼动画的最终变换姿势。
加载动画模型
当加载带有骨骼动画数据的模型文件后,引擎会自动为模型添加一个 SkeletonAnimation
组件,并将模型中的动画剪辑数据加入其中。可以直接在模型的根实体上获取 SkeletonAnimation
组件,并播放指定动画。
// 加载外部模型;
let soldier = await Engine3D.res.loadGltf('gltfs/glb/Soldier.glb');
soldier.rotationY = -90;
soldier.localScale.set(2, 2, 2);
scene.addChild(soldier);
// 获取动画控制器;
let animator = soldier.getComponentsInChild(SkeletonAnimation)[0];
animator.play('Walk');
获取动画名称
组件提供 getAnimationClips 方法,用来获取所有动画剪辑数据对象,该对象都有唯一的 name
属性,用以区分不同动画状态。
let clips = animation.getAnimationClips();
for (var i = 0; i < clips.length; i++) {
console.log("Name:", clips[i].name)
}
播放指定动画
SkeletonAnimation
组件提供 play 方法来播放指定动画:
// 播放 Walk 名称的动画
animator.play('Walk');
// 播放列表中首个动画
let clips = animation.getAnimationClips();
animator.play(clips[0].name);
调整播放速度
play
方法播放指定动画时,默认为正常速度播放(1.0)
,如需加速播放通过参数 speed
设置,数值越大播放速度越快,数值越小播放速度越慢,当该值为负时将进行倒播。
// 正常播放
animator.play('Walk', 1);
// 2倍减速
animator.play('Walk', 0.5);
// 3倍加速
animator.play('Walk', 3.0);
// 正常倒播
animator.play('Walk', -1.0);
// 3倍加速倒播
animator.play('Walk', -3.0);
也可通过 SkeletonAnimation
上的 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, Camera3D, ForwardRenderJob, DirectLight, HoverCameraController, Color, GUIHelp, CameraUtil, SkeletonAnimation, FXAAPost, HDRBloomPost, Vector3
} from "@orillusion/core";
async function demo() {
// 初始化引擎环境;
await Engine3D.init({});
// 启用GUI调试面板;
GUIHelp.init();
// 创建场景对象;
let scene = new Scene3D();
scene.exposure = 0.3;
// 初始化相机;
let mainCamera = CameraUtil.createCamera3DObject(scene);
Camera3D.mainCamera = mainCamera;
mainCamera.perspective(60, window.innerWidth / window.innerHeight, 0.1, 10000.0);
let hc = mainCamera.object3D.addComponent(HoverCameraController);
hc.setCamera(0, -15, 10, new Vector3(0, 1, 0));
// 初始化环境图;
scene.envMap = await Engine3D.res.loadHDRTextureCube('https://cdn.orillusion.com/hdri/1428_v5_low.hdr');
{
let ligthObj = new Object3D();
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 = 1.7;
}
// 加载外部模型;
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);
// 获取动画控制器;
let soldierAnimation = soldier.getComponentsInChild(SkeletonAnimation)[0];
soldierAnimation.play('Idle');
GUIHelp.addFolder('Animation');
GUIHelp.add(soldierAnimation, 'timeScale', -6, 6, 0.01);
GUIHelp.addButton('Idle', () => soldierAnimation.play('Idle'));
GUIHelp.addButton('Walk', () => soldierAnimation.play('Walk'));
GUIHelp.addButton('Run', () => {
soldierAnimation.play('Run')
});
GUIHelp.endFolder();
// 开启渲染任务(前向渲染);
let renderJob = new ForwardRenderJob(scene);
renderJob.addPost(new FXAAPost());
renderJob.addPost(new HDRBloomPost());
Engine3D.startRender(renderJob);
}
demo();
添加动画剪辑
一般情况动画师都会给模型独立制作动画剪辑,每个动画剪辑拥有一个唯一名称,但也有全部动画状态做在同一个动画剪辑中的情况。例如一个包含待机
、走
、跑
等状态的动画剪辑,通过不同时间段区分,比如 0~1s 为待机、1~3s为走路、3~6s为跑步。为了方便状态切换,可以在引擎中通过指定的动画剪辑创建多个子状态,并通过 addAnimationClip 方法添加到控制器中,例如:
// 获取首个动画剪辑数据对象
let clip = animator.getAnimationClips()[0];
// 裁剪并创建新的动画剪辑数据对象(截取待机动画)
animator.addAnimationClip(clip.createSubClip('Idel', 0, 1.0))
// 裁剪并创建新的动画剪辑数据对象(截取走路动画)
animator.addAnimationClip(clip.createSubClip('Walk', 1.0, 3.0))
// 裁剪并创建新的动画剪辑数据对象(截取跑步动画)
animator.addAnimationClip(clip.createSubClip('Run', 3.0, 6.0))
获取动画状态
可以使用 currName 属性来获取当前正在播放的动画名称,如需获取更详细的播放状态信息,可以通过 getAnimationClipState 方法获取:
// 获取当前在播放的动画名称
var currentPlayName = animator.currName;
// 获取当前在播放的动画剪辑状态;
const currentState = animator.getAnimationClipState(animator.currName);
设置动画循环
加载的动画剪辑默认状态为循环播放,可通过 setAnimIsLoop 方法设置是否循环:
// 设置死亡动画为单次播放(不循环播放)
animation.setAnimIsLoop('death', false);
// 播放死亡动画
animation.play('death');
也可直接修改 SkeletonAnimationClipState
的 loop
属性:
// 获取死亡动画的 SkeletonAnimationClipState 对象
var deathClipState = animation.getAnimationClipState('death');
// 设置死亡动画为单次播放(不循环播放)
deathClipState.loop = false;
// 播放死亡动画
animation.play('death');
动画过渡
可以使用 crossFade 方法来使当前动画过渡到指定状态。第一个参数为要过渡到的动画状态名称,第二个参数为过渡时间(秒)
。
// 播放走路动画
animation.play('Walk');
// 从走路状态历时1秒过度到跑步状态
animation.crossFade('Run', 1.0);
import {
Engine3D, Scene3D, Object3D, Camera3D, ForwardRenderJob, DirectLight, HoverCameraController, Color, GUIHelp, CameraUtil, SkeletonAnimation, FXAAPost, HDRBloomPost, Vector3
} from "@orillusion/core";
async function demo() {
// 初始化引擎环境;
await Engine3D.init();
// 启用GUI调试面板;
GUIHelp.init();
// 创建场景对象;
let scene = new Scene3D();
scene.exposure = 0.3;
// 初始化相机;
let mainCamera = CameraUtil.createCamera3DObject(scene);
Camera3D.mainCamera = mainCamera;
mainCamera.perspective(60, window.innerWidth / window.innerHeight, 0.1, 10000.0);
let hc = mainCamera.object3D.addComponent(HoverCameraController);
hc.setCamera(0, -15, 10, new Vector3(0, 1, 0))
// 初始化环境图;
scene.envMap = await Engine3D.res.loadHDRTextureCube('https://cdn.orillusion.com/hdri/1428_v5_low.hdr');
{
let ligthObj = new Object3D();
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 = 1.7;
}
// 加载外部模型;
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);
// 获取动画控制器;
let animation = soldier.getComponentsInChild(SkeletonAnimation)[0];
animation.play('Idle');
GUIHelp.addFolder("Animation-weight").open();
animation.getAnimationClipStates().forEach((clipState, _) => {
GUIHelp.add(clipState, 'weight', 0, 1.0, 0.01).name(clipState.name);
});
GUIHelp.endFolder();
GUIHelp.addFolder("Animation-play").open();
animation.getAnimationClipStates().forEach((clipState, _) => {
GUIHelp.addButton(clipState.name, () => animation.play(clipState.name));
});
GUIHelp.endFolder();
GUIHelp.addFolder("Animation-crossFade").open();
animation.getAnimationClipStates().forEach((clipState, _) => {
GUIHelp.addButton('crossFade(' + clipState.name + ')', () => animation.crossFade(clipState.name, 0.3));
});
GUIHelp.endFolder();
// 开启渲染任务(前向渲染);
let renderJob = new ForwardRenderJob(scene);
renderJob.addPost(new FXAAPost());
renderJob.addPost(new HDRBloomPost());
Engine3D.startRender(renderJob);
}
demo();
动画事件
可以通过 SkeletonAnimationClip
上的 addEvent
方法为 clip
添加事件点,该方法接受两个参数,第一个为时间名称,第二个为触发时刻(秒),当 clip
动画播放到指定时刻时,将触发事件:
// 获取指定名称的clip
const runClip = animation.getAnimationClip("Run");
// 在 0.0 秒时刻添加一个名称为 BeginRun 的事件
runClip.addEvent("BeginRun", 0);
// 在末尾时刻添加一个名称为 EndRun 的事件
runClip.addEvent("EndRun", runClip.totalTime);
// 添加 BeginRun 事件监听
animation.events.addEventListener("BeginRun", (e: AnimationEvent) => {
console.log("Run-Begin", e.skeletonAnimation.getAnimationClipState('Run').time)
}, this);
// 添加 EndRun 事件监听
animation.events.addEventListener("EndRun", (e: AnimationEvent) => {
console.log("Run-End:", e.skeletonAnimation.getAnimationClipState('Run').time)
e.skeletonAnimation.crossFade("Idle", 0.5);
}, this);
import {
Engine3D, Scene3D, Object3D, Camera3D, ForwardRenderJob, DirectLight, HoverCameraController, Color, GUIHelp, CameraUtil, SkeletonAnimation, FXAAPost, HDRBloomPost, Vector3
} from "@orillusion/core";
async function demo() {
// 初始化引擎环境;
await Engine3D.init();
// 启用GUI调试面板;
GUIHelp.init();
// 创建场景对象;
let scene = new Scene3D();
scene.exposure = 0.3;
// 初始化相机;
let mainCamera = CameraUtil.createCamera3DObject(scene);
Camera3D.mainCamera = mainCamera;
mainCamera.perspective(60, window.innerWidth / window.innerHeight, 0.1, 10000.0);
let hc = mainCamera.object3D.addComponent(HoverCameraController);
hc.setCamera(0, -15, 10, new Vector3(0, 1, 0))
// 初始化环境图;
scene.envMap = await Engine3D.res.loadHDRTextureCube('https://cdn.orillusion.com/hdri/1428_v5_low.hdr');
{
let ligthObj = new Object3D();
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 = 1.7;
}
// 加载外部模型;
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);
// 获取动画控制器;
let animation = soldier.getComponentsInChild(SkeletonAnimation)[0];
const runClip = animation.getAnimationClip("Run");
runClip.addEvent("Begin", 0);
runClip.addEvent("Mid", runClip.totalTime / 2);
runClip.addEvent("End", runClip.totalTime);
animation.events.addEventListener("Begin", (e: AnimationEvent) => {
console.log("Run-Begin", e.skeletonAnimation.getAnimationClipState('Run').time);
}, this);
animation.events.addEventListener("Mid", (e: AnimationEvent) => {
console.log("Run-Mid", e.skeletonAnimation.getAnimationClipState('Run').time);
}, this);
animation.events.addEventListener("End", (e: AnimationEvent) => {
console.log("Run-End:", e.skeletonAnimation.getAnimationClipState('Run').time);
e.skeletonAnimation.crossFade("Idle", 0.5);
}, this);
GUIHelp.addFolder("Animation-play").open();
animation.getAnimationClipStates().forEach((clipState, _) => {
GUIHelp.addButton(clipState.name, () => animation.play(clipState.name));
});
GUIHelp.endFolder();
// 开启渲染任务(前向渲染);
let renderJob = new ForwardRenderJob(scene);
renderJob.addPost(new FXAAPost());
renderJob.addPost(new HDRBloomPost());
Engine3D.startRender(renderJob);
}
demo();