OpenGL

[LearnOpenGL] 21. Model

coco_ball 2022. 8. 22. 22:50

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

private 함수들은 Assimp의 import 루틴 일부분을 처리

 

Draw function

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

 

Importing a 3D model into OpenGL

헤더 파일

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

 

Assimp namespace의 실제 Importer 객체 선언

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

 

ReadFile 함수

  • 첫 번째 파라미터 : 파일의 경로
  • 두 번째 파라미터 : post-processing(전처리)옵션
    • aiProcess-Triangulate : 모델이 삼각형으로만 이루어지지 않았다면 모델의 모든 primitive 도형들을 삼각형으로 변환
    • aiProcess-FlipUVs : 텍스처 좌표를 y축으로 뒤집음 ( OpenGL에서 대부분의 이미지들은 y축을 중심으로 거꾸로 되어있는데, 전처리 옵션으로 해결)
    • aiProcess_GenNormals : 모델이 법선 벡터를 가지고 있지 않으면 각 vertex에 대한 법선 벡터 생성
    • aiProcess_SplitLargeMeshes : 큰 mesh들을 여러개의 작은 서브 mesh로 나눔. 렌더링이 허용된 vertex 수의 최댓값을 가지고 있을 때 유용하고 오직 작은 mesh만 처리 가능
    • aiProcess_OptimizeMeshes : 여러 mesh들을 하나의 큰 mesh로 합침. 최적화를 위해 드로잉 호출을 줄일 수 있음
    • http://assimp.sourceforge.net/lib_html/postprocess_8h.html

 

loadModel function

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

AI_SCENE_FLAGS_INCOMPLETE = 데이터가 불완전하다는 플래그?

 

processNode function(scene의 모든 노드를 처리하기 위해 재귀적으로 동작 ( 종료 조건 : 모든 노드가 처리 되었을 경우))

void processNode(aiNode *node, const aiScene *scene)
{
    // 노드의 모든 mesh들을 처리(만약 있다면)
    for(unsigned int i = 0; i < node->mNumMeshes; i++)
    {
        aiMesh *mesh = scene->mMeshes[node->mMeshes[i]]; 
        meshes.push_back(processMesh(mesh, scene));			
    }
    // 그런 다음 각 자식들에게도 동일하게 적용
    for(unsigned int i = 0; i < node->mNumChildren; i++)
    {
        processNode(node->mChildren[i], scene);
    }
}

 

processMesh function(meshes vector에 저장할 수 있는 Mesh 객체를 리턴)

 

Assimp to Mesh

Mesh의 처리는 3-part process임

  1. vertex data를 얻음
  2. mesh의 indice를 얻음
  3. 연관된 material data를 얻음

처리된 데이터는 3개의 벡터 중 하나에 저장되고, Mesh가 만들어져서 리턴됨

  1. vertices
    • 각 loop를 돌 때마다 vertices 배열에 삽입할 vertex struct 정의
    • mesh→mNumVertices만큼 반복문 실행
    • Assimp는 자신만의 데이터 타입으로 데이터를 관리하기 때문에 vec3를 정의하여 변환함
    //vertex의 위치
    glm::vec3 vector; 
    vector.x = mesh->mVertices[i].x;
    vector.y = mesh->mVertices[i].y;
    vector.z = mesh->mVertices[i].z; 
    vertex.Position = vector;
    
    //normal vector
    vector.x = mesh->mNormals[i].x;
    vector.y = mesh->mNormals[i].y;
    vector.z = mesh->mNormals[i].z;
    vertex.Normal = vector;
    
    • Assimp는 각 vertex마다 최대 8개의 텍스처를 허용하는데, 우리는 하나의 텍스처만 사용하기 때문에 첫 번째 텍스처 좌표에만 집중함
    • mesh가 실제로 텍스처 좌표를 가지고 있는지도 확인해야함(항상 있는 것이 아님)
    if(mesh->mTextureCoords[0]) // mesh가 텍스처 좌표를 가지고 있는가?
    {
        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는 모두 채워졌고, vertices vector에 삽입한다. (이 처리는 mesh의 각 vertex마다 수행)
  2. Indices
    • 각각의 face는 하나의 primitive를 나타내고, 우리는 aiProcess_Triangulate 때문에 항상 삼각형임
    • face는 우리가 어떤 순서로 vertex를 그려야 하는지 정의하는 indices를 포함
    • 모든 face에 대해 반복문을 돌려 각 face의 indices를 indices vector에 저장
    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]);
    }
    
    여기까지 하면 mesh를 그리기 위한 vertex, index data가 설정됨
  3. Material
    • mesh의 material을 얻기 위해서는 scene의 mMaterial 배열을 인덱싱 해야함
    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());
    }
    
    material 객체는 텍스처 타입에 대한 텍스처 위치 배열을 저장 ( 텍스처 타입은 aiTextureType_ 접두사로 분류)
    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 : 텍스처를 불러오고 ID를 리턴
  4. loadMaterialTextures (material에서 텍스처를 얻고 texture struct의 벡터를 리턴)

 

An optimization

각각의 mesh마다 같은 텍스처가 사용됨에도 불구하고 mesh마다 새로운 텍스처가 불리고, 생성됨

→ 병목현상 발생 가능

불러온 텍스처들을 전역으로 저장하고 텍스처를 불러오고 싶을 때 이미 불러와졌는지 확인

 

Texture 수정

struct Texture {
    unsigned int id;
    string type;
    string path;  // 다른 텍스처와 비교하기 위해 텍스처의 경로를 저장
};

 

model class 맨 위에 private 벡터로 모든 텍스처 저장

vector<Texture> textures_loaded;

 

loadMaterialTextures (텍스처 경로를 textures_loaded 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)
        {   // 텍스처가 이미 불러와져있지 않다면 불러옵니다.
            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); // 불러온 텍스처를 삽입합니다.
        }
    }
    return textures;
}