Realization of skeletal animation for three-dimensional models

Hello! At the moment, there are a large number of articles on HabrΓ© devoted to computer graphics and the implementation of various effects, however, there are quite a few texts on the implementation of skeletal animation (especially from scratch). I will try to fill this gap with the help of this text with a description of the technology and an example of a simple implementation in C ++ and OpenGL 4.5 (SDL2).



Introduction


. , , ( ).


, , . .


, .


, . , , () , . .


, , :


-  
  - 
      -  
      -  
  -  
  -  

, , - , .


, , .



.


, ( ). , , , . .


, , . "", "", . .


( ) Autodesk 3ds Max. , , .



, . . . , 3D- , .


( ). ( ). , ( 0.0 1.0), . , . .


( β€” ).



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



( ) , . ( ) ( , , ).


, β€” , , , . . , .



, , , .


, , , FBX COLLADA. . ( ) assimp.



, , : , bind- ( ), , , . .


, . SRT- (scale, rotation, translation β€” , , /). , , , RT- ( R-). .


, "", . Joint (, ), .


, . , . . .


Pi→j, i— , j— . M, . RT- .


bind- (inverse bind pose matrix). Bi. .


bind- (xyz-) v, β€” vβ€².


:



, 3 . :



, 3 bind- B3, , .


, P0β†’M.


, bind- , Pi→j, .


(matrix palette), bind- . bind- .


, . .


, , , .



, , . t( ) . , , , β€” ( ).



, . , .


, , glm.


. :


struct Bone {
  uint8_t parentId; // ID   (      ROOT_BONE_PARENT_ID,  ,     ,  254 )
  glm::mat4 inverseBindPoseMatrix; //  bind-pose          

  static constexpr uint8_t ROOT_BONE_PARENT_ID = 255; // ID  
};

: 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() , Pi→j. , , , . .


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();
    }

    //          bind-
    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, "" .


, . :


  • . .
  • . , . t, t0t1, k=tβˆ’t0t1βˆ’t0.
  • . . , , ( , );

, , .


  //  ""         
  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++. - .


-

The full code can be found here . The sources/main.cppinitialization of OpenGL, loading of resources, updating the state of the scene and rendering is performed. An animation implementation is in sources/Animation.


I will be glad to answer questions, as well as receive objective criticism and comments.


If this topic is interesting, in the following articles you can consider the implementation of such elements as mixing clips, smooth transitions between them, a state machine for managing transitions.


All Articles