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

matrix order in skeletal animation using assimp

本文关键字:顺序 assimp 使用 动画      更新时间:2023-10-16

我遵循了本教程,并按预期获得了装配模型的输出动画。本教程使用assimp、glsl和c++从文件中加载装配好的模型。然而,有些事情我想不通。首先,assimp的变换矩阵是行主矩阵,本教程使用了Matrix4f类,该类使用这些变换矩阵,即行主序。Matrix4f类的构造函数如下所示:

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

产生正确的输出。我预计,为了使计算成立,每个矩阵都应该首先被转置为列顺序,然后进行上述计算,最后再次转置以获得后排顺序矩阵,这也在本链接中讨论。然而,这样做产生了可怕的结果。这里有我遗漏的东西吗?

您混淆了两件不同的事情:

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

经常有人声称,当使用行主与列主时,必须对事物进行转置,并反转矩阵的乘法阶相位但事实并非如此

事实是,在数学上,transpose(A*B) = transpose(B) * transpose(A)。然而,这在这里是无关紧要的,因为矩阵存储顺序与矩阵的数学解释无关,并且与之正交。

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

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

我们实际使用什么f并不重要(只要映射是双射的),运算C=A*B总是会有相同的结果。改变的只是内存中的数据,但我们还必须使用f来插入这些数据。我们可以编写一个简单的打印函数,也可以使用f,将矩阵打印为列x行中的2D数组,这是典型的人类所期望的。

当你在与矩阵函数的实现不同的布局中使用矩阵时,混淆就来自于这个事实。

如果你有一个矩阵库,它在内部假设列主布局,并以行主格式传递数据,就好像你以前转换过矩阵一样——只是在这一点上,事情才搞砸了。

更令人困惑的是,还有另一个与此相关的问题:矩阵*向量与向量*矩阵的问题。有些人喜欢写x' = x * M(其中v'v是行向量),而另一些人则喜欢写y' = N *y(带有列向量)。很明显,在数学上,M*x = transpose((transpose(x) * transpose(M)),所以人们经常将其与行与列的大阶效应混淆,但它也完全独立于此。这只是惯例的问题,如果你想使用其中一个。

因此,为了最终回答您的问题:

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

到目前为止,内存中的实际数据布局根本无关紧要。这才开始重要,因为现在,我们正在接口一个不同的API,它有自己的约定。总账的默认订单是列主订单。使用中的矩阵类是为行主内存布局而编写的。所以你只需要在这一点上转置,这样GL对矩阵的解释就可以与其他库的解释相匹配。

另一种选择是不转换它们,并通过将由此创建的隐式运算合并到系统中来说明这一点——要么通过更改着色器中的乘法顺序,要么通过调整最初创建矩阵的运算。然而,我不建议走这条路,因为生成的代码将完全不直观,因为最终,这意味着使用行主解释来处理矩阵类中的列主矩阵。

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

但是,根据文档页面:classai_matrix4x4t

assimp矩阵总是行主矩阵,而glm矩阵总是列主矩阵,这意味着你需要在转换时创建一个transponse:

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:abcd代表行,1234代表col