GPU骨骼动画的矩阵计算

14

我正在使用 Assimp 作为我的模型导入库,在 OpenGL 中尝试进行骨骼动画。

我需要对骨骼的 offsetMatrix 变量做什么?我需要将其乘以什么?


1
你对皮肤绑定熟悉吗?仅使用单个矩阵来处理整个网格是不够的。你是否完成过任何教程(例如这个)? - Nico Schertler
我实际上已经解决了我在这里发布的第一个问题。我找到了需要修复的错误。我使用了matrix.inverse()而不是transpose()来从行主序转换为列主序。我真是太蠢了...无论如何,我编辑了我的帖子,但我还有一个问题。 - McLovin
请发布您的绘制代码,包括顶点着色器。 - Nico Schertler
没有实际的皮肤,讨论换肤是没有意义的。如果mOffsetMatrix是反向绑定骨骼矩阵,你需要它来计算动画网格的实际骨骼变换。你还需要从动画中获取变换(可能是boneTransform = animTransform * mOffsetMatrix)。这些都是每个骨骼的矩阵,你需要将它们全部上传到GPU进行换肤。如果没有动画,boneTransform就简化为单位矩阵。 - Nico Schertler
我似乎仍然没有掌握反向绑定姿势矩阵背后的思想。您能否就我在此处的第二条评论发表意见?(关于另一个线程中“误导性”答案的问题)。如果骨骼(由Assimp中的节点表示)具有“反向绑定姿势”矩阵,那么它不应该等于节点.toRoot矩阵的逆吗?(因为toRoot矩阵将节点的顶点设置为模型的绑定姿势) - McLovin
显示剩余5条评论
3个回答

10

比如说,我曾经使用以下代码在游戏中制作角色动画。我还使用了Assimp来加载骨骼信息,并且我已经阅读了Nico指出的OGL教程。

glm::mat4 getParentTransform()
{
    if (this->parent)
        return parent->nodeTransform;
    else 
        return glm::mat4(1.0f);
}

void updateSkeleton(Bone* bone = NULL)
{ 
    bone->nodeTransform =  bone->getParentTransform() // This retrieve the transformation one level above in the tree
    * bone->transform //bone->transform is the assimp matrix assimp_node->mTransformation
    * bone->localTransform;  //this is your T * R matrix

    bone->finalTransform = inverseGlobal // which is scene->mRootNode->mTransformation from assimp
        * bone->nodeTransform  //defined above
        * bone->boneOffset;  //which is ai_mesh->mBones[i]->mOffsetMatrix


    for (int i = 0; i < bone->children.size(); i++) {
        updateSkeleton (&bone->children[i]);
    }
}

基本上,在 使用Assimp进行骨骼动画 中提到的GlobalTransform,或者正确地说是根节点的变换 scene->mRootNode->mTransformation 是从局部空间到全局空间的变换。举个例子,当你在3D建模软件(比如Blender)中创建网格或加载角色时,默认情况下它通常被放置在笛卡尔平面的原点,并且旋转设置为单位四元数。

但是,您可以将网格/角色从原点 (0,0,0) 移动到其他地方,并且在单个场景中甚至可以具有具有不同位置的多个网格。当您加载它们时,特别是如果您进行骨骼动画,则必须将它们返回到局部空间(即返回到原点 0,0,0),这就是为什么您必须通过 InverseGlobal 将所有内容乘以其逆变换(将网格返回到局部空间)的原因。

接下来,您需要将其乘以节点变换,该变换是parentTransform(树中上一级的变换,即整体变换)和transform(以前是assimp_node->mTransformation,即相对于节点父级的骨骼变换)以及要应用的本地变换(任何T * R),以进行正向运动学、逆向运动学或关键帧插值。

最终,有一个boneOffset(ai_mesh->mBones[i]->mOffsetMatrix),它在绑定姿势下从网格空间转换为骨骼空间,如文档所述。

这里有一个链接GitHub,如果您想查看我的Skeleton类的全部代码。

希望能有所帮助。


我为此苦苦挣扎了数周,在网上发布了各种问题。你提出的“nodeTransform”的概念解决了我的问题。非常感谢! - McLovin
1
很高兴能帮到你 :-) 我想我应该写一篇关于它的文章! - codingadventures
嘿,我又开始深入探讨这个话题了,因为我想要构建一个更好的骨骼动画系统。我想确认一件事情,请回答下一个问题:每个aiMesh都引用了一些带有偏移矩阵的骨骼。在不同的aiMesh中,一个特定的骨骼可以有不同的offsetMatrix吗? - McLovin
嘿Pilpel,你有没有找到上次问题的答案?我怀疑答案是否定的,但我找不到明确的答案。我猜你是想解耦网格和骨骼,以允许一个骨骼与多个网格重复使用? - Julian McKinlay

1
偏移矩阵定义了变换(平移、缩放、旋转),将网格空间中的顶点转换到“骨骼”空间。例如,考虑以下顶点和具有以下属性的骨骼;
Vertex Position<0, 1, 2>

