수업


✅ 주제

Assimp를 활용한 FBX 모델 파싱과 구조화

  • 외부 3D 모델 파일인 FBX 포맷을 게임 엔진에서 직접 활용 가능한 형태로 변환하기 위한 전체적인 과정

  • 핵심적으로 활용되는 것이 바로 Assimp (Open Asset Import Library) 라는 외부 라이브러리

  • 강의 목표
    1. FBX 모델이 내부적으로 어떤 구조로 구성되어 있는지를 파악하고,
    2. Assimp를 활용해 FBX 데이터를 메모리로 로드한 후,
    3. 트리 계층 구조로 파싱하여 각 노드별 정보를 추출하며,
    4. 데이터를 자신만의 커스텀 포맷(asMesh, asMaterial 등)으로 변환해 저장/활용할 수 있는 기반 만듦

  • FBX는 단순히 정점과 인덱스로 구성된 메시 파일이 아니라, 다양한 머티리얼, 텍스처, 조명, 애니메이션, 계층 구조 등의 정보를 포함.

  • 필요한 정보만 뽑아내고 효율적으로 사용할 수 있도록 정리하는 작업이 필수임

  • 수업 진행 과정

    • Assimp 라이브러리 세팅 및 프로젝트 구성
    • FBX 파일을 메모리에 로드하고 aiScene → aiNode → aiMesh 구조로 파싱
    • FBX 모델이 갖는 계층 구조를 재귀적으로 탐색하는 방법 습득
    • 추출한 데이터를 바탕으로 우리만의 커스텀 포맷 구조체(asMaterial, asMesh 등) 에 담는 작업
  • 향후 애니메이션, 본 스키닝, 최적화된 로딩 구조 등과도 밀접한 연관을 갖는 기초 작업


✅ 개념


1. FBX 포맷의 본질: 단순 메시X

  • FBX는 단순히 정점(Vertex)과 인덱스(Index) 정보만 담긴 메시 파일이 아님, 복잡한 형태의 트리 구조

    • 머티리얼 정보 (Material)
    • 텍스처 경로 (Texture)
    • 애니메이션 트랙
    • 본(Bone) 정보 및 스켈레톤
    • 카메라, 라이트 같은 씬 요소들
  • 트리(Tree) 구조의 계층화된 노드 안에 포함.

  • FBX : 부모-자식 관계를 갖는 트랜스폼 구조를 기본으로 한 계층적 3D 모델 데이터


2. Assimp란 무엇인가?

  • Assimp (Open Asset Import Library) 는 다양한 3D 모델 포맷(FBX, OBJ, DAE 등)을 지원하는 범용 파서 라이브러리.
  • Assimp는 복잡한 FBX 포맷을 다음과 같은 주요 구조로 나누어 메모리에 적재
    • aiScene – 전체 FBX 파일을 대표하는 루트 컨테이너
    • aiNode – 계층구조를 표현하는 노드. 이름, 트랜스폼, 자식 노드를 포함
    • aiMesh – 메시 데이터(정점, 인덱스, UV, 노멀 등)
    • aiMaterial – 텍스처 경로, 색상 정보 등 머티리얼 속성
    • aiMatrix4x4 – 트랜스폼 행렬(SRT 기반)

3. FBX의 계층 구조

(Tree Structure)

  • FBX 파일의 모든 구성요소는 트리 형태의 계층 구조로 배치
[aiScene]
 └── aiNode(Root)
      ├── aiNode(Head)
      │    ├── aiNode(Eye_L)
      │    └── aiNode(Eye_R)
      └── aiNode(Body)
           └── aiNode(Arm)
  • 각 노드는 부모 노드의 트랜스폼을 기준으로 한 상대 위치(로컬 좌표계)를 가짐
  • 실제 게임 씬에서 필요한 절대 위치(World Transform) 를 계산하기 위해서는 부모 트랜스폼들과의 누적 곱셈 필요
WorldTransform = ParentWorldTransform * LocalTransform;
  • 재귀 순회 방식(Tree Traversal) 을 통해 처리하는 것이 가장 적합

