본문 바로가기

OpenGL Study

OpenGL "Model"

[MODEL]

1. Model Class

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 class의 코드

- Mesh 객체들의 vector를 가지고 있고 생성자에 파일의 위치를 지정해야한다

: 생성자에서 호출되는 loadModel 함수를 통해 파일을 로드한다

- 이후 texture를 로드할 때 필요한 파일 경로의 디렉터리도 저장한다

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

- Draw 함수는 기본적으로 반복문을 사용하여 각 mesh들의 Draw 함수를 호출시킨다

2. Importing a 3D model into OpenGL

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

- mode을 불러오고 구조를 변환하기 위해서 Assimp의 적절한 헤더파일을 include 한다

- loadModel 함수는 생성자로부터 직접적으로 호출되어 사용한다

: 함수 내부에서 사용자는 scene 객체라고 불리는 Assimp의 데이터 구조에 model을 불러오기 위해 Assimp를 사용한다

: scene 객체를 가지게되면 불러온 모델로부터 사용자가 원하는 모든 데이터를 얻을 수 있다

Assimp::Importer importer;
const aiScene *scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);

- Assimp는 각자 다른 모든 파일 포멧들을 불러오는 것에 대한 기술적인 상세사항을 깔끔하게 추상화하고 있다

- 먼저 Assimp namesapce의 실제 Importer 객체를 선언한 후, ReadFile 함수를 호출한다

ReadFile

첫 번째 parameter : 파일의 경로

두 번째 parameter : 여러 전처리 옵션

- Assimp는 간단히 파일을 불러오는 것 외에도 불러온 데이터에 추가적인 계산/연산을 하는 여러 옵션들을 지정할 수 있도록 해준다

: aiProcess-Triangulate를 설정함으로써 Assimp에게 model이 삼각형으로만 이루어지지 않았다면 model의 모든 primitive 도형들을 삼각형으로 변환하라고 전달한다

: aiProcess-FlipUVs를 설정함으로써 texture coordinate를 y축으로 뒤집는다 (OpenGL에서 대부분의 이미지들은 y축을 중심으로 거꾸로 되기 때문에, 전처리 옵션을 사용해서 이 문제를 해결할 수 있다)

+) 추가적인 옵션

aiProcess_GenNormals 

: 모델이 법선 벡터들을 가지고 있지 않다면 각 vertex에 대한 법선을 실제로 생성한다

aiProcess_SplitLargeMeshes 

: 큰 mesh들을 여러개의 작은 서브 mesh들로 나눈다

: 렌더링이 허용된 vertex 수의 최댓값을 가지고 있을 때 유용하고 오직 작은 mesh들만 처리할 수 있다

aiProcess_OptimizeMeshes 

: aiProcess_SplitLargeMeshes와 반대로 여러 mesh들을 하나의 큰 mesh로 합친다

: 최적화를 위해 Drawing 호출을 줄일 수 있다

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

- 전체적인 loadModel 함수의 코드

- model을 불러온 후에 scene과 scene의 루트 노드가 null인지 확인하고 데이터가 불완전하다는 플래그가 세워져있는지 확인한다

: 에러 조건 중 하나라도 만족하면 importer의 GetErrorString 함수를 통해 에러를 출력하고 return한다

- 주어진 파일 경로의 디렉터리 경로를 얻는다

- scene의 노드들을 처리하기 위해 루트 노드를 재귀적으로 동작하는 processNode 함수로 전달한다

: 각 노드들은 자식 노드를 가질 수 있기 때문에 먼저 노드를 처리하고 이후 노드의 모든 자식 노드를 처리한다

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

- 전체적인 processNode 함수의 코드

- 해당 노드의 mesh index들을 확인하고 scene의 mMeshes 배열을 인덱싱하여 그에 따른 mesh들을 얻는다

- 반환된 mesh는 processMesh 함수로 전달되고, 이 함수는 meshes vector에 저장할 수 있는 Mesh  객체를 return한다

- 모든 mesh들이 처리되면 노드의 모든 자식들에게 반복하고, 동일한 processNode 함수를 호출한다

: 더이상의 자식이 없다면 함수의 실행을 종료한다

3. Assimp to Mesh

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

- processMesh 함수의 전체적인 구조

- Mesh를 처리하는 것은 기본적으로 세 부분으로 이루어진다

01] 모든 vertex 데이터를 얻는다

02] mesh의 indices를 얻는다

03] 연관된 material 데이터를 얻는다

: 처리된 데이터는 3개의 vector 중 하나에 저장되고 함수를 호출한 곳으로 vector가 return된다

- vertex data는 각 루프를 돌 때마다 vertices 배열에 삽입할 vertex struct를 정의하고, mesh에 존재하는 vertex의 개수(mesh->mNumVertices를 통해 얻는다)만큼 반복문을 수행하여 반복문 내부에서 모든 관련된 데이터로 이 struct를 채워넣는 형식으로 얻을 수 있다

glm::vec3 vector; 
vector.x = mesh->mVertices[i].x;
vector.y = mesh->mVertices[i].y;
vector.z = mesh->mVertices[i].z; 
vertex.Position = vector;

- vertex의 위치를 계산하는 코드

- Assimp 데이터를 변환하기 위해 vec3의 자리표시자를 지정한다

