Convert Model

Assimp(Open Asset Import Library)를 활용하여 FBX, OBJ 등 다양한 3D 모델 포맷을 게임 엔진에서 효율적으로 사용할 수 있는 최적화된 바이너리 포맷으로 변환하는 도구입니다. 주요 기능은 다음과 같습니다:

  • 3D 모델 파일(FBX, OBJ 등)의 메시, 재질, 본, 애니메이션 데이터 추출

  • 추출한 데이터를 게임 엔진에 적합한 형태로 변환

  • 변환된 데이터를 바이너리 형식으로 저장

  • 사용자 친화적인 인터페이스를 통한 간편한 모델 변환 작업

FileUtils 클래스 소개

FileUtils 클래스는 바이너리 파일 입출력을 간편하게 처리하는 유틸리티 클래스입니다. Windows API를 활용하여 파일을 열고, 읽고, 쓰는 기능을 제공합니다.

enum FileMode : uint8
{
    Write,
    Read,
};

class FileUtils
{
public:
    FileUtils();
    ~FileUtils();

    void Open(wstring filePath, FileMode mode);

    template<typename T>
    void Write(const T& data);

    template<>
    void Write<string>(const string& data);

    void Write(void* data, uint32 dataSize);
    void Write(const string& data);

    template<typename T>
    void Read(OUT T& data);

    template<typename T>
    T Read();

    void Read(void** data, uint32 dataSize);
    void Read(OUT string& data);

private:
    HANDLE _handle = INVALID_HANDLE_VALUE;
};

  • File Open

Open 메서드는 파일 경로와 모드(읽기/쓰기)를 받아 파일을 엽니다:

void FileUtils::Open(wstring filePath, FileMode mode)
{
    if (mode == FileMode::Write)
    {
        _handle = ::CreateFile(
            filePath.c_str(),
            GENERIC_WRITE,
            0,
            nullptr,
            CREATE_ALWAYS,
            FILE_ATTRIBUTE_NORMAL,
            nullptr
        );
    }
    else
    {
        _handle = ::CreateFile
        (
            filePath.c_str(),
            GENERIC_READ,
            FILE_SHARE_READ,
            nullptr,
            OPEN_EXISTING,
            FILE_ATTRIBUTE_NORMAL,
            nullptr
        );
    }

    assert(_handle != INVALID_HANDLE_VALUE);
}

소멸자에서는 열린 파일 핸들을 자동으로 닫습니다:

FileUtils::~FileUtils()
{
    if (_handle != INVALID_HANDLE_VALUE)
    {
        ::CloseHandle(_handle);
        _handle = INVALID_HANDLE_VALUE;
    }
}

  • File Write

데이터 타입에 따라 다양한 쓰기 메서드를 제공합니다:

// 일반 데이터 블록 쓰기
void FileUtils::Write(void* data, uint32 dataSize)
{
    uint32 numOfBytes = 0;
    assert(::WriteFile(_handle, data, dataSize, reinterpret_cast<LPDWORD>(&numOfBytes), nullptr));
}

// 문자열 쓰기 (길이 포함)
void FileUtils::Write(const string& data)
{
    uint32 size = (uint32)data.size();
    Write(size);

    if (data.size() == 0)
        return;

    Write((void*)data.data(), size);
}

템플릿을 활용하여 다양한 데이터 타입을 지원합니다:

template<typename T>
void Write(const T& data)
{
    DWORD numOfBytes = 0;
    assert(::WriteFile(_handle, &data, sizeof(T), (LPDWORD)&numOfBytes, nullptr));
}

  • Read File

쓰기와 마찬가지로 다양한 데이터 타입에 대한 읽기 메서드를 제공합니다:

// 데이터 블록 읽기
void FileUtils::Read(void** data, uint32 dataSize)
{
    uint32 numOfBytes = 0;
    assert(::ReadFile(_handle, *data, dataSize, reinterpret_cast<LPDWORD>(&numOfBytes), nullptr));
}

// 문자열 읽기 (길이 선행)
void FileUtils::Read(OUT string& data)
{
    uint32 size = Read<uint32>();

    if (size == 0)
        return;

    char* temp = new char[size + 1];
    temp[size] = 0;
    Read((void**)&temp, size);
    data = temp;
    delete[] temp;
}

템플릿을 활용한 데이터 읽기:

template<typename T>
void Read(OUT T& data)
{
    DWORD numOfBytes = 0;
    assert(::ReadFile(_handle, &data, sizeof(T), (LPDWORD)&numOfBytes, nullptr));
}

template<typename T>
T Read()
{
    T data;
    Read(data);
    return data;
}

모델 변환 프로세스 개요

모델 변환 과정은 AssimpTool::ConvertModel 함수에서 시작하여 Assimp 라이브러리를 활용해 3D 모델을 게임 엔진에 적합한 형태로 변환합니다. 이 과정은 다음과 같은 단계로 구성됩니다:

void AssimpTool::ConvertModel(const std::wstring& folderName, const std::wstring& fileName)
{
    Converter converter;
    converter.init(_hwnd, _device, _deviceContext);

    wstring selectedFile = wstring(L"../Resources/Assets/") + wstring(folderName.begin(), folderName.end()) + L"/" + wstring(fileName.begin(), fileName.end());
    converter.ReadAssetFile(selectedFile);
    
    wstring savePath = wstring(L"../Resources/Models/") + fileName;
    converter.ExportModelData(savePath);
    converter.ExportMaterialData(savePath);
}

이 코드에서 볼 수 있듯이, 모델 변환 프로세스는:

  1. Converter 객체 초기화

  2. 소스 에셋 파일 읽기 (ReadAssetFile)

  3. 모델 데이터 내보내기 (ExportModelData)

  4. 재질 데이터 내보내기 (ExportMaterialData)

단계로 구성됩니다. 각 단계를 자세히 살펴보겠습니다.

모델 파일 읽기 (ReadAssetFile)

void Converter::ReadAssetFile(wstring file)
{
    _importer = make_shared<Assimp::Importer>();

    wstring fullPath = _assetPath + file;
    string fileStr = Utils::ToString(fullPath);

    _scene = _importer->ReadFile(fileStr, 
        aiProcess_ConvertToLeftHanded |
        aiProcess_Triangulate |
        aiProcess_GenUVCoords |
        aiProcess_GenNormals |
        aiProcess_CalcTangentSpace
    );

    if (_scene == nullptr)
    {
        assert(false);
        return;
    }

    ReadModelData(_scene->mRootNode, -1, -1);
    ReadSkinData();
    ReadMaterialData();
}

이 함수는 다음과 같은 주요 작업을 수행합니다:

  1. Assimp 임포터 초기화: Assimp::Importer 객체를 생성하여 모델 파일을 읽을 준비를 합니다.

  2. 파일 로드 및 전처리: 다양한 옵션과 함께 파일을 로드합니다:

    1. aiProcess_ConvertToLeftHanded: DirectX에서 사용하는 왼손 좌표계로 변환

    2. aiProcess_Triangulate: 모든 면을 삼각형으로 변환

    3. aiProcess_GenUVCoords: UV 좌표 생성

    4. aiProcess_GenNormals: 노말 벡터 생성

    5. aiProcess_CalcTangentSpace: 탄젠트 공간 계산

    6. 데이터 추출: 모델의 다양한 데이터 추출:

      1. ReadModelData: 계층 구조 및 본 정보 추출

      2. ReadSkinData: 스키닝(본 가중치) 정보 추출

      3. ReadMaterialData: 재질 정보 추출

모델 계층 구조 및 본 정보 추출 (ReadModelData)

void Converter::ReadModelData(aiNode* node, int32 index, int32 parent)
{
    shared_ptr<asBone> bone = make_shared<asBone>();
    bone->index = index;
    bone->parent = parent;
    bone->name = node->mName.C_Str();
	
    // Relative Transform
    Matrix transform(node->mTransformation[0]);
    bone->transform = transform.Transpose();
	
    // 2) Root (Local)
    Matrix matParent = Matrix::Identity;
    if (parent >= 0)
        matParent = _bones[parent]->transform;
	
    // Local (Root) Transform
    bone->transform = bone->transform * matParent;
	
    _bones.push_back(bone);
	
    // Mesh
    ReadMeshData(node, index);
	
    // 재귀 함수
    for (uint32 i = 0; i < node->mNumChildren; i++)
        ReadModelData(node->mChildren[i], _bones.size(), index);
}

이 함수는 재귀적으로 모델의 계층 구조를 탐색하면서:

  1. 각 노드에 대한 본(bone) 정보를 생성 및 저장

  2. 노드의 변환 행렬을 DirectX의 Matrix 형식으로 변환

  3. 노드에 연결된 메시 데이터 처리 (ReadMeshData 호출)

  4. 재귀적으로 자식 노드들 처리

위와 같은 과정을 재귀적으로 수행하면서 모든 뼈들의 Transform은 Root기준의 Transform으로 변환됩니다.

메시 데이터 추출 (ReadMeshData)

프로젝트에서 사용하는 Mesh 데이터 구조체는 다음과 같습니다:

struct asMesh
{
    string name;
    aiMesh* mesh;
    vector<VertexTextureNormalTangentBlendData> vertices;
    vector<uint32> indices;

    int32 boneIndex;
    string materialName;
};

정점 데이터는 다음과 같은 구조체로 저장됩니다:

struct VertexTextureNormalTangentBlendData
{
    Vec3 position = { 0, 0, 0 };
    Vec2 uv = { 0, 0 };
    Vec3 normal = { 0, 0, 0 };
    Vec3 tangent = { 0, 0, 0 };
    Vec4 blendIndices = { 0, 0, 0, 0 };
    Vec4 blendWeights = { 0, 0, 0, 0 };
};

Mesh 데이터 읽기는 Converter::ReadMeshData 함수에서 처리됩니다:

void Converter::ReadMeshData(aiNode* node, int32 bone)
{
    if (node->mNumMeshes < 1)
        return;

    shared_ptr<asMesh> mesh = make_shared<asMesh>();
    mesh->name = node->mName.C_Str();
    mesh->boneIndex = bone;

    for (uint32 i = 0; i < node->mNumMeshes; i++)
    {
        uint32 index = node->mMeshes[i];
        const aiMesh* srcMesh = _scene->mMeshes[index];

        // Material Name
        const aiMaterial* material = _scene->mMaterials[srcMesh->mMaterialIndex];
        mesh->materialName = const_cast<aiMaterial*>(material)->GetName().C_Str();

        const uint32 startVertex = mesh->vertices.size();

        for (uint32 v = 0; v < srcMesh->mNumVertices; v++)
        {
            // Vertex
            VertexTextureNormalTangentBlendData vertex;
            ::memcpy(&vertex.position, &srcMesh->mVertices[v], sizeof(Vec3));

            // UV
            if (srcMesh->HasTextureCoords(0))
                ::memcpy(&vertex.uv, &srcMesh->mTextureCoords[0][v], sizeof(Vec2));

            // Normal
            if (srcMesh->HasNormals())
                ::memcpy(&vertex.normal, &srcMesh->mNormals[v], sizeof(Vec3));

            mesh->vertices.push_back(vertex);
        }
        
        // Index
         for (uint32 f = 0; f < srcMesh->mNumFaces; f++)
         {
            aiFace& face = srcMesh->mFaces[f];

            for (uint32 k = 0; k < face.mNumIndices; k++)
             mesh->indices.push_back(face.mIndices[k] + startVertex);
         }
    }

    _meshes.push_back(mesh);
}

이 함수는 노드에 연결된 각 메시에 대해:

  1. 메시 데이터 객체 생성 및 메타데이터 설정

  2. 메시와 연결된 재질 이름 설정

  3. 정점 데이터 추출: 위치, UV 좌표, 노말, 탄젠트

  4. 인덱스 버퍼 추출 (삼각형 정보)

스키닝 데이터 처리 (ReadSkinData)

void Converter::ReadSkinData()
{
    for (uint32 i = 0; i < _scene->mNumMeshes; i++)
    {
        aiMesh* srcMesh = _scene->mMeshes[i]; // 원본 mesh
        if (srcMesh->HasBones() == false)
            continue;

        shared_ptr<asMesh> mesh = _meshes[i]; // 커스텀된 mesh

        vector<asBoneWeights> tempVertexBoneWeights;
        tempVertexBoneWeights.resize(mesh->vertices.size());

        // Bone을 순회하면서 연관된 VertexId, Weight를 구해서 기록한다.
        for (uint32 b = 0; b < srcMesh->mNumBones; b++)
        {
            aiBone* srcMeshBone = srcMesh->mBones[b];
            uint32 boneIndex = GetBoneIndex(srcMeshBone->mName.C_Str()); // 파싱한 뼈의 인덱스 추출

            for (uint32 w = 0; w < srcMeshBone->mNumWeights; w++)
            {
	        uint32 index = srcMeshBone->mWeights[w].mVertexId;
		float weight = srcMeshBone->mWeights[w].mWeight;
		tempVertexBoneWeights[index].AddWeights(boneIndex, weight);
            }
        }

        // 최종 결과 계산
        for (uint32 v = 0; v < tempVertexBoneWeights.size(); v++)
        {
            tempVertexBoneWeights[v].Normalize();

            asBlendWeight blendWeight = tempVertexBoneWeights[v].GetBlendWeights();
            mesh->vertices[v].blendIndices = blendWeight.indices;
            mesh->vertices[v].blendWeights = blendWeight.weights;
         }
    }
}

이 함수는 스키닝 정보를 처리하며:

  1. 각 메시의 본 정보 확인

  2. 각 본이 영향을 주는 정점과 가중치 정보 추출

  3. 정점별로 영향 받는 본 목록 생성 및 가중치 정규화 (합이 1이 되도록)

  4. 정점 데이터에 본 인덱스와 가중치 저장

스키닝 처리에 사용되는 보조 클래스들:

// 정점별 본 가중치 정보
struct asBoneWeights
{
    // 가중치가 높은 순으로 정렬하여 추가
    void AddWeights(uint32 boneIndex, float weight)
    {
        if (weight <= 0.0f)
            return;

        auto findIt = std::find_if(boneWeights.begin(), boneWeights.end(),
            [weight](const Pair& p) { return weight > p.second; });

        boneWeights.insert(findIt, Pair(boneIndex, weight));
    }

    asBlendWeight GetBlendWeights()
    {
        asBlendWeight blendWeights;

        for (uint32 i = 0; i < boneWeights.size(); i++)
        {
            if (i >= 4)
                break;

            blendWeights.Set(i, boneWeights[i].first, boneWeights[i].second);
        }

        return blendWeights;
    }
    
    // 모든 가중치 합이 1이 되도록 정규화
    void Normalize()
    {
        if (boneWeights.size() >= 4)
            boneWeights.resize(4);

        float totalWeight = 0.f;
        for (const auto& item : boneWeights)
            totalWeight += item.second;

        float scale = 1.f / totalWeight;
        for (auto& item : boneWeights)
            item.second *= scale;
    }

    using Pair = pair<int32, float>; // bone index, weight
    vector<Pair> boneWeights;
};

struct asBlendWeight
{
    void Set(uint32 index, uint32 boneIndex, float weight)
    {
        float i = (float)boneIndex;
        float w = weight;

        switch (index)
        {
        case 0: indices.x = i; weights.x = w; break;
        case 1: indices.y = i; weights.y = w; break;
        case 2: indices.z = i; weights.z = w; break;
        case 3: indices.w = i; weights.w = w; break;
        }
    }

    Vec4 indices = Vec4(0, 0, 0, 0); // 4개의 본 인덱스
    Vec4 weights = Vec4(0, 0, 0, 0); // 4개 본의 영향 받는 가중치
};

재질 데이터 추출 (ReadMaterialData)

void Converter::ReadMaterialData()
{
    for (uint32 i = 0; i < _scene->mNumMaterials; i++)
    {
        aiMaterial* srcMaterial = _scene->mMaterials[i];

        shared_ptr<asMaterial> material = make_shared<asMaterial>();
        material->name = srcMaterial->GetName().C_Str();

        aiColor3D color;
        // Ambient
        srcMaterial->Get(AI_MATKEY_COLOR_AMBIENT, color);
	material->ambient = Color(color.r, color.g, color.b, 1.f);

	// Diffuse
	srcMaterial->Get(AI_MATKEY_COLOR_DIFFUSE, color);
	material->diffuse = Color(color.r, color.g, color.b, 1.f);

	// Specular
	srcMaterial->Get(AI_MATKEY_COLOR_SPECULAR, color);
	material->specular = Color(color.r, color.g, color.b, 1.f);
	srcMaterial->Get(AI_MATKEY_SHININESS, material->specular.w);

	// Emissive
	srcMaterial->Get(AI_MATKEY_COLOR_EMISSIVE, color);
	material->emissive = Color(color.r, color.g, color.b, 1.0f);

	aiString file;

	// Diffuse Texture
	srcMaterial->GetTexture(aiTextureType_DIFFUSE, 0, &file);
	material->diffuseFile = file.C_Str();

	// Specular Texture
	srcMaterial->GetTexture(aiTextureType_SPECULAR, 0, &file);
	material->specularFile = file.C_Str();

	// Normal Texture
	srcMaterial->GetTexture(aiTextureType_NORMALS, 0, &file);
	material->normalFile = file.C_Str();

	_materials.push_back(material);
    }
}

이 함수는 모델의 모든 재질 정보를 추출하며:

  1. 각 재질의 기본 정보(이름, 색상) 추출

  2. 재질의 텍스처 정보(디퓨즈, 스페큘러, 노말 맵) 추출

모델 데이터 내보내기 (ExportModelData)

모델 데이터 내보내기는 다음 단계로 진행됩니다:

void Converter::ExportModelData(wstring savePath)
{
    wstring finalPath = _modelPath + savePath + L".mesh";
    ReadModelData(_scene->mRootNode, -1, -1);
    ReadSkinData();

    WriteModelFile(finalPath);
}

실제 파일 저장은 WriteModelFile 함수에서 처리됩니다:

void Converter::WriteModelFile(wstring finalPath)
{
   auto path = filesystem::path(finalPath);

   // 폴더가 없으면 만든다.
   filesystem::create_directory(path.parent_path());

   shared_ptr<FileUtils> file = make_shared<FileUtils>();
   file->Open(finalPath, FileMode::Write);

    // 본 데이터 저장
    file.Write<uint32>((uint32)_bones.size());
    for (shared_ptr<asBone>& bone : _bones)
    {
        file.Write<int32>(bone->index);
        file.Write<int32>(bone->parent);
        file.Write(bone->name);
        file.Write(bone->transform);
    }

    // 메시 데이터 저장
    file.Write<uint32>((uint32)_meshes.size());
    for (shared_ptr<asMesh>& meshData : _meshes)
    {
        file.Write(meshData->name);
        file.Write<uint32>((uint32)meshData->vertices.size());
        file.Write(&meshData->vertices[0], sizeof(VertexTextureNormalTangentBlendData) * meshData->vertices.size());
        file.Write<uint32>((uint32)meshData->indices.size());
        file.Write(&meshData->indices[0], sizeof(uint32) * meshData->indices.size());
        file.Write<int32>(meshData->boneIndex);
        file.Write(meshData->materialName);
    }
}

이 함수는:

  1. 본 데이터 저장:

    1. 본 개수

    2. 각 본의 인덱스, 부모 인덱스, 이름, 변환 행렬

  2. 메시 데이터 저장:

    1. 메시 개수

    2. 각 메시의 이름, 정점 데이터, 인덱스 데이터, 연결된 본 인덱스, 재질 이름

    3. 바이너리 파일로 저장

재질 데이터 내보내기 (ExportMaterialData)

재질 데이터 내보내기도 비슷한 과정으로 진행됩니다:

void Converter::ExportMaterialData(wstring savePath)
{
    wstring finalPath = _texturePath + savePath + L".xml";
    ReadMaterialData();
    WriteMaterialData(finalPath);
}

실제 파일 저장은 WriteMaterialData 함수에서 처리됩니다:

void Converter::WriteMaterialData(wstring finalPath)
{
    auto path = filesystem::path(finalPath);

    // 폴더가 없으면 만든다.
    filesystem::create_directory(path.parent_path());

    string folder = path.parent_path().string();

    shared_ptr<tinyxml2::XMLDocument> document = make_shared<tinyxml2::XMLDocument>();

    tinyxml2::XMLDeclaration* decl = document->NewDeclaration();
    document->LinkEndChild(decl);

    tinyxml2::XMLElement* root = document->NewElement("Materials");
    document->LinkEndChild(root);

    for (shared_ptr<asMaterial> material : _materials)
    {
	tinyxml2::XMLElement* node = document->NewElement("Material");
	root->LinkEndChild(node);

	tinyxml2::XMLElement* element = nullptr;

	element = document->NewElement("Name");
	element->SetText(material->name.c_str());
	node->LinkEndChild(element);

	element = document->NewElement("DiffuseFile");
	element->SetText(WriteTexture(folder, material->diffuseFile).c_str());
	node->LinkEndChild(element);

	element = document->NewElement("SpecularFile");
	element->SetText(WriteTexture(folder, material->specularFile).c_str());
	node->LinkEndChild(element);

	element = document->NewElement("NormalFile");
	element->SetText(WriteTexture(folder, material->normalFile).c_str());
	node->LinkEndChild(element);

	element = document->NewElement("Ambient");
	element->SetAttribute("R", material->ambient.x);
	element->SetAttribute("G", material->ambient.y);
	element->SetAttribute("B", material->ambient.z);
	element->SetAttribute("A", material->ambient.w);
	node->LinkEndChild(element);

	element = document->NewElement("Diffuse");
	element->SetAttribute("R", material->diffuse.x);
	element->SetAttribute("G", material->diffuse.y);
	element->SetAttribute("B", material->diffuse.z);
	element->SetAttribute("A", material->diffuse.w);
        node->LinkEndChild(element);

	element = document->NewElement("Specular");
	element->SetAttribute("R", material->specular.x);
	element->SetAttribute("G", material->specular.y);
	element->SetAttribute("B", material->specular.z);
	element->SetAttribute("A", material->specular.w);
        node->LinkEndChild(element);
		
	element = document->NewElement("Emissive");
	element->SetAttribute("R", material->emissive.x);
	element->SetAttribute("G", material->emissive.y);
	element->SetAttribute("B", material->emissive.z);
        element->SetAttribute("A", material->emissive.w);
        node->LinkEndChild(element);
    }

    document->SaveFile(Utils::ToString(finalPath).c_str());
}

