مرحبا! في الوقت الحالي ، هناك عدد كبير من المقالات حول حبري مخصصة لرسومات الكمبيوتر وتنفيذ تأثيرات مختلفة ، ومع ذلك ، هناك عدد غير قليل من النصوص حول تنفيذ الرسوم المتحركة للهيكل العظمي (خاصة من الصفر). سأحاول ملء هذه الفجوة بمساعدة هذا النص مع وصف للتكنولوجيا ومثال على التنفيذ البسيط في C ++ و OpenGL 4.5 (SDL2).

المقدمة
. , , ( ).
, , . .
, .
, . , , () , . .
, , :
-  
  - 
      -  
      -  
  -  
  -  
, , - , .
, , .
.
, ( ). , , , . .
, , . "", "", . .
( ) Autodesk 3ds Max. , , .

, . . . , 3D- , .
( ). ( ). , ( 0.0 1.0), . , . .
( — ).

, ( , ). T- ( T) bind- .
( ) , . ( ) ( , , ).
, — , , , . . , .

, , , .
, , , FBX COLLADA. . ( ) assimp.
, , : , bind- ( ), , , . .
, . SRT- (scale, rotation, translation — , , /). , , , RT- ( R-). .
, "", . Joint (, ), .
, . , . . .
, — , — . , . RT- .
bind- (inverse bind pose matrix). . .
bind- (xyz-) , — .
:

, 3 . :

, 3 bind- , , .
, .
, bind- , , .
(matrix palette), bind- . bind- .
, . .
, , , .
, , . ( ) . , , , — ( ).
, . , .
, , glm.
. :
struct Bone {
  uint8_t parentId; 
  glm::mat4 inverseBindPoseMatrix; 
  static constexpr uint8_t ROOT_BONE_PARENT_ID = 255; 
};
: get/set , , , , . .
( , ):
struct Skeleton {
  std::vector<Bone> bones; 
};
. , . , , .
struct BoneAnimationPositionFrame {
  float time; 
  glm::vec3 position; 
};
struct BoneAnimationOrientationFrame {
  float time; 
  glm::quat orientation; 
};
— , float.
struct BoneAnimationChannel {
  std::vector<BoneAnimationPositionFrame> positionFrames;
  std::vector<BoneAnimationOrientationFrame> orientationFrames;
};
, . . .
struct BonePose {
  [[nodiscard]] glm::mat4 getBoneMatrix() const
  {
    return glm::translate(glm::identity<glm::mat4>(), position) * glm::mat4_cast(orientation);
  }
  glm::vec3 position = glm::vec3(0.0f);
  glm::quat orientation = glm::identity<glm::quat>();
};
getBoneMatrix() , . , , , . .
RT- , . , , :
inline BonePose operator*(const BonePose& a, const BonePose& b)
{
  BonePose result;
  result.orientation = a.orientation * b.orientation;
  result.position = a.position + glm::vec3(a.orientation * glm::vec4(b.position, 1.0f));
  return result;
}
RT- , , - .
:
struct AnimationMatrixPalette {
  std::vector<glm::mat4> bonesTransforms;
};
, , (, ). . , , . .
class AnimationPose {
 public:
  
  [[nodiscard]] const AnimationMatrixPalette& getMatrixPalette() const {
    
    m_matrixPalette.bonesTransforms[0] = bonesLocalPoses[0].getBoneMatrix();
    auto bonesCount = static_cast<uint8_t>(m_matrixPalette.bonesTransforms.size());
    
    for (uint8_t boneIndex = 1; boneIndex < bonesCount; boneIndex++) {
      m_matrixPalette.bonesTransforms[boneIndex] =
        m_matrixPalette.bonesTransforms[m_skeleton.bones[boneIndex].parentId] *
          bonesLocalPoses[boneIndex].getBoneMatrix();
    }
    
    for (uint8_t boneIndex = 0; boneIndex < bonesCount; boneIndex++) {
      m_matrixPalette.bonesTransforms[boneIndex] *= m_skeleton.bones[boneIndex].inverseBindPoseMatrix;
    }
    return m_matrixPalette;
  }
 public:
  std::vector<BonePose> bonesLocalPoses;
 private:
  Skeleton m_skeleton;
  mutable AnimationMatrixPalette m_matrixPalette;
};
. .
, (, , , ). .
class AnimationClip {
 
 private:
  Skeleton m_skeleton 
  std::vector<BoneAnimationChannel> m_bonesAnimationChannels 
  mutable AnimationPose m_currentPose; 
  float m_currentTime = 0.0f; 
  float m_duration = 0.0f; 
  float m_rate = 0.0f; 
, , :
  void increaseCurrentTime(float delta) {
    m_currentTime += delta * m_rate;
    if (m_currentTime > m_duration) {
      int overflowParts = static_cast<int>(m_currentTime / m_duration);
      m_currentTime -= m_duration * static_cast<float>(overflowParts);
    }
  }
— . ( — ).
, , . - , , .
. , AnimationPose . .
  [[nodiscard]] const AnimationPose& getCurrentPose() const {
    m_currentPose.bonesLocalPoses[0] = getBoneLocalPose(0, m_currentTime);
    auto bonesCount = static_cast<uint8_t>(m_skeleton.bones.size());
    for (uint8_t boneIndex = 1; boneIndex < bonesCount; boneIndex++) {
      m_currentPose.bonesLocalPoses[boneIndex] = getBoneLocalPose(boneIndex, m_currentTime);
    }
    return m_currentPose;
  }
, getBoneLocalPose(), .
  [[nodiscard]] BonePose getBoneLocalPose(uint8_t boneIndex, float time) const
  {
    const std::vector<BoneAnimationPositionFrame>& positionFrames =
      m_bonesAnimationChannels[boneIndex].positionFrames;
    
    auto position = getMixedAdjacentFrames<glm::vec3, BoneAnimationPositionFrame>(positionFrames, time);
    const std::vector<BoneAnimationOrientationFrame>& orientationFrames =
      m_bonesAnimationChannels[boneIndex].orientationFrames;
    
    auto orientation = getMixedAdjacentFrames<glm::quat, BoneAnimationOrientationFrame>(orientationFrames, time);
    return BonePose(position, orientation);
  }
, , getMixedAdjacentFrames, "" .
, . :
- . .
 - . , . , , .
 - . . , , ( , );
 
, , .
  