4. 메모리 로딩 → 커스텀 포맷으로 변환하는 이유

  • FBX 파일을 그대로 실시간으로 불러오면 다음과 같은 문제가 발생

    • 로드 시간 증가: FBX 내부의 복잡하고 불필요한 정보들로 인해 불필요한 파싱 비용이 발생
    • 불필요한 데이터: 조명, 카메라, 사용하지 않는 메시 등 게임에 필요 없는 정보가 많음
    • 성능 저하 및 디버깅 어려움: 매번 Assimp로 파싱하면 성능 손실 및 디버깅 난이도 증가
  • 필요한 정보만 골라내고, 그것을 커스텀 정의한 구조체(asMaterial, asMesh 등) 에 담아 커스텀 포맷으로 변환

  • 이후부터는 이 커스텀 포맷만 빠르게 로드하면 되기 때문에, 로딩 속도와 구조적인 관리 측면에서 모두 유리함


5. 핵심 개념

개념설명
FBX3D 모델, 트랜스폼, 애니메이션, 조명 등 복합 요소 포함
Assimp다양한 포맷을 지원하는 범용 3D 모델 로더
aiSceneFBX 전체 구조의 루트. 메시, 노드, 머티리얼 포함
aiNode트리 구조의 노드. 이름, 변환행렬, 자식 노드 포함
aiMesh실제 그리기 위한 메시 정보(정점, 노멀, UV 등)
aiMaterial텍스처 경로, 색상 등 시각적 정보를 담는 객체
Local ↔ World 좌표계부모 트랜스폼과의 행렬 곱으로 절대 위치 계산
Custom FormatasMesh, asMaterial 등 커스텀 구조체로 파싱 및 저장

용어정리

🔶 FBX

✅ FBX (Filmbox)

  • Autodesk가 만든 3D 에셋 포맷

  • 단순 메시(Vertex, Index)뿐만 아니라, 애니메이션, 머티리얼, 텍스처, 본, 라이트, 카메라 등 다양한 요소들이 함께 포함되어 있는 복합 구조 포맷

  • 계층 구조(Hierarchy) 를 바탕으로 씬(Scene)을 구성

✅ 계층 구조 (Hierarchy)

  • FBX 모델은 단일 메시가 아니라 여러 노드들이 부모-자식 관계로 구성된 트리 구조

  • 각 노드는 Transform 행렬을 포함하며, 부모 노드를 기준으로 한 상대 좌표(로컬 좌표)를 가짐

  • 즉, 각 노드를 파싱할 때는 재귀적으로 순회(Traversal) 하며, 부모 좌표계를 누적 곱해서 월드 좌표계를 계산해야 함


🔷 Assimp 핵심 클래스 및 자료형

✅ Assimp (Open Asset Import Library)

  • 다양한 3D 포맷을 로딩하고 파싱할 수 있는 오픈소스 라이브러리
  • 주요 포맷: FBX, OBJ, DAE(Collada), 3DS 등이 있음

✅ Assimp::Importer

  • FBX 파일을 메모리에 로드하기 위한 입구 클래스
  • 내부적으로 ReadFile()을 호출해 .fbx 파일을 메모리에 올리며, 결과로 aiScene*을 반환함

🔷 Assimp 구조체 계층

✅ aiScene

  • Assimp가 FBX를 로드했을 때 만들어지는 최상위 객체

  • 내부에는 다음과 같은 구성 요소가 존재

    • mRootNode : 씬의 루트 노드
    • mMeshes[] : 메시 리스트
    • mMaterials[] : 머티리얼 리스트

✅ aiNode

  • 씬 내에서 실제 트리 구조를 이루는 핵심 단위

  • 내부 구성:

    • mName: 노드 이름
    • mTransformation: 해당 노드의 Transform 행렬 (SRT)
    • mMeshes[]: 이 노드에 포함된 메시 인덱스들
    • mChildren[]: 자식 노드 배열
  • 이 구조를 재귀적으로 순회하여 모든 메시를 탐색해야 함