이 함수는:

  1. 재질 개수 저장

  2. 각 재질 정보 저장:

    1. 재질 이름, 색상 정보(환경광, 확산광, 반사광, 발광)\

    2. 텍스처 파일 경로(디퓨즈, 스페큘러, 노말)

  3. 텍스처 파일 복사 및 경로 저장 (WriteTexture 함수 사용)

  4. XML형태로 정보 저장

애니메이션 데이터 변환 프로세스

Assimp 라이브러리를 사용하여 애니메이션 데이터를 읽고, 변환하고, 내보내는 과정에 대해 소개하겠습니다.

애니메이션 관련 데이터 구조

먼저 애니메이션 처리에 사용되는 주요 데이터 구조를 살펴보겠습니다:

// 개별 키프레임의 변환 데이터
struct asKeyframeData
{
    float time;          // 키프레임 시간
    Vec3 scale;          // 스케일
    Quaternion rotation; // 회전
    Vec3 translation;    // 위치
};

// 특정 본에 대한 키프레임 모음
struct asKeyframe
{
    string boneName;                      // 본 이름
    vector<asKeyframeData> transforms;    // 시간별 변환 데이터
};

// 전체 애니메이션 정보
struct asAnimation
{
    string name;                          // 애니메이션 이름
    uint32 frameCount;                    // 총 프레임 수
    float frameRate;                      // 프레임 레이트
    float duration;                       // 애니메이션 지속 시간
    vector<shared_ptr<asKeyframe>> keyframes; // 본별 키프레임 데이터
};

// 애니메이션 노드 캐시 (중간 처리용)
struct asAnimationNode
{
    aiString name;                        // 노드 이름
    vector<asKeyframeData> keyframe;      // 키프레임 데이터
};

이 구조체들을 통해 애니메이션의 계층적 구조를 표현합니다:

  • asAnimation: 전체 애니메이션 정보 (이름, 프레임 수, 프레임 레이트, 지속 시간)

  • asKeyframe: 특정 본에 대한 애니메이션 데이터

  • asKeyframeData: 특정 시간에서의 변환 정보 (위치, 회전, 스케일)

  • asAnimationNode: 애니메이션 파싱 중에 임시로 사용되는 캐시 구조체

애니메이션 변환 시작

애니메이션 변환 프로세스는 AssimpTool::ConvertAnimation 함수에서 시작합니다:

void AssimpTool::ConvertAnimation(const std::wstring& folderName, const std::wstring& fileName, const std::wstring& animName)
{
    shared_ptr<Converter> converter = make_shared<Converter>();

    std::wstring filePath = folderName + L"/" + fileName;
    converter->ReadAssetFile(filePath);

    std::wstring basePath = folderName + L"/" + animName;
    converter->ExportAnimationData(basePath);
}
  1. Converter 객체를 생성

  2. 모델 파일을 읽기 위해 ReadAssetFile 호출

  3. 애니메이션 데이터를 내보내기 위해 ExportAnimationData 호출

애니메이션 데이터 내보내기

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);
}
  1. 저장할 파일 경로 생성 (.clip 확장자 사용)

  2. 인덱스가 유효한지 확인

  3. ReadAnimationData 함수를 호출하여 애니메이션 데이터 읽기

  4. WriteAnimationData 함수를 호출하여 변환된 데이터 저장

