본문 바로가기

Game/Graphics

Learn OpenGL - Model Loading : Model

link : https://learnopengl.com/Model-Loading/Model


Model


 이제 Assimp로 손을 더럽히고 실제 로딩 및 변환 코드를 작성해라. 이 튜토리얼의 목표는


모델 전체를 나타내는 또 다른 클래스, 즉 다중 객체가 포함된 다중 메쉬를 포함하는 모델을 만드는 것이다.


목조 발코니, 타워 및 수영장이 포함된 집은 여전히 단일 모델로 로드될 수 있다.


Assimp를 통해 모델을 로드하고, 마지막 튜토리얼에서 마든 여러 메쉬 객체로 변환한다.



 더 이상 고민하지 않고 Model 클래스의 클래스 구조를 설명하겠다:

class Model 
{
    public:
        /*  Functions   */
        Model(char *path)
        {
            loadModel(path);
        }
        void Draw(Shader shader);	
    private:
        /*  Model Data  */
        vector<Mesh> meshes;
        string directory;
        /*  Functions   */
        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 객체의 벡터를 포함하고 있으며 생성자에 파일 위치를 지정해야한다.


그런 다음 생성자에서 호출된 loadModel 함수를 통해 즉시 파일을 로드한다.


private 함수는 모두 Assimp의 임포트 루틴의 일부를 처리하도록 설계되었으며 곧 우리는 그들을 다룰 것이다.


또한, 텍스처를 로드할 때 나중에 필요할 파일 경로의 디렉토리를 저장한다.



 Draw 함수는 특별한 것이 없으며 기본적으로 각각의 Draw 함수를 호출하기 위해 각 메쉬 위로 반복된다:

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






Importing a 3D model into OpenGL


 모델을 가져와서 우리 자신의 구조로 변환하기 위해서는 컴파일러가 우리에게 불평하지 않도록


Assimp의 적절한 헤더를 포함시켜야한다:

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

  우리가 호출하는 첫 번째 함수는 생성자에서 직접 호출되는 loadModel입니다.


loadModel 내에서 Assimp를 사용해 장면 객체라고 하는 Assimp의 데이터 구조에


모델을 로드합니다. 당신은 모델 로딩 시리즈의 첫 튜토리얼에서 이것이 Assimp의


데이터 인터페이스의 근원이라는 것을 기억할 것이다. 장면 객체가 생기면 로드된 모델에서


필요한 모든 데이터에 액세스 할 수 있다.



 Assimp의 가장 큰 장점은 모든 다른 파일 형식을 로드하는 모든 기술적 세부 사항을


깔끔하게 추상화하고 모든 것을 단일한 줄로 처리한다는 것이다:

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

 먼저 Assimp의 네임 스페이스에서 실제 Importer 객체를 선언한 다음 ReadFile 함수를 호출한다.


함수는 파일 경로와 두 번째 인수로 여러 후처리 옵션을 필요로 한다.


파일을 단순히 로딩하는 것 외에도 Assimp는 Assimp가 가져온 데이터에 대해 추가 계산/연산을


수행하도록하는 몇 가지 옵션을 지정할 수 있다. airProcess_Triangulate를 설정함으로써


모델이 (전체적으로) 삼각형으로 구성되지 않으면 모든 모델의 기본 모양을 삼각형으로 변환해야한다과


Assimp에 설정한다. airProcess_FlipUVs는 처리 중에 필요한 경우 y축의 텍스처 좌표를 뒤집는다.


(텍스처 튜토리얼에서 OpenGL의 대부분 이미지가 y축을 중심으로 반전되어 이 작은 후처리 옵션이

 우리를 위해 수정되었음을 기억할 것이다)


몇 가지 다른 유용한 옵션은 다음과 같다:


    - airProcess_GenNormals : 모델에 법선 벡터가 없는 경우 실제로 각 꼭지점의 법선을 만든다.


    - airProcess_SplitLargeMeshes : 큰 메쉬를 작은 하위 메쉬로 분할한다. 렌더링에 허용되는 최대 개수의


  꼭지점이 있고, 작은 메쉬만 처리할 수 있는 경우 유용하다.


    - airProcess_OptimizeMeshes : 실제로 여러 메쉬를 하나의 큰 메쉬에 조인해 역순으로 수행하여 최적화를


위해 드로잉 호출을 줄인다.


 Assimp는 많은 후처리 지침을 제공하며 여기에서 모두 찾을 수 있다.


실제로 Assimp를 통해 모델을 로드하는 것은 (알다시피) 놀랍도록 쉽게 수행할 수 있다.


어려운 작업은 반환된 장면 객체를 사용해 로드된 데이터를 Mesh 객체 배열로 변환하는 것이다.


완전한 loadModel 함수는 다음과 같다:

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

