1. 背景
由于某种原因, 需要提取某个使用LayaAir开发的应用里的模型. LayaAir本身是开源的, 所以读取模型数据过程并不困难. 使用AssimpNet很快就输出了正确的网格. 但是加入了骨骼之后, 模型立刻就毁了.
LayaAir模型中的一块数据叫做bindPoseDatas, 这块数据会保存到mesh._inverseBindPoses, 注释是绑定动作逆矩阵.
而这个矩阵无法简单的对应到AssimpNet中的Bone.OffsetMatrix, 尽管注释中写道也被称做inverse bind pose.
调查了一下LayaAir的导出方式, 它的工作流是先将模型导入到Unity, 然后通过插件将网格数据导出, 其中读取的是Mesh.bindposes.
至此出现的3个截然不同的术语, 我感到事情没有那么简单, 决定好好地调查一下绑定姿势到底是什么.
2. 蒙皮动画
在蒙皮动画中, 顶点不再只受到一个关节的控制, 而是受到1个或者多个骨骼的控制. 在关节动画中, 所有的动画操作都是对着关节空间进行的, 而网格挂在关节上, 所以关节空间也就是网格空间. 但是在蒙皮动画中, 所有的操作都是对着骨骼空间进行的, 那么这里就需要先进行一个从网格空间到骨骼空间的变换.
3. 绑定姿势
要完成这个变换, 就需要让网格与骨骼产生关联, 这个关联操作叫做绑定(Bind), 绑定时模型的动作就被叫做绑定姿势(Bind Pose), 大多数情况下绑定姿势是成T型的, 所以也叫做T-Pose. 绑定时骨骼基本上与模型的相关位置一一对应.绑定姿势是一个状态, 一般用B表示, 与之相对的是当前姿势(Current Pose), 一般用C表示.
4. 绑定姿势矩阵与逆绑定姿势矩阵
Game Engine Architecture(2nd Edition)在11.5.2.1定义:
绑定姿势矩阵(Bind Pose Matrix), 是在绑定姿势时从关节空间变换到模型空间的矩阵
绑定姿势逆矩阵(Inverse Bind Pose Matrix), 是在绑定姿势时, 从模型空间变换到关节空间的矩阵
这里提到的关节(Joint)就是骨骼.
注意到这里提到的变换是到模型空间.
对于蒙皮动画来说, 大多数情况下关节不再有意义, 所有的顶点都可以按照绑定姿势时在模型空间下的位置进行保存, 网格空间也就是模型空间.
但如果仍然保持了关节的结构, 那么就需要先将顶点从网格空间变换到模型空间.
5. Bone Offset Matrix
这是一个Direct X系的术语, 而assimp使用了这个术语.
从微软的文档能看到一个绝对正确的定义:
public void SetBoneOffsetMatrix( int bone, Matrix boneTransform ); boneTransform Microsoft.DirectX.Matrix A Matrix object that represents the bone offset matrix.
AssimpNet中则注释道:
////// Gets or sets the matrix that transforms from bone space to mesh space in bind pose. This matrix describes the /// position of the mesh in the local space of this bone when the skeleton was bound. Thus it can be used directly to determine a desired vertex /// position, given the world-space transform of the bone when animated, and the position of the vertex in mesh space. /// /// It is sometimes called an inverse-bind matrix or inverse-bind pose matrix. ///
这个注释最后一句明确说道: Bone Offset Matrix就是绑定姿势逆矩阵, 但是第一句却说, 这个矩阵是从骨骼空间到网格空间的一个变换. 有人甚至提交了一个问题: Offset matrix is wrong documented.
但是开发者显然没有打算修改这个注释, 他解释到:
这取决于你怎么看待变换, 在矩阵右乘的情况下, 你可以认为顶点进行了一次变换, 所以从网格空间到了骨骼空间. 但是在矩阵左乘的情况下, 你可以认为是空间进行了一次变化, 从骨骼空间到网格空间.
6. 加入乱战的Unity
Mesh.bindposes定义如下:
The bind poses. The bind pose at each index refers to the bone with the same index. The bind pose is the inverse of the transformation matrix of the bone, when the bone is in the bind pose.
bindpose是在绑定姿势下, 骨骼的逆转换矩阵, 这里的定义还只是含糊不清.
在示例代码中则有:
// The bind pose is bone's inverse transformation matrix // In this case the matrix we also make this matrix relative to the root // So that we can move the root game object around freely bindPoses[0] = bones[0].worldToLocalMatrix * transform.localToWorldMatrix;
bindPose是骨骼的逆转换矩阵, 在这里我们可以让这个矩阵相对与root, 这样我们就能自由地移动root物件了.
然后再结合bindpose定义这篇文章, 简直完美匹配.
这些式子把模型空间给抛弃了, 引入了一个世界空间.
并且把bind pose的定义改成了网格空间到骨骼空间的变换, 而不是模型空间到骨骼空间的变换.
7. AssimpNet的巨坑
尽管现在可以确认Inverse Bind Pose, Bone Offset Matrix定义是一致的, 但是并不代表可以直接使用这个矩阵.
矩阵是左乘还是右乘, 旋转是左手法则还是右手法则, 对矩阵都是产生影响的.
观察到网格和骨骼的位置已经是一一对应了, 我决定直接计算绑定姿势逆矩阵.
但是怎么尝试都不对, 然后发现了AssimpNet的一个巨坑.
AssimpNet中Matrix类注释如下:
////// Represents a 4x4 column-vector matrix (X base is the first column, Y base is the second, Z base the third, and translation the fourth). /// Memory layout is row major. Right handed conventions are used by default. ///
明确表示了该矩阵是列主序的, 那么理论上就应该左乘向量.
对于TRS矩阵应该就有
TRS(t, r, s) = t * r * s
但实际上, 查看operator *的代码
////// Performs matrix multiplication. Multiplication order is B x A. That way, SRT concatenations /// are left to right. /// /// First matrix /// Second matrix /// Multiplied matrix public static Matrix4x4 operator *(Matrix4x4 a, Matrix4x4 b) { return new Matrix4x4( a.A1 * b.A1 + a.B1 * b.A2 + a.C1 * b.A3 + a.D1 * b.A4, a.A2 * b.A1 + a.B2 * b.A2 + a.C2 * b.A3 + a.D2 * b.A4, a.A3 * b.A1 + a.B3 * b.A2 + a.C3 * b.A3 + a.D3 * b.A4, a.A4 * b.A1 + a.B4 * b.A2 + a.C4 * b.A3 + a.D4 * b.A4, a.A1 * b.B1 + a.B1 * b.B2 + a.C1 * b.B3 + a.D1 * b.B4, a.A2 * b.B1 + a.B2 * b.B2 + a.C2 * b.B3 + a.D2 * b.B4, a.A3 * b.B1 + a.B3 * b.B2 + a.C3 * b.B3 + a.D3 * b.B4, a.A4 * b.B1 + a.B4 * b.B2 + a.C4 * b.B3 + a.D4 * b.B4, a.A1 * b.C1 + a.B1 * b.C2 + a.C1 * b.C3 + a.D1 * b.C4, a.A2 * b.C1 + a.B2 * b.C2 + a.C2 * b.C3 + a.D2 * b.C4, a.A3 * b.C1 + a.B3 * b.C2 + a.C3 * b.C3 + a.D3 * b.C4, a.A4 * b.C1 + a.B4 * b.C2 + a.C4 * b.C3 + a.D4 * b.C4, a.A1 * b.D1 + a.B1 * b.D2 + a.C1 * b.D3 + a.D1 * b.D4, a.A2 * b.D1 + a.B2 * b.D2 + a.C2 * b.D3 + a.D2 * b.D4, a.A3 * b.D1 + a.B3 * b.D2 + a.C3 * b.D3 + a.D3 * b.D4, a.A4 * b.D1 + a.B4 * b.D2 + a.C4 * b.D3 + a.D4 * b.D4); }
注释中很令人无语地写道: a * b的含义是b * a, 你应该从左向右的对SRT做乘法
所有对矩阵进行计算的地方都需要注意, 除了TRS, 还有计算节点的LocalToWorld, 公式是:
ThisNode.LocalToWorld = RootNode.Transform * ChildNode1.Transform * … * ThisNode.Tranform
但实际代码应该反过来写成:
ThisNode.LocalToWorld = ThisNode.Transform * … * ChildNode1.Transform * RootNode.Transform