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使用了这个术语.
从微软的文档能看到一个绝对正确的定义:html
public void SetBoneOffsetMatrix( int bone, Matrix boneTransform ); boneTransform Microsoft.DirectX.Matrix A Matrix object that represents the bone offset matrix.
AssimpNet中则注释道:git
/// <summary> /// 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. /// </summary>
这个注释最后一句明确说道: Bone Offset Matrix就是绑定姿式逆矩阵, 可是第一句却说, 这个矩阵是从骨骼空间到网格空间的一个变换. 有人甚至提交了一个问题: Offset matrix is wrong documented.
可是开发者显然没有打算修改这个注释, 他解释到:
github
这取决于你怎么看待变换, 在矩阵右乘的状况下, 你能够认为顶点进行了一次变换, 因此从网格空间到了骨骼空间. 可是在矩阵左乘的状况下, 你能够认为是空间进行了一次变化, 从骨骼空间到网格空间.
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是在绑定姿式下, 骨骼的逆转换矩阵, 这里的定义还只是含糊不清.
在示例代码中则有:this
// 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类注释以下:spa
/// <summary> /// 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. /// </summary>
明确表示了该矩阵是列主序的, 那么理论上就应该左乘向量.
对于TRS矩阵应该就有
TRS(t, r, s) = t * r * s
但实际上, 查看operator *的代码插件
/// <summary> /// Performs matrix multiplication. Multiplication order is B x A. That way, SRT concatenations /// are left to right. /// </summary> /// <param name="a">First matrix</param> /// <param name="b">Second matrix</param> /// <returns>Multiplied matrix</returns> 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
3d