 모델을 로드한 후 장면의 씬 노드와 루트 노드가 null이 아니었는지 확인하고, 플래그 중 하나를 확인해


반환된 데이터가 불완전한지 확인한다. 이러한 오류 조건 중 하나라도 충족되면 가져오기 프로그램의


GetErrorString 함수를 통해 오류를 보고 반환한다. 또한, 주어진 파일 경로의 디렉토리 경로를 검색한다.



 아무것도 잘못되지 않는다면 장면의 노드 중 하나를 처리해 첫 번째 노드 (루트 노드)를 재귀적


processNode 함수로 전달한다. 각 노드는 일련의 자식을 포함하기 때문에 문제의 노드를 먼저 처리하고


모든 노드의 자식을 계속 처리하는 등의 작업을 수행한다. 이것은 재귀적 구조를 만족하므로 재귀 함수를


정의할 것이다. 재귀 함수는 일부 처리를 수행하고 특정 조건이 충족 될 때까지 다른 매개 변수로


동일한 함수를 재귀적으로 호출하는 함수입니다. 우리의 경우 모든 노드가 처리되면 종료 조건이 충족된다.



 Assimp의 구조에서 기억할 수 있듯이 각 노드는 각 인덱스가 장면 객체에 있는 특정 메쉬를 가리키는


일련의 메쉬 인덱스를 포함한다. 따라서 우리는 이러한 메쉬 인덱스를 검색하고 각 메쉬를 처리한 다음


노드의 각 자식 노드에 대해 이 작업을 다시 수행하려고 한다. 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);
    }
}  

 먼저 각 노드의 메쉬 인덱스를 확인하고 해당 메쉬의 인덱스를 장면의 mMeshes 배열로 가져와서 해당 메쉬를 검색한다.


변환된 메쉬는 mesh/list에 저장할 수 있는 mesh 객체를 반환하는 processMesh함수에 전달된다.



 모든 메쉬가 처리되면 노드의 모든 자식을 반복하고, 각 노드의 자식에 대해 동일한 processNode 함수를 호출한다.


노드에 더 이상 자식이 없으면 이 함수는 실행을 중지한다.

 신중한 독자는 우리가 기본적으로 모든 노드를 처리하는 것을 잊어버릴 수 있고, 모든 복잡한 요소를 인덱스없이 직접 수행해 모든 장면의 메쉬를 직접 루프 할 수 있음을 알았을 것이다. 이것이 우리가 하고 있는 이유는 이 같은 노드를 사용하는 초기 아이디어가 메쉬 사이에 부모-자식 관계를 정의하는 것이기 때문이다. 이러한 관계를 반복적으로 반복함으로써 실제로 특정 메쉬를 다른 메쉬의 부모로 정의할 수 있다.
 이러한 시스템의 유스 케이스는 자동차 메쉬를 translate하고 모든 자식이 변환되도록하려는 경우이다. 이러한 시스템은 부모-자식 관계를 사용해 쉽게 생성된다.

 그러나 지금은 그런 시스템을 사용하지 않고 있지만 일반적으로 메쉬 데이터를 추가로 제어하고자 할 때마다 이 방법을 사용하는 것이 좋다. 이 노드와 같은 관계는 결국 모델을 만든 아티스트에 의해 정의된다.

 다음 단계는 실제로 Assimp의 데이터를 마지막 튜토리얼에서 생성한 Mesh 클래스로 처리하는 것이다.






Assimp to Mesh


 aiMesh 객체를 우리 자신의 메쉬 객체로 변환하는 것은 그렇게 어렵지 않다. 우리가 해야할 것은 메쉬의 각 관련 속성에


액세스하여 우리 자신의 객체에 저장하는 것뿐이다. processMesh 함수의 일반적인 구조는 다음과 같다.

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

 메쉬 처리는 기본적으로 모든 정점 데이터를 검색하고, 메쉬의 인덱스를 검색하고, 마지막으로 관련 재질 데이터를


검색하는 3개의 섹션으로 구성된다. 처리된 데이터는 3개의 벡터 중 하나에 저장되고 메쉬가 생성되어


함수의 호출자에게 반환된다.



 정점 데이터를 검색하는 것은 매우 간단하다. 각 반복 후에 정점 배열에 추가하는 정점 구조체를 정의한다.


메쉬 내에 많은 정점들이 존재할 때 반복한다. 반복 내에서 이 구조체를 모든 관련 데이터로 채운다.