✅ aiMatrix4x4

  • 4x4 변환 행렬로, 각 노드의 SRT (Scale, Rotation, Translation) 담고 있음

  • 좌표 변환 시, 부모의 월드 행렬과 곱하여 월드 트랜스폼 계산에 사용됨


🔷 메시 및 렌더링 정보

✅ aiMesh

  • 하나의 실제 메시 정보를 담고 있는 구조체

  • 내부 구성:

    • mVertices[]: 정점 정보(Position)
    • mNormals[]: 노멀 벡터
    • mTextureCoords[]: UV 좌표
    • mFaces[]: 인덱스 정보 (삼각형 면)
    • mMaterialIndex: 해당 메시가 사용하는 머티리얼의 인덱스

✅ aiMaterial

  • 메시가 사용하는 머티리얼 정보를 가지고 있음

  • 내부 구성:

    • Ambient, Diffuse, Specular, Emissive 등 색상 정보
    • 텍스처 파일 경로 (aiTextureType_DIFFUSE, aiTextureType_NORMALS 등)
    • 이름 정보는 aiMaterial->Get(AI_MATKEY_NAME, aiString&) 으로 추출 가능

🔷 기타

✅ aiString

  • Assimp에서 사용하는 전용 문자열 타입

  • C++의 std::string과 유사하지만, 내부 구조가 달라서 반드시 .C_Str()을 통해 C 스타일 문자열로 변환해서 사용

✅ SRT (Scale, Rotation, Translation)

  • 트랜스폼을 구성하는 기본 요소

  • FBX의 각 노드(aiNode)는 이 SRT 정보로부터 행렬(aiMatrix4x4) 를 구성

  • 이 행렬을 기준으로 부모로부터의 상대적 위치를 표현


🔶 커스텀 포맷 (Custom Format)

  • Assimp 구조체(aiScene, aiNode, aiMesh 등)를 그대로 사용하는 것은 비효율적

  • 따라서, 필요한 정보만 추려 asMesh, asMaterial 같은 커스텀 구조체를 만듦

  • 이를 기반으로 .mesh, .skel 같은 자체 이진 포맷으로 저장

  • 자체 이진 포맷의 목적 :

    • FBX 대비 불필요한 정보 제거
    • 빠른 로딩 속도
    • 엔진 렌더링 시스템에 맞춤형 데이터 구조 사용 가능

✅ 코드

1. Unity 에셋의 구조 이해

  • UnityChan 같은 에셋을 보면 .fbx 모델 안에 Mesh, Material, Light, Camera 정보가 포함

  • 이 모델들은 하나의 평면 구조가 아니라, 트리 형태의 계층 구조로 구성

  • 본격적인 FBX 파싱을 시작하기 전에, 우리는 이 구조를 코드로 재귀 순회할 수 있어야 함


2. 좌표계 개념 정리

Local ↔ World Transform

WorldTransform = ParentWorld * LocalTransform;
  • FBX는 각 노드마다 자기 자신만의 로컬 행렬을 가지고 있음

  • 부모의 월드 행렬에 자신의 로컬 행렬을 곱해 나가야 최종 월드 행렬이 됨.

  • 이 구조를 처리하려면 트리 구조 순회 + 행렬 곱이 함께 이루어져야 함.


3. Assimp 세팅

헤더와 라이브러리 연결

#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>

#ifdef _DEBUG
#pragma comment(lib, "Assimp/assimp-vc143-mtd.lib")
#else
#pragma comment(lib, "Assimp/assimp-vc143-mt.lib")
#endif
  • Importer.hpp: FBX 등 3D 파일을 읽기 위한 진입점
  • scene.h: aiScene, aiMesh, aiNode 등 구조 정의
  • postprocess.h: 후처리 옵션 제공

4. FBX 파싱 클래스

Converter

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

    void ReadAssetFile(wstring file);

private:
    wstring _assetPath = L"../Resources/Assets/";
    shared_ptr<Assimp::Importer> _importer;
    const aiScene* _scene;
};

Constructor/Destructor

Converter::Converter()
{
    _importer = make_shared<Assimp::Importer>();
}