  template<class T, class S>
  [[nodiscard]] T getMixedAdjacentFrames(const std::vector<S>& frames, float time) const
  {
    S tempFrame;
    tempFrame.time = time;
    
    auto frameIt = std::upper_bound(frames.begin(), frames.end(),
      tempFrame, [](const S& a, const S& b) {
        return a.time < b.time;
      });
    if (frameIt == frames.end()) {
      
      return (frames.size() > 0) ? getKeyframeValue<T, S>(*frames.rbegin()) : getIdentity<T>();
    }
    else {
      T next = getKeyframeValue<T, S>(*frameIt);
      
      T prev = (frameIt == frames.begin()) ? getIdentity<T>() : getKeyframeValue<T, S>(*std::prev(frameIt));
      
      float currentFrameTime = frameIt->time;
      float prevFrameTime = (frameIt == frames.begin()) ? 0 : std::prev(frameIt)->time;
      float framesTimeDelta = currentFrameTime - prevFrameTime;
      return getInterpolatedValue<T>(prev, next, (time - prevFrameTime) / framesTimeDelta);
    }
  }
(getInterpolatedValue()), (getKeyframeValue()) - (getIdentity).
template<>
glm::vec3 AnimationClip::getIdentity() const
{
  return glm::vec3(0.0f);
}
template<>
glm::quat AnimationClip::getIdentity() const
{
  return glm::identity<glm::quat>();
}
template<>
glm::vec3 AnimationClip::getKeyframeValue(const BoneAnimationPositionFrame& frame) const
{
  return frame.position;
}
template<>
glm::quat AnimationClip::getKeyframeValue(const BoneAnimationOrientationFrame& frame) const
{
  return frame.orientation;
}
template<>
glm::vec3 AnimationClip::getInterpolatedValue(const glm::vec3& first, const glm::vec3& second, float delta) const
{
  return glm::mix(first, second, delta);
}
template<>
glm::quat AnimationClip::getInterpolatedValue(const glm::quat& first, const glm::quat& second, float delta) const
{
  return glm::slerp(first, second, delta);
}
, (glm::mix()), — (glm::slerp())
, , , . : , .
, , :
struct VertexPos3Norm3UVSkinned {
  glm::vec3 pos = {0.0f, 0.0f, 0.0f};
  glm::vec3 norm = {0.0f, 0.0f, 0.0f};
  glm::vec2 uv = {0.0f, 0.0f};
  glm::u8vec4 bonesIds = {0, 0, 0, 0};
  glm::u8vec4 bonesWeights = {0, 0, 0, 0};
};
, , . 0 1, 0 255 . 4- .
: , .
:
#version 450 core
layout (location = 0) in vec3 attrPos;
layout (location = 1) in vec3 attrNorm;
layout (location = 2) in vec2 attrUV;
//    
layout (location = 4) in uvec4 attrBonesIds;
layout (location = 5) in uvec4 attrBonesWeights;
// ...
//         128 
struct AnimationPalette {
    mat4 palette[128];
};
uniform AnimationPalette animation;
// ...
void main() {
    vec4 position = vec4(attrPos, 1.0);
    //            
    vec4 newPosition = (float(attrBonesWeights[0]) / 255.0) * animation.palette[attrBonesIds[0]] * position + 
(float(attrBonesWeights[1]) / 255.0) * animation.palette[attrBonesIds[1]] * position +
                (float(attrBonesWeights[2]) / 255.0) * animation.palette[attrBonesIds[2]] * position +
                (float(attrBonesWeights[3]) / 255.0) * animation.palette[attrBonesIds[3]] * position;
    outVertexData.uv = attrUV;
    gl_Position = scene.cameraToProjection * scene.worldToCamera * transform.localToWorld * (vec4(newPosition.xyz, 1.0));
}
- :
static void updateScene(float delta)
{
  g_animationClip->increaseCurrentTime(delta);
}
:
static void renderScene()
{
  
  const AnimationMatrixPalette& currentMatrixPalette = g_animationClip->getCurrentPose().getMatrixPalette();
  setShaderArrayParameter(g_vertexShader, "animation.palette[0]", currentMatrixPalette.bonesTransforms);
  
}
, C++. - .
يمكن العثور على الرمز الكامل هنا . يتم تنفيذ sources/main.cppتهيئة OpenGL وتحميل الموارد وتحديث حالة المشهد والعرض. تنفيذ الرسوم المتحركة في sources/Animation.
يسعدني أن أجيب على الأسئلة وأن أتلقى انتقادات وتعليقات موضوعية.
إذا كان هذا الموضوع مثيرًا للاهتمام ، في المقالات التالية ، يمكنك التفكير في تنفيذ عناصر مثل خلط المقاطع ، والانتقالات السلسة بينها ، وآلة حالة لإدارة التحولات.