애니메이션 데이터 읽기

shared_ptr<asAnimation> Converter::ReadAnimationData(aiAnimation* srcAnimation)
{
    shared_ptr<asAnimation> animation = make_shared<asAnimation>();
    animation->name = srcAnimation->mName.C_Str();
    animation->frameRate = (float)srcAnimation->mTicksPerSecond;
    animation->frameCount = (uint32)srcAnimation->mDuration + 1;

    map<string, shared_ptr<asAnimationNode>> cacheAnimNodes;

    for (uint32 i = 0; i < srcAnimation->mNumChannels; i++)
    {
        aiNodeAnim* srcNode = srcAnimation->mChannels[i];

        // 애니메이션 노드 데이터 파싱( SRT 추출 )
        shared_ptr<asAnimationNode> node = ParseAnimationNode(animation, srcNode);

        // 현재 찾은 노드 중에 제일 긴 시간으로 애니메이션 시간 갱신
        animation->duration = max(animation->duration, node->keyframe.back().time);

        cacheAnimNodes[srcNode->mNodeName.C_Str()] = node;
    }

    ReadKeyframeData(animation, _scene->mRootNode, cacheAnimNodes);

    return animation;
}

이 함수는 다음 작업을 수행합니다:

  1. asAnimation 객체 생성 및 기본 정보 설정 (이름, 프레임 레이트, 프레임 수)

  2. 모든 애니메이션 채널(각 본의 애니메이션)을 순회하며:

    • ParseAnimationNode 함수를 호출하여 노드 데이터 파싱

    • 애니메이션 지속 시간 갱신 (가장 긴 시간 기준)

    • 캐시 맵에 노드 데이터 저장

  3. ReadKeyframeData 함수를 호출하여 계층적 키프레임 데이터 구성

  4. 완성된 애니메이션 객체 반환

애니메이션 노드 파싱

shared_ptr<asAnimationNode> Converter::ParseAnimationNode(shared_ptr<asAnimation> animation, aiNodeAnim* srcNode)
{
    std::shared_ptr<asAnimationNode> node = make_shared<asAnimationNode>();
    node->name = srcNode->mNodeName;

    uint32 keyCount = max(max(srcNode->mNumPositionKeys, srcNode->mNumScalingKeys), srcNode->mNumRotationKeys);

    for (uint32 k = 0; k < keyCount; k++)
    {
        asKeyframeData frameData;
        frameData.scale = Vec3(1.0f, 1.0f, 1.0f);  // 스케일을 항상 1로 고정
        bool found = false;
        uint32 t = node->keyframe.size();

        // Position
        if (::fabsf((float)srcNode->mPositionKeys[k].mTime - (float)t) <= 0.0001f)
        {
            aiVectorKey key = srcNode->mPositionKeys[k];
            frameData.time = (float)key.mTime;
            ::memcpy_s(&frameData.translation, sizeof(Vec3), &key.mValue, sizeof(aiVector3D));
            found = true;
        }

        // Rotation
        if (::fabsf((float)srcNode->mRotationKeys[k].mTime - (float)t) <= 0.0001f)
        {
            aiQuatKey key = srcNode->mRotationKeys[k];
            frameData.time = (float)key.mTime;
            frameData.rotation.x = key.mValue.x;
            frameData.rotation.y = key.mValue.y;
            frameData.rotation.z = key.mValue.z;
            frameData.rotation.w = key.mValue.w;
            found = true;
        }

        if (found == true)
            node->keyframe.push_back(frameData);
    }

    // Keyframe 늘려주기
    if (node->keyframe.size() < animation->frameCount)
    {
        uint32 count = animation->frameCount - node->keyframe.size();
        asKeyframeData keyFrame = node->keyframe.back();

        for (uint32 n = 0; n < count; n++)
            node->keyframe.push_back(keyFrame);
    }

    return node;
}

이 함수는 다음 작업을 수행합니다:

  1. asAnimationNode 객체 생성 및 이름 설정

  2. 위치, 회전, 스케일 키의 최대 개수 계산

  3. 모든 키를 순회하며:

    • 키프레임 데이터 객체 생성

    • 현재 시간에 위치 키가 있는지 확인하고 데이터 추출

    • 현재 시간에 회전 키가 있는지 확인하고 데이터 추출

    • 위치 또는 회전 데이터가 있는 경우 키프레임 추가

  4. 부족한 프레임 수만큼 마지막 키프레임을 복제하여 채우기

  5. 완성된 노드 객체 반환

