Skip to content

关键帧动画

创建关键帧动画

创建

  1. 给需要设置关键帧动画的模型命名

    js
    mesh.name = "Box";
  2. KeyframeTrack设置关键帧数据

    位置:

    js
    const 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);

    颜色:

    js
    const colorKF = new THREE.KeyframeTrack("Box.material.color", [2, 5], [1, 0, 0, 0, 0, 1]);
  3. 创建关键帧动画AnimationClip

js
// 创建一个clip关键帧动画对象,命名"test",动画持续时间6s
const clip = new THREE.AnimationClip("test", 6, [posKF, colorKF]);

播放

  1. 播放器

    js
    //包含关键帧动画的模型对象作为AnimationMixer的参数创建一个播放器mixer
    const mixer = new THREE.AnimationMixer(mesh);
    const clipAction = mixer.clipAction(clip);
    //.play()控制动画播放,默认循环播放
    clipAction.play();
  2. 更新播放时间

    js
    const 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 播放模式为非循环模式,设置物体状态停留在动画结束,不回到开头状态

    js
    clipAction.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]);

骨骼动画不同动作切换

一个模型可能定义了多个动画状态如:跑、走、禁止等,对这些状态进行切换有如下两种方法:

  1. 切换动画的播放与暂停
  2. 改变动画权重属性.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();
});