Object Loading (Assimp)

Date:     Updated:

카테고리:

태그:

Main Reference
- Learn OpenGL
- Rinthel Kwon - OpenGL Lecture


Assimp Library

10_assimp_structure

Assimp(Asset Import) Library는 3D 모델의 다양한 포맷을 로드할 수 있게 지원하는 오픈 소스 라이브러리이다. C/C++ 인터페이스로 구성되어 있으며 임포트된 에셋은 모델 파일 포맷과 관계 없이 유지되기 때문에 cross-platform을 지원한다. Assimp 라이브러리의 구조는 대략 위 그림과 같다. Scene 객체에 Root Node, Meshes, Materials가 존재하고, Root Node는 자식 노드들을 가지고, 자식 노드 역시 자식 노드를 갖는 트리 구조로 이루어져 잇다.


Model Model Architecture
20240309044641 20240309044620

Assimp 라이브러리가 트리 구조로 이루어진 이유는 대부분의 모델들이 부모/자식 관계로 이루이져 있기 때문이다. 위 예시를 보면 캐릭터의 각 파트의 transform 정보는 부모 파트의 local coordinate 기준으로 기술된다.


Model Import

// mesh.h
#ifndef __MESH_H__
#define __MESH_H__

struct Vertex {
    glm::vec3 position;
    glm::vec3 normal;
    glm::vec2 texCoord;
};

CLASS_PTR(Material);
class Material {
public:
    static MaterialUPtr Create() {
        return MaterialUPtr(new Material());
    }
    TexturePtr diffuse;
    TexturePtr specular;
    float shininess { 32.0f };

    void SetToProgram(const Program* program) const;

private:
    Material() {}
};



CLASS_PTR(Mesh);
class Mesh {
public:
    static MeshUPtr Create(
        const std::vector<Vertex>& vertices,
        const std::vector<uint32_t>& indices,
        uint32_t primitiveType);

    const VertexLayout* GetVertexLayout() const { return m_vertexLayout.get(); }
    BufferPtr GetVertexBuffer() const { return m_vertexBuffer; }
    BufferPtr GetIndexBuffer() const { return m_indexBuffer; }
    void SetMaterial(MaterialPtr material) { m_material = material; }
    MaterialPtr GetMaterial() const { return m_material; }

    void Draw(const Program* program) const;

private:
    Mesh() {}
    void Init(
        const std::vector<Vertex>& vertices,
        const std::vector<uint32_t>& indices,
        uint32_t primitiveType);

    uint32_t m_primitiveType { GL_TRIANGLES };
    VertexLayoutUPtr m_vertexLayout;
    BufferPtr m_vertexBuffer;
    BufferPtr m_indexBuffer;
    MaterialPtr m_material;
};

#endif // __MESH_H__
  • 하나의 모델은 트리 구조로 이루어진 여러 mesh들로 이루어져 있음
  • VBO, VAO, 바인딩, Draw 등 메시를 그리는 과정에 필요한 작업들 전부 mesh 클래스 내부로 이동
    • VAO는 unique pointer, VBO, IBO는 shared pointer 사용한 이유?
    • VBO, IBO는 다른 VAO와 바인딩해 재사용 할 수 있지만, VAO는 해당 메시에서만 사용


.obj 파일을 뜯어보면..

1

  • mtl(material) file name
  • v : vertex position
  • vt : texture coord
  • vn : normal vector
  • f : face(한 면)을 이루는 삼각형 vertex 3개 (line of position/texcoord/normal)


// model.h
#ifndef __MODEL_H__
#define __MODEL_H__

CLASS_PTR(Model);
class Model {
public:
    static ModelUPtr Load(const std::string& filename);

    int GetMeshCount() const { return (int)m_meshes.size(); }
    MeshPtr GetMesh(int index) const { return m_meshes[index]; }
    void Draw(const Program* program) const;

private:
    Model() {}
    bool LoadByAssimp(const std::string& filename);
    void ProcessMesh(aiMesh* mesh, const aiScene* scene);
    void ProcessNode(aiNode* node, const aiScene* scene);
        
    std::vector<MeshPtr> m_meshes;
    std::vector<MaterialPtr> m_materials;
};
  • Assimp library를 이용해 로드한 모델은 ai(asset importer) - Node, Mesh, Material 변수 타입으로 저장
  • Draw 함수의 경우 단순하게 각 메시들의 draw 함수 호출


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

    auto dirname = filename.substr(0, filename.find_last_of("/"));

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

    for (uint32_t i = 0; i < scene->mNumMaterials; i++) {
        auto material = scene->mMaterials[i];
        auto glMaterial = Material::Create();
        glMaterial->diffuse = LoadTexture(material, aiTextureType_DIFFUSE);
        glMaterial->specular = LoadTexture(material, aiTextureType_SPECULAR);
        m_materials.push_back(std::move(glMaterial));
    }

    ProcessNode(scene->mRootNode, scene);
    return true;
}
  • LoadByAssimp 함수는 기본 Scene 객체 로드.
  • m_materials에 material(texture) 추가
  • LoadTexture 함수는 lamda를 이용해 함수 내부에 구현
  • 루트 노드를 이용해 ProcessNode 함수 호출


void Model::ProcessNode(aiNode* node, const aiScene* scene) {
    for (uint32_t i = 0; i < node->mNumMeshes; i++) {
        auto meshIndex = node->mMeshes[i];
        auto mesh = scene->mMeshes[meshIndex];
        ProcessMesh(mesh, scene);
    }

    for (uint32_t i = 0; i < node->mNumChildren; i++) {
        ProcessNode(node->mChildren[i], scene);
    }
}

void Model::ProcessMesh(aiMesh* mesh, const aiScene* scene) {
    SPDLOG_INFO("process mesh: {}, #vert: {}, #face: {}", 
    mesh->mName.C_Str(), mesh->mNumVertices, mesh->mNumFaces);

    std::vector<Vertex> vertices;
    vertices.resize(mesh->mNumVertices);
    for (uint32_t i = 0; i < mesh->mNumVertices; i++) {
        auto& v = vertices[i];
        v.position = glm::vec3(mesh->mVertices[i].x, mesh->mVertices[i].y, mesh->mVertices[i].z);
        v.normal = glm::vec3(mesh->mNormals[i].x, mesh->mNormals[i].y, mesh->mNormals[i]. z);
        v.texCoord = glm::vec2(mesh->mTextureCoords[0][i].x, mesh->mTextureCoords[0][i].y);
    }

    std::vector<uint32_t> indices;
    indices.resize(mesh->mNumFaces * 3);
    for (uint32_t i = 0; i < mesh->mNumFaces; i++) {
        indices[3*i  ] = mesh->mFaces[i].mIndices[0];
        indices[3*i+1] = mesh->mFaces[i].mIndices[1];
        indices[3*i+2] = mesh->mFaces[i].mIndices[2];
    }

    auto glMesh = Mesh::Create(vertices, indices, GL_TRIANGLES);
    if (mesh->mMaterialIndex >= 0)
        glMesh->SetMaterial(m_materials[mesh->mMaterialIndex]);
    m_meshes.push_back(std::move(glMesh));
}


  • 각 노드별로 노드에 포함되는 메시 생성 후 자식 노드로 ProcessNode 재귀 호출
  • assimp로 로드한 aiMesh 객체는 이미 vertices position/normal/texCoord 심지어 indices까지 구분해 놓음
  • 위 데이터를 바탕으로 우리 프로젝트에서 사용하는 mesh 객체 생성


Result

015152



맨 위로 이동하기

OpenGL 카테고리 내 다른 글 보러가기

댓글 남기기