Skip to content

Instantly share code, notes, and snippets.

@luke10x
Last active July 4, 2025 02:52
Show Gist options
  • Save luke10x/b27f4e1f2b394bcf4831d9f901db822d to your computer and use it in GitHub Desktop.
Save luke10x/b27f4e1f2b394bcf4831d9f901db822d to your computer and use it in GitHub Desktop.
gen-anim-from-glb-using-assimp.cpp
#ifndef STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
#endif
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/fwd.hpp>
#include <glm/gtc/quaternion.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtx/quaternion.hpp>
#include <assimp/postprocess.h>
#include <assimp/scene.h>
#include <assimp/Importer.hpp>
#include <vector>
#include <map>
#include <iostream>
#include <iostream>
#include <fstream>
#include <string>
#include <map>
#include <vector>
#include <sstream>
#include <iostream>
#include <fstream>
#include <string>
#include <map>
#include <sstream>
#include <vector>
#include <cmath> // For std::ceil
struct AnimationMixer
{
// TODO only use Assimp during loading,
// not for entire lifespan of this application
// Assimp::Importer importer;
const aiScene *scene;
const aiMesh *mesh;
glm::mat4 globalInverseTransform;
std::map<std::string, uint> boneNameToIndex;
std::vector<glm::mat4> boneOffsets;
void applyBoneTransformsFromNodeTree(
const aiAnimation &animation0,
float currentTick0,
const aiAnimation &animation1,
float currentTick1,
float blendingFactor,
const aiNode *pNode,
const glm::mat4 &parentTransform,
std::vector<glm::mat4> &resultsBuffer);
private:
aiVector3D calcInterpolatedPosition(
float currentTick,
const aiNodeAnim *pNodeAnim);
aiQuaternion calcInterpolatedRotation(
float currentTick,
const aiNodeAnim *pNodeAnim);
aiVector3D calcInterpolatedScaling(
float currentTick,
const aiNodeAnim *pNodeAnim);
const aiNodeAnim *findChannel(
const aiAnimation &Animation,
const std::string &NodeName);
};
struct AnimationConfigItem
{
std::string name;
std::string alias;
float frequency = -1.0f;
int samples = -1;
};
#define VERBOSE 1
#define MAX_BONES (200)
glm::mat4 assimpToGlmMatrix(aiMatrix4x4 mat);
// Helper function to trim leading and trailing spaces
std::string trim(const std::string &str)
{
size_t first = str.find_first_not_of(' ');
if (std::string::npos == first)
{
return str;
}
size_t last = str.find_last_not_of(' ');
return str.substr(first, (last - first + 1));
}
void printMat4(const glm::mat4& mat) {
std::cout << " {";
for (int i = 0; i < 4; ++i) {
std::cout << " ";
for (int j = 0; j < 4; ++j) {
std::cout << mat[i][j];
if (j < 3) std::cout << ", ";
}
if (i < 3) std::cout << ",";
else std::cout << "";
}
std::cout << "},\n";
// for (int i = 0; i < 4; ++i) {
// std::cout << "[ ";
// for (int j = 0; j < 4; ++j) {
// std::cout << mat[i][j] << " ";
// }
// std::cout << "]\n";
// }
}
int main(int argc, char *argv[])
{
std::string glbFilePath;
std::string meshName;
size_t numBones;
std::string variableName;
std::vector<AnimationConfigItem> animationConfigItems;
/* 🗂️ Parsing config file */ {
std::cerr << "Tool works now fuck off" << std::endl;
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " <file_path>" << std::endl;
return 1;
}
char *iniFilePath = argv[1];
std::ifstream file(iniFilePath);
if (!file.is_open())
{
std::cerr << "Failed to open file: " << iniFilePath << std::endl;
return 1;
}
std::string line;
int ln = 0;
while (std::getline(file, line))
{
// std::cerr << "processing line " << ++ln << ": " << line << std::endl;
if (line.empty() || line[0] == '#' || line[0] == ';')
{
continue;
}
// Check if the line is an animation header
if (line == "[animation]")
{
animationConfigItems.push_back(AnimationConfigItem());
continue;
}
std::istringstream iss(line);
std::string key, value;
std::getline(iss, key, '=');
std::getline(iss, value);
// Trim leading and trailing spaces
key = trim(key);
value = trim(value);
// Root attribut prooperties
if (key == "file")
{
glbFilePath = std::string(value);
}
if (key == "mesh")
{
meshName = value;
}
else if (key == "variable")
{
variableName = std::string(value);
}
// Animation properties
else if (key == "name")
{
if (value.empty())
{
std::cerr << "Invalid animation name" << std::endl;
return 1;
}
animationConfigItems.back().name = value;
}
else if (key == "alias")
{
if (value.empty())
{
std::cerr << "Invalid animation alias" << std::endl;
return 1;
}
animationConfigItems.back().alias = value;
}
else if (key == "frequency")
{
float freq;
if (sscanf(value.c_str(), "%f", &freq) == 1)
{
animationConfigItems.back().frequency = freq;
}
else
{
std::cerr << "Invalid frequency value: " << value << std::endl;
return 1;
}
}
else if (key == "samples")
{
int samples;
if (sscanf(value.c_str(), "%d", &samples) == 1)
{
animationConfigItems.back().samples = samples;
}
else
{
std::cerr << "Invalid samples value: " << value << std::endl;
return 1;
}
}
}
std::cerr << "after loop\n"
<< std::endl;
// Check if there is at least one animation
if (animationConfigItems.empty())
{
std::cerr << "No animations found in the file" << std::endl;
return 1;
}
// // Print the parsed data
// std::cout << "Mesh: " << config["mesh"] << std::endl;
// std::cout << "Variable: " << config["variable"] << std::endl;
// for (const auto& animation : animations) {
// std::cout << "Animation: " << animation.name << std::endl;
// std::cout << " Alias: " << animation.alias << std::endl;
// if (animation.frequency > 0.0f) {
// std::cout << " Frequency: " << animation.frequency << " seconds" << std::endl;
// } else {
// std::cout << " Samples: " << animation.samples << std::endl;
// }
// }
} // end of parsing
// For every animation -> for every key at interval -> for every bone
std::vector<std::map<int, std::vector<glm::mat4>>> animationCaches;
animationCaches.resize(animationConfigItems.size());
// Going to use them everywhere
Assimp::Importer importer;
const aiScene *scene;
const aiMesh *mesh;
// Also will fullfil this
std::map<std::string, uint> boneNameToIndex;
std::vector<glm::mat4> boneOffsets;
/* 🍑 Load some ass */ {
/* Load scene from filePath */ {
scene = importer.ReadFile(
glbFilePath, // CLI arg file name
aiProcess_Triangulate | // Convert any polygons to triangles
aiProcess_FlipUVs | // Flip UV coordinates to match typical texture mapping
aiProcess_CalcTangentSpace // Calculate tangent space for normals (Instead of loading from file)
); // Pointer to the parsed 3D model data
if (
!scene || // Scene cannot be loaded
scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || // Or loaded incomplete
!scene->mRootNode // Or it has no root node
)
{
std::cerr << "Error loading scene "
<< "from file " << glbFilePath << " :"
<< importer.GetErrorString() << std::endl;
return -1;
}
}
mesh = nullptr;
/* Try loading mesh */ {
// Iterate through all the meshes in the scene
unsigned int meshIndex = 0;
for (meshIndex = 0; meshIndex < scene->mNumMeshes; meshIndex++)
{
aiMesh *currentMesh = scene->mMeshes[meshIndex];
if (currentMesh->mName.length > 0)
{
const char *currentMeshName = currentMesh->mName.C_Str();
if (strcmp(currentMeshName, meshName.c_str()) == 0)
{
std::cerr << "Found matching mesh: " << meshName
<< std::endl;
mesh = currentMesh;
break; // Exit the loop
}
}
}
if (mesh == nullptr)
{
// If mesh not loaded list all possible meshes
std::cerr << "Error loading mesh: " << meshName
<< " File " << glbFilePath << " contains the following meshes:"
<< std::endl; // Fallback name
for (meshIndex = 0; meshIndex < scene->mNumMeshes; meshIndex++)
{
aiMesh *currentMesh = scene->mMeshes[meshIndex];
// Print the name of the mesh
if (currentMesh->mName.length > 0)
{
std::cerr << " - " << currentMesh->mName.C_Str() << std::endl;
}
}
exit(1);
}
}
// glm::mat4 globalInverseTransform;
/* Init bones */ {
if (mesh->mNumBones > MAX_BONES)
{
std::cerr << "This model has too many bones "
<< mesh->mNumBones << std::endl;
assert(0);
}
std::cerr << "Mesh " << meshName << " has " << mesh->mNumBones << " bones" << std::endl;
numBones = mesh->mNumBones;
// For each bone
for (uint boneInd = 0; boneInd < mesh->mNumBones; boneInd++)
{
auto pBone = mesh->mBones[boneInd];
std::string boneName = std::string(pBone->mName.C_Str());
// Add bone to index mapping
boneNameToIndex[boneName] = boneInd;
// reads binding pose offsets
boneOffsets.push_back(
assimpToGlmMatrix(pBone->mOffsetMatrix));
}
}
}
int buffersCopied = 0;
for (unsigned long i = 0; i < animationConfigItems.size(); i++) // 🌳 Generate caches using animation system
{
const auto &animationConfigItem = animationConfigItems[i];
const char *animationName = animationConfigItem.name.c_str();
float frequency = animationConfigItem.frequency;
int frames = animationConfigItem.samples;
aiAnimation *animation = nullptr;
int tickInterval;
/* try loading animation */ {
unsigned int aniIndex = 0;
for (aniIndex = 0; aniIndex < scene->mNumAnimations; aniIndex++)
{
aiAnimation *currentAnimation = scene->mAnimations[aniIndex];
if (currentAnimation->mName.length > 0)
{
const char *currentAnimationName = currentAnimation->mName.C_Str();
if (strcmp(currentAnimationName, animationName) == 0)
{
std::cerr << "Found matching animation: " << animationName
<< std::endl;
animation = currentAnimation;
break; // Exit the loop
}
}
}
if (animation == nullptr)
{
// If mesh not loaded list all possible meshes
std::cerr << "Error loading animation: " << animationName << " of mesh " << meshName
<< " File " << glbFilePath << " contains the following animations:"
<< std::endl;
for (aniIndex = 0; aniIndex < scene->mNumAnimations; aniIndex++)
{
aiAnimation *currentAnimation = scene->mAnimations[aniIndex];
// Print the name of the mesh
if (currentAnimation->mName.length > 0)
{
std::cerr << " - " << currentAnimation->mName.C_Str() << std::endl;
}
}
exit(1);
}
float safeTicksPerSecond = animation->mTicksPerSecond == 0
? 30.0f
: (float)animation->mTicksPerSecond;
float durationInSeconds = (float)animation->mDuration / safeTicksPerSecond;
std::cerr << " " << durationInSeconds << " seconds" << std::endl;
tickInterval = frames > 0
? (int)(animation->mDuration / (float)frames)
: (int)(safeTicksPerSecond * frequency);
if (frames < 1) {
// exit(1);
animationConfigItems[i].samples = static_cast<int>(std::ceil(durationInSeconds / frequency));
std::cerr << "tetes gher " << animationConfigItems[i].samples << std::endl;
}
}
/* Print animation info */ {
std::cerr << "Animation length: " << std::endl;
std::cerr << " " << animation->mDuration << " ticks" << std::endl;
std::cerr << " " << animation->mTicksPerSecond << " ticks per second" << std::endl;
// Print the number of channels
std::cerr << "Number of channels: " << animation->mNumChannels << std::endl;
std::cerr << "Sampling interval: every " << tickInterval << " ticks" << std::endl;
std::cerr << "Animation number of samples " << animationConfigItems[i].samples << std::endl;
// Print the number of keys for each type (position, rotation, scale) for each channel
if (VERBOSE == 2)
{
for (unsigned int i = 0; i < animation->mNumChannels; ++i)
{
const aiNodeAnim *channel = animation->mChannels[i];
uint boneIndex = boneNameToIndex[channel->mNodeName.data];
std::cerr << "Channel " << i
<< ", " << "Bone: " << boneIndex
<< " " << " (" << channel->mNodeName.C_Str() << ")"
<< " keys: "
<< channel->mNumPositionKeys << " pos, "
<< channel->mNumRotationKeys << " rot, "
<< channel->mNumScalingKeys << " scale"
<< std::endl;
}
}
}
/* Use animation mixer to extract caches */ {
AnimationMixer am;
am.scene = scene;
am.mesh = mesh;
am.boneNameToIndex = boneNameToIndex;
am.boneOffsets = boneOffsets;
// this is important
am.globalInverseTransform = glm::inverse(
assimpToGlmMatrix(scene->mRootNode->mTransformation));
std::vector<glm::mat4> resultsBuffer;
resultsBuffer.resize(mesh->mNumBones);
for (int currentTick = 0; currentTick < animation->mDuration; currentTick += tickInterval)
{
// Recurse starts here
glm::mat4 rootParentTransform(1.0f);
am.applyBoneTransformsFromNodeTree(
*animation,
(float)currentTick,
*animation, // yes I know
(float)currentTick, // this is the same shit
0.0f, scene->mRootNode, rootParentTransform,
resultsBuffer);
// no need to move unless resultsBuffer won't be reused
animationCaches[i][currentTick] = resultsBuffer;
buffersCopied++;
}
// here it gives me 2Mil. should be 300+
}
}
std::cerr << "Buffers copied " << buffersCopied << std::endl;
/* Final printout */ {
std::cout << R"(
#pragma once
#include <glm/glm.hpp>
#include <vector>
)" << std::endl;
std::cout << "enum ENUM_" << variableName << " {" << std::endl;
for (size_t i = 0; i < animationConfigItems.size(); i++) {
AnimationConfigItem item = animationConfigItems[i];
std::cout << " " << item.alias << "," << std::endl;
}
int totalFrameCount = 0;
for (size_t i = 0; i < animationCaches.size(); ++i) {
AnimationConfigItem item = animationConfigItems[i];
totalFrameCount += item.samples;
}
std::cout << " __LAST_ENUM" << variableName << std::endl;
std::cout << "};" <<std::endl;
std::cout << "size_t " << variableName << "FrameOffset[] = {" << std::endl;
int offset = 0;
for (size_t i = 0; i < animationCaches.size(); i++) {
AnimationConfigItem item = animationConfigItems[i];
std::cout << " " << offset << ", //"<< item.alias << std::endl;
offset += item.samples;
}
std::cout << "};" << std::endl;
std::cout << "size_t " << variableName << "LengthInFrames[] = {" << std::endl;
for (size_t i = 0; i < animationCaches.size(); i++) {
AnimationConfigItem item = animationConfigItems[i];
std::cout << " " << item.samples << ", // "<< (item.samples * (int)numBones * 64) / 1024 << "KB " << item.alias << std::endl;
}
std::cout << "};" << std::endl;
std::cout << "" <<std::endl;
std::cout << "const uint " << variableName << "TotalFrameCount = " << totalFrameCount << ";" << std::endl;
std::cout << "const uint " << variableName <<"BoneCount = "<< numBones << ";" <<std::endl;
std::cout << "glm::mat4 " << variableName << "["<< variableName << "TotalFrameCount]["<<variableName<<"BoneCount] = {" <<std::endl;
size_t j = 0;
for (size_t i = 0; i < animationCaches.size(); ++i) {
// std::cerr << "Animation " << i << ":\n";
for (const auto& [tick, matrices] : animationCaches[i]) {
std::cout << " {" << std::endl;
for (size_t k = 0; k < matrices.size(); k++) {
glm::mat4 mat = matrices[k];
printMat4(mat);
}
std::cout << " }, // " << j << " Tick: " << tick << " " << " \n" << std::endl;
j++;
}
}
std::cout << "};" <<std::endl;
}
return 0;
}
glm::mat4 assimpToGlmMatrix(aiMatrix4x4 mat)
{
glm::mat4 m;
for (int y = 0; y < 4; y++)
{
for (int x = 0; x < 4; x++)
{
m[x][y] = mat[(uint)y][(uint)x];
}
}
return m;
}
void AnimationMixer::applyBoneTransformsFromNodeTree(
const aiAnimation &animation0,
float currentTick0,
const aiAnimation &animation1,
float currentTick1,
float blendingFactor,
const aiNode *pNode,
const glm::mat4 &parentTransform,
std::vector<glm::mat4> &resultsBuffer)
{
std::string nodeName(pNode->mName.data);
glm::mat4 nodeTransform(
assimpToGlmMatrix(pNode->mTransformation));
const aiNodeAnim *channel0 = findChannel(animation0, nodeName);
const aiNodeAnim *channel1 = findChannel(animation1, nodeName);
if (channel0 && channel1)
{
// Get TRS components from animation
aiVector3D aiPosition0 =
calcInterpolatedPosition(currentTick0, channel0);
aiVector3D aiPosition1 =
calcInterpolatedPosition(currentTick1, channel1);
aiVector3D blendedPosition =
(1.0f - blendingFactor) * aiPosition0 +
aiPosition1 * blendingFactor;
glm::vec3 position(
blendedPosition.x, blendedPosition.y, blendedPosition.z);
aiQuaternion aiRotation0 =
calcInterpolatedRotation(currentTick0, channel0);
aiQuaternion aiRotation1 =
calcInterpolatedRotation(currentTick1, channel1);
aiQuaternion blendedRotation;
aiQuaternion::Interpolate(
blendedRotation, aiRotation0, aiRotation1, blendingFactor);
glm::quat rotation(
blendedRotation.w, blendedRotation.x, blendedRotation.y,
blendedRotation.z);
aiVector3D aiScaling0 =
calcInterpolatedScaling(currentTick0, channel0);
aiVector3D aiScaling1 =
calcInterpolatedScaling(currentTick1, channel1);
aiVector3D blendedScaling =
(1.0f - blendingFactor) * aiScaling0 +
aiScaling1 * blendingFactor;
glm::vec3 scale(
blendedScaling.x, blendedScaling.y, blendedScaling.z);
// Inflate them into matrices
glm::mat4 positionMat = glm::mat4(1.0f);
positionMat = glm::translate(positionMat, position);
glm::mat4 rotationMat = glm::toMat4(rotation);
glm::mat4 scaleMat = glm::mat4(1.0f);
scaleMat = glm::scale(scaleMat, scale);
// Multiply TRS matrices
nodeTransform = positionMat * rotationMat * scaleMat;
}
glm::mat4 cascadeTransform =
parentTransform * nodeTransform;
auto isBoneNode = this->boneNameToIndex.find(nodeName) !=
this->boneNameToIndex.end();
if (isBoneNode)
{
uint boneIndex = this->boneNameToIndex[nodeName];
resultsBuffer[boneIndex] = glm::transpose(
this->globalInverseTransform * cascadeTransform *
boneOffsets[boneIndex]);
}
else
{
// Because there are some nodes at the root of the mesh,
// that are not bones, but they have some transformations
// Therefore, here I apply them to the globalTransformation
cascadeTransform =
cascadeTransform * nodeTransform;
}
for (uint i = 0; i < pNode->mNumChildren; i++)
{
// Go deeper into recursion
applyBoneTransformsFromNodeTree(
animation0, currentTick0, animation1, currentTick1,
blendingFactor, pNode->mChildren[i], cascadeTransform,
resultsBuffer);
}
}
const aiNodeAnim *AnimationMixer::findChannel(
const aiAnimation &animation,
const std::string &nodeName)
{
for (uint i = 0; i < animation.mNumChannels; i++)
{
const aiNodeAnim *channel = animation.mChannels[i];
if (std::string(channel->mNodeName.data) == nodeName)
{
return channel;
}
}
return nullptr;
}
aiVector3D AnimationMixer::calcInterpolatedPosition(
float currentTick,
const aiNodeAnim *pNodeAnim)
{
if (pNodeAnim->mNumPositionKeys == 1)
{
return pNodeAnim->mPositionKeys[0].mValue;
}
uint positionIndex = 0;
for (uint i = 0; i < pNodeAnim->mNumPositionKeys - 1; i++)
{
float t = (float)pNodeAnim->mPositionKeys[i + 1].mTime;
if (currentTick < t)
{
positionIndex = i;
break;
}
}
uint nextPositionIndex = positionIndex + 1;
assert(nextPositionIndex < pNodeAnim->mNumPositionKeys);
float t1 = (float)pNodeAnim->mPositionKeys[positionIndex].mTime;
if (t1 > currentTick)
{
return pNodeAnim->mPositionKeys[positionIndex].mValue;
}
float t2 =
(float)pNodeAnim->mPositionKeys[nextPositionIndex].mTime;
float deltaTime = t2 - t1;
float factor = (currentTick - t1) / deltaTime;
assert(factor >= 0.0f && factor <= 1.0f);
const aiVector3D &start =
pNodeAnim->mPositionKeys[positionIndex].mValue;
const aiVector3D &end =
pNodeAnim->mPositionKeys[nextPositionIndex].mValue;
return start + factor * (end - start);
}
aiQuaternion AnimationMixer::calcInterpolatedRotation(
float currentTick,
const aiNodeAnim *pNodeAnim)
{
if (pNodeAnim->mNumRotationKeys == 1)
{
return pNodeAnim->mRotationKeys[0].mValue;
}
uint rotationIndex;
assert(pNodeAnim->mNumRotationKeys > 0);
for (uint i = 0; i < pNodeAnim->mNumRotationKeys - 1; i++)
{
float t = (float)pNodeAnim->mRotationKeys[i + 1].mTime;
if (currentTick < t)
{
rotationIndex = i;
break;
}
}
uint nextRotationIndex = rotationIndex + 1;
assert(nextRotationIndex < pNodeAnim->mNumRotationKeys);
aiQuaternion quat;
float t1 = (float)pNodeAnim->mRotationKeys[rotationIndex].mTime;
if (t1 > currentTick)
{
quat = pNodeAnim->mRotationKeys[rotationIndex].mValue;
}
else
{
float t2 =
(float)pNodeAnim->mRotationKeys[nextRotationIndex].mTime;
float deltaTime = t2 - t1;
float factor = (currentTick - t1) / deltaTime;
assert(factor >= 0.0f && factor <= 1.0f);
const aiQuaternion &startRotationQ =
pNodeAnim->mRotationKeys[rotationIndex].mValue;
const aiQuaternion &endRotationQ =
pNodeAnim->mRotationKeys[nextRotationIndex].mValue;
aiQuaternion::Interpolate(
quat, startRotationQ, endRotationQ, factor);
}
quat.Normalize();
return quat;
}
aiVector3D AnimationMixer::calcInterpolatedScaling(
float currentTick,
const aiNodeAnim *pNodeAnim)
{
aiVector3D Out;
// we need at least two values to interpolate...
if (pNodeAnim->mNumScalingKeys == 1)
{
return pNodeAnim->mScalingKeys[0].mValue;
}
uint scalingIndex;
assert(pNodeAnim->mNumScalingKeys > 0);
for (uint i = 0; i < pNodeAnim->mNumScalingKeys - 1; i++)
{
float t = (float)pNodeAnim->mScalingKeys[i + 1].mTime;
if (currentTick < t)
{
scalingIndex = i;
break;
}
}
uint nextScalingIndex = scalingIndex + 1;
assert(nextScalingIndex < pNodeAnim->mNumScalingKeys);
float t1 = (float)pNodeAnim->mScalingKeys[scalingIndex].mTime;
if (t1 > currentTick)
{
return pNodeAnim->mScalingKeys[scalingIndex].mValue;
}
float t2 = (float)pNodeAnim->mScalingKeys[nextScalingIndex].mTime;
float deltaTime = t2 - t1;
float factor = (currentTick - (float)t1) / deltaTime;
assert(factor >= 0.0f && factor <= 1.0f);
const aiVector3D &start =
pNodeAnim->mScalingKeys[scalingIndex].mValue;
const aiVector3D &end =
pNodeAnim->mScalingKeys[nextScalingIndex].mValue;
return start + factor * (end - start);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment