如何从blender导出bind和keyframe骨骼姿势以在OpenGL中使用

时间:2012-11-20 05:58:27

标签: opengl animation blender

**** EDIT2:**

请避免指出效率/优化问题;这根本不是最终的代码。这只是我尝试基础知识:)。一旦我把一切都搞定了,我就会去清理和优化。

编辑:我决定用更简单的术语重新表述这个问题,看看是否有人可以帮我解决这个问题。

基本上,我将网格,骨架和动作从混合器导出到我正在处理的各种引擎中。但我让动画错了。我可以看出正在遵循的基本运动路径,但总是一个平移或旋转的轴是错误的。我认为问题很可能不在我的引擎代码(基于OpenGL)中,而是在我对骨架动画/皮肤化背后的理论的某些部分的误解,或者我在导出器脚本中从blender导出适当的关节矩阵的方式

我将解释理论,引擎动画系统和我的blender导出脚本,希望有人可能会在其中一个或全部中发现错误。

理论:(我使用了列主要排序,因为我在引擎中使用它导致它基于OpenGL)

  • 假设我有一个由单个顶点v组成的网格,以及一个 变换矩阵M,它从网格中获取顶点v 当地空间到世界空间。也就是说,如果我要渲染网格物体 没有骨架,最终位置将是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。我需要:

  1. 将v从世界空间带到联合j空间
  2. 修改j(而v在j空间中保持固定,因此被带入 转型)
  3. 将v从修改后的j空间带回世界空间
  4. 所以上面的数学实现将是: 的 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个立方体组成,其顶点绑定到单关节骨架中的单个关节:

    enter image description here

    假设还有一个60帧,3个关键帧动画,速度为60 fps。动画本质上是:

    • 关键帧0:关节处于绑定/静止姿势(您在图像中看到它的方式)。
    • 关键帧30:关节向上平移(搅拌器中的+ z)一些量,同时顺时针旋转pi / 4 rad。
    • 关键帧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进入查看体积。然后我也预乘(在我们到达引擎的时候完成它,而不是在矩阵乘法顺序方面的post或pre意义上)网格矩阵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的反转。这是我不需要在引擎中反转它。还要注意我去了matrix_local,假设这是Bj。另一种选择是简单的"矩阵",据我所知,只有不均匀的。

    • 对于关节当前/关键帧姿势:

      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文档中的描述,我并不十分清楚我应该选择哪一个,尽管我认为它是普通矩阵。另请注意,在这种情况下我不会反转矩阵。

    实施(Engine / 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%变换。我认为它应该来自我的动画子系统,我假设它依次遵循我为不止一个关节的情况所解释的理论。

    有什么想法吗?

0 个答案:

没有答案