Converter::~Converter() {}
  • _importer는 FBX 파일을 읽고 내부 구조(aiScene)를 메모리에 올려주는 핵심 객체

5. Assimp로 FBX 읽기

ReadAssetFile

void Converter::ReadAssetFile(wstring file)
{
    wstring fileStr = _assetPath + file;
    auto p = std::filesystem::path(fileStr);
    assert(std::filesystem::exists(p)); // 파일 존재 확인

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

    assert(_scene != nullptr);
}

옵션 설명

  • ConvertToLeftHanded: DirectX 좌표계에 맞게 좌수계 변환
  • Triangulate: 모든 면을 삼각형으로 변환
  • GenUVCoords: UV 좌표 자동 생성
  • GenNormals: 법선 벡터 생성
  • CalcTangentSpace: 탄젠트 벡터 계산

6. FBX 계층 순회

ParseNode()

void Converter::ParseNode(aiNode* node, const aiMatrix4x4& parentTransform)
{
    aiMatrix4x4 localTransform = node->mTransformation;
    aiMatrix4x4 worldTransform = parentTransform * localTransform;

    string nodeName = node->mName.C_Str();

    for (UINT i = 0; i < node->mNumMeshes; i++)
    {
        UINT meshIndex = node->mMeshes[i];
        aiMesh* mesh = _scene->mMeshes[meshIndex];
        // 정점, 인덱스, 머티리얼 매핑 작업 예정
    }

    for (UINT i = 0; i < node->mNumChildren; i++)
        ParseNode(node->mChildren[i], worldTransform);
}
  • 각 노드를 재귀적으로 순회하면서, 메시와 행렬 정보를 수집

7. Material 정보 파싱

ReadMaterial()

void Converter::ReadMaterial()
{
    uint32 materialCount = _scene->mNumMaterials;

    for (uint32 i = 0; i < materialCount; i++)
    {
        asMaterial material;
        aiMaterial* srcMaterial = _scene->mMaterials[i];

        aiString str;
        srcMaterial->Get(AI_MATKEY_NAME, str);
        material.name = str.C_Str();

        aiColor3D color;
        if (srcMaterial->Get(AI_MATKEY_COLOR_AMBIENT, color) == AI_SUCCESS)
            material.ambient = Color(color.r, color.g, color.b, 1.0f);
        if (srcMaterial->Get(AI_MATKEY_COLOR_DIFFUSE, color) == AI_SUCCESS)
            material.diffuse = Color(color.r, color.g, color.b, 1.0f);
        if (srcMaterial->Get(AI_MATKEY_COLOR_SPECULAR, color) == AI_SUCCESS)
            material.specular = Color(color.r, color.g, color.b, 1.0f);
        if (srcMaterial->Get(AI_MATKEY_COLOR_EMISSIVE, color) == AI_SUCCESS)
            material.emissive = Color(color.r, color.g, color.b, 1.0f);

        aiString texturePath;
        if (srcMaterial->GetTexture(aiTextureType_DIFFUSE, 0, &texturePath) == AI_SUCCESS)
            material.diffuseFile = texturePath.C_Str();
        if (srcMaterial->GetTexture(aiTextureType_SPECULAR, 0, &texturePath) == AI_SUCCESS)
            material.specularFile = texturePath.C_Str();
        if (srcMaterial->GetTexture(aiTextureType_NORMALS, 0, &texturePath) == AI_SUCCESS)
            material.normalFile = texturePath.C_Str();

        _materials.push_back(material);
    }
}

8. Mesh 정보 파싱

ReadMesh()

