我们学了关键帧动画、变形动画,这节来学下骨骼动画。
顾名思义,骨骼动画是基于骨架来运动的动画。
比如人跳舞的时候:

涉及到一堆顶点的复杂运动。
这些运动有啥规律么?
比如大腿抬起,会影响小腿也要跟着运动。
也就是说不同部位之间是有关联的,是一棵树。
我们定义一个骨架,改变一个骨头的位置的时候,就会让关联的骨头一起动。
这就是骨骼动画的思路。
我们写代码体验下 Three.js 的骨骼动画的 api:
npx create-vite bone-animation

进入项目,安装依赖:
npm install
npm install --save three
npm install --save-dev @types/three
改下 src/main.js
import './style.css';
import * as THREE from 'three';
import {
OrbitControls
} from 'three/addons/controls/OrbitControls.js';
import mesh from './mesh.js';
const scene = new THREE.Scene();
scene.add(mesh);
const directionLight = new THREE.DirectionalLight(0xffffff, 2);
directionLight.position.set(500, 400, 300);
scene.add(directionLight);
const ambientLight = new THREE.AmbientLight();
scene.add(ambientLight);
const width = window.innerWidth;
const height = window.innerHeight;
const helper = new THREE.AxesHelper(500);
scene.add(helper);
const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000);
camera.position.set(200, 300, 300);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height)
function render() {
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
document.body.append(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
创建 Scene、Light、Camera、Renderer。
改下 style.css
body {
margin: 0;
}
然后创建 mesh.js
import * as THREE from 'three';
const bone1 = new THREE.Bone();
const bone2 = new THREE.Bone();
const bone3 = new THREE.Bone();
bone1.add(bone2);
bone2.add(bone3);
bone1.position.x = 100;
bone2.position.y = 100;
bone3.position.y = 50;
const group = new THREE.Group();
group.add(bone1);
const skeletonHelper = new THREE.SkeletonHelper(group);
group.add(skeletonHelper);
export default group;
用 Bone 创建了 3 个关节,就像手臂一样。
然后用 SkeletonHelper 可视化。
跑一下:
npm run dev

是这样的:

因为 bone2 包含 bone3,那 bone3 的 position.y 就是在 bone2 的基础之上的,也就是 100 + 50。
打印下它的世界坐标:

const pos = new THREE.Vector3();
bone3.getWorldPosition(pos);
console.log(pos);

骨骼动画很容易理解,就像腿一样,大腿运动会带动小腿。
比如这样:

第一个关节旋转 45 度
bone1.rotateZ(Math.PI/4);

其余两个关节自然会跟着动。
然后再让第二个关节旋转 -45 度:

bone2.rotateZ(-Math.PI/4);

那第三个关节也会跟着动。
根据这个规律就可以算出几何体顶点应该怎么去变化。
这种骨骼一般不会自己去写,都是在建模软件里搞好了,然后加载进来。
我们找一个 gltf 模型来看一下:
下载 three.js 的 github 仓库里的这个模型:

点击右边的下载按钮即可。
放到项目的 public 目录下:

创建 mesh2.js
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader();
const mesh = new THREE.Group();
loader.load("./Michelle.glb", function (gltf) {
console.log(gltf);
mesh.add(gltf.scene);
gltf.scene.scale.set(100, 100, 100);
})
export default mesh;
引入看一下:


打开 devtools:

可以看到它有一棵关节树。
这个关节树是什么样的呢?
用 SkeletonHelper 可视化一下:

const helper = new THREE.SkeletonHelper(gltf.scene);
mesh.add(helper);
只要传入任意一个对象,SkeletonHelper 都会遍历找到它下面所有的关节展示出来。
看下效果:

关节树的位置和人体的位置对应。
这样关节动了就可以让那个位置的几何体顶点跟着关节一起动。
关联了关节的 Mesh 叫做 SkinnedMesh,就是会跟着关节动的网格模型。
整个人体就是这个 SkinnedMesh:

它的 skeleton 属性就是所有的骨架,下面的 blones 是关节数组:

也就是说骨架的关节位置和人体的位置是对应的,这样关节动了,就可以让 SkinnedMesh 对应位置的顶点移动。
我找了一个关节,让它旋转下:

gltf.scene.traverse(obj => {
if(obj.isBone && obj.name === "mixamorigSpine2") {
obj.rotateX(-Math.PI / 3);
}
})
就是这样:

我们可以通过关键帧来定义骨骼动画:

const track1 = new THREE.KeyframeTrack('mixamorigSpine2.position', [0, 3], [0, 0, 0, 0, 0, 30]);
const clip = new THREE.AnimationClip("bbb", 3, [track1]);
const mixer = new THREE.AnimationMixer(mesh);
const clipAction = mixer.clipAction(clip);
clipAction.play();
const clock = new THREE.Clock();
function render() {
const delta = clock.getDelta();
mixer.update(delta);
requestAnimationFrame(render);
}
render();
我定义了一个关键帧动画,0 到 3 秒,这个关节的位置往前运动到 0,0,30 的位置。
看下效果:

可以看到,这就是骨骼动画。
当然,我们自己写的骨骼动画不大合理,一般都是建模软件里写好了,直接播放就行。
我们播放一下模型上的第一个动画:


这样就是一个比较合理的骨骼动画。
案例代码上传了小册仓库。
总结
这节我们学习了骨骼动画,它会定义一个由关节 Bone 构成的骨架 Skeleton,这些关节和物体的身体位置一一对应。
这样当骨架运动的时候,就可以带动物体周围的顶点来一起运动。
有骨架的 Mesh 叫做 SkinnedMesh 蒙皮网格模型,它的 skeletons 属性定义了骨架。
当 Bone 运动的时候,SkinnedMesh 在关节部位的顶点就会跟着运动。
一般骨骼动画都是在建模软件里设置好,我们直接用 AnimationMixer 播放就好了。