본문 바로가기

Game/Graphics

Learn OpenGL - Model Loading : Mesh

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


Mesh


 Assimp를 사용하면 많은 다른 모델을 응용 프로그램에 로드할 수 있지만,


일단 로드되면 모두 Assimp의 데이터 구조에 저장된다. 우리가 결국 원하는 것은


OpenGL이 이해할 수 있는 형식으로 데이터를 변환해 객체를 렌더링 할 수 있게 하는 것이다.


이전 튜토리얼에서 메쉬는 하나의 drawable entity를 나타내므로 우리 자신의 메쉬 클래스를 정의해보겠다.



 지금까지 배운 것들을 검토해 메쉬가 데이터로서 갖는 최소한의 역할에 대해 생각해보자.


메쉬에는 적어도 각 꼭지점에 위치 벡터, 법선 벡터, 텍스처 좌표 벡터가 포함된 일련의 꼭지점이 있어야한다.


메쉬에는 인덱스된 드로잉에 대한 인덱스와 텍스처 형식의 소재 데이터 (diffuse / specular map)가 포함되어야한다.



 이제 메쉬 클래스에 대한 최소 요구 사항을 설정했으므로 OpenGL에서 정점을 정의할 수 있다:

struct Vertex {
    glm::vec3 Position;
    glm::vec3 Normal;
    glm::vec2 TexCoords;
};

 각 꼭지점 속성을 인덱스하는 데 사용할 수 있는 Vertex라는 구조체에 필요한 벡터를 저장한다.


꼭지점 구조체를 제외하고 텍스처 구조체에 텍스처 데이터를 구성하려고 한다.

struct Texture {
    unsigned int id;
    string type;
};  

 텍스처의 ID와 유형을 저장한다. (diffuse or specular)



 정점과 텍스처의 실제 표현을 알면 메쉬 클래스의 구조를 정의할 수 있다:

class Mesh {
    public:
        /*  Mesh Data  */
        vector<Vertex> vertices;
        vector<unsigned int> indices;
        vector<Texture> textures;
        /*  Functions  */
        Mesh(vector<Vertex> vertices, vector<unsigned int> indices, vector<Texture> textures);
        void Draw(Shader shader);
    private:
        /*  Render data  */
        unsigned int VAO, VBO, EBO;
        /*  Functions    */
        void setupMesh();
};  

 보시다시피 클래스는 복잡하지 않다. 생성자에서 메쉬에 필요한 모든 데이터를 제공하고,


setupMesh 함수에서 버퍼를 초기화한 다음 마지막으로 Draw함수를 통해 메쉬를 그린다.


우리는 Draw 함수에 쉐이더를 제공한다. 쉐이더를 메쉬로 전달함으로써 드로잉 전에 여러


유니폼을 설정할 수 있다. (샘플러를 텍스처 유닛에 연결하는 것과 같다)



 생성자의 함수 내용은 매우 간단하다. 우리는 단순히 클래스의 public 변수를 생성자의 해당


인수 변수로 설정한다. 또한, 생성자에서 setupMesh 함수를 호출한다:

Mesh(vector<Vertex> vertices, vector<unsigned int> indices, vector<Texture> textures)
{
    this->vertices = vertices;
    this->indices = indices;
    this->textures = textures;

    setupMesh();
}

 여기에는 특별한게 없다. 이제 setupMesh 함수를 살펴보자.






Initialization


 생성자 덕분에 이제는 렌더링에 사용할 수 있는 메쉬 데이터가 많이 있다.


우리는 적절한 버퍼를 설정하고 꼭지점 속성 포인터를 통해 버텍스 쉐이더 레이아웃을


지정해야한다. 지금까지는 이러한 개념에 문제가 없었지만 이번에는 구조체에 정점 데이터를


도입해 약간 익숙해졌다:

void setupMesh()
{
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &EBO);
  
    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);

    glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);  

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), 
                 &indices[0], GL_STATIC_DRAW);

    // vertex positions
    glEnableVertexAttribArray(0);	
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
    // vertex normals
    glEnableVertexAttribArray(1);	
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));
    // vertex texture coords
    glEnableVertexAttribArray(2);	
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords));

    glBindVertexArray(0);
}  

 코드는 예상했던 것보다 크게 다르지 않지만 Vertex 구조체의 도움으로 약간의 트릭이 사용됐다.


구조체는 C++에서 메모리 레이아웃이 순차적이라는 훌륭한 속성을 가지고 있다.


즉, 구조체를 데이터 배열로 나타내려면 구조체의 변수가 순차적으로 포함되어 배열 버퍼로


필요한 float 배열로 직접 변환된다. 예를 들어, 채워진 Vertex 구조체가 있으면 메모리 레이아웃은


다음과 같다:

Vertex vertex;
vertex.Position  = glm::vec3(0.2f, 0.4f, 0.6f);
vertex.Normal    = glm::vec3(0.0f, 1.0f, 0.0f);
vertex.TexCoords = glm::vec2(1.0f, 0.0f);
// = [0.2f, 0.4f, 0.6f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f];

 이 유용한 속성 덕분에 버퍼의 데이터로서 Vertex 구조체의 큰 목록에 대한 포인터를 직접


전달할 수 있으며 glBufferData가 인수로 기대하는 바를 완벽하게 변환할 수 있다:

glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), vertices[0], GL_STATIC_DRAW);   

 당연히 sizeof 연산자는 구조체에서 적절한 크기로 사용될 수 있다.