void Converter::ReadMesh()
{
    uint32 meshCount = _scene->mNumMeshes;

    for (uint32 i = 0; i < meshCount; i++)
    {
        asMesh mesh;
        aiMesh* srcMesh = _scene->mMeshes[i];
        mesh.name = srcMesh->mName.C_Str();
        mesh.mesh = srcMesh;
        mesh.boneIndex = -1;

        for (uint32 v = 0; v < srcMesh->mNumVertices; v++)
        {
            VetexType vertex;
            auto& pos = srcMesh->mVertices[v];
            vertex.position = Vec3(pos.x, pos.y, pos.z);

            if (srcMesh->HasTextureCoords(0))
                vertex.uv = Vec2(srcMesh->mTextureCoords[0][v].x, srcMesh->mTextureCoords[0][v].y);

            if (srcMesh->HasNormals())
                vertex.normal = Vec3(srcMesh->mNormals[v].x, srcMesh->mNormals[v].y, srcMesh->mNormals[v].z);

            if (srcMesh->HasTangentsAndBitangents())
                vertex.tangent = Vec3(srcMesh->mTangents[v].x, srcMesh->mTangents[v].y, srcMesh->mTangents[v].z);

            mesh.vertices.push_back(vertex);
        }

        for (uint32 f = 0; f < srcMesh->mNumFaces; f++)
        {
            aiFace& face = srcMesh->mFaces[f];
            for (uint32 k = 0; k < face.mNumIndices; k++)
                mesh.indicies.push_back(face.mIndices[k]);
        }

        mesh.materialName = _materials[srcMesh->mMaterialIndex].name;
        _meshes.push_back(mesh);
    }
}

9. 저장 전략

왜 커스텀 포맷으로 바꿔?

이유설명
로딩 속도FBX → 수 초, Custom Format → 수 밀리초
불필요한 정보 제거카메라, 라이트, 애니메이션 등 제거 가능
렌더링 최적화정점 버퍼, 인덱스 버퍼 미리 계산 가능
  • 저장 예시는 .mesh, .model, .mat 파일 등으로 나누어 직렬화하는 방식 사용


10. Vertex 구조체 설계

  • 사용할 정점 데이터 구조는 단순한 위치(Position) 정보만 포함하는 것이 아님
  • 노멀(Normal), UV 좌표, 탄젠트(Tangent)는 물론이고, 추후 애니메이션을 위한 본 인덱스 및 가중치(BlendIndices, BlendWeights)까지 고려된 확장형 정점 구조

VertexTextureNormalTangentBlendData 구조체

struct VertexTextureNormalTangentBlendData
{
    Vec3 position = { 0, 0, 0 };
    Vec2 uv = { 0, 0 };
    Vec3 normal = { 0, 0, 0 };
    Vec3 tangent = { 0, 0, 0 };
    Vec4 blendIndicies = { 0,0,0,0 };	// 애니메이션 스키닝용 본 인덱스
    Vec4 blendWeights = { 0,0,0,0 };	// 애니메이션 스키닝용 가중치
};
  • 이 구조체는 일반 메시에도 쓰이지만, 애니메이션 지원을 위해 Blend 정보를 기본으로 포함
  • 확장성과 유연성을 고려한 설계로, 추후 애니메이션 데이터가 들어와도 구조 변경 없이 그대로 활용 가능

📌 asMaterial 구조체 정의

  • FBX의 aiMaterial에서 파싱한 데이터를 저장하기 위한 구조체
struct asMaterial
{
    string name;
    Color ambient;
    Color diffuse;
    Color specular;
    Color emissive;
    string diffuseFile;
    string specularFile;
    string normalFile;
};
  • name: 머티리얼 이름(aiMaterial의 AI_MATKEY_NAME)

  • ambient, diffuse, specular, emissive: 컬러 정보

  • diffuseFile, specularFile, normalFile: 각각의 텍스처 경로

  • Assimp에서 제공하는 aiMaterial 구조체에서 해당 키를 통해 값을 추출


📌 asMesh 구조체 정의

struct asMesh
{
    string name;
    aiMesh* mesh; // 원본 메시 포인터
    vector<VetexType> vertices;
    vector<uint32> indicies;

    int32 boneIndex;          // 계층 구조 상에서 부모 본 인덱스
    string materialName;      // 연결된 머티리얼 이름
};
  • vertices: 우리가 정의한 정점(VertexType) 배열
  • indicies: 정점 인덱스 정보
  • boneIndex: 해당 메시가 소속된 본의 인덱스(초기엔 -1)
  • materialName: 메시와 연결된 머티리얼 이름

