骨骼原理及源码

429 阅读5分钟

Bone

Bone和骨骼动画的本质

从代码的角度,Bone是从Object3D派生来的,而且在渲染时,不需要给Bone提供顶点属性(即、BufferGeometry),Bone在渲染中起的作用,是对顶点再次进行变换。

在渲染有骨骼的对象时,需要使用SkinnedMesh,它本身也是一个Object3D,它也有变换矩阵,这个变换,是要把局部空间的顶点转换到世界空间。

公式1Positionworld=MatrixWorldSkinedMesh×LocalPosition公式1:{Position}_{world}={MatrixWorld}_{SkinedMesh}\times{LocalPosition}

然后,再通过Bone进行一次变换。但是在Bone变换之前,要把世界空间坐标转换到Bone变换之前的空间(这里我们记作originorigin)。

公式2Positionworld=MatrixWorldBone×MatrixWorldBoneorigin1×Positionworld公式2:{Position}_{world}^{'}={MatrixWorld}_{Bone}\times{MatrixWorld}_{{Bone}_{origin}}^{-1}\times{Position}_{world}

MatrixWorldBoneorigin1{MatrixWorld}^{-1}_{{Bone}_{origin}}Bone初始状态变换矩阵的逆矩阵。我们要先把世界空间下的坐标,变换到Bone没有进行变换时的空间中,之后,再对Bone空间进行变换,这时,顶点也就跟着一起进行变换。

通过上面的公式可以看到,在初始状态下MatrixWorldBone=MatrixWorldBoneorigin{MatrixWorld}_{Bone}={MatrixWorld}_{{Bone}_{origin}}

Bone进行变换时MatrixWorldBone{MatrixWorld}_{Bone}就不再和初始的矩阵相等了,表现为“顶点随着骨骼进行变换”,这时,就是动画的一帧,所以,骨骼动画其实时MatrixWorldBone{MatrixWorld}_{Bone}的动画。

一个顶点可以被多个Bone进行变换

一个顶点可以“绑定”多个骨骼,在Three.js中,一个顶点最多可以“绑定”4个Bone变换,可以通过给指定weight,控制每个Bone的影响程度。

这里,把上面公式1和公式2中间过程的变换记作Matrixi,(i表示第iBone)Matrix_{i},(i表示第i个Bone)

Positionworld=i=14Matrixi×LocalPosition×weighti{Position}_{world}^{'}=\sum_{i=1}^4{Matrix}_{i}\times{LocalPosition}\times{weight}_i

源码分析

对应公式1使用的矩阵:SkinnedMesh.bind()

bind( skeleton, bindMatrix ) {

  this.skeleton = skeleton;

  if ( bindMatrix === undefined ) {

    this.updateMatrixWorld( true );

    this.skeleton.calculateInverses();

    bindMatrix = this.matrixWorld;

  }

  this.bindMatrix.copy( bindMatrix );
  this.bindMatrixInverse.copy( bindMatrix ).invert();
}

这个方法很简单,就是把SkinnedMesh的世界矩阵,赋值给bindMatrix,用来把局部空间的坐标转换到世界空间。

对应公式2使用的矩阵:Skeleton.update()

update() {

  const bones = this.bones;
  const boneInverses = this.boneInverses;
  const boneMatrices = this.boneMatrices;
  const boneTexture = this.boneTexture;

  // flatten bone matrices to array

  for ( let i = 0, il = bones.length; i < il; i ++ ) {

    // compute the offset between the current and the original transform

    const matrix = bones[ i ] ? bones[ i ].matrixWorld : _identityMatrix;

    _offsetMatrix.multiplyMatrices( matrix, boneInverses[ i ] );
    _offsetMatrix.toArray( boneMatrices, i * 16 );

  }

  if ( boneTexture !== null ) {

    boneTexture.needsUpdate = true;

  }

}

上面其实最重要的是这两句:

_offsetMatrix.multiplyMatrices( matrix, boneInverses[ i ] );
_offsetMatrix.toArray( boneMatrices, i * 16 );

其实就是应用上面说的公式2,计算每个BoneBoneMatrix,这个矩阵用来吧骨骼变换前的世界坐标,转换到骨骼变换之后。

然后,把这矩阵放到一个boneMatrices,通过uniform发送给GPU。

最后,注意一下官方文档对这个方法的一段描述:

This is called automatically by the WebGLRenderer if the skeleton is used with a SkinnedMesh.

如果SkeletonSkinnedMesh一起使用,WebGLRenderer会自动调用这个update()方法。

最终运算:顶点着色器

这部分代码做了大量的删减,只留下我们关心的部分

// 这个矩阵就是SkinnedMesh.bind()中的bindMatrix
// 作用就是将局部空间的坐标转换到世界空间坐标
uniform mat4 bindMatrix;

// 这个数组就是Skeleton.update()中计算的boneMatrices
// 是所有Bone对象对应公式2中的变换矩阵
uniform mat4 boneMatrices[ MAX_BONES ];

// 从boneMatrices取出对应的Bone对象的的变换矩阵
mat4 getBoneMatrix( const in float i ) {
  mat4 bone = boneMatrices[ int(i) ];
  return bone;
}


void main() {

  // position是局部空间坐标 这里是为了转换成vec3类型
  vec3 transformed = vec3(position);

  // 取到该顶点绑定的4个bone对象的变换矩阵
  mat4 boneMatX = getBoneMatrix( skinIndex.x );
  mat4 boneMatY = getBoneMatrix( skinIndex.y );
  mat4 boneMatZ = getBoneMatrix( skinIndex.z );
  mat4 boneMatW = getBoneMatrix( skinIndex.w );

  // 应用公式1,把局部空间坐标转换到世界坐标
  vec4 skinVertex = bindMatrix * vec4( transformed, 1.0 );
  vec4 skinned = vec4( 0.0 );

  // 对4个bone矩阵分别应用公式2,然后分别乘weight,最后求和
  skinned += boneMatX * skinVertex * skinWeight.x;
  skinned += boneMatY * skinVertex * skinWeight.y;
  skinned += boneMatZ * skinVertex * skinWeight.z;
  skinned += boneMatW * skinVertex * skinWeight.w;
}

Skeleton

bones: Array<Bone>

The array of bones. Note this is a copy of the original array, not a reference, so you can modify the original array without effecting this one.

通过Array将骨架中的骨头保存起来,通过构造器传入的参数进行初始化,初始化时,并不是直接复制了引用,而是通过Array.prototype.slice(0)创建了一个副本,保证在Skeleton创建之后,外部修改bones时,Sekeleton内部不受影响。

constructor(bones, boneInverses) {
    // ...
    this.bones = bones.slice( 0 );
    //...
}

还要注意的一点就是,这里的bones属性不代表数据的逻辑结构Bone本身就是Object3D,因为Bone也有父子关系,Bone逻辑结构树形

boneInverses: Array<Matrix4>

An array of Matrix4s that represent the inverse of the matrixWorld of the individual bones.

代表每一个bone的世界矩阵的逆矩阵。

矩阵可以表示“姿态(pose)”

我们先不考虑逆矩阵(逆矩阵只是一种存储的方式,为了便于渲染过程中的计算),先来说一下matrixWorld矩阵。这个矩阵我们已经很熟悉了,通过matrixWorld矩阵可以把局部空间的顶点坐标转换到世界空间。

现在,每一个Bone都有一个matrixWorld,如果我们把每个Bone局部空间中的原点(0, 0, 0)通过自己的matrixWorld进行转换,就会把这些“原点”转换为世界空间中的点,然后,根据Bone的父子关系进行“连线”,最后,我们就得到了SkeletonHelper的绘制结果。(这一段也是SkeletonHelper的实现原理

从这个角度看,每个Bone局部空间的原点,可以看作是一个骨骼节点,父和子两个节点,构成一个“视觉上”的骨骼。

所以,变换矩阵本来就是描述平移、旋转、缩放,所以,它可以表示“姿态”。

pose()

Returns the skeleton to the base pose.

即,将“骨架”恢复到“基础姿态”。

通过上面的描述,如果接受了matrix就是姿态,我们也很容易想到,就是将Bone的局部变换矩阵matrix和世界变换矩阵matrixWorld都恢复到基础(base)状态。

boneInverses的逆矩阵,就是matrixWorld的基础状态,通过matrixWorld,我们也可以计算matrix

因为

MatrixWorldchild=MatrixWorldparent×MatrixLocalchild{MatrixWorld}_{child}={MatrixWorld}_{parent}\times{MatrixLocal}_{child}

所以

MatrixLocalchild=MatrixWorldparent1×MatrixWorldchild{MatrixLocal}_{child}={MatrixWorld}^{-1}_{parent}\times{MatrixWorld}_{child}

pose()也分为了两段:

  1. 通过boneInverses恢复matrixWorld

  2. 通过matrixWorld计算matrix,通过matrix,重新设置positionquaternionscale

pose() {

  // recover the bind-time world matrices

  for ( let i = 0, il = this.bones.length; i < il; i ++ ) {

    const bone = this.bones[ i ];

    if ( bone ) {
      // boneInverses存储的时逆矩阵
      // 恢复时,需要求逆矩阵
      bone.matrixWorld.copy( this.boneInverses[ i ] ).invert();
    }
  }

  // compute the local matrices, positions, rotations and scales

  for ( let i = 0, il = this.bones.length; i < il; i ++ ) {

    const bone = this.bones[ i ];

    if ( bone ) {

      if ( bone.parent && bone.parent.isBone ) {
        // 和我们上面推导的公式符合
        // 用父世界矩阵的逆矩阵叉乘自己的世界矩阵即可得到自己的局部矩阵
        bone.matrix.copy( bone.parent.matrixWorld ).invert();
        bone.matrix.multiply( bone.matrixWorld );
      } else {
        bone.matrix.copy( bone.matrixWorld );
      }
      // 通过matrix恢复position quaternion scale
      bone.matrix.decompose( bone.position, bone.quaternion, bone.scale );
    }
  }
}