32바이트여야한다.



 구조체의 또 다른 장점은 구조체의 첫 번째, 두 번째 인수로 struct의 변수 이름을 취하는


offsetof(s,m)라는 전처리기 지시문이다. 매크로는 해당 변수의 바이트 오프셋을 구조체의


시작 부분에서 반환한다. 이것은 glVertexAttribPointer 함수의 오프셋 매개 변수를


정의하는데 적합하다:

glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));  

  오프셋은 이제 offsetof 매크로를 사용해 정의된다. 이 경우 법선 벡터의 바이트 오프셋을


구조체의 법선 벡터의 바이트 오프셋과 같게 설정한다. 이 오프셋은 3개 부동 소수점


12바이트이다. 또한, Stride 매개 변수를 Vertex 구조체의 크기와 같게 설정한다.



 이와 같은 구조체를 사용하면 더 읽기 쉬운 코드를 제공할뿐만 아니라 구조를 쉽게 확장할 수 있다.


또 다른 정점 속성을 원한다면 단순히 구조체에 추가할 수 있다. 유연한 특성으로 인해 렌더링 코드가


손상되지 않는다.







Rendering


 Mesh 클래스를 완성하기 위해 정의해야 할 마지막 함수는 Draw 함수이다.


실제로 메쉬를 렌더링하기 전에 glDrawElements를 호출하기 전에 먼저 적절한 텍스처를 바인딩하고 싶다.


그러나 처음부터 메쉬의 텍스처가 얼마나 많은지와 유형이 무엇인지 알지 못하기 때문에


실제로는 약간 어렵다. 그렇다면 쉐이더에서 텍스처 단위와 샘플러를 어떻게 설정할까?



 이 문제를 해결하기 위해 특정 명명 규칙을 가정한다. 각 diffuse 텍스처의 이름은


texture_diffuseN이고, 각 반사 텍스처의 이름은 texture_specularN으로 지정해야한다.


여기서 N은 1에서 허용되는 최대 텍스처 샘플러 수까지의 숫자이다.


특정 메쉬에 대해 3개의 diffuse 텍스처와 2개의 specular 텍스처가 있다고 가정하면


텍스처 샘플러를 호출해야한다:

uniform sampler2D texture_diffuse1;
uniform sampler2D texture_diffuse2;
uniform sampler2D texture_diffuse3;
uniform sampler2D texture_specular1;
uniform sampler2D texture_specular2;

 이 규칙에 따라 우리는 쉐이더에서 원하는만큼의 텍스처 샘프러를 정의할 수 있다.


그리고 메쉬에 실제로 많은 텍스처가 포함되어 있으면 이름이 무엇인지 알 수 있다.


이 규칙에 따라 우리는 하나의 메쉬에서 임의의 텍스처를 처리 할 수 있으며,


개발자는 적절한 샘플러를 정의해 원하는만큼 많은 텍스처를 자유롭게 사용할 수 있다.

이와 같은 문제에 대한 많은 해결책이 있다. 이 특정 솔루션이 마음에 들지 않으면 창의력을 발휘하고 나만의 솔루션을 제시하는 것은 당신에게 달려있다.

 결과 drawing 코드는 다음과 같다:

void Draw(Shader shader) 
{
    unsigned int diffuseNr = 1;
    unsigned int specularNr = 1;
    for(unsigned int i = 0; i < textures.size(); i++)
    {
        glActiveTexture(GL_TEXTURE0 + i); // activate proper texture unit before binding
        // retrieve texture number (the N in diffuse_textureN)
        string number;
        string name = textures[i].type;
        if(name == "texture_diffuse")
            number = std::to_string(diffuseNr++);
        else if(name == "texture_specular")
            number = std::to_string(specularNr++);

        shader.setFloat(("material." + name + number).c_str(), i);
        glBindTexture(GL_TEXTURE_2D, textures[i].id);
    }
    glActiveTexture(GL_TEXTURE0);

    // draw mesh
    glBindVertexArray(VAO);
    glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
    glBindVertexArray(0);
}  

 우리는 먼저 텍스처 타입당 N-컴포넌트를 계산하고, 그것을 텍스처의 타입 스트링에


연결해 적절한 유니폼 이름을 얻는다. 그런 다음 적절한 샘플러를 찾고, 현재 활성 텍스처


유닛에 해당하는 위치 값을 지정하고 텍스처를 바인딩한다. Draw 기능에서 쉐이더가 필요한


이유이기도하다. 우리는 또한 "material"이라고 덧붙였다. 우리가 대개 재료 구조체에 텍스처를


저장하기 때문에 결과적으로 통일된 이름으로 바꾼다.

diffuse 및 specular 카운터를 문자열로 변환하는 순간 카운터를 증가시킨다. 여기서는 std::string에 전달된 값이 원래 카운터 값이다. 그 다음 값은 다음 라운드를 위해 증가한다.

 방금 정의한 Mesh 클래스는 초기 튜토리얼에서 설명한 많은 주제에 대한 깔끔한 추상화이다.


다음 튜토리얼에서는 여러 메쉬 객체의 컨테이너 역할을 하는 모델을 만들고,


실제로 Assimp의 로딩 인터페이스를 구현한다.





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


공모전 준비하느라 바빴는데 이번 튜토리얼이 짧아서 다행이다.


축구도 기가 막히게 역전에 역전승으로 이기고 좋은 마무리!