Skip to content

45. 骨骼动画:关节带动顶点运动

Published:

我们学了关键帧动画、变形动画,这节来学下骨骼动画。

顾名思义,骨骼动画是基于骨架来运动的动画。

比如人跳舞的时候:

2025-04-06 22.31.54.gif

涉及到一堆顶点的复杂运动。

这些运动有啥规律么?

比如大腿抬起,会影响小腿也要跟着运动。

也就是说不同部位之间是有关联的,是一棵树。

我们定义一个骨架,改变一个骨头的位置的时候,就会让关联的骨头一起动。

这就是骨骼动画的思路。

我们写代码体验下 Three.js 的骨骼动画的 api:

npx create-vite bone-animation

image.png

进入项目,安装依赖:

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

image.png

是这样的:

image.png

因为 bone2 包含 bone3,那 bone3 的 position.y 就是在 bone2 的基础之上的,也就是 100 + 50。

打印下它的世界坐标:

image.png

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

image.png

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

比如这样:

image.png

第一个关节旋转 45 度

bone1.rotateZ(Math.PI/4);

image.png

其余两个关节自然会跟着动。

然后再让第二个关节旋转 -45 度:

image.png

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

image.png

那第三个关节也会跟着动。

根据这个规律就可以算出几何体顶点应该怎么去变化。

这种骨骼一般不会自己去写,都是在建模软件里搞好了,然后加载进来。

我们找一个 gltf 模型来看一下:

下载 three.js 的 github 仓库里的这个模型:

image.png

点击右边的下载按钮即可。

放到项目的 public 目录下:

image.png

创建 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;

引入看一下:

image.png

image.png

打开 devtools:

image.png

可以看到它有一棵关节树。

这个关节树是什么样的呢?

用 SkeletonHelper 可视化一下:

image.png

const helper = new THREE.SkeletonHelper(gltf.scene);
mesh.add(helper);

只要传入任意一个对象,SkeletonHelper 都会遍历找到它下面所有的关节展示出来。

看下效果:

image.png

关节树的位置和人体的位置对应。

这样关节动了就可以让那个位置的几何体顶点跟着关节一起动。

关联了关节的 Mesh 叫做 SkinnedMesh,就是会跟着关节动的网格模型。

整个人体就是这个 SkinnedMesh:

image.png

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

image.png

也就是说骨架的关节位置和人体的位置是对应的,这样关节动了,就可以让 SkinnedMesh 对应位置的顶点移动。

我找了一个关节,让它旋转下:

image.png

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

就是这样:

2025-04-06 23.23.03.gif

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

image.png

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 的位置。

看下效果:

2025-04-06 23.31.12.gif

可以看到,这就是骨骼动画。

当然,我们自己写的骨骼动画不大合理,一般都是建模软件里写好了,直接播放就行。

我们播放一下模型上的第一个动画:

image.png

2025-04-06 23.33.10.gif

这样就是一个比较合理的骨骼动画。

案例代码上传了小册仓库

总结

这节我们学习了骨骼动画,它会定义一个由关节 Bone 构成的骨架 Skeleton,这些关节和物体的身体位置一一对应。

这样当骨架运动的时候,就可以带动物体周围的顶点来一起运动。

有骨架的 Mesh 叫做 SkinnedMesh 蒙皮网格模型,它的 skeletons 属性定义了骨架。

当 Bone 运动的时候,SkinnedMesh 在关节部位的顶点就会跟着运动。

一般骨骼动画都是在建模软件里设置好,我们直接用 AnimationMixer 播放就好了。

评论