Bone
Bone和骨骼动画的本质
从代码的角度,Bone
是从Object3D
派生来的,而且在渲染时,不需要给Bone
提供顶点属性(即、BufferGeometry
),Bone
在渲染中起的作用,是对顶点再次进行变换。
在渲染有骨骼的对象时,需要使用SkinnedMesh
,它本身也是一个Object3D
,它也有变换矩阵,这个变换,是要把局部空间的顶点转换到世界空间。
然后,再通过Bone
进行一次变换。但是在Bone
变换之前,要把世界空间坐标转换到Bone
变换之前的空间(这里我们记作)。
是Bone
初始状态变换矩阵的逆矩阵。我们要先把世界空间下的坐标,变换到Bone
没有进行变换时的空间中,之后,再对Bone
空间进行变换,这时,顶点也就跟着一起进行变换。
通过上面的公式可以看到,在初始状态下
当Bone
进行变换时就不再和初始的矩阵相等了,表现为“顶点随着骨骼进行变换”,这时,就是动画的一帧,所以,骨骼动画其实时的动画。
一个顶点可以被多个Bone进行变换
一个顶点可以“绑定”多个骨骼,在Three.js
中,一个顶点最多可以“绑定”4个Bone
变换,可以通过给指定weight
,控制每个Bone
的影响程度。
这里,把上面公式1和公式2中间过程的变换记作,
源码分析
对应公式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,计算每个Bone
的BoneMatrix
,这个矩阵用来吧骨骼变换前的世界坐标,转换到骨骼变换之后。
然后,把这些矩阵放到一个boneMatrices
,通过uniform
发送给GPU。
最后,注意一下官方文档对这个方法的一段描述:
This is called automatically by the WebGLRenderer if the skeleton is used with a SkinnedMesh.
如果Skeleton
与SkinnedMesh
一起使用,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
:
因为
所以
pose()
也分为了两段:
-
通过
boneInverses
恢复matrixWorld
-
通过
matrixWorld
计算matrix
,通过matrix
,重新设置position
、quaternion
、scale
。
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 );
}
}
}