****編集2:**
効率や最適化の問題を指摘することは避けてください。これは最終的なコードではありません。これは私が基本を試しているだけです:)。すべてを理解したら、クリーンアップと最適化に取り掛かります。
編集:誰かが私にこれを手伝ってくれるかどうかを確認するために、質問をより簡単な言葉で再定式化することにしました。
基本的に、私はメッシュ、スケルトン、およびアクションをブレンダーから私が取り組んでいる種類のエンジンにエクスポートしています。しかし、私はアニメーションを間違っています。基本的なモーション パスがたどられていることはわかりますが、常に間違った移動軸または回転軸があります。問題はエンジン コード (OpenGL ベース) にある可能性が最も高いと思いますが、骨格アニメーション/スキニングの背後にある理論の一部を誤解しているか、エクスポーター スクリプトでブレンダーから適切なジョイント マトリックスをエクスポートする方法に問題があると思います。 .
理論、エンジン アニメーション システム、および私のブレンダー エクスポート スクリプトについて説明し、誰かがこれらのいずれかまたはすべてでエラーを見つけてくれることを願っています。
理論:(エンジンで使用しているのはOpenGLベースであるため、列優先の順序付けを使用しています)
- メッシュのローカル空間からワールド空間に頂点 v を取得する変換行列 M と共に、単一の頂点 v で構成されるメッシュがあるとします。つまり、スケルトンなしでメッシュをレンダリングすると、最終的な位置は gl_Position = ProjectionMatrix * M * v になります。
- ここで、バインド/レスト ポーズのジョイント j が 1 つあるスケルトンがあるとします。j は実際には別の行列です。j のローカル空間からその親空間への変換。これを Bj とします。j がスケルトンのジョイント階層の一部である場合、Bj は j 空間から j-1 空間 (つまり、その親空間) に移動します。ただし、この例では j が唯一のジョイントであるため、M が v に対して行ったように、Bj は j 空間からワールド空間を取得します。
- さらに、フレームのセットがあり、それぞれが 2 番目の変換 Cj を持ち、結合 j の異なる任意の空間構成に対して Bj と同じように機能すると仮定します。Cj は引き続き j 空間からワールド空間に頂点を取得しますが、j は回転および/または平行移動および/またはスケーリングされます。
上記を考えると、キーフレーム n で頂点 v をスキニングするには、次のようにします。する必要がある:
- v をワールド空間からジョイント j 空間に取る
- j を変更します (一方、v は j 空間に固定されているため、変換に使用されます)
- 修正された j 空間からワールド空間に v を戻す
したがって、上記の数学的な実装は次のようになります: v' = Cj * Bj^-1 * v . 実際、ここで 1 つの疑問があります. v が属するメッシュには、モデル空間からワールド空間への変換 M があると言いました。また、モデル空間からジョイント空間に変換する必要があることをいくつかの教科書で読みました。しかし、v は世界から共同空間に変換する必要があるとも 1 で述べました。基本的に、 v' = Cj * Bj^-1 * vまたはv' = Cj * Bj^-1 * M * vを実行する必要があるかどうかはわかりません。現在、私の実装は、v ではなく M で v' を乗算しています。しかし、これを変更しようとしましたが、別の方法で問題が発生するだけです。
- 最後に、頂点をジョイント j0 の子であるジョイント j1 にスキニングする場合、Bj1 は Bj0 * Bj1 になり、Cj1 は Cj0 * Cj1 になります。ただし、スキニングはv' = Cj * Bj^-1 * vとして定義されるため、Bj1^-1 は元の製品を構成する逆数の逆連結になります。つまり、v' = Cj0 * Cj1 * Bj1^-1 * Bj0^-1 * v
実装に移りましょう (Blender 側):
次のメッシュが 1 つの立方体で構成され、その頂点が単一関節スケルトンの単一関節にバインドされているとします。
また、60 fps で 60 フレーム、3 キーフレームのアニメーションがあるとします。アニメーションは基本的に次のとおりです。
- キーフレーム 0: ジョイントはバインド/レスト ポーズです (画像で見る方法)。
- キーフレーム 30: ジョイントは上方向に移動し (ブレンダーでは +z)、同時に時計回りに pi/4 ラジアン回転します。
- キーフレーム 59: ジョイントはキーフレーム 0 と同じ構成に戻ります。
ブレンダー側での私の最初の混乱の原因は、その座標系 (OpenGL のデフォルトとは対照的に) と、Python API を介してアクセスできるさまざまな行列です。
現在、これは私のエクスポート スクリプトがBlender の座標系を 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 を前もって乗算します (エンジンに到達するまでに完了しているという意味で、行列の乗算順序に関するポストまたはプリの意味ではありません)。エンジンのドローコールごと。
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 を選択したことにも注意してください。もう1つのオプションは単純な「マトリックス」です。これは、私が知る限り、均質ではないという点だけが同じです。
現在のジョイント/キーフレーム ポーズの場合:
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')
ここでは、data.bones の代わりにskeleton.pose.bones を使用し、matrix、matrix_basis、matrix_channel の 3 つの行列を選択できることに注意してください。python API docs の説明から、どれを選択すればよいかわかりませんが、単純なマトリックスだと思います。また、この場合、行列を反転しないことに注意してください。
実装 (エンジン/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 軸に沿って)。また、回転角度は時計回りではなく反時計回りです。
複数のジョイントを試してみると、2 番目のジョイントが独自の異なる座標空間で回転し、親の変換に 100% 従わないように見えます。私のアニメーションサブシステムから、複数の関節の場合について説明した理論に従うと仮定します。
何かご意見は?