: Assimp는 vector, matrix, string 등을 자신만의 데이터 타입으로 관리하고 GLM의 데이터 타입으로 정상적으로 변환되지 않기 떄문이다

- Assimp는 직관적이지 않은 vertex 위치 배열 mVertices를 호출한다

vector.x = mesh->mNormals[i].x;
vector.y = mesh->mNormals[i].y;
vector.z = mesh->mNormals[i].z;
vertex.Normal = vector;

- 법선 벡터를 계산하는 코드

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

- texture coordinate는 거의 비슷하지만 Assimp는 각 vertex마다 최대 8개의 texture를 허용한다

: 우리는 하나의 texture만 사용하기 때문에 첫 번째 texture coordinate만 사용한다

- mesh가 실제로 texture coordinate를 가지고 있는지 확인한다

- 앞서 사용한 모든 함수와 코드는 mesh의 각 vertex마다 수행된다

4. Indices

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

- Assimp의 인터페이스는 각 mesh들이 face의 배열을 가지고 있도록 정의했고, 각 face는 하나의 primitive를 나타낸다

: 우리는 aiProcess_Triangulate 옵션 때문에 항상 삼각형이다

- face는 어떠한 순서로 vertex를 그려야하는지를 정의하는 indices를 가지고 있다

: 따라서 모든 face에 대해 반복문을 돌려 모든 face의 indices를 indices vector에 저장해야한다

- 외부 loop가 끝나면 glDrawElements 함수를 통해 mesh를 그리기 위한 vertex, index data가 완벽히 설정된 것이다

5. Material

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

- 노드와 마찬가지로 mesh는 오직 mataerial 객체의 index만 가지고 있다

: 실제 mesh의 material을 얻기 위해서는 scene의 mMaterial 배열을 인덱싱해야한다

- mesh의 material index는 mMaterialIndex 속성에 설정되어 있다

: 이 속성을 통해 mesh가 실제로 material을 가지는지에 대한 유무를 확인할 수 있다

- 먼저 scene의 mMaterials 배열로부터 aiMaterial 객체를 얻고, mesh의 diffuse, specular texture를 불러온다

- material 객체는 내부적으로 각 texture type에 대한 texture 위치의 배열을 저장힌디

: 여러 texture type들은 aiTextureType_접두사로 분류된다

- loadMaterialTextures 함수를 통해 material에서 texture를 얻을 수 있다

- texture struct의 vector를 return하고 이것을 model의 textures vector의 끝에 저장한다

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

- loadMaterialTexture 함수는 주어진 texture type의 모든 texture 위치들에 대해 반복문을 돌리고 texture file의 위치를 얻은 다음 불러오고 texture를 생성하며 이 정보를 vertex struct에 저장한다

- 먼저 GetTextureCount 함수를 통해 이 material에 저장된 texture의 갯수를 확인한다

: GetTextureCount 함수는 texture type 중 하나를 parameter로 받는다

- GetTextureCount의 결과를 aiString에 저장하는 GetTexture 함수를 통해 각 texture file의 위치를 얻는다

- 마지막으로 TextureFromFile 함수를 통해 (SOIL과 함께) texture를 불러오고 이 texture의 ID를 return한다

+) model 파일의 texture file 경로가 model file 경로와 동일하다고 가정하고 진행하므로, 간단히 loadModel 함수에서 얻은 texture 위치 문자열과 디렉터리 문자열을 결합하여 완전한 텍스처 경로를 얻을 수 있다 (GetTexture 함수도 디렉터리 문자열을 필요로 하는 이유)

 

[AN OPTIMIZATION]

- 대부분의 scene들은 여러 mesh들에 여러가지 texture를 재사용한다

struct Texture {
    unsigned int id;
    string type;
    string path;  // we store the path of the texture to compare with other textures
};

- 불러온 모든 texture를 전역으로 저장하여 model 코드를 수정한다

- texture를 로드할 곳은 어느 곳에서 먼저 로드되지는 않았는지 확인하고, 먼저 로드가 되었다면 texture를 가져오고 전체적인 texture의 로딩 과정을 건너뛴다

: processing power를 절약할 수 있다

- texture를 비교하기 위해 texture의 경로를 저장하여 사용한다

vector<Texture> textures_loaded;

- 불러온 모든 texture를 model class의 맨 위에 private으로 선언된 또다른 vector에 저장한다

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

- loadMaterialTexture 함수에서 texture 경로를 textures_loaded vector에 있는 모든 texture의 경로와 비교하여 현재 texture 경로가 다른 것들과 같은지를 확인한다

: 같다면 texture를 불러오고 생성하는 과정을 모두 생략하고 간단히 texture struct에 존재하는 것을 사용한다

 

[NO MORE CONTAINERS]

- 이제 컨테이너가 아닌 실제 3D model을 사용할 수 있다

- 코드에서 model 객체를 선언하고 model file의 위치를 전달하면 model은 자동적으로 불러와지고, game loop에서 Draw 함수를 사용하여 object가 그려진다

- 최종 출력 결과

'OpenGL Study' 카테고리의 다른 글

OpenGL "Stencil testing"  (0) 2023.02.01
OpenGL "Depth Testing"  (0) 2023.01.31
OpenGL "Mesh"  (0) 2023.01.24
OpenGL "Assimp"  (0) 2023.01.24
OpenGL "Multiple Lights"  (0) 2023.01.24