애니메이션 데이터

Jaemyeong Lee·2025년 4월 2일
0

1. 개요

이번 포스팅에서는 Assimp를 통해 FBX 파일에서 애니메이션 정보를 추출하고, 이를 직접 정의한 구조체에 담아 바이너리 파일(.clip)로 저장하는 시스템을 정리한다.
핵심은 "애니메이션이란 프레임 단위로 본(Bone)의 SRT(Scale, Rotation, Translation)를 정의한 데이터 집합"이라는 개념을 바탕으로 실습을 진행한다.


2. 애니메이션 데이터 구조 설계

// 애니메이션 한 프레임의 데이터
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;
};

3. 애니메이션 데이터 파싱 흐름

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: 변환한 데이터를 바이너리 형태로 저장

4. SRT(Keyframe) 정보 추출

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

5. Keyframe 채우기 및 계층 순회

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

6. 바이너리 파일로 저장하기

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 확장자를 사용한 커스텀 바이너리 파일 생성
  • 빠른 로딩을 위한 이진 구조 사용

7. 실행 및 디버깅 팁

  • 애니메이션 파일들이 .clip으로 잘 저장되었는지 확인
  • 디버깅 시 ReadAnimationDataReadKeyframeData에 중단점을 설정하면 구조를 빠르게 파악 가능
  • 모델이 뜨지 않는 경우 Intermediate/Debug 폴더 삭제 후 전체 빌드 추천

profile
李家네_공부방

0개의 댓글