Skip to content

一、骨骼动画、变形动画

骨骼动画和变形动画是非常常见的动画形式,对于three.ts三维引擎来说自然要支持。 一般开发过程中都是加载3D美术绘制好的骨骼动画模型或者变形动画模型。

skin-01

  • 所谓骨骼动画,以人体为例简单地说,人体的骨骼运动,骨骼运动会带动肌肉和人体皮肤的空间移动和表面变化, 下面将会提到的蒙皮概念你可以类比人体的皮肤。
  • three.ts骨骼动画需要通过骨骼网格模型类SkinnedMesh来实现, 一般来说骨骼动画模型都是3D美术创建,然后程序员通过three.ts引擎加载解析, 为了让大家更深入理解骨骼动画,下面就通过three.ts程序编写一个简易的骨骼动画。

1. 相关类

  • 直接使用three.ts编写一个骨骼动画还是比较复杂的,首先应该了解骨头关节Bone、 骨骼网格模型SkinnedMesh、骨架对象Skeleton这三个骨骼相关的类, 除此之外还需要了解几何体Geometry和骨骼动画相关的顶点数据。
  • Bone
    • 通过Bone类可以实例化一个骨关节对象,然后通过多个骨关节对象可以构成一个骨骼层级系统, Bone基类是Object3D,可以通过add方法给一个骨关节对象Bone添加一个子骨关节Bone
      javascript
      const Bone1 = new THREE.Bone(); //关节1,用来作为根关节
      const Bone2 = new THREE.Bone(); //关节2
      const Bone3 = new THREE.Bone(); //关节3
      // 设置关节父子关系   多个骨头关节构成一个树结构
      Bone1.add(Bone2);
      Bone2.add(Bone3);
      // 设置关节之间的相对位置
      //根关节Bone1默认位置是(0,0,0)
      Bone2.position.y = 60; //Bone2相对父对象Bone1位置
      Bone3.position.y = 40; //Bone3相对父对象Bone2位置

2. 骨架Skeleton

  • 通过Skeleton类可以把所有骨关节对象Bone包含进来。
    javascript
      // 所有Bone对象插入到Skeleton中,全部设置为.bones属性的元素
      const skeleton = new THREE.Skeleton([Bone1, Bone2, Bone3]); //创建骨骼系统
      // 查看.bones属性中所有骨关节Bone
      console.log(skeleton.bones);
      // 返回所有关节的世界坐标
      skeleton.bones.forEach(elem => {
        console.log(elem.getWorldPosition(new THREE.Vector3()));
      });
  • Geometry(.skinWeights和.skinIndices属性)
    • 几何体Geometry的属性.skinWeights.skinIndices主要作用是用来设置几何体的顶点位置 是如何受骨关节运动影响的。比如几何体Geometry的顶点位置数据是你皮肤上的一个个点位, 如果骨关节运动了,皮肤外形会跟着变化,就相当于Geometry的顶点坐标需要跟着骨关节变化, 这时候需要注意,关节外面包裹的一层皮肤,不同区域变形程度不同,那也就是说如果骨关节Bone变化了, 几何体Geometry顶点要像皮肤一样不同区域的顶点变化程度不同。这也正是.skinWeights.skinIndices属性出现的原因, .skinWeights的字面意思就是设置骨骼蒙皮的权重。

3. 骨骼网格模型SkinnedMesh

  • SkinnedMesh类的字面意思就是骨骼网格模型,骨骼网格模型SkinnedMesh的基类是普通网格模型MeshSkinnedMeshMesh一样都是网格模型,只是一个有骨骼动画功能,一个没有骨骼动画功能。
  • 骨骼网格模型SkinnedMesh绑定骨骼系统。
    javascript
    //骨骼关联网格模型
    SkinnedMesh.add(Bone1); //根骨头关节添加到网格模型
    SkinnedMesh.bind(skeleton); //网格模型绑定到骨骼系统

4. 创建一个骨骼动画

  • 通过SkinnedMesh构造函数创建一个骨骼动画
    javascript
    /**
     * 创建骨骼网格模型SkinnedMesh
     */
    // 创建一个圆柱几何体,高度120,顶点坐标y分量范围[-60,60]
    const geometry = new THREE.CylinderGeometry(5, 10, 120, 50, 300);
    geometry.translate(0, 60, 0); //平移后,y分量范围[0,120]
    console.log("name", geometry.vertices); //控制台查看顶点坐标
    //
    /**
     * 设置几何体对象Geometry的蒙皮索引skinIndices、权重skinWeights属性
     * 实现一个模拟腿部骨骼运动的效果
     */
    //遍历几何体顶点,为每一个顶点设置蒙皮索引、权重属性
    //根据y来分段,0~60一段、60~100一段、100~120一段
    for (let i = 0; i < geometry.vertices.length; i++) {
      const vertex = geometry.vertices[i]; //第i个顶点
      if (vertex.y <= 60) {
        // 设置每个顶点蒙皮索引属性  受根关节Bone1影响
        geometry.skinIndices.push(new THREE.Vector4(0, 0, 0, 0));
        // 设置每个顶点蒙皮权重属性
        // 影响该顶点关节Bone1对应权重是1-vertex.y/60
        geometry.skinWeights.push(new THREE.Vector4(1 - vertex.y / 60, 0, 0, 0));
      } else if (60 < vertex.y && vertex.y <= 60 + 40) {
        // Vector4(1, 0, 0, 0)表示对应顶点受关节Bone2影响
        geometry.skinIndices.push(new THREE.Vector4(1, 0, 0, 0));
        // 影响该顶点关节Bone2对应权重是1-(vertex.y-60)/40
        geometry.skinWeights.push(new THREE.Vector4(1 - (vertex.y - 60) / 40, 0, 0, 0));
      } else if (60 + 40 < vertex.y && vertex.y <= 60 + 40 + 20) {
        // Vector4(2, 0, 0, 0)表示对应顶点受关节Bone3影响
        geometry.skinIndices.push(new THREE.Vector4(2, 0, 0, 0));
        // 影响该顶点关节Bone3对应权重是1-(vertex.y-100)/20
        geometry.skinWeights.push(new THREE.Vector4(1 - (vertex.y - 100) / 20, 0, 0, 0));
      }
    }
    // 材质对象
    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); //网格模型添加到场景中
    /**
     * 骨骼系统
     */
    const Bone1 = new THREE.Bone(); //关节1,用来作为根关节
    const Bone2 = new THREE.Bone(); //关节2
    const Bone3 = new THREE.Bone(); //关节3
    // 设置关节父子关系   多个骨头关节构成一个树结构
    Bone1.add(Bone2);
    Bone2.add(Bone3);
    // 设置关节之间的相对位置
    //根关节Bone1默认位置是(0,0,0)
    Bone2.position.y = 60; //Bone2相对父对象Bone1位置
    Bone3.position.y = 40; //Bone3相对父对象Bone2位置
    // 所有Bone对象插入到Skeleton中,全部设置为.bones属性的元素
    const skeleton = new THREE.Skeleton([Bone1, Bone2, Bone3]); //创建骨骼系统
    // console.log(skeleton.bones);
    // 返回所有关节的世界坐标
    // skeleton.bones.forEach(elem => {
    //   console.log(elem.getWorldPosition(new THREE.Vector3()));
    // });
    //骨骼关联网格模型
    SkinnedMesh.add(Bone1); //根骨头关节添加到网格模型
    SkinnedMesh.bind(skeleton); //网格模型绑定到骨骼系统
    console.log(SkinnedMesh);
    /**
     * 骨骼辅助显示
     */
    const skeletonHelper = new THREE.SkeletonHelper(SkinnedMesh);
    scene.add(skeletonHelper);
    // 转动关节带动骨骼网格模型出现弯曲效果  好像腿弯曲一样
    skeleton.bones[1].rotation.x = 0.5;
    skeleton.bones[2].rotation.x = 0.5;

5. 实现骨骼动画

  • 通过骨骼骨骼系统代码实现骨骼动画效果。
    javascript
    const 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 = skeleton.bones[0].rotation.x - step;
        skeleton.bones[1].rotation.x = skeleton.bones[1].rotation.x + step;
        skeleton.bones[2].rotation.x = skeleton.bones[2].rotation.x + 2 * step;
      }
      if (n < 2 * T && n > T) {
        skeleton.bones[0].rotation.x = skeleton.bones[0].rotation.x + step;
        skeleton.bones[1].rotation.x = skeleton.bones[1].rotation.x - step;
        skeleton.bones[2].rotation.x = skeleton.bones[2].rotation.x - 2 * step;
      }
      if (n === 2 * T) {
        n = 0;
      }
    }
    render();