키프레임 데이터 읽기

void Converter::ReadKeyframeData(shared_ptr<asAnimation> animation, aiNode* srcNode, map<string, shared_ptr<asAnimationNode>>& cache)
{
    shared_ptr<asKeyframe> keyframe = make_shared<asKeyframe>();
    keyframe->boneName = srcNode->mName.C_Str();

    shared_ptr<asAnimationNode> findNode = cache[srcNode->mName.C_Str()];

    for (uint32 i = 0; i < animation->frameCount; i++)
    {
        asKeyframeData frameData;
        frameData.scale = Vec3(1.0f, 1.0f, 1.0f);  // 스케일을 1로 고정

        if (findNode == nullptr)
        {
            Matrix transform(srcNode->mTransformation[0]);
            transform = transform.Transpose();
            frameData.time = (float)i;

            // 스케일은 무시하고 회전과 이동만 추출
            Vec3 scale;
            transform.Decompose(OUT scale, OUT frameData.rotation, OUT frameData.translation);
        }
        else
        {
            frameData = findNode->keyframe[i];
            frameData.scale = Vec3(1.0f, 1.0f, 1.0f);
        }

        keyframe->transforms.push_back(frameData);
    }

    animation->keyframes.push_back(keyframe);

    for (uint32 i = 0; i < srcNode->mNumChildren; i++)
        ReadKeyframeData(animation, srcNode->mChildren[i], cache);
}

이 함수는 다음 작업을 수행합니다:

  1. asKeyframe 객체 생성 및 본 이름 설정

  2. 캐시에서 해당 노드의 애니메이션 데이터 찾기

  3. 각 프레임마다:

    1. 키프레임 데이터 객체 생성

    2. 캐시에 노드가 없는 경우:

      1. 노드의 변환 행렬에서 회전과 이동 데이터 추출

    3. 캐시에 노드가 있는 경우:

      1. 캐시된 키프레임 데이터 사용

      2. 키프레임 변환 데이터 추가

  4. 완성된 키프레임 객체를 애니메이션에 추가

  5. 재귀적으로 모든 자식 노드에 대해 동일한 처리 수행

애니메이션 데이터 저장

void Converter::WriteAnimationData(shared_ptr<asAnimation> animation, wstring finalPath)
{
    auto path = filesystem::path(finalPath);

    // 폴더가 없으면 만든다.
    filesystem::create_directory(path.parent_path());

    shared_ptr<FileUtils> file = make_shared<FileUtils>();
    file->Open(finalPath, FileMode::Write);

    file->Write<string>(animation->name);
    file->Write<float>(animation->duration);
    file->Write<float>(animation->frameRate);
    file->Write<uint32>(animation->frameCount);

    file->Write<uint32>(animation->keyframes.size());

    for (shared_ptr<asKeyframe> keyframe : animation->keyframes)
    {
        file->Write<string>(keyframe->boneName);

        file->Write<uint32>(keyframe->transforms.size());
        file->Write(&keyframe->transforms[0], sizeof(asKeyframeData) * keyframe->transforms.size());
    }
}

이 함수는 다음 작업을 수행합니다:

  1. 저장 경로의 디렉토리 생성

  2. FileUtils 객체를 생성하여 파일 열기

  3. 애니메이션 기본 정보 저장:

    1. 이름

    2. 지속 시간

    3. 프레임 레이트

    4. 프레임 수

  4. 키프레임 개수 저장

  5. 각 키프레임(본)마다:

    1. 본 이름 저장

    2. 변환 데이터 개수 저장

    3. 모든 변환 데이터 저장 (시간, 스케일, 회전, 위치)

이 과정을 통해 FBX 등의 파일에 포함된 애니메이션 데이터를 게임 엔진에서 효율적으로 사용할 수 있는 최적화된 형식으로 변환합니다. 특히 다음과 같은 최적화와 변환이 적용됩니다:

  • 모든 본에 대해 일정한 프레임 수 유지

  • 애니메이션 지속 시간 및 프레임 레이트 정보 보존

  • 계층적 본 구조에 맞게 모든 본의 애니메이션 데이터 포함

  • 바이너리 형식으로 저장하여 로딩 속도와 크기 최적화

Last updated