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

3
我按照this教程操作,成功得到了预期的带骨骼模型动画输出。该教程使用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

产生正确的输出。我期望为了使计算成立,每个矩阵都应该首先转置以更改为列顺序,然后进行上述计算,最后再次转置以获得回行顺序矩阵,这也在此link中讨论过。然而,这样做会产生可怕的输出。我是否漏掉了什么?


FYI,从a2、b2、c2和d2的更新来看,有些东西真的很奇怪。在它们的第二列向下查看,它数了2、3、4、5——我认为它们应该都是1。 - Enigma22134
2个回答

3
你混淆了两个不同的概念:
1.数据在内存中的布局(行优先 vs. 列优先) 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,将矩阵作为2D数组按列x行打印出来,这是一个典型的人所期望的方式。
混淆来自于这个事实: 当你在不同于矩阵函数实现所设计的布局中使用矩阵时,就会出现混淆。
如果你有一个内部假设为列优先布局的矩阵库,并传入行优先格式的数据,则好像在此之前转换了该矩阵,仅在这一点上,事情才会出错。
更加令人困惑的是,这还涉及到矩阵*向量与向量*矩阵的问题。有些人喜欢写成x' = x * M(其中v'v为行向量),而其他人则喜欢写成y' = N *y(使用列向量)。从数学上讲,M*x=transpose((transpose(x)*transpose(M))),因此人们经常将其与行优先或列优先顺序效应混淆——但它完全独立于此。如果您想使用其中之一,那么显然只是一种约定。
那么,最终回答您的问题:
这里创建的转换矩阵是为了按矩阵*向量的约定编写的,因此Mparent * Mchild是正确的矩阵乘法顺序。
直到这一点,内存中的实际数据布局都毫不重要。它开始变得重要是因为我们正在接口一个具有自己约定的不同API。GL的默认顺序是列优先的。使用的矩阵类是针对行优先内存布局编写的。所以在这一点上,你需要进行转置,以便GL对该矩阵的解释与你的其他库相匹配。
另一种选择是不进行转换,并将此隐式操作纳入系统——通过在着色器中更改乘法顺序或通过调整创建矩阵的操作。然而,我不建议走这条路,因为生成的代码将完全不直观,因为最终意味着使用一个使用行优先解释的矩阵类处理列优先矩阵。

1

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

但是,根据文档页面:classai_matrix4x4t

Assimp矩阵始终是行主序,而glm矩阵始终是列主序,这意味着在转换时需要创建一个转置:

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代表行,1234代表列。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接