Appearance
关键帧动画
创建关键帧动画
创建
给需要设置关键帧动画的模型命名
jsmesh.name = "Box";
KeyframeTrack
设置关键帧数据位置:
jsconst times = [0, 3, 6]; //时间轴上,设置三个时刻0、3、6秒 // times中三个不同时间点,物体分别对应values中的三个xyz坐标 const values = [0, 0, 0, 100, 0, 0, 0, 0, 100]; // 创建关键帧,把模型位置和时间对应起来 const posKF = new THREE.KeyframeTrack("Box.position", times, values);
颜色:
jsconst colorKF = new THREE.KeyframeTrack("Box.material.color", [2, 5], [1, 0, 0, 0, 0, 1]);
创建关键帧动画
AnimationClip
js
// 创建一个clip关键帧动画对象,命名"test",动画持续时间6s
const clip = new THREE.AnimationClip("test", 6, [posKF, colorKF]);
播放
播放器
js//包含关键帧动画的模型对象作为AnimationMixer的参数创建一个播放器mixer const mixer = new THREE.AnimationMixer(mesh); const clipAction = mixer.clipAction(clip); //.play()控制动画播放,默认循环播放 clipAction.play();
更新播放时间
jsconst clock = new THREE.Clock(); function loop() { requestAnimationFrame(loop); //clock.getDelta()方法获得loop()两次执行时间间隔 const frameT = clock.getDelta(); // 更新播放器相关的时间 mixer.update(frameT); } loop();
动画播放(暂停、倍速、循环)
循环.loop
控制动画是否循环播放
js
const clipAction = mixer.clipAction(clip);
//.play()控制动画播放,默认循环播放
clipAction.play();
//不循环播放
clipAction.loop = THREE.LoopOnce;
.clampWhenFinished
播放模式为非循环模式,设置物体状态停留在动画结束,不回到开头状态jsclipAction.loop = THREE.LoopOnce; clipAction.clampWhenFinished = true;
停止结束动画.stop()
动画终止并结束,模型回到动画开始状态
js
clipAction.stop();
是否暂停播放.paused
默认值 false,动画正常执行,如果想暂停设置为 true
js
if (clipAction.paused) {
//暂停状态
clipAction.paused = false; //切换为播放状态
btn.innerHTML = "暂停"; // 如果改变为播放状态,按钮文字设置为“暂停”
} else {
//播放状态
clipAction.paused = true; //切换为暂停状态
btn.innerHTML = "继续"; // 如果改变为暂停状态,按钮文字设置为“继续”
}
倍速播放.timeScale
js
clipAction.timeScale = 1; //默认
clipAction.timeScale = 2; //2倍速
拖动条调整播放速度
添加 gui 辅助调整工具
js
const gui = new GUI(); //创建GUI对象
// 0~6倍速之间调节
gui.add(clipAction, "timeScale", 0, 6);
动画播放(拖动任意时间状态)
控制动画播放特定时间段
js
const clip = new THREE.AnimationClip("test", 6, [posKF, colorKF]);
console.log("clip.duration", clip.duration);
.duration
设置播放总(结束)时间.time
设置播放开始时间
js
//从1秒时刻动画开始播放, 5秒时刻动画状态停止
clipAction.time = 1;
clip.duration = 5;
// 物体状态停留在动画结束的时候
clipAction.loop = THREE.LoopOnce;
clipAction.clampWhenFinished = true;
效果生效还需如下配置:
js
clipAction.loop = THREE.LoopOnce;
// 物体状态停留在动画结束的时候
clipAction.clampWhenFinished = true;
动画定格任意时间节点
先将动画设为暂停,再通过 time 调节定格节点
js
clipAction.paused = true;
clipAction.time = 1; //物体状态为动画1秒对应状态
clipAction.time = 3; //物体状态为动画3秒对应状态
通过 gui 进度条控制
js
// 先暂停
clipAction.paused = true;
引入 gui 并设置步长
js
import { GUI } from "three/addons/libs/lil-gui.module.min.js";
const gui = new GUI(); //创建GUI对象
gui.add(clipAction, "time", 0, 6).step(0.1);
解析外部模型关键帧动画
模型父对象作为播放器参数
即便把 mesh
的父对象 group
作为播放器 AnimationMixer
的参数,播放器也能根据 KeyframeTrack
参数 1 包含的模型名字.name
确定关键帧动画对应的模型对象。
js
mesh.name = "Box";
const group = new THREE.Group();
group.add(mesh);
const posKF = new THREE.KeyframeTrack("Box.position", times, values);
const clip = new THREE.AnimationClip("test", 6, [posKF]);
//包含关键帧动画的模型对象作为AnimationMixer的参数创建一个播放器mixer
const mixer = new THREE.AnimationMixer(group);
查看 gltf 模型动画数据
- 实际开发的时候,美术一般会创建好动画,程序员通过 threejs 加载模型及其包含的帧动画数据
- 帧动画数据在 gltf 对象的动画属性
.animations
中获取,结构是一个数组,无则为空数组
js
const loader = new GLTFLoader();
loader.load("../工厂.glb", function (gltf) {
console.log("动画数据", gltf.animations);
});
播放动画
在渲染循环中更新播放器时间
js
let mixer = null; //声明一个播放器变量
loader.load("../工厂.glb", function (gltf) {
model.add(gltf.scene);
//包含帧动画的模型作为参数创建一个播放器
mixer = new THREE.AnimationMixer(gltf.scene);
// 获取gltf.animations[0]的第一个clip动画对象
const clipAction = mixer.clipAction(gltf.animations[0]); //创建动画clipAction对象
clipAction.play(); //播放动画
});
// 创建一个时钟对象Clock
const clock = new THREE.Clock();
function render() {
requestAnimationFrame(render);
if (mixer !== null) {
//clock.getDelta()方法获得两帧的时间间隔
// 更新播放器相关的时间
mixer.update(clock.getDelta());
}
}
render();
变形动画原理
原理:定义要变形的模型形状,获取其顶点位置坐标。将原有模型顶点位置替换为要变形的顶点坐标
.morphAttributes
设置几何体变形目标顶点数据
js
//几何体两组顶点一一对应,位置不同
const geometry = new THREE.BoxGeometry(50, 50, 50);
// 为geometry提供变形目标的顶点数据(注意和原始geometry顶点数量一致)
const target1 = new THREE.BoxGeometry(50, 200, 50).attributes.position; //变高
const target2 = new THREE.BoxGeometry(10, 50, 10).attributes.position; //变细
// 几何体顶点变形目标数据,可以设置1组或多组
geometry.morphAttributes.position = [target1, target2];
const mesh = new THREE.Mesh(geometry, material);
注意:
给一个几何体 geometry 设置顶点变形数据.morphAttributes
时候,要在执行代码new THREE.Mesh()
之前设置,否则报错
.morphTargetInfluences
权重系数控制变形程度
- 网格(mesh)、点、线模型都有一个权重属性
.morphTargetInfluences
,表示变形目标影响权重,范围 0~1 - 该属性为一个数组,与
.morphAttributes.position
中变形目标相对应,通过索引可以指定影响的变形目标
js
//权重1:物体形状对应target1表示形状
mesh.morphTargetInfluences[0] = 1.0;
//权重0.5:物体形状对应geometry和target1变形中间状态
mesh.morphTargetInfluences[0] = 0.5;
多个变形目标综合影响模型形状
js
// 两个变形目标同时影响模型形状
mesh.morphTargetInfluences[1] = 0.5;
mesh.morphTargetInfluences[0] = 0.5;
生成变形动画
创建变形动画权重系数的关键帧数据:
js
mesh.name = "Box";
// 设置变形目标1对应权重随着时间的变化
const KF1 = new THREE.KeyframeTrack("Box.morphTargetInfluences[0]", [0, 5], [0, 1]);
// 设置变形目标2对应权重随着时间的变化
const KF2 = new THREE.KeyframeTrack("Box.morphTargetInfluences[1]", [5, 10], [0, 1]);
// 创建一个剪辑clip对象
const clip = new THREE.AnimationClip("t", 10, [KF1, KF2]);
其余播放器控制代码和之前设置相同,此处省略。通常开发中,由美术在三维软件中先设置好变形的关键帧动画,开发只负责调用播放
骨骼关节 Bone
骨骼关节 Bone
树结构
模拟人或动物的关节运动,控制身体表面变形,来生成骨骼动画
js
const Bone1 = new THREE.Bone(); //关节1,用来作为根关节
const Bone2 = new THREE.Bone(); //关节2
const Bone3 = new THREE.Bone(); //关节3
// 设置关节父子关系 多个骨头关节构成一个树结构
Bone1.add(Bone2);
Bone2.add(Bone3);
设置关节模型的位置和姿态角度
js
//根关节Bone1默认位置是(0,0,0)
Bone2.position.y = 60; //Bone2相对父对象Bone1位置
Bone3.position.y = 30; //Bone3相对父对象Bone2位置
//平移Bone1,Bone2、Bone3跟着平移
Bone1.position.set(50, 0, 50);
js
// 骨骼关节旋转
Bone1.rotateX(Math.PI / 6);
Bone2.rotateX(Math.PI / 6);
SkeletonHelper
可视化骨骼关节
js
// 骨骼关节可以和普通网格模型一样作为其他模型子对象,添加到场景中
const group = new THREE.Group();
group.add(Bone1);
// SkeletonHelper会可视化参数模型对象所包含的所有骨骼关节
const skeletonHelper = new THREE.SkeletonHelper(group);
group.add(skeletonHelper);
查看外部模型骨骼动画
可视化外部模型骨骼关节
js
const loader = new GLTFLoader();
loader.load("../骨骼动画.glb", function (gltf) {
model.add(gltf.scene);
// 骨骼辅助显示
const skeletonHelper = new THREE.SkeletonHelper(gltf.scene);
model.add(skeletonHelper);
});
根据骨骼名称读取骨骼关节
js
// 在三维软件中,骨骼关节层层展开,可以看到下面三个骨骼关节
const bone1 = gltf.scene.getObjectByName("Bone1"); //关节1
const bone2 = gltf.scene.getObjectByName("Bone2"); //关节2
骨骼网格模型SkinnedMesh
表示骨骼动画模型的外表面,可以跟着自己的骨架.skeleton 变化
js
// 根据节点名字获取某个骨骼网格模型
const SkinnedMesh = gltf.scene.getObjectByName("身体");
骨骼网格模型的骨架SkinnedMesh.skeleton
js
// 根据节点名字获取某个骨骼网格模型
const SkinnedMesh = gltf.scene.getObjectByName("身体");
console.log("骨架", SkinnedMesh.skeleton);
- 骨架的骨骼关节属性
.skeleton.bones
骨架SkinnedMesh.skeleton
的关节属性.bones
是一个数组包含了所有骨骼关节,可以层层展开下去,查看它的子孙关节
js
console.log("骨架所有关节", SkinnedMesh.skeleton.bones);
console.log("根关节", SkinnedMesh.skeleton.bones[0]);
骨骼动画不同动作切换
一个模型可能定义了多个动画状态如:跑、走、禁止等,对这些状态进行切换有如下两种方法:
- 切换动画的播放与暂停
- 改变动画权重属性
.weight
方法二有点繁琐,具体自行参考官网上的做法
js
loader.load("./士兵.glb", function (gltf) {
model.add(gltf.scene);
// 播放器
const mixer = new THREE.AnimationMixer(gltf.scene);
const actionObj = {
IdleAction: mixer.clipAction(gltf.animations[0]),
RunAction: mixer.clipAction(gltf.animations[1]),
TposeAction: mixer.clipAction(gltf.animations[2]),
WalkAction: mixer.clipAction(gltf.animations[3]),
};
actionObj.IdleAction.play();
let ActionState = actionObj.IdleAction; //当前处于播放状态的动画动作对象
const obj = { status: "IdleAction" };
gui
.add(obj, "status", {
直立: "IdleAction",
跑: "RunAction",
静止: "TposeAction",
走: "WalkAction",
})
.name("动作")
.onChange((val) => {
ActionState.stop(); //播放状态动画终止
actionObj[val].play();
ActionState = actionObj[val];
});
const clock = new THREE.Clock();
function loop() {
requestAnimationFrame(loop);
const frameT = clock.getDelta();
// 更新播放器相关的时间
mixer.update(frameT);
}
loop();
});