Computer Graphics/OpenGL

OpenGL 정리 - 17. 오브젝트 로더 구현 (Assimp)

surkim 2024. 9. 24. 11:23

Objective

박스 외의 오브젝트를 렌더링해보자. 실제 게임 등에 사용되는 오브젝트를 렌더링하기 위해 어떤 작업을 해야 할까?

  1. 3D Modeling Tool을 이용해 3D 모델을 디자인.
  2. 3D 모델 파일 포맷으로 저장.
  3. 저장된 파일을 읽어들이는 Object Loader를 구현.

3D Modeling Tool

3D 모델을 생성하고 편집할 수 있는 도구들이 있으며, 다양한 작업을 처리할 수 있다:

  • Modeling: 기본적인 형태를 만들고 수정하는 과정.
  • Sculpting: 세밀한 디테일을 더하는 과정.
  • UV Unwrapping: 3D 표면을 평면으로 펼쳐 텍스처를 입히는 과정.
  • Rigging: 애니메이션을 위한 뼈대를 구성.
  • Animation: 물체에 움직임을 부여.

대표적인 툴:

  • Blender 3D: 오픈소스, 강력한 기능을 제공.
  • 3D Studio Max: 상업용 소프트웨어.
  • Maya: 영화, 게임 등에서 널리 사용되는 툴.

3D Model Files

모델링 툴을 통해 만들어진 다양한 파일 포맷이 존재한다:

  • FBX, OBJ, Collada(dae), GLTF, PLY 등이 있으며, 각각의 포맷이 서로 다른 정보를 포함.
  • 각 포맷에 맞는 파서(Parser)를 직접 만드는 것은 어렵기 때문에 Assimp 라이브러리를 사용해 여러 포맷을 지원하는 것이 일반적이다.

Assimp

Assimp(Open Asset Import Library)는 다양한 3D 모델 파일을 지원하는 라이브러리로, FBX, OBJ 등의 여러 파일을 쉽게 읽어들일 수 있다.

  • Cross-platform: 다양한 플랫폼에서 사용 가능.
  • C/C++ Interface: C와 C++에서 사용 가능한 API 제공.

Object Loader 구현

이제 실제로 Assimp를 사용하여 3D 모델을 불러오고 렌더링하는 과정을 구현해보자. 모델을 로드하고, 메쉬와 재질 정보를 읽어들여 렌더링하는 흐름을 설명하겠다.

1. Context에서 모델 로드

먼저, Context::Init()에서 Assimp를 사용해 backpack.obj 파일을 로드한다.

bool Context::Init() {
    m_box = Mesh::CreateBox();
    m_model = Model::Load("./model/backpack.obj"); // 모델 로드
    if (!m_model)
        return false;
    ...
}

Model::Load() 함수는 Assimp 라이브러리를 사용해 모델 파일을 읽어들이고, 메쉬와 재질을 처리한다.

2. Model 클래스와 Assimp를 통한 로드

모델을 로드하는 핵심 부분은 Assimp 라이브러리를 통해 파일을 읽고, 그 안의 메쉬재질 정보를 추출하는 것이다.

bool Model::LoadByAssimp(const std::string& filename) {
    Assimp::Importer importer;
    auto scene = importer.ReadFile(filename, aiProcess_Triangulate | aiProcess_FlipUVs);

    if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) {
        SPDLOG_ERROR("failed to load model: {}", filename);
        return false;
    }

    ProcessNode(scene->mRootNode, scene); // 노드를 처리해 메쉬 추출
    return true;
}

3. Mesh 정보 처리

ProcessMesh() 함수는 aiMesh에서 정점, 법선, 텍스처 좌표와 같은 기하학적 정보를 읽어온다.

void Model::ProcessMesh(aiMesh* mesh, const aiScene* scene) {
    std::vector<Vertex> vertices(mesh->mNumVertices);
    for (uint32_t i = 0; i < mesh->mNumVertices; i++) {
        vertices[i].position = glm::vec3(mesh->mVertices[i].x, mesh->mVertices[i].y, mesh->mVertices[i].z);
        vertices[i].normal = glm::vec3(mesh->mNormals[i].x, mesh->mNormals[i].y, mesh->mNormals[i].z);
        vertices[i].texCoord = glm::vec2(mesh->mTextureCoords[0][i].x, mesh->mTextureCoords[0][i].y);
    }
    // 인덱스와 메쉬 정보 저장
}

4. Material 정보 처리

재질 정보는 aiMaterial을 통해 불러오며, Diffuse Map, Specular Map 등의 텍스처 정보를 가져올 수 있다.

auto LoadTexture = [&](aiMaterial* material, aiTextureType type) -> TexturePtr {
    if (material->GetTextureCount(type) <= 0)
        return nullptr;
    aiString filepath;
    material->GetTexture(type, 0, &filepath);
    return Texture::CreateFromImage(Image::Load(filepath.C_Str()).get());
};

5. 모델 그리기

로드된 모델을 OpenGL에서 그리는 부분은 Model::Draw() 함수에서 실행된다.

void Model::Draw(const Program* program) const {
    for (auto& mesh : m_meshes) {
        mesh->Draw(program); // 각 메쉬를 그린다
    }
}

6. Context에서 모델 렌더링

이제 Context::Render()에서 로드한 모델을 화면에 그릴 수 있다.

void Context::Render() {
    m_program->Use();
    m_model->Draw(m_program.get()); // 모델 그리기
}

결과