gpu 蒙皮的矩阵计算

Posted

技术标签:

【中文标题】gpu 蒙皮的矩阵计算【英文标题】:Matrix calculations for gpu skinning 【发布时间】:2015-06-16 09:32:37 【问题描述】:

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

对于骨骼的offsetMatrix 变量,我到底需要什么?我需要乘以什么?

【问题讨论】:

您对剥皮有多熟悉?对整个网格使用单个矩阵是不够的。您是否浏览过任何教程(例如this)? 我实际上通过了我在这里发布的第一个问题。我发现了需要修复的错误。我使用 matrix.inverse() 而不是 transpose() 从行优先转换为列优先。愚蠢的我......无论如何,我编辑了我的帖子,我还有一个问题。 顺便说一句,this 线程中的答案似乎具有误导性?答案的作者清楚地说“......如果你将单位矩阵应用于模型中的每个关节,那么你将得到的是你的模型处于绑定姿势”。我尝试使用单位矩阵(而不是常规的节点层次矩阵乘法),我得到了一个看起来很奇怪的模型。 请发布您的绘制代码,包括顶点着色器。 pastebin.com/CUUurQ2J 。请注意,我什至没有从蒙皮过程开始(目前,我只将顶点传递给着色器,甚至没有传递 UV),因为我仍在努力理解诸如 aiBone 的 @987654326 之类的基本内容@数据成员。 (他们称之为反向绑定姿势)。编辑:我以为你只要求提供着色器代码,让我也添加我的绘制代码,等一下...... 【参考方案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>

如果我们在这种情况下将一个顶点乘以偏移矩阵,我们将得到一个 的顶点位置。

我们如何使用它?关于如何使用此矩阵,您有两种选择,这取决于我们如何将顶点数据存储在顶点缓冲区中。选项是;

1) 将网格顶点存储在网格空间中 2) 将网格顶点存储在骨骼空间中

在 #1 的情况下,我们将使用 offsetMatrix 并将其应用于在我们构建顶点缓冲区时受骨骼影响的顶点。然后当我们为网格设置动画时,我们稍后会为该骨骼应用动画矩阵。

在 #2 的情况下,我们将在转换存储在顶点缓冲区中的顶点时将 offsetMatrix 与该骨骼的动画矩阵结合使用。所以它会是这样的(注意:你可能需要在这里切换矩阵连接);

anim_vertex = (offset_matrix * anim_matrix) * mesh_vertex

这有帮助吗?

【讨论】:

我很好奇您将如何在骨骼空间中存储顶点。您会为每个受影响的骨骼多次存储所有顶点吗? 是的,我就是这样做的。在这种情况下,我将在 CPU 或 GPU 上使用流输出和保存骨骼矩阵的统一块执行骨骼变换。我尽量避免通过顶点着色器在三角形渲染管道中做这种事情,以将着色器复杂性降至最低并简化整体渲染系统设计。 它“可能”是,但请记住,对于单个顶点,您必须执行额外的矩阵乘法,但我想您可以缓存逆偏移 * 动画偏移矩阵。但是对于四种影响,无论您将顶点存储在骨骼空间还是网格空间中,您仍然需要多次乘以顶点。额外的成本实际上来自于局部骨骼位置、法线和切线的输入带宽。 you could cache the inverse offset * anim offset matrices 这正是正确的做法。我从未见过有人上传这两个矩阵。您唯一要节省的就是在更新骨架时在 CPU 上进行矩阵乘法,代价是使顶点着色器更重。【参考方案2】:

正如我已经假设的那样,mOffsetMatrix 是反向绑定姿势矩阵。 This tutorial 说明了线性混合蒙皮所需的正确转换:

您首先需要评估您的动画状态。这将为您提供从动画骨骼空间到每个骨骼的世界空间的系统转换(本教程中的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 循环。

本教程为boneMatrix 使用了一个额外的m_GlobalInverseTransform。但是,我不知道他们为什么这样做。基本上,这会撤消整个场景的整体转换。可能是用来让模型在视图中居中。

【讨论】:

无法理解该行:No assume an animated bone and transform...。你能改写吗?另外,如果我错了,请纠正我,但由于每个网格的顶点都存储在它们的 节点空间 中,boneMatrix 应该是(顺序从右到左):finalBoneMatrix = Inverse(offsetMatrix) * animationMatrix * offsetMatrix * meshNode.toRoot;..?文本顺序:节点空间到根空间(也称为世界空间),世界空间到骨骼空间,骨骼空间到动画(骨骼)空间,动画空间到根空间。我有什么问题吗? 缺少w。您在骨骼空间中拥有顶点位置,然后通过假设动画骨骼继续转换到世界空间。有两种层次结构:骨架层次结构和网格层次结构。如果网格层次结构包含其他变换,则需要在编写时添加它们。但这对于蒙皮网格来说是相当不寻常的。第一个Inverse(offsetMatrix) 是错误的。这会将顶点从骨骼空间转换到世界空间。但是在使用animationMatrix 进行转换后,您已经在世界空间中。 animationMatrix 基本上只是一个 T/R 矩阵,它是我从动画结构中得到的,基于时间戳。为什么要将顶点转换为世界空间? 因为这就是动画矩阵所做的。可以将其视为在世界空间中定位骨骼的模型变换。例如。上臂骨骼的动画矩阵将包含将骨骼从原点移动到肩部位置的平移,并且它可能包含额外的旋转。 "but since the vertices for each mesh are stored in their node space" 他们不是。它们存储在对象空间中。【参考方案3】:

让我们以这段代码为例,我曾经用它来为我工作的游戏中的角色设置动画。我也使用 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]);
    

本质上是教程Skeletal Animation with Assimp 中提到的GlobalTransform 或根节点scene-&gt;mRootNode-&gt;mTransformation 的转换是从局部空间到全局空间的转换。举个例子,当在 3D 建模器中(例如选择 Blender)创建网格或加载角色时,它通常(默认情况下)位于笛卡尔平面的原点,并且其旋转设置为身份四元数。

但是,您可以将网格/角色从原点 (0,0,0) 转换/旋转到其他地方,并在单个场景中甚至具有不同位置的多个网格。当你加载它们时,特别是如果你做骨骼动画,必须将它们翻译回本地空间(即回到原点 0,0,0 ),这就是你必须将所有内容乘以 InverseGlobal 的原因(这将您的网格带回本地空间)。

之后,您需要将其乘以节点变换,即 parentTransform 的乘积(在树中向上一级变换,这是整体变换)transform(以前的 assimp_node-&gt;mTransformation只是骨骼相对于节点父节点的变换)和您想要应用的局部变换(任何 T * R):正向运动学、反向运动学或关键帧插值。

最终会有 boneOffset (ai_mesh-&gt;mBones[i]-&gt;mOffsetMatrix) 在绑定姿势中从网格空间转换到骨骼空间,如文档中所述。

如果您想查看我的 Skeleton 类的完整代码,这里有一个指向 GitHub 的链接。

希望对你有帮助。

【讨论】:

为此苦苦挣扎了好几个星期。在网上到处张贴问题。您拥有nodeTransform 的概念成功了。谢谢你! 很高兴它有帮助 :-) 我认为我应该写一篇关于它的文章! 嘿伙计,我再次深入研究这个主题,因为我想建立一个更好的骨骼动画系统。我想确定一些事情,请回答下一个问题:每个 aiMesh 都指的是一些带有偏移矩阵的骨骼。特定骨骼在不同的aiMesh中可以有不同的offsetMatrix吗? 嘿 Pilpel,你有没有找到上一个问题的答案?我怀疑答案是否定的,但我找不到明确的答案。我假设您正在尝试将网格和骨架解耦以允许重复使用具有多个网格的骨架?

以上是关于gpu 蒙皮的矩阵计算的主要内容,如果未能解决你的问题,请参考以下文章

GPU可以用于Android Environmement上的数值计算(复矩阵乘法)吗?

Python GPU 加速数据科学 | 计算距离矩阵在用 cuPy 时快了约 100 倍

Python GPU 加速数据科学 | 计算距离矩阵在用 cuPy 时快了约 100 倍

并行计算——OpenMP加速矩阵相乘

使用张量流矩阵乘法测试 GPU

使用 cublasSgetriBatched 在 gpu 上求逆两个矩阵