Three.js 骨骼动画 SkinnedMesh 实现详解

494 阅读6分钟

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-10表示不受影响,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绑定骨骼系统与几何体,利用蒙皮权重与索引定义顶点变形规则。实际开发中,建议:

  1. 复杂模型优先使用 3D 软件创建,通过加载器导入 Three.js。

  2. 调试时可启用SkeletonHelper可视化骨骼结构。

  3. 动画混合器AnimationMixer可处理多段动画的融合与切换。

  4. 蒙皮权重的精细调整是实现自然动画的关键,可通过工具或代码动态优化。

通过理解骨骼动画的底层原理,能更高效地处理角色动作、物体变形等复杂场景,为 3D 交互体验增添动态魅力。