📌 FBX → 커스텀 포맷 저장 전략 요약

항목설명
aiMaterial → asMaterial머티리얼 이름, 색상, 텍스처 경로 추출
aiMesh → asMesh정점/인덱스 정보 추출 및 머티리얼 연결
aiScene → 계층 순회메시 포함 노드를 순회하면서 메시 파싱

📌 커스텀 포맷으로 저장 시 장점

  1. 성능: FBX 파일은 불필요한 데이터가 많고, 로딩에 시간이 오래 걸림 → 필요한 정보만 저장한 커스텀 포맷은 훨씬 빠름
  2. 구조 최적화: 정점 버퍼, 인덱스 버퍼, 바운딩 박스 등을 사전 계산해 저장 가능
  3. 유연한 관리: 메시, 머티리얼, 텍스처를 우리가 설계한 규칙과 이름으로 저장 가능

🎯 핵심


✅ 1. FBX는 단순 메시 파일이 아님

FBX는 단순히 정점(Vertex)과 인덱스(Index) 정보만 담긴 메시 파일 X

  • 내부에는 계층 구조(Hierarchy)를 기반으로 애니메이션, 본(Bone), 조명, 카메라, 머티리얼, 텍스처 등 수많은 데이터가 트리 형태로 얽혀 있음

  • 따라서 이 구조를 파싱하려면 단순 반복이 아닌 재귀적 트리 순회(Tree Traversal)가 필수

    • 각 노드는 부모 기준의 상대 좌표(Local Transform)를 가짐
    • 이걸 부모의 Transform과 누적 곱 연산을 통해 월드 좌표(World Transform)로 환산
  • 트랜스폼 누적 방식은 나중에 본 스키닝 애니메이션 처리에도 반드시 필요한 핵심 로직


✅ 2. Assimp는 로딩만 해줌

파싱과 변환은 우리가 직접

  • Assimp는 다양한 3D 포맷(FBX, OBJ 등)을 지원하는 훌륭한 로더지만, 우리가 사용하는 렌더링 시스템이나 엔진 구조와 직접적으로 맞지 않음

  • Assimp로 로딩한 결과물은 aiScene → aiNode → aiMesh → aiMaterial 등의 구조로 메모리에 올라가지만, 이걸 그대로 사용할 수는 없음

  • 결국 Assimp에서 추출한 데이터는 반드시 우리가 정의한 구조체(asMaterial, asMesh 등) 로 파싱하여 엔진 친화적인 커스텀 포맷으로 변환해 저장해야 함


✅ 3. 커스텀 포맷 전략은 선택이 아닌 필수

  • FBX 파일은 로딩 속도가 매우 느림
  • 반면, 한번 추출한 데이터를 .mesh, .model 같은 커스텀 바이너리 포맷으로 저장해두면, 다음부터는 수 밀리초만에 로딩이 가능
  • 커스텀 포맷은 렌더링 파이프라인에 맞춰 미리 정리된 정보만을 담고 있으므로, 불필요한 데이터가 제거되어 성능과 메모리 효율성도 탁월

✅ 4. 파싱 핵심은 구조체와 자료의 대응

FBX 로딩 후 구조체 매핑 흐름은 다음과 같음

FBX → Assimp::Importer → aiScene 메모리 구조
    └→ aiNode (계층 구조 순회)
        └→ aiMesh (정점, 인덱스, 텍스처 좌표, 노멀, 탄젠트)
            └→ aiMaterial (색상, 텍스처 파일 경로 등)
    ↓
우리의 구조체 asMesh, asMaterial, VertexTextureNormalTangentBlendData 등에 파싱
    ↓
커스텀 포맷(.mesh, .mat 등)으로 저장
  • 특히, 정점 구조체 VertexTextureNormalTangentBlendData는 일반 메시뿐 아니라 애니메이션 본 스키닝까지 고려한 확장성 있는 구조

  • 텍스처 경로, 색상 정보는 aiMaterial에서 반드시 파싱해야 하며, 누락 시 렌더링 오류로 이어질 수 있음


profile
李家네_공부방

0개의 댓글