Realización de animación esquelética para modelos tridimensionales.

¡Hola! Por el momento, hay una gran cantidad de artículos sobre Habré dedicados a los gráficos por computadora y la implementación de diversos efectos, sin embargo, hay bastantes textos sobre la implementación de la animación esquelética (especialmente desde cero). Intentaré llenar este vacío con la ayuda de este texto con una descripción de la tecnología y un ejemplo de una implementación simple en C ++ y OpenGL 4.5 (SDL2).



Introducción


. , , ( ).


, , . .


, .


, . , , () , . .


, , :


-  
  - 
      -  
      -  
  -  
  -  

, , - , .


, , .



.


, ( ). , , , . .


, , . "", "", . .


( ) Autodesk 3ds Max. , , .



, . . . , 3D- , .


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


( — ).



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



( ) , . ( ) ( , , ).


, — , , , . . , .



, , , .


, , , FBX COLLADA. . ( ) assimp.



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


, . SRT- (scale, rotation, translation — , , /). , , , RT- ( R-). .


, "", . Joint (, ), .


, . , . . .


Pij, i— , j— . M, . RT- .


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


bind- (xyz-) v, — v.


:



, 3 . :



, 3 bind- B3, , .


, P0M.


, bind- , Pij, .


(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() , Pij. , , , . .


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=tt0t1t0.
  • . . , , ( , );

, , .


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


-

El código completo se puede encontrar aquí . Se realiza la sources/main.cppinicialización de OpenGL, carga de recursos, actualización del estado de la escena y renderización. Una implementación de animación está en sources/Animation.


Estaré encantado de responder preguntas, así como recibir críticas objetivas y comentarios.


Si este tema es interesante, en los siguientes artículos puede considerar la implementación de elementos como mezclar clips, transiciones suaves entre ellos, una máquina de estado para administrar las transiciones.


All Articles