Home [learn-opengl] Model Loading: Model
Post
Cancel

[learn-opengl] Model Loading: Model

Model

  • 이제 Assimp를 사용해야한다.
    • 실제 로딩, 변환 코드를 생성할 것이다.
  • 여러 메시를 가지는 전체적인 모델을 나타내는 클래스를 만들것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Model
{
    public:
        Model(char *path)
        {
            loadModel(path);
        }
        void Draw(Shader &shader);
    private:
        // model data
        vector<Mesh> meshes;
        string directory;

        void loadModel(string path);
        void processNode(aiNode *node, const aiScene *scene);
        Mesh processMesh(aiMesh *mesh, const aiScene *scene);
        vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type,
                                             string typeName);
};
  • 이 Model 클래스는 Mesh객체들의 vector를 가지고 있고, 생성자에서 파일의 위치를 요구한다.

    • 그런 다음, loadModel함수를 생성자에서 호출하여 파일을 불러온다.
    • private함수들은 Assimp의 import루틴의 일부분을 처리한다.
    • 나중에 텍스처를 로드할 때를 위해 directory 변수가 있음을 주의해라
  • Draw함수는 특별한 것은 없고, 기본적으로 반복문을 이용하여 mesh들의 Draw함수를 호출시킨다.

1
2
3
4
5
void Draw(Shader &shader)
{
    for(unsigned int i = 0; i < meshes.size(); i++)
        meshes[i].Draw(shader);
}

Importing a 3D model into OpenGL

  • 모델을 가져와서 우리 자신의 구조로 변환하기 위해서는, 적절히 Assimp의 헤더를 포함시켜야한다.
1
2
3
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
  • 먼저 우리가 호출하는 첫 함수는 생성자에서 직접 호출되는 loadModel이다.

    • 이 함수내부에서는 scene 객체라는 Assimp의 데이터 구조에 모델을 로드해야한다.
    • 이 객체는 Assimp 데이터 인터페이스의 루트 객체이다.
    • scene 객체를 통해 원하는 모든 데이터를 얻을 수 있다.
  • Assimp는 다양한 파일 포멧들을 불러올 수 있게 추상화한 라이브러리이다.

1
2
Assimp::Importer importer;
const aiScene *scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
  • 먼저 Importer 객체를 선언해야한다.

    • 이객체의 ReadFile 함수를 호출한다.
    • 이 함수를 호출할 때 파일의 경로, 전처리 옵션들을 지정할 수 있다.
  • 전처리 옵션

    • 볼러온 데에터에 추가적인 계산/연산
    • aiProcess_Triangulate: Assimp에게 모델이 삼각형으로만 이루어지지 않았다면, 모든 primitive 도형들을 삼각형으로 변환하라고 말해준다.
    • aiProcess-FlipUVs: 텍스처 좌표를 y축으로 뒤집어준다.(OpenGL에서 대부분의 이미지들은 y축을 중심으로 거꾸로되어 있음.)
    • aiProcess_GenNormals: 모델이 법선 벡터들을 가지고 있지 않다면, 각 vertex에 대한 법선을 생성
    • aiProcess_SplitLargeMeshes: 큰 메쉬를 더 작은 하위 메시로 나눔. 렌더링에 최대 정점이 허용되고 더 작은 메쉬만 처리할 수 있는 경우에 유용.
    • aiProcess_OptimizeMeshes: 여러 메시들을 하나의 큰 메시로 합침. 최적화를 위해 드로잉 호출을 줄임.
  • Assimp는 많은 훌륭한 전처리 옵션을 제공함.

    • 어려운 작업은 반환된 scene객체를 사용하여 불러온 데이터를 Mesh객체들의 배열로 변환하는것.
    • 완성된 loadModel함수는 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void loadModel(string path)
{
    Assimp::Importer import;
    const aiScene *scene = import.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);

    if(!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode)
    {
        cout << "ERROR::ASSIMP::" << import.GetErrorString() << endl;
        return;
    }
    directory = path.substr(0, path.find_last_of('/'));

    processNode(scene->mRootNode, scene);
}
  • 위 코드는 모델을 불러온 후에 제대로 모델이 로드됬는지 확인한다.

    • 또한 주어진 파일 경로의 디렉터리 경로를 얻는다.
  • scene의 노드들을 처리하기 위해, 첫 번째 노드(루트노드)를 재귀함수인 processNode로 전달한다.

    • 각 노드는 자식들을 가지고 있기 때문에, 먼저 노드를 처리하고, 그런 다음 모든 자식들을 처리한다.
    • 이는 재귀적인 구조에 적합하므로, 재귀함수로 정의한것.
    • 종료조건은 모든 노드들을 처리되었을 때 만족한다.
  • 각 노드는 mesh index들의 모음을 가지고 있다.

    • indexscene객체 내부의 특정한 mesh를 가리킨다.
    • 이러한 인덱스들을 얻고 각 mesh들을 얻고 그 후 각 mesh들을 처리한다.
    • 그리고 나서 자식들도 이러한 작업을 반복한다.
    • processNode함수의 내용은 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void processNode(aiNode *node, const aiScene *scene)
{
    // process all the node's meshes (if any)
    for(unsigned int i = 0; i < node->mNumMeshes; i++)
    {
        aiMesh *mesh = scene->mMeshes[node->mMeshes[i]];
        meshes.push_back(processMesh(mesh, scene));
    }
    // then do the same for each of its children
    for(unsigned int i = 0; i < node->mNumChildren; i++)
    {
        processNode(node->mChildren[i], scene);
    }
}
  • mesh 인덱스를 확인하고, scene의 mMeshes 배열에 접근하여, mesh들을 얻는다.

    • 반환된 mesh는 processMesh 함수로 전달된다.
    • 이 함수는 meshes vector에 저장할 수 있는 Mesh객체를 리턴한다.
  • 모든 mesh들이 처리되면, 자식노드들을 처리한다.
  • 지금 당장은 이런 시스템을 사용하지는 않지만, 일반적으로 mesh 데이터들에 관한 추가적인 관리를 위해서 권장된다.

    • 이러한 노드 관계는 모델을 만든 아티스트들에 의해 정의된다.
  • 다음 단계는 Assimp 데이터를 처리하여, Mesh클래스 형태로 변환하는 것