정점 위치의 경우 다음과 같이 수행된다:

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는 벡터, 행렬, 문자열 등을 위한


자체 데이터 유형을 유지하고 glm의 데이터 유형으로 변환하지 않기 때문에 자리 표시자가 필요하다.

Assimp는 너무 직관적이지 않은 꼭지점 위치 배열 mVertices를 호출한다.

 법선에 대한 절차는 현재 놀랄 일이 아니다:

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

 텍스쳐 좌표는 거의 동일하지만 Assimp는 모델이 우리가 사용하지 않을 정점당 최대 8개의 다른 텍스처


좌표를 가질 수 있도록하기 때문에 텍스처 좌표의 첫 번째 세트만 신경 써야한다. 또한, 메쉬에 실제로


텍스처 좌표가 포함되어 있는지 확인하려고한다:

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

  이제 정점 구조체는 필요한 정점 속성으로 완전히 채워지고 반복의 끝에 정점 벡터의 뒤쪽으로 밀어


넣을 수 있다. 이 과정은 각 메쉬의 정점에 대해 반복된다.






Indices


 Assimp의 인터페이스는 각면이 하나의 프리미티브를 나타내는 면의 배열을 갖는 각 메쉬를 정의했다.


우리의 경우 (aiProcess_Triangulate 옵션으로 인해) 항상 삼각형이다. 얼굴은 각 프리미티브에 대해


어떤 순서로 드로잉해야 하는지를 정의하는 인덱스를 포함하므로 모든 얼굴을 반복하고,


모든 얼굴의 인덱스를 인덱스 벡터에 저장하면 우리는 모두 설정된다:

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 객체에 대한 인덱스만 포함하고 있으며, 씬의 mMaterials 배열을


인덱싱하는 데 필요한 메쉬의 실제 material을 검색한다. 메쉬의 material 인덱스는 mMaterialIndex 속성에


설정되어 있다. 이 속성은 메쉬에 실제로 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());
}  

  먼저 장면의 mMaterials 배열에서 aiMaterial 객체를 가져온다. 그런 다음 메쉬의 diffuse 또는 specular 텍스처를


로드하려고 한다. material 객체는 내부적으로 각 텍스처 유형에 대한 텍스처 위치 배열을 저장한다.


다른 텍스처 유형에는 모두 aiTextureType_ 접두어가 붙는다. loadMaterialTextures라는 도우미 함수를


사용해 material에서 텍스처를 가져온다. 이 함수는 모델 텍스처 벡터의 끝 부분에 저장하는 텍스처 구조체의


벡터를 반환한다.



 loadMaterialTextures 함수는 주어진 텍스처 유형의 모든 텍스처 위치를 반복하고, 텍스처의 파일 위치를


검색한 다음 텍스처를 로드해 생성하고 정보를 Vertex 구조체에 저장한다. 다음과 같이 보인다:

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

 우리는 먼저 주어진 텍스처 유형 중 하나를 기대하는 GetTextureCount 함수를 통해 material에


저장된 텍스처 양을 확인한다. 그런 다음 결과를 aiString에 저장하는 GetTexture 함수를 통해


텍스처의 각 파일 위치를 검색한다. 그런 다음 TextureFromFile이라는 다른 도우미 함수를


사용해 우리에게 텍스처를 로드하고 텍스처 ID를 반환한다. 그러한 함수가 어떻게 스여졌는지


확실하지 않다면 그 코드의 마지막 부분에 있는 코드 목록을 검사할 수 있다.

 모델 파일 내의 텍스처 파일 경로가 실제 모델 객체에 국부적이라는 가정을 한다. 모델 자체의 위치와 동일한 디렉토리가 있다. 그런 다음 이전에 검색한 텍스처 위치 문자열과 디렉토리 문자열을 연결해 전체 텍스처 경로를 얻을 수 있다.

 인터넷을 통해 발견된 일부 모델은 각 컴퓨터에서 작동하지 않는 텍스처 위치에 대한 절대 경로를 사용한다. 이 경우 텍스처에 대한 로컬 경로를 사용하도록 파일을 수동으로 편집하려고 할 수 있다.

 그리고 그것이 Assimp를 사용해 모델을 가져오는 것이다.







A large optimization


 우리는 아직 완전히 완료되지 않았다. 우리가 만들고자하는 커다란 최적화가 여전히


존재하기 때문이다. 대부분의 장면은 몇 가지 메쉬에 텍스처 중 몇 개를 다시 사용한다.


벽에 화강암 텍스처가 있는 집을 생각해보자. 이 텍스처는 바닥, 천장, 계단, 아마도