Bone Position<10, 2, 4>
Bone Rotation<0,0,0,1>  // Note - no rotation
Bone Scale<1, 1, 1>

如果我们在这种情况下将顶点乘以偏移矩阵,我们会得到顶点位置为<-10,-1,2>。
我们如何使用它?你有两个选项来使用这个矩阵,这取决于我们如何在顶点缓冲区中存储顶点数据。选项如下:
1)将网格顶点存储在网格空间中 2)将网格顶点存储在骨骼空间中
对于第一种情况,我们会将偏移矩阵应用于受该骨骼影响的顶点,当我们构建顶点缓冲区时。然后,在我们动画网格时,我们稍后应用该骨骼的动画矩阵。
对于第二种情况,我们会在变换存储在顶点缓冲区中的顶点时,将偏移矩阵与该骨骼的动画矩阵结合使用。因此,它可能是以下内容(注意:你可能需要在这里切换矩阵连接);
anim_vertex = (offset_matrix * anim_matrix) * mesh_vertex

这有帮助吗?


我很好奇你会如何在骨骼空间中存储顶点。你是否会为每个受影响的骨骼都存储一遍所有的顶点? - Nico Schertler
没错,这基本上是我会做的方式。在这种情况下,我会使用流式输出和一个持有骨头矩阵的统一块,在CPU或GPU上执行骨骼变换。我尽量避免通过顶点着色器在三角形渲染管道中完成这种操作,以使着色器复杂度最小化,并简化整个渲染系统设计。 - James Steele
可能是这样,但请记住,对于单个顶点,您必须执行额外的矩阵乘法,但我想您可以缓存反向偏移*动画偏移矩阵。但是对于四个影响,无论您将顶点存储在骨骼空间还是网格空间中,都需要多次乘以顶点。额外的成本实际上来自于本地骨骼位置、法线和切线的输入带宽。 - James Steele
你可以缓存反向偏移 * 动画偏移矩阵,这正是正确的方法。我从未见过有人上传两个矩阵。你唯一能节省的就是在更新骨架时在 CPU 上进行矩阵乘法,但代价是使顶点着色器更加繁重。 - Tara

1

正如我之前所想的那样,mOffsetMatrix是反向绑定姿势矩阵。这个教程说明了您需要进行线性混合蒙皮的正确变换:

首先,您需要评估动画状态。这将为每个骨骼提供从动画骨骼空间到世界空间的系统变换(教程中的GlobalTransformation)。mOffsetMatrix是从世界空间到绑定姿势骨骼空间的系统变换。因此,对于蒙皮,您需要执行以下操作(假设特定顶点受单个骨骼影响):使用mOffsetMatrix将顶点转换到骨骼空间。现在假设一个动画骨骼,并将中间结果从动画骨骼空间转换回世界空间。所以:

boneMatrix[i] = animationMatrix[i] * mOffsetMatrix[i]

如果一个顶点受多个骨骼的影响,LBS(线性蒙皮绑定)会简单地对结果进行平均。这就是权重发挥作用的地方。通常在顶点着色器中实现蒙皮。
vec4 result = vec4(0);
for each influencing bone i
    result += weight[i] * boneMatrix[i] * vertexPos;

通常,影响骨骼的最大数量是固定的,您可以展开 for 循环。
教程使用额外的 m_GlobalInverseTransform 来处理 boneMatrix。然而,我不知道他们为什么这样做。基本上,这会撤销整个场景的总体变换。可能它被用来将模型居中在视图中。

无法理解这行代码:No assume an animated bone and transform...。你能否重写一下?另外,如果我没记错的话,由于每个网格的顶点都存储在它们的_node space_中,所以boneMatrix应该是(顺序从右到左):finalBoneMatrix = Inverse(offsetMatrix) * animationMatrix * offsetMatrix * meshNode.toRoot;..? 文字顺序为:节点空间到根空间(也称世界空间),世界空间到骨骼空间,骨骼空间到动画(骨骼)空间,动画空间到根空间。我有什么地方说错了吗? - McLovin
有一个 w 没有了。你在骨骼空间中拥有顶点位置,然后通过假设动画骨骼来继续转换到世界空间。有两个层次结构:骨架层次结构和网格层次结构。如果网格层次结构包含其他变换,则需要像您编写的那样添加它们。但这对于蒙皮网格而言相当不寻常。第一个 Inverse(offsetMatrix) 是错误的。这将把顶点从骨骼空间转换到世界空间。但是,在使用 animationMatrix 进行转换后,您已经处于世界空间中了。 - Nico Schertler
但是animationMatrix基本上只是我从动画结构中获取的基于时间戳的T/R矩阵。为什么它会将顶点转换为世界空间? - McLovin
1
因为这就是动画矩阵的作用。它可以被看作是一个模型变换,将骨骼定位在世界空间中。例如,上臂骨骼的动画矩阵会包含一个平移,将骨骼从原点移动到肩膀位置,并可能包含额外的旋转。 - Nico Schertler
但是,每个网格的顶点都存储在它们的对象空间中,而不是节点空间中。 - Tara

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