Three.js 骨骼动画 SkinnedMesh 实现详解
所谓骨骼动画,以人体运动为例,是通过骨骼的位移与旋转带动 “皮肤” 变形的过程。这里的 “蒙皮” 概念可类比人体皮肤,而 Three.js 中的骨骼动画主要通过SkinnedMesh类实现。通常,3D 美术人员会创建骨骼动画模型,程序员再通过 Three.js 引擎加载解析。下面将从基础概念到实战代码,全面解析 Three.js 骨骼动画的实现原理。
核心相关类:骨骼动画的基础架构
若想理解 Three.js 骨骼动画,需先掌握三个核心类及相关顶点数据:
Bone(骨关节) :构成骨骼层级的基本单元,继承自Object3D,可通过add()方法构建父子关节关系。Skeleton(骨架) :整合所有Bone对象,形成完整的骨骼系统。SkinnedMesh(骨骼网格模型) :绑定骨骼系统与几何体,实现骨骼驱动网格变形的效果。Geometry(几何体) :通过.skinWeights(蒙皮权重)和.skinIndices(蒙皮索引)定义顶点受骨骼影响的方式。
Bone:构建骨骼层级树
Bone类用于创建单个骨关节,多个骨关节可组成树形结构的骨骼系统:
// 创建三个骨关节
const bone1 = new THREE.Bone(); // 根关节
const bone2 = new THREE.Bone(); // 子关节1
const bone3 = new THREE.Bone(); // 子关节2
// 建立父子关系(形成树结构)
bone1.add(bone2);
bone2.add(bone3);
// 设置关节相对位置(基于父关节坐标系)
bone2.position.y = 60; // bone2在bone1的y轴正方向60单位处
bone3.position.y = 40; // bone3在bone2的y轴正方向40单位处
Skeleton:整合骨骼系统
Skeleton类用于管理所有Bone对象,形成完整的骨骼系统:
// 将所有Bone对象存入Skeleton
const skeleton = new THREE.Skeleton([bone1, bone2, bone3]);
// 查看骨骼系统中的所有关节
console.log(skeleton.bones); // 输出[bone1, bone2, bone3]
// 获取所有关节的世界坐标
skeleton.bones.forEach(bone => {
const worldPos = new THREE.Vector3();
bone.getWorldPosition(worldPos);
console.log(worldPos);
});
Geometry:蒙皮权重与索引的核心作用
几何体的.skinWeights和.skinIndices属性决定了顶点如何随骨骼运动变形,类似人体皮肤随骨骼运动的不同区域形变程度:
.skinWeights:存储顶点受各关节影响的权重值(0-1 之间),每个顶点对应一个Vector4(最多 4 个关节影响)。.skinIndices:存储影响顶点的关节索引,与.skinWeights一一对应。
SkinnedMesh:绑定骨骼与网格的桥梁
SkinnedMesh需同时关联骨骼系统与几何体,并通过bind()方法建立驱动关系:
// 创建骨骼网格模型(传入几何体与材质)
const skinnedMesh = new THREE.SkinnedMesh(geometry, material);
// 关联骨骼系统
skinnedMesh.add(bone1); // 将根关节添加到网格模型
skinnedMesh.bind(skeleton); // 绑定骨架到网格模型
实战:用代码创建简易骨骼动画
以下代码通过SkinnedMesh实现一个模拟腿部弯曲的骨骼动画,核心是为几何体顶点设置蒙皮权重与索引,并通过骨骼旋转驱动变形:
1. 初始化几何体与蒙皮数据
// 创建圆柱几何体(模拟腿部)
const geometry = new THREE.CylinderGeometry(5, 10, 120, 50, 300);
geometry.translate(0, 60, 0); // 平移后顶点y范围为[0, 120]
// 为顶点设置蒙皮索引与权重(按y坐标分段)
for (let i = 0; i < geometry.vertices.length; i++) {
const vertex = geometry.vertices[i];
if (vertex.y <= 60) {
// 底部顶点受根关节bone1影响
geometry.skinIndices.push(new THREE.Vector4(0, 0, 0, 0));
geometry.skinWeights.push(new THREE.Vector4(1 - vertex.y / 60, 0, 0, 0));
} else if (vertex.y <= 100) {
// 中部顶点受bone2影响
geometry.skinIndices.push(new THREE.Vector4(1, 0, 0, 0));
geometry.skinWeights.push(new THREE.Vector4(1 - (vertex.y - 60) / 40, 0, 0, 0));
} else {
// 顶部顶点受bone3影响
geometry.skinIndices.push(new THREE.Vector4(2, 0, 0, 0));
geometry.skinWeights.push(new THREE.Vector4(1 - (vertex.y - 100) / 20, 0, 0, 0));
}
}
// 创建材质(需开启skinning)
const material = new THREE.MeshPhongMaterial({
skinning: true, // 允许蒙皮动画
// wireframe: true, // 可选:显示线框
});
// 创建骨骼网格模型并添加到场景
const skinnedMesh = new THREE.SkinnedMesh(geometry, material);
skinnedMesh.position.set(50, 120, 50);
skinnedMesh.rotateX(Math.PI); // 旋转模型使其垂直向下
scene.add(skinnedMesh);
2. 构建骨骼系统并绑定模型
// 创建三个关节并设置层级关系
const bone1 = new THREE.Bone(); // 根关节
const bone2 = new THREE.Bone(); // 膝盖
const bone3 = new THREE.Bone(); // 脚踝
bone1.add(bone2);
bone2.add(bone3);
// 设置关节相对位置
bone2.position.y = 60;
bone3.position.y = 40;
// 创建骨架并绑定到网格模型
const skeleton = new THREE.Skeleton([bone1, bone2, bone3]);
skinnedMesh.add(bone1);
skinnedMesh.bind(skeleton);
// 辅助显示骨骼(可选)
const skeletonHelper = new THREE.SkeletonHelper(skinnedMesh);
scene.add(skeletonHelper);
3. 实现骨骼动画循环
通过周期性改变关节旋转角度,实现腿部弯曲的动画效果:
let n = 0;
const T = 50; // 动画周期
const step = 0.01; // 旋转步长
function render() {
renderer.render(scene, camera);
requestAnimationFrame(render);
n += 1;
if (n < T) {
// 前半周期:关节向一个方向旋转
skeleton.bones[0].rotation.x -= step;
skeleton.bones[1].rotation.x += step;
skeleton.bones[2].rotation.x += 2 * step;
} else if (n < 2 * T) {
// 后半周期:关节向反方向旋转
skeleton.bones[0].rotation.x += step;
skeleton.bones[1].rotation.x -= step;
skeleton.bones[2].rotation.x -= 2 * step;
} else {
n = 0; // 重置周期
}
}
render();
加载外部骨骼动画模型
实际开发中,通常由 3D 软件导出骨骼动画模型(如 FBX、GLTF 格式),通过 Three.js 加载器解析:
1. 使用GLTFLoader加载模型并播放动画
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
let mixer = null; // 动画混合器
const loader = new GLTFLoader();
loader.load('./model.gltf', (gltf) => {
const model = gltf.scene;
scene.add(model);
// 获取骨骼网格模型(假设第一个子对象是SkinnedMesh)
const skinnedMesh = model.children.find(child => child.isSkinnedMesh);
if (skinnedMesh) {
// 创建动画混合器
mixer = new THREE.AnimationMixer(skinnedMesh);
// 解析动画剪辑(如跑步动画)
if (gltf.animations.length > 0) {
const clip = gltf.animations[0]; // 第一个动画剪辑
const action = mixer.clipAction(clip);
action.play(); // 播放动画
}
}
});
2. 动画更新循环
通过Clock计算时间间隔,更新动画混合器:
const clock = new THREE.Clock();
function render() {
renderer.render(scene, camera);
requestAnimationFrame(render);
if (mixer) {
const delta = clock.getDelta();
mixer.update(delta); // 更新动画
}
}
render();
深入理解:蒙皮权重与顶点索引
.skinWeights:顶点变形的 “力度控制器”
- 每个顶点最多受 4 个关节影响,权重值存储在
Vector4中(如new THREE.Vector4(w1, w2, w3, w4))。 - 权重值范围为
0-1:0表示不受影响,1表示完全受对应关节影响。 - 示例:若顶点仅受第一个关节影响,则权重设为
new THREE.Vector4(1, 0, 0, 0)。
.skinIndices:顶点与关节的 “映射表”
- 存储影响顶点的关节索引,与
.skinWeights一一对应。 - 示例:若顶点受索引为
1的关节影响,则索引设为new THREE.Vector4(1, 0, 0, 0)。
查看骨骼动画顶点数据
无论是Geometry还是BufferGeometry,均可通过以下方式查看蒙皮数据:
// 针对Geometry类型
console.log('蒙皮权重数据:', skinnedMesh.geometry.skinWeights);
console.log('蒙皮索引数据:', skinnedMesh.geometry.skinIndices);
// 针对BufferGeometry类型
console.log('蒙皮权重数据:', skinnedMesh.geometry.attributes.skinWeights);
console.log('蒙皮索引数据:', skinnedMesh.geometry.attributes.skinIndices);
总结与实践建议
Three.js 骨骼动画的核心在于通过SkinnedMesh绑定骨骼系统与几何体,利用蒙皮权重与索引定义顶点变形规则。实际开发中,建议:
-
复杂模型优先使用 3D 软件创建,通过加载器导入 Three.js。
-
调试时可启用
SkeletonHelper可视化骨骼结构。 -
动画混合器
AnimationMixer可处理多段动画的融合与切换。 -
蒙皮权重的精细调整是实现自然动画的关键,可通过工具或代码动态优化。
通过理解骨骼动画的底层原理,能更高效地处理角色动作、物体变形等复杂场景,为 3D 交互体验增添动态魅力。