Assimp to Mesh

  • mesh들의 관련된 속성들에 접근하여 자신만의 객체에 저장하는 것.
    • processMesh함수의 일반적인 구조는 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Mesh processMesh(aiMesh *mesh, const aiScene *scene)
{
    vector<Vertex> vertices;
    vector<unsigned int> indices;
    vector<Texture> textures;

    for(unsigned int i = 0; i < mesh->mNumVertices; i++)
    {
        Vertex vertex;
        // process vertex positions, normals and texture coordinates
 //       [...]
        vertices.push_back(vertex);
    }
    // process indices
   // [...]
    // process material
    if(mesh->mMaterialIndex >= 0)
    {
     //   [...]
    }

    return Mesh(vertices, indices, textures);
}
  • Mesh를 처리하는 것은 기본적으로 3부분.

    1. 모든 vertex 데이터를 얻고.
    2. mesh의 indices를 얻고
    3. 마지막으로 연관된 material 데이터를 얻는것.

Vertex 데이터 얻기

  • 각 루프를 돌때마다 vertices 배열에 삽입할 Vertex struct를 정의한다.
    • mesh에 존재하는 vertex의 갯수만큼 반복문을 실행한다.
    • 그런 다음 반복문 내부에서 모든 관련된 데이터로 이 struct를 채워넣어야한다.
1
2
3
4
5
glm::vec3 vector;
vector.x = mesh->mVertices[i].x;
vector.y = mesh->mVertices[i].y;
vector.z = mesh->mVertices[i].z;
vertex.Position = vector;
  • Assimp 데이터 변환을 위해, vec3 변수를 정의한다.(Assimp만의 데이터 타입과 호환 안될 가능성이 크므로)

  • 법선은 다음과같다.

1
2
3
4
vector.x = mesh->mNormals[i].x;
vector.y = mesh->mNormals[i].y;
vector.z = mesh->mNormals[i].z;
vertex.Normal = vector;
  • 텍스처 좌표는 다음과 같다.
    • Assimp는 모델이 꼭지점당 최대 8개의 다른 텍스처 좌표를 허용한다.
    • 지금은 하나의 텍스처만 사용하기 때문에, 첫 번째만 신경쓰면됨.
    • mesh가 실제로 텍스처 좌표를 가지고 있는지 확인해야한다.(항상 가지고 있는것이 아니므로)
1
2
3
4
5
6
7
8
9
if(mesh->mTextureCoords[0]) // does the mesh contain texture coordinates?
{
    glm::vec2 vec;
    vec.x = mesh->mTextureCoords[0][i].x;
    vec.y = mesh->mTextureCoords[0][i].y;
    vertex.TexCoords = vec;
}
else
    vertex.TexCoords = glm::vec2(0.0f, 0.0f);
  • vertex struct는 이제 필요한 vertex 속성들로 완전히 채워졌다.
    • 이 것을 vertices vector의 삽입한다.
    • 이는 mesh의 각 vertex 마다 수행된다.

