Hi all,
I just completed my code for skeletal animation. But my animations look skewed. I feel like I’m missing something minor that’s causing the issue but I spent days now trying to figure out to no avail.
Just rendering the model itself works and the issue I believe comes down the matrix calculations because if I send identity matrices for the current pose, it renders it fine(without the animation obviously).
I followed the thin matrix tutorial as well as https://github.com/hasinaxp/skeletal_animation-_assimp_opengl/blob/master/main.cpp
So my code is mix of both and my own.
Here’s my class for the animation.
Header:
class SkeletalAnimation {
typedef struct Bone{
int id;
std::string name;
glm::mat4 offset;
std::vector<Bone> children;
} Bone;
typedef struct {
std::vector<float> translationTimestamps;
std::vector<float> rotationTimetamps;
std::vector<float> scalingTimetamps;
std::vector<glm::vec3> translations;
std::vector<glm::quat> rotations;
std::vector<glm::vec3> scalings;
} BoneTransforms;
typedef struct Animation {
float duration;
float ticksPerSecond;
std::unordered_map<std::string, BoneTransforms> boneTransforms;
Animation(float pDuration, float ticksPerSecond) :
duration(pDuration),
ticksPerSecond(ticksPerSecond),
boneTransforms({})
{}
Animation() {}
} Animation;
typedef std::unordered_map<std::string, std::pair<int, glm::mat4>> BoneData;
typedef std::unordered_map<std::string, Animation> AnimationMap;
typedef std::vector<glm::mat4> Pose;
typedef struct{
unsigned int segment;
float fracture;
} Segment;
typedef struct {
Pose pose;
BoneData boneData;
unsigned int boneCount;
std::string name;
Bone skeleton;
} MeshEntry;
typedef std::unordered_map<std::string, MeshEntry> MeshBoneMap;
private:
const std::string mPath;
SDL_Renderer* mRenderer;
std::vector<MeshEntry> mMeshEntries;
std::vector<SkeletalMesh*> mMeshes;
std::vector<ImageTexture*> mTextures;
std::vector<unsigned int> mMeshToTexture;
std::string* mCurrentAnimation;
std::vector<std::string> mAnimations;
AnimationMap mAnimationMap;
Segment mCurrentSegment;
glm::mat4 mGlobalInverseTransform;
MeshBoneMap mMeshBoneMap;
static glm::mat4 sIdentityMatrix;
void LoadNode(aiNode* pNode, const aiScene* pScene);
void LoadSkeletalMesh(aiMesh* pMesh, const aiScene* pScene);
bool LoadBones(Bone& pBone, aiNode* pNode, BoneData& pBoneData);
void LoadAnimations(const aiScene* pScene);
void LoadMaterials(const aiScene* pScene);
void Animate(float pDeltaTime, Bone& pSkeleton, Pose& pPose, glm::mat4& pParentTransform);
static inline glm::mat4 aiToGlmMat4(const aiMatrix4x4& pAiMat);
static inline glm::vec3 aiToGlmVec3(const aiVector3D& pAiVec);
static inline glm::quat aiToGlmQuat(const aiQuaternion& pAiVec);
static inline void GetSegment(Segment* pSegment,const std::vector<float>& pTimestamps,const float pDeltaTime);
public:
SkeletalAnimation(const std::string pPath, SDL_Renderer* pRenderer);
~SkeletalAnimation();
void LoadAnimation();
void GetAllAnimations(std::vector<std::string>* pAnimations);
void SetAnimation(std::string pAnimation);
void RenderAnimation(float pDeltaTime, SkeletalAnimationShader* pSkeletalAnimationShader);
void ClearModel();
};
}
And source
glm::mat4 SkeletalAnimation::sIdentityMatrix = glm::mat4();
SkeletalAnimation::SkeletalAnimation(const std::string pPath, SDL_Renderer* pRenderer) :
mPath(pPath),
mRenderer(pRenderer),
mCurrentAnimation(new std::string()),
mAnimations({}),
mAnimationMap({}),
mMeshBoneMap({})
{}
SkeletalAnimation::~SkeletalAnimation() {
}
void SkeletalAnimation::LoadAnimation() {
Assimp::Importer _importer;
const aiScene* _scene = _importer.ReadFile(mPath, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_GenSmoothNormals | aiProcess_JoinIdenticalVertices);
if (!_scene) {
SDL_Log("Assimp Error Loading Animation at path: %s \n Error: %s .", mPath.c_str(), _importer.GetErrorString());
return;
}
mGlobalInverseTransform = glm::inverse(aiToGlmMat4(_scene->mRootNode->mTransformation));
LoadNode(_scene->mRootNode, _scene);
LoadAnimations(_scene);
LoadMaterials(_scene);
}
void SkeletalAnimation::LoadNode(aiNode* pNode, const aiScene* pScene) {
for (size_t i = 0; i < pNode->mNumMeshes; i++) {
LoadSkeletalMesh(pScene->mMeshes[pNode->mMeshes[i]], pScene);
}
for (size_t i = 0; i < pNode->mNumChildren; i++) {
LoadNode(pNode->mChildren[i], pScene);
}
}
void SkeletalAnimation::LoadSkeletalMesh(aiMesh* pMesh, const aiScene* pScene) {
MeshEntry _meshEntry;
_meshEntry.boneCount = pMesh->mNumBones;
_meshEntry.name = std::string(pMesh->mName.C_Str());
_meshEntry.pose = {};
_meshEntry.pose.resize(pMesh->mNumBones, sIdentityMatrix);
_meshEntry.boneData = {};
SkeletalMeshData _meshData;
for (size_t i = 0; i < pMesh->mNumVertices; i++) {
_meshData.vertices.insert(_meshData.vertices.end(),
{
pMesh->mVertices[i].x,
pMesh->mVertices[i].y ,
pMesh->mVertices[i].z });
if (pMesh->mTextureCoords[0]) {
_meshData.uvs.insert(_meshData.uvs.end(),
{
pMesh->mTextureCoords[0][i].x,
pMesh->mTextureCoords[0][i].y
});
}
else {
_meshData.uvs.insert(_meshData.uvs.end(),
{
0.0f,
0.0f
});
}
_meshData.normals.insert(_meshData.normals.end(),
{
pMesh->mNormals[i].x,
pMesh->mNormals[i].y ,
pMesh->mNormals[i].z });
}
for (size_t i = 0; i < pMesh->mNumFaces; i++) {
aiFace _face = pMesh->mFaces[i];
for (size_t j = 0; j < _face.mNumIndices; j++) {
_meshData.indices.push_back(_face.mIndices[j]);
}
}
unsigned int _indexCount = pMesh->mNumVertices * 4;
_meshData.weights.resize(_indexCount, 0.0f);
_meshData.boneIDs.resize(_indexCount, 0);
for (size_t i = 0; i < pMesh->mNumBones; i++) {
aiBone* _bone = pMesh->mBones[i];
glm::mat4 _offset = aiToGlmMat4(_bone->mOffsetMatrix);
_meshEntry.boneData[_bone->mName.C_Str()] = std::make_pair(i , _offset);
for (size_t j = 0; j < _bone->mNumWeights; j++) {
aiVertexWeight _weight = _bone->mWeights[j];
unsigned int _vertexID = _weight.mVertexId * 4;
for (size_t k = 0; k < 4; k++) {
if (_meshData.weights[_vertexID + k] == 0.0f) {
_meshData.weights[_vertexID + k] = _weight.mWeight;
_meshData.boneIDs[_vertexID + k] = i;
break;
}
}
}
}
/*for (size_t i = 0; i < _meshData.weights.size(); i+=4) {
float _totalWeight =
_meshData.weights[i] +
_meshData.weights[i+1] +
_meshData.weights[i+2] +
_meshData.weights[i+3];
if (_totalWeight > 0.0f) {
_meshData.weights[i] /= _totalWeight;
_meshData.weights[i+1] /= _totalWeight;
_meshData.weights[i+2] /= _totalWeight;
_meshData.weights[i+3] /= _totalWeight;
}
}*/
SkeletalMesh* _newMesh = new SkeletalMesh();
_newMesh->BuildMesh(_meshData);
mMeshes.push_back(_newMesh);
mMeshToTexture.push_back(pMesh->mMaterialIndex);
LoadBones(_meshEntry.skeleton, pScene->mRootNode, _meshEntry.boneData);
mMeshEntries.push_back(_meshEntry);
}
bool SkeletalAnimation::LoadBones(Bone& pBone ,aiNode* pNode, BoneData& pBoneData) {
if (pBoneData.find(pNode->mName.C_Str()) != pBoneData.end()) {
pBone.name = pNode->mName.C_Str();
pBone.id = pBoneData[pBone.name].first;
pBone.offset = pBoneData[pBone.name].second;
for (size_t i = 0; i < pNode->mNumChildren; i++) {
Bone _child;
LoadBones(_child, pNode->mChildren[i], pBoneData);
pBone.children.push_back(_child);
}
return true;
}
else {
for (size_t i = 0; i < pNode->mNumChildren; i++) {
if (LoadBones(pBone, pNode->mChildren[i], pBoneData)) {
return true;
}
}
}
return false;
}
void SkeletalAnimation::LoadAnimations(const aiScene* pScene) {
for (size_t i = 0; i < pScene->mNumAnimations; i++) {
Animation _currentInternalAnimation(0.0f, 1.0f);
aiAnimation* _currentAiAnimation = pScene->mAnimations[i];
mAnimations.push_back(std::string(_currentAiAnimation->mName.C_Str()));
if (i == 0) {
*mCurrentAnimation = mAnimations[0];
}
if (_currentAiAnimation->mTicksPerSecond != 0.0f) {
_currentInternalAnimation.ticksPerSecond = _currentAiAnimation->mTicksPerSecond;
}
else {
_currentInternalAnimation.ticksPerSecond = 1;
}
_currentInternalAnimation.duration = _currentAiAnimation->mDuration * _currentAiAnimation->mTicksPerSecond;
_currentInternalAnimation.boneTransforms = {};
BoneTransforms _transforms;
for (size_t j = 0; j < _currentAiAnimation->mNumChannels; j++) {
aiNodeAnim* _channel = _currentAiAnimation->mChannels[j];
for (size_t k = 0; k < _channel->mNumPositionKeys; k++) {
_transforms.translations.push_back(aiToGlmVec3(_channel->mPositionKeys[k].mValue));
_transforms.translationTimestamps.push_back(_channel->mPositionKeys[k].mTime);
}
for (size_t k = 0; k < _channel->mNumRotationKeys; k++) {
_transforms.rotations.push_back(aiToGlmQuat(_channel->mRotationKeys[k].mValue));
_transforms.rotationTimetamps.push_back(_channel->mRotationKeys[k].mTime);
}
for (size_t k = 0; k < _channel->mNumScalingKeys; k++) {
_transforms.scalings.push_back(aiToGlmVec3(_channel->mScalingKeys[k].mValue));
_transforms.scalingTimetamps.push_back(_channel->mScalingKeys[k].mTime);
}
_currentInternalAnimation.boneTransforms[_channel->mNodeName.C_Str()] = _transforms;
}
mAnimationMap[_currentAiAnimation->mName.C_Str()] = _currentInternalAnimation;
}
}
void SkeletalAnimation::LoadMaterials(const aiScene* pScene) {
mTextures.resize(pScene->mNumMaterials);
for (size_t i = 0; i < pScene->mNumMaterials; i++) {
aiMaterial* _material = pScene->mMaterials[i];
mTextures[i] = nullptr;
if (_material->GetTextureCount(aiTextureType_DIFFUSE)) {
aiString _path;
if (_material->GetTexture(aiTextureType_DIFFUSE, 0, &_path) == AI_SUCCESS) {
int _idx = std::string(_path.data).rfind("\\");
std::string _fileName = std::string(_path.data).substr(_idx + 1);
std::string _texturePath = std::string("assets/") + _fileName;
SDL_Log("Model Loading Texture at path: %s .", _texturePath.c_str());
mTextures[i] = new ImageTexture(_texturePath, mRenderer);
mTextures[i]->Load();
if (!mTextures[i]->IsLoaded()) {
delete mTextures[i];
mTextures[i] = nullptr;
SDL_Log("Model Error Loading Texture at path: %s .", _texturePath.c_str());
}
}
}
}
}
void SkeletalAnimation::Animate(float pDeltaTime, Bone& pSkeleton, Pose& pPose, glm::mat4& pParentTransform) {
Animation& _currentAnimation = mAnimationMap[*mCurrentAnimation];
BoneTransforms& _boneTransforms = _currentAnimation.boneTransforms[pSkeleton.name];
pDeltaTime = fmod(pDeltaTime, _currentAnimation.duration);
//Calculate translations
GetSegment(&mCurrentSegment, _boneTransforms.translationTimestamps, pDeltaTime);
glm::vec3 _translation = glm::mix(
_boneTransforms.translations[mCurrentSegment.segment - 1],
_boneTransforms.translations[mCurrentSegment.segment],
mCurrentSegment.fracture);
//Calculate rotations
GetSegment(&mCurrentSegment, _boneTransforms.rotationTimetamps, pDeltaTime);
glm::quat _rotation = glm::slerp(
_boneTransforms.rotations[mCurrentSegment.segment - 1],
_boneTransforms.rotations[mCurrentSegment.segment],
mCurrentSegment.fracture);
//Calculate scalings
GetSegment(&mCurrentSegment, _boneTransforms.scalingTimetamps, pDeltaTime);
glm::vec3 _scaling = glm::mix(
_boneTransforms.scalings[mCurrentSegment.segment - 1],
_boneTransforms.scalings[mCurrentSegment.segment],
mCurrentSegment.fracture);
glm::mat4 _translationMatrix = glm::mat4(1.0f);
_translationMatrix = glm::translate(_translationMatrix, _translation);
glm::mat4 _rotationMatrix = glm::toMat4(_rotation);
glm::mat4 _scalingMatrix = glm::mat4(1.0f);
_scalingMatrix = glm::scale(_scalingMatrix, _scaling);
glm::mat4 _localTransform = _translationMatrix * _rotationMatrix * _scalingMatrix;
glm::mat4 _globalTransform = pParentTransform * _localTransform;
pPose[pSkeleton.id] = mGlobalInverseTransform * _globalTransform * pSkeleton.offset;
for (Bone& _child : pSkeleton.children) {
Animate(pDeltaTime, _child, pPose, _globalTransform);
}
}
void SkeletalAnimation::GetAllAnimations(std::vector<std::string>* pAnimations) {
pAnimations->clear();
*pAnimations = mAnimations;
}
void SkeletalAnimation::SetAnimation(std::string pAnimation) {
assert(std::find(mAnimations.begin(), mAnimations.end(), pAnimation) != mAnimations.end(), "Animation does not exist.");
*mCurrentAnimation = pAnimation;
}
void SkeletalAnimation::RenderAnimation(float pDeltaTime, SkeletalAnimationShader* pSkeletalAnimationShader) {
for (size_t i = 0; i < mMeshes.size(); i++) {
unsigned int _materialIndex = mMeshToTexture[i];
if (_materialIndex < mTextures.size() && mTextures[_materialIndex]) {
mTextures[_materialIndex]->Enable();
}
MeshEntry& _entry = mMeshEntries[i];
Animate(pDeltaTime, _entry.skeleton, _entry.pose, sIdentityMatrix);
pSkeletalAnimationShader->SetBoneTransforms(_entry.boneCount, _entry.pose);
mMeshes[i]->Render();
}
}
void SkeletalAnimation::ClearModel() {
for (size_t i = 0; i < mMeshes.size(); i++) {
if (mMeshes[i]) {
delete mMeshes[i];
mMeshes[i] = nullptr;
}
}
for (size_t i = 0; i < mTextures.size(); i++) {
if (mTextures[i]) {
delete mTextures[i];
mTextures[i] = nullptr;
}
}
}
void SkeletalAnimation::GetSegment(Segment* pSegment,const std::vector<float>& pTimestamps, const float pDeltaTime) {
unsigned int _segment = 0;
while (pDeltaTime > pTimestamps[_segment]) {
_segment++;
}
float _start = pTimestamps[_segment - 1];
float _end = pTimestamps[_segment];
float _fracture = (pDeltaTime - _start) / (_end - _start);
pSegment->segment = _segment;
pSegment->fracture = _fracture;
}
glm::mat4 SkeletalAnimation::aiToGlmMat4(const aiMatrix4x4& pAiMat) {
glm::mat4 _glmMat;
for (int y = 0; y < 4; y++)
{
for (int x = 0; x < 4; x++)
{
_glmMat[x][y] = pAiMat[y][x];
}
}
return _glmMat;
}
glm::vec3 SkeletalAnimation::aiToGlmVec3(const aiVector3D& pAiVec) {
return glm::vec3(pAiVec.x, pAiVec.y, pAiVec.z);
}
glm::quat SkeletalAnimation::aiToGlmQuat(const aiQuaternion& pAiQuat) {
return glm::quat(pAiQuat.w, pAiQuat.x, pAiQuat.y, pAiQuat.z);
}
My vertex shader:
layout (location = 0) in vec3 position;
layout (location = 1) in vec2 uv;
layout (location = 2) in vec3 normal;
layout (location = 3) in ivec4 boneIds;
layout (location = 4) in vec4 boneWeights;
out vec2 textureUV;
out vec3 lightNormal;
out vec4 worldPosition;
uniform mat4 model;
uniform mat4 projectionView;
uniform mat4 boneTransforms[50];
void main()
{
mat4 boneTransform = mat4(0.0f);
for(int i = 0; i < 4; i++){
boneTransform += boneTransforms[boneIds[i]] * boneWeights[i];
}
worldPosition = boneTransform * vec4(position, 1.0f);
worldPosition = model * worldPosition;
gl_Position = projectionView * worldPosition;
textureUV = uv;
lightNormal = mat3(transpose(inverse(model * boneTransform))) * normal;
}
The result:
Any help is appreciated.