****编辑2:**
请避免指出效率/优化问题;这根本不是最终代码。这只是我在尝试基础知识:)。一旦我把所有东西都搞定了,我就会去清理和优化。
编辑:我决定用更简单的术语重新表述这个问题,看看是否有人可以帮我解决这个问题。
基本上,我将网格、骨架和动作从搅拌机导出到我正在研究的各种引擎中。但是我弄错了动画。我可以看出正在遵循基本的运动路径,但总有一个平移或旋转轴是错误的。我认为问题很可能不在于我的引擎代码(基于 OpenGL),而在于我对骨骼动画/蒙皮背后的某些部分理论的误解,或者我在导出器脚本中从搅拌机导出适当关节矩阵的方式.
我将解释理论、引擎动画系统和我的搅拌机导出脚本,希望有人能在其中一个或所有这些中发现错误。
理论:(我使用的是列优先排序,因为这是我在引擎中使用的,因为它是基于 OpenGL 的)
- 假设我有一个由单个顶点 v 组成的网格,以及一个将顶点 v 从网格的局部空间带到世界空间的变换矩阵 M。也就是说,如果我要在没有骨架的情况下渲染网格,最终位置将是 gl_Position = ProjectionMatrix * M * v。
- 现在假设我有一个骨架,其中一个关节 j 处于绑定/静止姿势。j 实际上是另一个矩阵。从 j 的本地空间到其父空间的转换,我将其表示为 Bj。如果 j 是骨架中关节层次结构的一部分,则 Bj 将从 j 空间到 j-1 空间(即到其父空间)。然而,在这个例子中 j 是唯一的关节,所以 Bj 从 j 空间到世界空间,就像 M 对 v 一样。
- 现在进一步假设我有一组帧,每个帧都有第二个变换 Cj,它与 Bj 的工作方式相同,只是对于连接 j 的不同的、任意空间配置。Cj 仍然将顶点从 j 空间带到世界空间,但 j 被旋转和/或平移和/或缩放。
鉴于上述情况,为了在关键帧 n 处对顶点 v 进行蒙皮。我需要:
- 将 v 从世界空间带到联合 j 空间
- 修改 j(而 v 在 j 空间中保持固定,因此在转换中被带走)
- 将 v 从修改后的 j 空间带回世界空间
所以上面的数学实现是: v' = Cj * Bj^-1 * v。实际上,我在这里有一个疑问。我说过 v 所属的网格具有从模型空间到世界空间的变换 M。而且我还在几本教科书中读到它需要从模型空间转换为关节空间。但是我在 1 中也说过 v 需要从世界转换为联合空间。所以基本上我不确定我是否需要做v' = Cj * Bj^-1 * v或v' = Cj * Bj^-1 * M * v。现在我的实现将 v' 乘以 M 而不是 v。但我已经尝试改变它,它只是以不同的方式把事情搞砸了,因为还有其他问题。
- 最后,如果我们想将顶点蒙皮到关节 j1,而关节 j1 又是关节 j0 的子节点,Bj1 将是 Bj0 * Bj1,Cj1 将是 Cj0 * Cj1。但是由于蒙皮定义为v' = Cj * Bj^-1 * v,所以 Bj1^-1 将是构成原始产品的倒数的反向串联。也就是说,v' = Cj0 * Cj1 * Bj1^-1 * Bj0^-1 * v
现在开始执行(Blender 端):
假设以下网格由 1 个立方体组成,其顶点绑定到单关节骨架中的单个关节:
假设还有一个 60 帧、3 个关键帧的动画,速度为 60 fps。动画本质上是:
- 关键帧 0:关节处于绑定/静止姿势(您在图像中看到的方式)。
- 关键帧 30:关节向上平移(搅拌器中的 +z)一些量,同时顺时针旋转 pi/4 rad。
- 关键帧 59:关节回到关键帧 0 中的相同配置。
我在搅拌机方面的第一个困惑来源是它的坐标系(与 OpenGL 的默认值相反)和可通过 python api 访问的不同矩阵。
现在,这就是我的导出脚本将搅拌机的坐标系转换为 OpenGL 的标准系统的操作:
# World transform: Blender -> OpenGL
worldTransform = Matrix().Identity(4)
worldTransform *= Matrix.Scale(-1, 4, (0,0,1))
worldTransform *= Matrix.Rotation(radians(90), 4, "X")
# Mesh (local) transform matrix
file.write('Mesh Transform:\n')
localTransform = mesh.matrix_local.copy()
localTransform = worldTransform * localTransform
for col in localTransform.col:
file.write('{:9f} {:9f} {:9f} {:9f}\n'.format(col[0], col[1], col[2], col[3]))
file.write('\n')
因此,如果您愿意,我的“世界”矩阵基本上是将搅拌机坐标系更改为默认 GL 坐标系的行为,其中 +y 向上、+x 向右和 -z 进入查看体积。然后我还对网格矩阵 M 进行预乘(在我们到达引擎时已经完成,而不是在矩阵乘法顺序方面的 post 或 pre 的意义上),这样我就不需要再乘一次了每个引擎中的绘图调用。
关于从 Blender 关节(Blender 用语中的骨骼)中提取的可能矩阵,我正在执行以下操作:
对于关节绑定姿势:
def DFSJointTraversal(file, skeleton, jointList): for joint in jointList: bindPoseJoint = skeleton.data.bones[joint.name] bindPoseTransform = bindPoseJoint.matrix_local.inverted() file.write('Joint ' + joint.name + ' Transform {\n') translationV = bindPoseTransform.to_translation() rotationQ = bindPoseTransform.to_3x3().to_quaternion() scaleV = bindPoseTransform.to_scale() file.write('T {:9f} {:9f} {:9f}\n'.format(translationV[0], translationV[1], translationV[2])) file.write('Q {:9f} {:9f} {:9f} {:9f}\n'.format(rotationQ[1], rotationQ[2], rotationQ[3], rotationQ[0])) file.write('S {:9f} {:9f} {:9f}\n'.format(scaleV[0], scaleV[1], scaleV[2])) DFSJointTraversal(file, skeleton, joint.children) file.write('}\n')
请注意,我实际上抓住了我认为的绑定姿势变换 Bj 的倒数。这样我就不需要在引擎中反转它了。另请注意,假设这是 Bj,我选择了 matrix_local。另一种选择是普通的“矩阵”,据我所知,它与非同质的相同。
对于联合当前/关键帧姿势:
for kfIndex in keyframes: bpy.context.scene.frame_set(kfIndex) file.write('keyframe: {:d}\n'.format(int(kfIndex))) for i in range(0, len(skeleton.data.bones)): file.write('joint: {:d}\n'.format(i)) currentPoseJoint = skeleton.pose.bones[i] currentPoseTransform = currentPoseJoint.matrix translationV = currentPoseTransform.to_translation() rotationQ = currentPoseTransform.to_3x3().to_quaternion() scaleV = currentPoseTransform.to_scale() file.write('T {:9f} {:9f} {:9f}\n'.format(translationV[0], translationV[1], translationV[2])) file.write('Q {:9f} {:9f} {:9f} {:9f}\n'.format(rotationQ[1], rotationQ[2], rotationQ[3], rotationQ[0])) file.write('S {:9f} {:9f} {:9f}\n'.format(scaleV[0], scaleV[1], scaleV[2])) file.write('\n')
请注意,这里我选择的是skeleton.pose.bones 而不是data.bones,并且我可以选择3 个矩阵:matrix、matrix_basis 和matrix_channel。从 python API 文档中的描述来看,我不是很清楚应该选择哪一个,尽管我认为它是普通矩阵。另请注意,在这种情况下,我不会反转矩阵。
实现(引擎/OpenGL端):
我的动画子系统在每次更新时执行以下操作(为了简单起见,我省略了更新循环中确定哪些对象需要更新并且时间在此处硬编码的部分):
static double time = 0;
time = fmod((time + elapsedTime),1.);
uint16_t LERPKeyframeNumber = 60 * time;
uint16_t lkeyframeNumber = 0;
uint16_t lkeyframeIndex = 0;
uint16_t rkeyframeNumber = 0;
uint16_t rkeyframeIndex = 0;
for (int i = 0; i < aClip.keyframesCount; i++) {
uint16_t keyframeNumber = aClip.keyframes[i].number;
if (keyframeNumber <= LERPKeyframeNumber) {
lkeyframeIndex = i;
lkeyframeNumber = keyframeNumber;
}
else {
rkeyframeIndex = i;
rkeyframeNumber = keyframeNumber;
break;
}
}
double lTime = lkeyframeNumber / 60.;
double rTime = rkeyframeNumber / 60.;
double blendFactor = (time - lTime) / (rTime - lTime);
GLKMatrix4 bindPosePalette[aSkeleton.jointsCount];
GLKMatrix4 currentPosePalette[aSkeleton.jointsCount];
for (int i = 0; i < aSkeleton.jointsCount; i++) {
F3DETQSType& lPose = aClip.keyframes[lkeyframeIndex].skeletonPose.joints[i];
F3DETQSType& rPose = aClip.keyframes[rkeyframeIndex].skeletonPose.joints[i];
GLKVector3 LERPTranslation = GLKVector3Lerp(lPose.t, rPose.t, blendFactor);
GLKQuaternion SLERPRotation = GLKQuaternionSlerp(lPose.q, rPose.q, blendFactor);
GLKVector3 LERPScaling = GLKVector3Lerp(lPose.s, rPose.s, blendFactor);
GLKMatrix4 currentTransform = GLKMatrix4MakeWithQuaternion(SLERPRotation);
currentTransform = GLKMatrix4TranslateWithVector3(currentTransform, LERPTranslation);
currentTransform = GLKMatrix4ScaleWithVector3(currentTransform, LERPScaling);
GLKMatrix4 inverseBindTransform = GLKMatrix4MakeWithQuaternion(aSkeleton.joints[i].inverseBindTransform.q);
inverseBindTransform = GLKMatrix4TranslateWithVector3(inverseBindTransform, aSkeleton.joints[i].inverseBindTransform.t);
inverseBindTransform = GLKMatrix4ScaleWithVector3(inverseBindTransform, aSkeleton.joints[i].inverseBindTransform.s);
if (aSkeleton.joints[i].parentIndex == -1) {
bindPosePalette[i] = inverseBindTransform;
currentPosePalette[i] = currentTransform;
}
else {
bindPosePalette[i] = GLKMatrix4Multiply(inverseBindTransform, bindPosePalette[aSkeleton.joints[i].parentIndex]);
currentPosePalette[i] = GLKMatrix4Multiply(currentPosePalette[aSkeleton.joints[i].parentIndex], currentTransform);
}
aSkeleton.skinningPalette[i] = GLKMatrix4Multiply(currentPosePalette[i], bindPosePalette[i]);
}
最后,这是我的顶点着色器:
#version 100
uniform mat4 modelMatrix;
uniform mat3 normalMatrix;
uniform mat4 projectionMatrix;
uniform mat4 skinningPalette[6];
uniform lowp float skinningEnabled;
attribute vec4 position;
attribute vec3 normal;
attribute vec2 tCoordinates;
attribute vec4 jointsWeights;
attribute vec4 jointsIndices;
varying highp vec2 tCoordinatesVarying;
varying highp float lIntensity;
void main()
{
tCoordinatesVarying = tCoordinates;
vec4 skinnedVertexPosition = vec4(0.);
for (int i = 0; i < 4; i++) {
skinnedVertexPosition += jointsWeights[i] * skinningPalette[int(jointsIndices[i])] * position;
}
vec4 skinnedNormal = vec4(0.);
for (int i = 0; i < 4; i++) {
skinnedNormal += jointsWeights[i] * skinningPalette[int(jointsIndices[i])] * vec4(normal, 0.);
}
vec4 finalPosition = mix(position, skinnedVertexPosition, skinningEnabled);
vec4 finalNormal = mix(vec4(normal, 0.), skinnedNormal, skinningEnabled);
vec3 eyeNormal = normalize(normalMatrix * finalNormal.xyz);
vec3 lightPosition = vec3(0., 0., 2.);
lIntensity = max(0.0, dot(eyeNormal, normalize(lightPosition)));
gl_Position = projectionMatrix * modelMatrix * finalPosition;
}
结果是动画在方向上显示错误。也就是说,它不是上下摆动,而是上下摆动(根据我在导出剪辑中的变换,我认为是 Z 轴)。并且旋转角度是逆时针而不是顺时针。
如果我尝试使用多个关节,那么就好像第二个关节在它自己的不同坐标空间中旋转并且不遵循 100% 其父级的变换。我假设它应该来自我的动画子系统,我假设它遵循我为多个关节的情况解释的理论。
有什么想法吗?