Indices

  • Assimp의 인터페이스는 각 mesh들이 face의 배열을 가지고 있도록 정의한다.
    • 각 face들은 하나의 primitive를 나타낸다.
    • aiProcess_Triangulate 옵션에 의하여 항상 삼각형이 된다.
    • face는 어떠한 순서로 vertex들을 그려야하는지를 정의하는 indices를 가지고 있다.
    • 그래서 우리는 모든 face에 대해 반복문을 돌려, 모든 face의 indices를 indices vector에 저장해야한다.
1
2
3
4
5
6
for(unsigned int i = 0; i < mesh->mNumFaces; i++)
{
    aiFace face = mesh->mFaces[i];
    for(unsigned int j = 0; j < face.mNumIndices; j++)
        indices.push_back(face.mIndices[j]);
}
  • 이제, glDrawElements 함수로 그릴 수 있지만, material 또한 처리해야한다.

Material

  • 노드와 마찬가지로 mesh는 오직 material 객체의 index만 가지고 있다.
    • 앞에서처럼 mMaterial 배열을 인덱싱해야한다.
    • mesh의 material index는 mMaterialIndex 속성에 설정되어 있다.
    • 이 속성으로 mesh가 실제로 material을 가지고 있는지 아닌지 확인 가능.
1
2
3
4
5
6
7
8
if(mesh->mMaterialIndex >= 0)
{
    aiMaterial *material = scene->mMaterials[mesh->mMaterialIndex];
    vector<Texture> diffuseMaps = loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse");
    textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());
    vector<Texture> specularMaps = loadMaterialTextures(material, aiTextureType_SPECULAR, "texture_specular");
    textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());
}

궁금점: 퐁모델이 아닌경우에도 셰이더에 diffuse 와 specular?

  • scene의 mMaterials 배열로부터 aiMaterial 객체를 얻는다.

    • 그리고, meshdiffuse, specular 텍스처들을 로드한다.
    • material 객체는 내부적으로 각 텍스처 타입에 대한 texture location 배열을 저장한다.
    • 여러 텍스처 타입들은 aiTexture Type_ 접두사로 분류된다.
  • loadMaterialTextures: 이 함수는 material에서 텍스처를 얻는다.

    • texture struct의 vector를 리턴하고, 이 것을 model의 textures vector의 끝에 저장한다.
    • 주어진 텍스처 타입의 모든 texture location을 순회하며, 텍스처 파일의 위치를 얻은 다음 로드하고 텍스처를 생성하며, 이 정보를 Vertex struct에 저장한다.
    • GetTextureCount함수를 통해 이 material에 저장된 해당하는 텍스처의 타입의 수를 확인한다.
    • GetTexture 함수를 통해 각 텍스처 파일의 위치를 얻는다.
    • TextureFromFile 함수로 텍스처를 불러오고 텍스처 아이디를 얻는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unsigned int TextureFromFile(const char *path, const string &directory, bool gamma = false);

vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName)
{
    vector<Texture> textures;
    for(unsigned int i = 0; i < mat->GetTextureCount(type); i++)
    {
        aiString str;
        mat->GetTexture(type, i, &str);
        Texture texture;
        texture.id = TextureFromFile(str.C_Str(), directory);
        texture.type = typeName;
        texture.path = str;
        textures.push_back(texture);
    }
    return textures;
}
  • TextureFromFile은 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
unsigned int TextureFromFile(const char *path, const string &directory, bool gamma)
{
    string filename = string(path);
    filename = directory + '/' + filename;

    unsigned int textureID;
    glGenTextures(1, &textureID);

    int width, height, nrComponents;
    unsigned char *data = stbi_load(filename.c_str(), &width, &height, &nrComponents, 0);
    if (data)
    {
        GLenum format;
        if (nrComponents == 1)
            format = GL_RED;
        else if (nrComponents == 3)
            format = GL_RGB;
        else if (nrComponents == 4)
            format = GL_RGBA;

        glBindTexture(GL_TEXTURE_2D, textureID);
        glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

        stbi_image_free(data);
    }
    else
    {
        std::cout << "Texture failed to load at path: " << path << std::endl;
        stbi_image_free(data);
    }

    return textureID;
}

model 파일의 텍스처 파일 경로가 model 파일과 동일하다고 가정했다. 간단히 텍스처 위치 문자열과(loadModel 함수로 얻은) 디렉터리 문자열을 결합하여 완전한 텍스처 경로를 얻을 수 있다. (GetTexture함수에 디렉터리 문자열이 필요한 이유)

