如何计算每个骨骼的起始矩阵(t-pose)(使用 collada 和 opengl)

Posted

技术标签:

【中文标题】如何计算每个骨骼的起始矩阵(t-pose)(使用 collada 和 opengl)【英文标题】:How do I calculate the start matrix for each bone(t-pose) (using collada and opengl) 【发布时间】:2020-11-09 21:59:05 【问题描述】:

我已经加载了顶点、材质、法线、权重、关节 ID、关节本身和父子级信息(层次结构),我还设法将它们全部渲染,当我旋转或平移其中一个关节时,孩子与父母一起旋转。 我的问题是,父母在错误的点或偏移量上旋转(希望你明白我的意思),这意味着我的初始偏移量是错误的,对吧?为了获得起始t-pose,我猜我不需要旋转或平移,只需要关节位置的偏移量,但我不知道如何获得它,被卡了好久。在 Collada 文件中,每个关节都有一个变换,我也加载了那个,但我不知道如何正确实现它,我的 3d 模型变形并且看起来不对。 如果你回答这个问题,请把它当作你在向猴子(我)解释它,如果可能的话,一步一步来,我不熟悉这些绑定和反向绑定术语,并且非常困惑。我想如果我能做到这一点,我最终会自己弄清楚骨骼动画的其余部分,所以这只是这件小事。

【问题讨论】:

【参考方案1】:

我最近得到了骨骼、关节和节点的工作,所以我将尝试解释我是如何实现它的。请注意,我是使用 Assimp 导入我的 DAE 文件,但据我所知,Assimp 并没有对数据做任何处理,所以这个解释应该直接与 Collada 文件中的数据相关。

我只是自己学习所有这些,所以我可能会弄错。如果我这样做了,请告诉我,我会相应地更新这个答案。

语义

mesh 是一组顶点、法线、纹理坐标和面。存储在网格中的点处于绑定姿势,或静止姿势。这通常是(但不总是)T-pose。

皮肤是一个控制器。它指的是单个网格,并包含将修改该网格的骨骼列表(这是存储骨骼的位置)。您可以将皮肤元素视为将要渲染的实际模型(或模型的一部分)。

bone 是名称和相关矩阵的平面列表。这里没有分层数据,它只是一个平面列表。层次结构由引用骨骼的节点提供。

nodejoint 是一个分层数据元素。它们存储在层次结构中,父节点具有零个或多个子节点。一个节点可以链接到零个或多个骨骼,并且可以链接到零个或多个皮肤。应该只有一个根节点。关节与节点相同,因此我将连接称为节点。

请注意节点和骨骼是分开的。您无需修改​​骨骼来为模型设置动画。相反,您修改了一个节点,该节点会在渲染模型时应用于骨骼。

皮肤

皮肤是你要渲染的东西。皮肤总是指一个单一的网格。作为同一模型(或场景)的一部分,您可以在一个 DAE 文件中拥有多个皮肤。有时,模型会通过变换网格来重用网格。例如,您可能有一个用于单个手臂的网格,并在身体的另一侧重复使用该手臂,镜像。我相信这就是皮肤的bind_shape_matrix 值的用途。到目前为止,我还没有使用过这个,而且我的矩阵总是恒等的,所以我不能说它的用法。

骨头

bone 是将变换应用到模型的部分。您不修改骨骼。相反,您修改控制骨骼的节点。稍后会详细介绍。

骨骼由以下部分组成:

一个名称,用于查找控制此骨骼的节点 (Name_array) 绑定姿势矩阵,有时称为“逆绑定矩阵”或“偏移矩阵”(bind_poses 数组) 骨骼将影响的顶点索引列表(vertex_weights 元素) 上述相同长度的权重列表,说明骨骼对该顶点的影响程度。 (weights数组)

节点

节点是一个分层数据元素,描述了模型在渲染时是如何转换的。您将始终从一个根节点开始,然后沿着节点树向上移动,按顺序应用变换。我为此使用了深度优先算法。

节点说明在渲染或动画时应如何转换模型、皮肤和骨骼。

一个节点可以指一个皮肤。这意味着皮肤将用作此模型渲染的一部分。如果您看到一个节点引用了一个皮肤,它会在渲染时被包含在内。

一个节点由以下部分组成:

名称(sid 属性) 一个变换矩阵(transform 元素) 子节点(node 元素)

GlobalInverseTransform 矩阵

GlobalInverseTransform 矩阵是通过将第一个节点的Transform 矩阵取反来计算的。就这么简单。

算法

现在我们可以开始着手进行真正的蒙皮和渲染了。

计算节点的LocalTransform

每个节点都应该有一个矩阵,称为LocalTransform 矩阵。此矩阵不在 DAE 文件中,而是由您的软件计算得出。它基本上是该节点的Transform 矩阵及其所有父节点的累加。

第一步是遍历节点层次结构。

从第一个节点开始,使用节点的Transform 矩阵和父节点的LocalTransform 计算该节点的LocalTransform。如果节点没有父节点,则使用单位矩阵作为父节点的LocalTransform 矩阵。

Node.LocalTransform = ParentNode.LocalTransform * Node.Transform

对这个节点中的每个子节点递归地重复这个过程。

计算骨骼的 FinalTransform 矩阵

就像一个节点,一个骨骼应该有一个FinalTransform 矩阵。同样,这并不存储在 DAE 文件中,它是由您的软件在渲染过程中计算出来的。

对于使用的每个网格,对于该网格中的每个骨骼,应用以下算法:

For each mesh used:
    For each bone in mesh:
        If a node with the same name exists:
            Bone.FinalTransform = Bone.InverseBind * Node.LocalTransform * GlobalInverseTransform
        Otherwise:
            Bone.FinalTransform = Bone.InverseBind * GlobalInverseTransform

我们现在拥有模型中每个骨骼的FinalTransform 矩阵。

计算一个顶点的位置

计算完所有骨骼后,我们就可以将网格的点转换为它们的最终渲染位置。这是我使用的算法。这不是执行此操作的“正确”方法,因为它应该由顶点着色器即时计算,但它可以演示正在发生的事情。

From the root node:
    For each mesh referred to by node:
        Create an array to hold the transformed vertices, the same size as your source vertices array.
        Create an array to hold the transformed normals, the same size as your source vertices array (normals and vertices arrays should be the same length at the beginning.

        If the mesh has no bones:
            Copy source vertices and source normals to output arrays - mesh is not skinned
        Otherwise:
            For every bone in the mesh:
                For every weight in the bone:
                    OutputVertexArray(Weight.VertexIndex) = Mesh.InputVertexArray(Weight.VertexIndex) * Bone.FinalTransform * Weight.TransformWeight
                    OutputNormalArray(Weight.VertexIndex) = Normalize(Mesh.InputNormalArray(Weight.VertexIndex) * Bone.FinalTransform * Weight.TransformWeight)
        
        Render the mesh, using OutputVertexArray, OutputNormalArray, Mesh.InputTexCoordsArray and the mesh's face indices.

    Recursively call this process for each child node.

这应该会为您提供正确渲染的输出。

请注意,使用此系统,可以多次重复使用网格。

动画

只是关于动画的简要说明。我对此没有做太多,Assimp 隐藏了 Collada 的许多血腥细节(并引入了它自己的血腥形式),但是要使用文件中的预定义动画,你需要对平移、旋转和缩放进行一些插值用一个矩阵表示节点在单个时间点的动画状态。

请记住,矩阵构造遵循 TRS(平移、旋转、缩放)约定,其中首先发生平移,然后是旋转,然后是缩放。

AnimatedNodeTransform = TranslationMatrix * RotationMatrix * ScaleMatrix

生成的矩阵完全替代了节点的Transform矩阵——它没有与矩阵结合。

我仍在努力研究如何正确执行动态动画(想想逆运动学)。对于我尝试的某些模型,效果很好。我可以将四元数应用于节点的变换矩阵,它会起作用。但是,其他一些模型会做一些奇怪的事情,比如围绕原点旋转节点,所以我认为我仍然缺少一些东西。如果我最终解决了这个问题,我会更新这一部分以反映我的发现。

希望这会有所帮助。如果我遗漏了什么,或者有什么不对的地方,任何人都请随时纠正我。我自己只是在学习这些东西。如果我发现任何错误,我会编辑答案。

另外,请注意我使用的是 Direct3D,因此我的矩阵乘法顺序可能与您的相反。您可能需要在我的答案中翻转某些操作的乘法顺序。

【讨论】:

非常好的答案!然而,它仍然留下一个问题:GlobalInverseTransform 究竟是做什么的?翻遍网络,几乎所有关于 Skeletal Animation + Assimp 的资源都使用了这个 GlobalInverseTransform,但没有人解释它是干什么用的?对于给定的骨骼,我们将其坐标系从顶点空间转换为骨骼空间(偏移矩阵)。然后遍历节点变换层次,从骨骼空间变换到动画空间(反向绑定姿势矩阵)。这应该足以转换给定动画键的顶点,不是吗?为什么是 GlobalInverseTransform?

以上是关于如何计算每个骨骼的起始矩阵(t-pose)(使用 collada 和 opengl)的主要内容,如果未能解决你的问题,请参考以下文章

Unity中BVH骨骼动画驱动的可视化理论与实现

Unity中BVH骨骼动画驱动的可视化理论与实现

gpu 蒙皮的矩阵计算

GLSL:用旋转矢量旋转?

如何计算 COLLADA 文件的父子联合变换?

Unity3D之Mecanim动画系统学习笔记:模型导入