이번 포스팅에서는 Assimp를 통해 FBX 파일에서 애니메이션 정보를 추출하고, 이를 직접 정의한 구조체에 담아 바이너리 파일(.clip)로 저장하는 시스템을 정리한다.
핵심은 "애니메이션이란 프레임 단위로 본(Bone)의 SRT(Scale, Rotation, Translation)를 정의한 데이터 집합"이라는 개념을 바탕으로 실습을 진행한다.
// 애니메이션 한 프레임의 데이터
struct asKeyframeData
{
float time;
Vec3 scale;
Quaternion rotation;
Vec3 translation;
};
// 하나의 본(Bone)이 가지는 모든 프레임
struct asKeyframe
{
string boneName;
vector<asKeyframeData> transforms;
};
// 전체 애니메이션 클립 정보
struct asAnimation
{
string name;
uint32 frameCount;
float frameRate;
float duration;
vector<asKeyframe> keyframes;
};
// Assimp 데이터 캐시용
struct asAnimationNode
{
aiString name;
vector<asKeyframeData> keyframe;
};
void Converter::ExportAnimationData(wstring savePath, uint32 index)
{
wstring finalPath = _modelPath + savePath + L".clip";
assert(index < _scene->mNumAnimations);
shared_ptr<asAnimation> animation = ReadAnimationData(_scene->mAnimations[index]);
WriteAnimationData(animation, finalPath);
}
ReadAnimationData: Assimp의 aiAnimation을 우리가 정의한 asAnimation으로 변환WriteAnimationData: 변환한 데이터를 바이너리 형태로 저장shared_ptr<asAnimationNode> Converter::ParseAnimationNode(shared_ptr<asAnimation> animation, aiNodeAnim* srcNode)
{
auto node = make_shared<asAnimationNode>();
node->name = srcNode->mNodeName;
uint32 keyCount = max({srcNode->mNumPositionKeys, srcNode->mNumScalingKeys, srcNode->mNumRotationKeys});
for (uint32 k = 0; k < keyCount; k++)
{
asKeyframeData frameData;
uint32 t = node->keyframe.size();
if (fabsf((float)srcNode->mPositionKeys[k].mTime - (float)t) <= 0.0001f)
{
frameData.time = (float)srcNode->mPositionKeys[k].mTime;
memcpy_s(&frameData.translation, sizeof(Vec3), &srcNode->mPositionKeys[k].mValue, sizeof(aiVector3D));
}
if (fabsf((float)srcNode->mRotationKeys[k].mTime - (float)t) <= 0.0001f)
{
auto& q = srcNode->mRotationKeys[k].mValue;
frameData.rotation = Quaternion(q.x, q.y, q.z, q.w);
}
if (fabsf((float)srcNode->mScalingKeys[k].mTime - (float)t) <= 0.0001f)
{
memcpy_s(&frameData.scale, sizeof(Vec3), &srcNode->mScalingKeys[k].mValue, sizeof(aiVector3D));
}
node->keyframe.push_back(frameData);
}
// 부족한 프레임을 마지막 키로 채워줌
while (node->keyframe.size() < animation->frameCount)
node->keyframe.push_back(node->keyframe.back());
return node;
}
void Converter::ReadKeyframeData(shared_ptr<asAnimation> animation, aiNode* node, map<string, shared_ptr<asAnimationNode>>& cache)
{
auto keyframe = make_shared<asKeyframe>();
keyframe->boneName = node->mName.C_Str();
auto findNode = cache[node->mName.C_Str()];
for (uint32 i = 0; i < animation->frameCount; i++)
{
asKeyframeData frameData;
if (findNode)
frameData = findNode->keyframe[i];
else
{
Matrix transform(node->mTransformation[0]);
transform = transform.Transpose();
transform.Decompose(frameData.scale, frameData.rotation, frameData.translation);
}
keyframe->transforms.push_back(frameData);
}
animation->keyframes.push_back(keyframe);
// 자식 노드 재귀 호출
for (uint32 i = 0; i < node->mNumChildren; i++)
ReadKeyframeData(animation, node->mChildren[i], cache);
}
void Converter::WriteAnimationData(shared_ptr<asAnimation> animation, wstring finalPath)
{
filesystem::create_directory(filesystem::path(finalPath).parent_path());
auto file = make_shared<FileUtils>();
file->Open(finalPath, FileMode::Write);
file->Write(animation->name);
file->Write(animation->duration);
file->Write(animation->frameRate);
file->Write(animation->frameCount);
file->Write((uint32)animation->keyframes.size());
for (auto& keyframe : animation->keyframes)
{
file->Write(keyframe->boneName);
file->Write((uint32)keyframe->transforms.size());
file->Write(&keyframe->transforms[0], sizeof(asKeyframeData) * keyframe->transforms.size());
}
}
.clip 확장자를 사용한 커스텀 바이너리 파일 생성.clip으로 잘 저장되었는지 확인ReadAnimationData와 ReadKeyframeData에 중단점을 설정하면 구조를 빠르게 파악 가능Intermediate/Debug 폴더 삭제 후 전체 빌드 추천