그러므로 만약 텍스처 위치에 대해서 절대 경로를 사용하는 모델일 경우, 위 코드는 일부 기기에서 작동하지 않을 수 있다.(로컬 경로를 사용하기 위해 파일을 수정해야함)

An optimization

  • 대부분의 scene들은 여러 mesh들에 여러가지 텍스처들을 재사용한다.
  • 집같은 경우 벽, 바닥, 천장, 계단, 테이블 등에 같은 텍스처를 사용할 수 있음.
  • 텍스처를 로드하는것은 비용이 많이 드는 연산이다.
  • 현재 구현한 상태로는 각 mesh마다 새로운 텍스처가 로드되고 생성된다.

    • 즉, 같은 텍스처가 여러번 로드 => 병목현상이 쉽게 발생할 수 있다.
  • 따라서 불러온 모든 텍스처들을 전역으로 저장하고, 텍스처를 불러오고 싶을때마다 그 텍스처가 이미 로드됬는지 확인한다.
    • 이를 위해 경로 또한 저장해야한다.
1
2
3
4
5
struct Texture {
    unsigned int id;
    string type;
    string path;  // we store the path of the texture to compare with other textures
};
  • 그다음, 모델 클래스의 맨 위에 private 변수로 텍스처 vector를 선언하자
1
vector<Texture> textures_loaded;
  • 그다음, loadMaterialTextures함수에서 텍스처 경로를 textures_loaded vector에 있는 모든 텍스처의 경로롸 비교하여 현재 텍스처 경로가 다른 것들과 같은지 확인한다.
    • 같으면 텍스처 로드 생성 생략하고 기존꺼 넣는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName)
{
    vector<Texture> textures;
    for(unsigned int i = 0; i < mat->GetTextureCount(type); i++)
    {
        aiString str;
        mat->GetTexture(type, i, &str);
        bool skip = false;
        for(unsigned int j = 0; j < textures_loaded.size(); j++)
        {
            if(std::strcmp(textures_loaded[j].path.data(), str.C_Str()) == 0)
            {
                textures.push_back(textures_loaded[j]);
                skip = true;
                break;
            }
        }
        if(!skip)
        {   // if texture hasn't been loaded already, load it
            Texture texture;
            texture.id = TextureFromFile(str.C_Str(), directory);
            texture.type = typeName;
            texture.path = str.C_Str();
            textures.push_back(texture);
            textures_loaded.push_back(texture); // add to loaded textures
        }
    }
    return textures;
}

Assimp의 일부 버전들은 IDE 디버그 버전이나 모드를 사용할 때 model을 꽤 느리게 로드하는 경향이 있다. 그러므로 로딩하는 시간이 느리다면, 릴리즈 버전으로 테스트해보자.

전체 코드

No more containers!

  • 이제 실제 모델을 사용해보자.
  • 이 모델을 사용할 것임.(다운)

    • free3d 에서 다양한 모델들을 얻을 수 있음.
    • 주의: 절대경로인 경우 상대 경로로 텍스처 파일들을 수정해야함.
    • 이때 모든 텍스처 파일들과 모델 파일들은 동일한 디렉터리에 있어야한다.
  • 이 모델은 diffuse, specular, normal maps이 연결된 .mtl과 함께 .obj 파일로 export된 것이다.

  • 이제 코드에서 Model 객체를 선언하고, 모델 파일의 경로를 전달한다.
    • 그런 다음, 이 모델은 로드되고, 게임 루프에서 Draw함수를 사용하여 오브젝트를 그려야한다.
    • 더 이상의 버퍼 할당과 속성 포인터 및 렌더링 명령이 필요하지는 않다.
    • fragment shader가 diffuse texture 컬러만 출력하면 결과는 다음과 같다.

전체 코드

  • 이제 두 개의 point light와 specular map 들을 사용하면 다음과 같은 결과를 얻을 수 있다.

  • Assimp를 사용하여 많은 모델들을 로드할 수 있다.
  • 일부 모델들은 잘 로드되지 않을 수 있는데, 이 경우 텍스처 경로가 잘못되었거나 Assimp가 불러오지 못하는 파일 포멧으로 추출된것.

출처

Model

Model

How To Texture Wavefront (.obj)Models for OpengGL

This post is licensed under CC BY 4.0 by the author.

[learn-opengl] Model Loading: Mesh

[백준][C++] 2206: 벽부수고 이동하기 (bfs)