如何计算每个骨骼的起始矩阵(t-pose)(使用 collada 和 opengl)
How do I calculate the start matrix for each bone(t-pose) (using collada and opengl)
我已经加载了顶点、材质、法线、权重、关节 ID、关节本身和 parent children 信息(层次结构),我还设法渲染了这一切,当我旋转或平移其中一个关节,children 与 parent 一起旋转。
我的问题是,parent 在错误的点或偏移量上旋转(希望你明白我的意思),这意味着我的初始偏移量是错误的,对吧?要开始t-pose,我猜我不需要旋转和平移,只需要关节位置的偏移量,但我不知道如何得到它,被卡了好久。在 Collada 文件中,每个关节都有一个变换,我也加载了那个,但我不知道如何正确实现它,我的 3d 模型变形并且看起来不对。
如果你回答这个问题,请把它当作你在向一只猴子(我)解释它,如果可能的话,一步一步来,我不熟悉这些绑定和反向绑定术语,并且非常困惑。我想如果我能做到这一点,我最终会自己弄清楚其余的骨骼动画,所以这只是一件小事。
我最近让骨骼、关节和节点正常工作,所以我将尝试解释我是如何实现的。请注意,我正在使用 Assimp 导入我的 DAE 文件,但据我所知,Assimp 不会对数据进行任何处理,因此此说明应该与 Collada 文件中的数据直接相关。
我只是自己学习所有这些,所以我可能会出错。如果我这样做了,任何人,请告诉我,我会相应地更新这个答案。
语义
一个mesh是一组顶点、法线、纹理坐标和面。存储在网格中的点处于 绑定姿势 或静止姿势。这通常是 T-pose.
,但并非总是如此
一个皮肤是一个控制器。它指的是单个网格,并包含将修改该网格的骨骼列表(这是存储骨骼的地方)。您可以将皮肤元素视为将要渲染的实际模型(或模型的一部分)。
A bone 是名称和关联矩阵的平面列表。这里没有分层数据,它只是一个平面列表。层次结构由引用骨骼的节点提供。
A node,或joint,是分层数据元素。它们存储在层次结构中,parent 节点具有零个或多个 child 节点。一个节点可以链接到零个或多个骨骼,也可以链接到零个或多个皮肤。应该只有一个根节点。关节与节点相同,因此我将连接称为节点。
请注意节点和骨骼是分开的。您不需要修改骨骼来为您的模型制作动画。相反,您修改一个节点,该节点会在渲染模型时应用于骨骼。
皮肤
A skin 是你要渲染的东西。皮肤总是指一个单一的网格。作为同一模型(或场景)的一部分,您可以在一个 DAE 文件中拥有多个皮肤。有时,模型会通过转换网格来重用它们。例如,您可能有一个用于单臂的网格,并在 body 的另一侧重复使用镜像的该臂。我相信这就是皮肤的 bind_shape_matrix
值的用途。到目前为止,我还没有用过这个,我的矩阵总是恒等的,所以我不能说它的用法。
骨头
A bone 是将转换应用于您的模型。您不修改骨骼。相反,您修改控制骨骼的节点。稍后会详细介绍。
骨骼由以下部分组成:
- 一个名称,用于查找控制该骨骼的节点(
Name_array
)
- 绑定姿势矩阵,有时称为“逆绑定矩阵”或“偏移矩阵”(
bind_poses
数组)
- 骨骼将影响的顶点索引列表(
vertex_weights
元素)
- 上面相同长度的权重列表,表明骨骼对该顶点的影响程度。 (
weights
数组)
节点
A node 是分层数据元素,描述模型在渲染时如何转换。您将始终从一个根节点开始,沿着节点树向上移动,依次应用变换。我为此使用 depth-first 算法。
该节点告诉模型、皮肤和骨骼在渲染或动画时应如何转换。
一个节点可能指的是一个皮肤。这意味着皮肤将用作此模型渲染的一部分。如果您看到一个节点引用了一个皮肤,它会在渲染时包含在内。
一个节点由以下部分组成:
- 名称(
sid
属性)
- 一个变换矩阵(
transform
个元素)
- Child 个节点(
node
个元素)
全局逆变换矩阵
GlobalInverseTransform
矩阵是通过获取第一个节点的 Transform
矩阵并将其求逆来计算的。就这么简单。
算法
现在我们可以进入正题 - 实际的蒙皮和渲染。
正在计算节点的 LocalTransform
每个节点应该有一个矩阵,称为LocalTransform
矩阵。该矩阵不在 DAE 文件中,而是由您的软件计算得出。它基本上是节点的 Transform
个矩阵及其所有 parent 矩阵的累加。
第一步是遍历节点层次结构。
从第一个节点开始,使用节点的Transform
矩阵和parent的LocalTransform
计算节点的LocalTransform
。如果节点没有 parent,使用单位矩阵作为 parent 的 LocalTransform
矩阵。
Node.LocalTransform = ParentNode.LocalTransform * Node.Transform
对该节点中的每个 child 节点递归地重复此过程。
正在计算骨骼的 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
模型中每个骨骼的矩阵。
正在计算顶点的位置
计算完所有骨骼后,我们就可以将网格的点转换为它们的最终渲染位置。这是我使用的算法。这不是执行此操作的“正确”方法,因为它应该由顶点着色器计算 on-the-fly,但它可以演示正在发生的事情。
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.
这应该会为您提供正确呈现的输出。
请注意,使用此系统,可以多次 re-use 一个网格。
动画
关于动画的简要说明。我对此没有做太多,Assimp 隐藏了 Collada 的很多血淋淋的细节(并引入了它自己的血腥形式),但是要使用文件中的预定义动画,您需要对平移、旋转和缩放进行一些插值得到一个表示节点在单个时间点的动画状态的矩阵。
请记住,矩阵构造遵循 TRS(平移、旋转、缩放)惯例,其中平移首先发生,然后是旋转,然后是缩放。
AnimatedNodeTransform = TranslationMatrix * RotationMatrix * ScaleMatrix
生成的矩阵完全替换了节点的变换矩阵——它没有与矩阵结合。
我仍在努力研究如何正确执行 on-the-fly 动画(想想反向运动学)。对于我尝试的某些模型,效果很好。我可以将四元数应用于节点的变换矩阵,它会起作用。然而,其他一些模型会做一些奇怪的事情,比如围绕原点旋转节点,所以我认为我仍然遗漏了一些东西。如果我最终解决了这个问题,我将更新此部分以反映我的发现。
希望这对您有所帮助。如果我遗漏了什么,或者有什么不对的地方,请随时纠正我。我只是自己学习这些东西。如果我发现任何错误,我会编辑答案。
此外,请注意我使用的是 Direct3D,因此我的矩阵乘法顺序可能与您的相反。您可能需要翻转我的回答中某些运算的乘法顺序。
我已经加载了顶点、材质、法线、权重、关节 ID、关节本身和 parent children 信息(层次结构),我还设法渲染了这一切,当我旋转或平移其中一个关节,children 与 parent 一起旋转。 我的问题是,parent 在错误的点或偏移量上旋转(希望你明白我的意思),这意味着我的初始偏移量是错误的,对吧?要开始t-pose,我猜我不需要旋转和平移,只需要关节位置的偏移量,但我不知道如何得到它,被卡了好久。在 Collada 文件中,每个关节都有一个变换,我也加载了那个,但我不知道如何正确实现它,我的 3d 模型变形并且看起来不对。 如果你回答这个问题,请把它当作你在向一只猴子(我)解释它,如果可能的话,一步一步来,我不熟悉这些绑定和反向绑定术语,并且非常困惑。我想如果我能做到这一点,我最终会自己弄清楚其余的骨骼动画,所以这只是一件小事。
我最近让骨骼、关节和节点正常工作,所以我将尝试解释我是如何实现的。请注意,我正在使用 Assimp 导入我的 DAE 文件,但据我所知,Assimp 不会对数据进行任何处理,因此此说明应该与 Collada 文件中的数据直接相关。
我只是自己学习所有这些,所以我可能会出错。如果我这样做了,任何人,请告诉我,我会相应地更新这个答案。
语义
一个mesh是一组顶点、法线、纹理坐标和面。存储在网格中的点处于 绑定姿势 或静止姿势。这通常是 T-pose.
,但并非总是如此一个皮肤是一个控制器。它指的是单个网格,并包含将修改该网格的骨骼列表(这是存储骨骼的地方)。您可以将皮肤元素视为将要渲染的实际模型(或模型的一部分)。
A bone 是名称和关联矩阵的平面列表。这里没有分层数据,它只是一个平面列表。层次结构由引用骨骼的节点提供。
A node,或joint,是分层数据元素。它们存储在层次结构中,parent 节点具有零个或多个 child 节点。一个节点可以链接到零个或多个骨骼,也可以链接到零个或多个皮肤。应该只有一个根节点。关节与节点相同,因此我将连接称为节点。
请注意节点和骨骼是分开的。您不需要修改骨骼来为您的模型制作动画。相反,您修改一个节点,该节点会在渲染模型时应用于骨骼。
皮肤
A skin 是你要渲染的东西。皮肤总是指一个单一的网格。作为同一模型(或场景)的一部分,您可以在一个 DAE 文件中拥有多个皮肤。有时,模型会通过转换网格来重用它们。例如,您可能有一个用于单臂的网格,并在 body 的另一侧重复使用镜像的该臂。我相信这就是皮肤的 bind_shape_matrix
值的用途。到目前为止,我还没有用过这个,我的矩阵总是恒等的,所以我不能说它的用法。
骨头
A bone 是将转换应用于您的模型。您不修改骨骼。相反,您修改控制骨骼的节点。稍后会详细介绍。
骨骼由以下部分组成:
- 一个名称,用于查找控制该骨骼的节点(
Name_array
) - 绑定姿势矩阵,有时称为“逆绑定矩阵”或“偏移矩阵”(
bind_poses
数组) - 骨骼将影响的顶点索引列表(
vertex_weights
元素) - 上面相同长度的权重列表,表明骨骼对该顶点的影响程度。 (
weights
数组)
节点
A node 是分层数据元素,描述模型在渲染时如何转换。您将始终从一个根节点开始,沿着节点树向上移动,依次应用变换。我为此使用 depth-first 算法。
该节点告诉模型、皮肤和骨骼在渲染或动画时应如何转换。
一个节点可能指的是一个皮肤。这意味着皮肤将用作此模型渲染的一部分。如果您看到一个节点引用了一个皮肤,它会在渲染时包含在内。
一个节点由以下部分组成:
- 名称(
sid
属性) - 一个变换矩阵(
transform
个元素) - Child 个节点(
node
个元素)
全局逆变换矩阵
GlobalInverseTransform
矩阵是通过获取第一个节点的 Transform
矩阵并将其求逆来计算的。就这么简单。
算法
现在我们可以进入正题 - 实际的蒙皮和渲染。
正在计算节点的 LocalTransform
每个节点应该有一个矩阵,称为LocalTransform
矩阵。该矩阵不在 DAE 文件中,而是由您的软件计算得出。它基本上是节点的 Transform
个矩阵及其所有 parent 矩阵的累加。
第一步是遍历节点层次结构。
从第一个节点开始,使用节点的Transform
矩阵和parent的LocalTransform
计算节点的LocalTransform
。如果节点没有 parent,使用单位矩阵作为 parent 的 LocalTransform
矩阵。
Node.LocalTransform = ParentNode.LocalTransform * Node.Transform
对该节点中的每个 child 节点递归地重复此过程。
正在计算骨骼的 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
模型中每个骨骼的矩阵。
正在计算顶点的位置
计算完所有骨骼后,我们就可以将网格的点转换为它们的最终渲染位置。这是我使用的算法。这不是执行此操作的“正确”方法,因为它应该由顶点着色器计算 on-the-fly,但它可以演示正在发生的事情。
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.
这应该会为您提供正确呈现的输出。
请注意,使用此系统,可以多次 re-use 一个网格。
动画
关于动画的简要说明。我对此没有做太多,Assimp 隐藏了 Collada 的很多血淋淋的细节(并引入了它自己的血腥形式),但是要使用文件中的预定义动画,您需要对平移、旋转和缩放进行一些插值得到一个表示节点在单个时间点的动画状态的矩阵。
请记住,矩阵构造遵循 TRS(平移、旋转、缩放)惯例,其中平移首先发生,然后是旋转,然后是缩放。
AnimatedNodeTransform = TranslationMatrix * RotationMatrix * ScaleMatrix
生成的矩阵完全替换了节点的变换矩阵——它没有与矩阵结合。
我仍在努力研究如何正确执行 on-the-fly 动画(想想反向运动学)。对于我尝试的某些模型,效果很好。我可以将四元数应用于节点的变换矩阵,它会起作用。然而,其他一些模型会做一些奇怪的事情,比如围绕原点旋转节点,所以我认为我仍然遗漏了一些东西。如果我最终解决了这个问题,我将更新此部分以反映我的发现。
希望这对您有所帮助。如果我遗漏了什么,或者有什么不对的地方,请随时纠正我。我只是自己学习这些东西。如果我发现任何错误,我会编辑答案。
此外,请注意我使用的是 Direct3D,因此我的矩阵乘法顺序可能与您的相反。您可能需要翻转我的回答中某些运算的乘法顺序。