6. 解析外部骨骼动画模型

  • 骨骼动画除了需要创建一个骨骼动画模型SkinnedMesh外,还需要通过帧动画存储相关的关节动画数据。

    javascript
    const mixer = null; //声明一个混合器变量
    loader.load("./marine_anims_core.json", function(obj) {
      console.log(obj)
      scene.add(obj); //添加到场景中
      //从返回对象获得骨骼网格模型
      const SkinnedMesh = obj.children[0];
      //骨骼网格模型作为参数创建一个混合器
      mixer = new THREE.AnimationMixer(SkinnedMesh);
      // 查看骨骼网格模型的帧动画数据
      // console.log(SkinnedMesh.geometry.animations)
      // 解析跑步状态对应剪辑对象clip中的关键帧数据
      const AnimationAction = mixer.clipAction(SkinnedMesh.geometry.animations[1]);
      // 解析步行状态对应剪辑对象clip中的关键帧数据
      // const AnimationAction = mixer.clipAction(SkinnedMesh.geometry.animations[3]);
      AnimationAction.play();
      // 骨骼辅助显示
      // const skeletonHelper = new THREE.SkeletonHelper(SkinnedMesh);
      // scene.add(skeletonHelper);
    })
    javascript
    // 创建一个时钟对象Clock
    const clock = new THREE.Clock();
    // 渲染函数
    function render() {
      renderer.render(scene, camera);
      requestAnimationFrame(render);
      if (mixer !== null) {
        //clock.getDelta()方法获得两帧的时间间隔
        // 更新混合器相关的时间
        mixer.update(clock.getDelta());
      }
    }
    render();
  • 皮肤顶点权重属性.skinWeights

    • .skinWeights表示的是几何体顶点权重数据,当使用骨骼动画网格模型SkinnedMesh的时候, 每个顶点最多可以有4个骨关节Bone影响它。skinWeights属性是一个权重值数组,对应于几何体中顶点的顺序。 例如,第一个skinWeight将对应于几何体中的第一个顶点。由于每个顶点可以被4个骨关节Bone修改, 因此使用四维向量对象Vector4表示一个顶点的权重。

    • 四维向量Vector4每个分量的值通常应在01之间。当设置为0时, 骨关节Bone变换将不起作用;设置为0.5时,将产生50%的影响; 设置为100%时,会产生100%的影响。 如果只有一个骨关节Bone与顶点关联, 那么只需要考虑设置四维向量Vector4的第一个分量,其余分量的可以忽略并设置为0

  • 顶点索引属性.skinIndices

    • 顶点索引属性.skinIndices就像skinWeights属性一样,skinIndices的值对应几何体的顶点。 每个顶点最多可以有4个与之关联的骨关节Bone
  • 骨骼动画顶点数据

    • 骨骼动画的相关的一些顶点数据,主要是描述几何体表面的顶点数据是如何受骨骼系统关节运动影响的。 加载外部模型你可以访问骨骼动画网格模型的几何体对象查看骨骼动画相关顶点数据。 网格模型的几何体类型可能是Geometry,也可能是BufferGeometry, 一般是缓冲类型的几何体BufferGeometry比较常见。
    • Geometry的骨骼动画顶点数据,直接查看.skinWeights.skinIndices属性。
      javascript
      console.log('骨骼动画皮肤顶点权重数据',SkinnedMesh.geometry.skinWeights);
      console.log('骨骼动画皮肤顶点索引数据',SkinnedMesh.geometry.skinIndices);
    • BufferGeometry的骨骼动画顶点数据,熟悉BufferGeometry的结构,应该都知道该几何体通过 .attributes属性存储各种顶点数据。
      javascript
      console.log('骨骼动画皮肤顶点权重数据',SkinnedMesh.geometry.attributes.skinWeights);
      console.log('骨骼动画皮肤顶点索引数据',SkinnedMesh.geometry.attributes.skinIndices);

Released under the MIT License.