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

[learn-opengl] Model Loading: Mesh

Mesh

  • Assimp로 불러온 모델의 데이터를 OpenGL이 이해할 수 있는 포맷으로 변환시켜 오브젝트를 렌더링할 수 있도록 해야한다.

  • mesh는 그려질 수 있는 하나의 독립체

  • Mesh가 최소한 어떠한 데이터들을 가지고 있어햐는지 정해야한다.

    • 위치벡터, 법선 벡터, 텍스처 좌표 벡터를 포함하고 있는 vertex들의 모음이 필요하다.
    • 또한 mesh는 인덱스를 사용하여 그리기 위한 index들을 포함할 수 있으며, 텍스처 형태(diffuse/specular map)의 material 데이터도 포함가능
  • 아래와 같이 OpenGL에 vertex를 정의할 수 있다.

1
2
3
4
5
struct Vertex {
    glm::vec3 Position;
    glm::vec3 Normal;
    glm::vec2 TexCoords;
};
  • vertex attribute들을 찾는데 사용할 수 있는 벡터들을 위의 구조체에 저장한다.

  • 텍스처 데이터는 아래와 같은 구조체에 저장한다.

    • 텍스처의 id와 타입을 저장
    • ex. diffuse 텍스처, specular 텍스처
1
2
3
4
struct Texture {
    unsigned int id;
    string type;
};
  • vertex와 텍스처에 대해 실제 이해했다면, 이제 mesh 클래스 의 구조를 정의할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Mesh {
    public:
        // mesh data
        vector<Vertex>       vertices;
        vector<unsigned int> indices;
        vector<Texture>      textures;

        Mesh(vector<Vertex> vertices, vector<unsigned int> indices, vector<Texture> textures);
        void Draw(Shader &shader);
    private:
        //  render data
        unsigned int VAO, VBO, EBO;

        void setupMesh();
};
  • mesh 클래스

    • 생성자에게 mesh의 필수적인 모든 데이터를 준다.
    • setupMesh함수: 버퍼들을 초기화하고, 마지막으로 Draw함수를 통해 mesh를 그린다.
    • Draw함수에 shader를 준다: shader를 전해줌으로써 그리기 전에 여러가지 uniform들을 설정할 수 있다.(sampler들을 텍스처 유닛에 연결하는 것과 같은)
  • 생성자 함수는 다음과 같다.

    • 내부에서 setupMesh함수를 호출한다.
1
2
3
4
5
6
7
8
Mesh(vector<Vertex> vertices, vector<unsigned int> indices, vector<Texture> textures)
{
    this->vertices = vertices;
    this->indices = indices;
    this->textures = textures;

    setupMesh();
}

Initialization

  • setupMesh() 덕분에 우리는 렌더링에 사용할 수 있는 mesh 데이터의 목록을 가질 수 있다.
    • 적절한 버퍼들을 설정하고,
    • vertex attribute pointer를 통해 vertex shader layout을 지정해주어야한다.
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
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 struct의 도움을 받는다는 점만 빼면.
  • C++에서의 Struct의 속성들은 메모리의 위치가 순차적으로 저장된다.
    • 즉, struct 배열을 생성하면 순차적으로 변수들이 정렬되어, array buffer에 필요한 float(실제로는 byte)배열로 변환한다.
    • Vertex struct 를 채워넣으면 이 메모리 레이아웃은 다음과 같음.
1
2
3
4
5
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 struct들을 buffer데이터로 전달할 수 있다.
    • 이것들은 glBufferData함수에 파라미터로 들어갈 값들로 완벽하게 변환될 수 있다.
1
2
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), vertices[0], GL_STATIC_DRAW);

  • sizeof Vertex: 적절한 바이트 크기 32바이트(8floats*4바이트)

  • Struct의 또다른 사용법

    • offsetof(s, m): 전처리기 지시문
      • 파라미터1: struct 타입
      • 파라미터2: 해당 struct 타입의 멤버 변수 이름
    • 이 매크로는 struct에서 입력된 변수의 시작점 바이트 offset을 리턴한다
    • glVertexAttribPointer함수의 offset 파라미터를 정의하기에 좋다.
    • 아래의 경우 법선 벡터의 바이트 offset은 12바이트(3floats * 4 바이트)로 설정된다.
      • stride 파라미터는 Vertex struct의 크기로 설정하여 해당 속성만 알려줌
1
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));

Rendering

  • Mesh 클래스를 완성하기 위해 Draw함수를 정의해야한다.
  • mesh를 렌더링하기전, glDrawElements 함수를 호출하기 전에 적절한 텍스처를 바인딩해야한다.

    • 하지만, 텍스처이 개수를 모르며, 어떤 타입인지 모르기 때문에 어려움
  • 이 문제를 해결하기 위해 특별한 네이밍 관습을 적용할 것이다.
    • 각, diffuse 텍스처는 texture_diffuseN 이라 이름을 붙이고
    • 각, specular 텍스처는 texture_specularN 이라 이름을 붙인다.
    • N1부터 텍스처 sampler에 허용되는 최댓값 사이의 어떠한 숫자이다.
    • 3개의 diffuse와 2개의 specular 텍스처를 가지고 있다고 해보자
    • 이들 sampler는 다음과 같다.
1
2
3
4
5
uniform sampler2D texture_diffuse1;
uniform sampler2D texture_diffuse2;
uniform sampler2D texture_diffuse3;
uniform sampler2D texture_specular1;
uniform sampler2D texture_specular2;
  • 이 네이밍 관습으로 인해 shader에서 텍스처 sampler를 있는만큼 모두 정의할 수 있다.
  • 최종 드로잉 코드는 다음과 같다.
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
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 값을 계산, 적절한 uniform 이름을 얻기위해 텍스처 타입 문자열에 결합
    • 적절한 sampler를 위치 시킴,
    • 현재 활성화된 텍스처 유닛에 부합되는 위치값을 주고, 텍스처를 바인딩(Shader 를 파라미터로 받는 이유)

전체 코드

출처

Mesh

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

[learn-opengl] Model Loading: Assimp

[learn-opengl] Model Loading: Model