使用assimp的骨骼动画中的矩阵顺序

matrix order in skeletal animation using assimp

我遵循了 this 教程并按预期获得了装配模型的输出动画。本教程使用 assimp、glsl 和 c++ 从文件加载装配模型。但是,有些事情我无法弄清楚。 首先,assimp 的变换矩阵是行主矩阵,本教程使用 Matrix4f class,它按原样使用这些变换矩阵,即行主阶。 Matrix4f class 的构造函数如下所示:

Matrix4f(const aiMatrix4x4& AssimpMatrix)
{
    m[0][0] = AssimpMatrix.a1; m[0][2] = AssimpMatrix.a2; m[0][2] = AssimpMatrix.a3; m[0][3] = AssimpMatrix.a4;
    m[1][0] = AssimpMatrix.b1; m[1][3] = AssimpMatrix.b2; m[1][2] = AssimpMatrix.b3; m[1][3] = AssimpMatrix.b4;
    m[2][0] = AssimpMatrix.c1; m[2][4] = AssimpMatrix.c2; m[2][2] = AssimpMatrix.c3; m[2][3] = AssimpMatrix.c4;
    m[3][0] = AssimpMatrix.d1; m[3][5] = AssimpMatrix.d2; m[3][2] = AssimpMatrix.d3; m[3][3] = AssimpMatrix.d4;
}

然而,在计算最终节点变换的教程中,计算是在期望矩阵按列主顺序排列的情况下完成的,如下所示:

Matrix4f NodeTransformation;
NodeTransformation = TranslationM * RotationM * ScalingM;  //note here
Matrix4f GlobalTransformation = ParentTransform * NodeTransformation;

    if(m_BoneMapping.find(NodeName) != m_BoneMapping.end())
{
    unsigned int BoneIndex = m_BoneMapping[NodeName];
    m_BoneInfo[BoneIndex].FinalTransformation = m_GlobalInverseTransform * GlobalTransformation * m_BoneInfo[BoneIndex].BoneOffset;
m_BoneInfo[BoneIndex].NodeTransformation = GlobalTransformation;
}

最后,由于计算的矩阵是按行优先顺序排列的,因此在着色器中传递矩阵时通过在以下函数中设置 GL_TRUE 标志来指定。然后,openGL 知道它是行主顺序,因为 openGL 本身使用列主顺序。

void SetBoneTransform(unsigned int Index, const Matrix4f& Transform)
{
glUniformMatrix4fv(m_boneLocation[Index], 1, GL_TRUE, (const GLfloat*)Transform);
}

那么,考虑列主序的计算是如何完成的

transformation = translation * rotation * scale * vertices

产生正确的输出。我预计为了计算成立,首先应该将每个矩阵转置为列顺序,然后进行上述计算,最后再次转置以获得后排顺序矩阵,这也在link中讨论。但是,这样做会产生可怕的输出。我在这里遗漏了什么吗?

你混淆了两个不同的东西:

  1. 数据在内存中的布局(行与列的主要顺序)
  2. 运算的数学解释(比如乘法顺序)

人们经常声称,在处理行专业与列专业时,必须转置事物并且必须颠倒矩阵乘法顺序。 但这不是真的

从数学上讲,transpose(A*B) = transpose(B) * transpose(A) 是正确的。然而,这在这里无关紧要,因为矩阵存储顺序独立于矩阵的数学解释,并且正交于矩阵的数学解释。

我的意思是:在数学中,矩阵的行和列是明确定义的,每个元素都可以通过这两个来唯一寻址"coordinates"。所有的矩阵运算都是基于这个约定定义的。例如C=A*B中,C的第一行第一列的元素计算为A的第一行的点积(转置为列向量)和 B.

的第一列

现在,矩阵存储顺序只是定义了矩阵数据在内存中的布局方式。作为一般化,我们可以定义一个函数 f(row,col) 将每个 (row, col) 对映射到某个内存地址。我们现在可以使用 f 编写或矩阵函数,并且我们可以更改 f 以适应行优先、列优先或完全不同的东西(比如 Z 顺序曲线,如果我们想要一些乐趣)。

没关系我们实际使用什么f(只要映射是双射的),操作C=A*B总是有同样的结果。改变的只是内存中的数据,但我们还必须使用 f 来插入该数据。我们可以只编写一个简单的打印函数,也使用 f,将矩阵打印为列 x 行的二维数组,就像一般人所期望的那样。

当您在与矩阵函数的实现设计不同的布局中使用矩阵时,就会产生混淆。

如果您有一个内部采用列优先布局的矩阵库,并以行优先格式传入数据,就好像您之前转换过该矩阵一样 - 只有在这一点上,事情才搞砸了.

更令人困惑的是,还有一个与此相关的问题:矩阵*向量与向量*矩阵问题。有些人喜欢写 x' = x * Mv'v 是行向量),而其他人喜欢写 y' = N *y(列向量)。很明显,从数学上讲,M*x = transpose((transpose(x) * transpose(M)),因此人们经常也会将其与行主序与列主序效应混淆——但它也完全独立于此。如果您想使用其中一个,这只是 约定 的问题。

所以,最后回答你的问题:

那里创建的变换矩阵是按照乘法矩阵*向量的约定编写的,因此Mparent * Mchild是正确的矩阵乘法顺序。

到目前为止,内存中的实际数据布局根本不重要。它只是开始变得重要,因为现在,我们正在连接一个不同的 API,它有自己的约定。 GL 的默认顺序是列优先。使用的矩阵 class 是为行优先内存布局编写的。所以您此时只需转置,以便 GL 对该矩阵的解释与您的其他库的解释相匹配。

另一种方法是不转换它们并通过将由此创建的隐式操作合并到系统中来解决这个问题 - 通过更改着色器中的乘法顺序,或者通过调整在第一个中创建矩阵的操作地方。但是,我不建议走那条路,因为生成的代码将完全不直观,因为最终,这将意味着使用行优先解释在矩阵 class 中处理列优先矩阵。

是的,glm 和 assimp 的内存布局相似:data.html

但是,根据文档页面:classai_matrix4x4t

assimp 矩阵始终是 row-major,而 glm 矩阵始终是 col-major,这意味着您需要在转换时创建转置:

inline static Mat4 Assimp2Glm(const aiMatrix4x4& from)
        {
            return Mat4(
                (double)from.a1, (double)from.b1, (double)from.c1, (double)from.d1,
                (double)from.a2, (double)from.b2, (double)from.c2, (double)from.d2,
                (double)from.a3, (double)from.b3, (double)from.c3, (double)from.d3,
                (double)from.a4, (double)from.b4, (double)from.c4, (double)from.d4
            );
        }
inline static aiMatrix4x4 Glm2Assimp(const Mat4& from)
        {
            return aiMatrix4x4(from[0][0], from[1][0], from[2][0], from[3][0],
                from[0][1], from[1][1], from[2][1], from[3][1],
                from[0][2], from[1][2], from[2][2], from[3][2],
                from[0][3], from[1][3], from[2][3], from[3][3]
            );
        }

PS: assimp中abcd代表row,1234代表col。