테이블과 어쩌면 작은 우물에도 적용될 수 있다. 텍스처를 로드하는 것은 저렴한 연산이


아니며 현재 구현에서는 이전에 똑같은 텍스처가 여러 번 로드 되었더라도


각 텍스처에 대해 새 텍스처가 로드되고 생성된다. 이는 곧 모델 로딩 구현의 병목 현상이 된다.



 따라서 우리는 로드된 텍스처를 전체적으로 저장함으로써 모델 코드에 하나의 작은


조정을 추가하고, 아직 로드되지 않은 경우 처음 검사하는 텍스처를 로드하려는 위치에 추가한다.


그렇다면 우리는 그 텍스처를 취하고 전체 로딩 루틴을 건너 뛰어 많은 처리 능력을 절약할


수 있다. 실제로 텍스처를 비교하려면 경로도 함께 저장해야한다:

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

 그런 다음 로드된 모든 텍스처를 모델의 클래스 파일 맨 위에 선언된 다른 벡터에 개인 변수로


저장한다:

vector<Texture> textures_loaded; 

 그런 다음 loadMaterialTextures 함수에서 텍스처 경로를 textures_loaded 벡터의 모든 텍스처와


비교해 현재 텍스처 경로가 그 중 어떤 텍스처 경로와 유사한지 확인한다. 그렇다면 텍스처


로딩/생성 부분을 건너뛰고 위치된 텍스처 구조체를 메쉬의 텍스처로 사용하기만 하면 된다.


기능은 다음과 같다:

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의 디버그 버전 및 디버그 모드를 사용할 때 모델을 매우 느리게 로드하는 경향이 있으므로 느린 로딩 시간에 실행하는 경우 릴리스 버전에서도 테스트해야한다.






No more containers!


 따라서 실제 예술가가 만든 모델을 실제로 가져와서 창의적인 천재에 의해 만들어진 모델이 아닌


실제 모델을 가져와서 구현을 해보겠다. (당신이 보았을때, 그 컨테이너는 아마도 가장 아름다운


입방체 중 하나였을 것이다) 왜냐하면 나는 너무 많은 크레딧을 주고 싶지 않기 때문에 때때로


일부 다른 아티스트들이 계급에 참여할 수 있게 하고, 이번에는 Crysis의 게임 Crysis에서


사용한 원래의 나노 두더지를 로드할 것이다. (tf3dm.com에서 다운로드 한 예를 들어,


다른 어떤 모델도 사용될 수 있다) 모델은 모델의 확산, 반사 및 노멀 맵을 포함하는 .mtl 파일과


함께 .obj 파일로 내보내진다. 가져오기가 쉬운 모델을 다운로드 할 수 있다. 모든 텍스처와


모델 파일은 텍스처가 로드될 때 같은 디렉토리에 있어야한다.

이 웹사이트에서 다운로드할 수 있는 버전은 원본 소스에서 다운로드한 경우 각 텍스처 파일 경로가 절대 경로 대신 로컬 상대 경로로 수정된 수정 버전이다.

 이제 코드에서 Model 객체를 선언하고 모델의 파일 위치를 전달한다. 그러면 모델이 자동으로


로드되고, 그리기 기능을 사용해 게임 루프에 개체를 그린다. 더 단순한 한 줄짜리 버퍼 할당


속성 포인터 및 렌더 명령이 필요없다. 그런 다음 조각 쉐이더가 객체의 확산 텍스처 색상만


출력하는 간단한 쉐이더 세트를 만들면 결과는 다음과 같이 보인다:



 또한, 조명 튜토리얼에서 배운 바와 같이 반사식에 두 가지 점 광원을 도입할 수 있고,


반사 맵과 함께 놀라운 결과를 얻을 수 있다:



 심지어 이것은 아마도 지금까지 사용했던 컨테이너보다 약간 더 화려하다는 것을 인정해야한다.


Assimp를 사용하면 인터넷에서 발견되는 수많은 모델을 로드 할 수 있다. 몇 가지 파일 형식으로


다운로드 할 수 있는 무료 3D 모델을 제공하는 리소스 웹 사이트가 꽤 많이 있다.


일부 모델은 여전히 제대로 로드되지 않으며, 작동하지 않는 텍스처 경로가 있거나 Assimp가


읽을 수 없는 형식으로 단순히 내보낼 수 있다.




** ** ** ** ** ** ** ** ** **


모델을 띄웠다. 라이팅 넣으려고 했는데 다른 일이 생겨서 우선 천천히 넣어봐야겠다.


일 마치고 물리 공부를 시작한다!