눈팅하는 게임개발자 블로그

OpenGL Model Loading 4-2 Mesh 본문

공부한거/OpenGL

OpenGL Model Loading 4-2 Mesh

Palamore 2020. 12. 7. 19:24

원문 사이트

learnopengl.com/Model-Loading/Mesh

 

LearnOpenGL - Mesh

Mesh Model-Loading/Mesh With Assimp we can load many different models into the application, but once loaded they're all stored in Assimp's data structures. What we eventually want is to transform that data to a format that OpenGL understands so that we can

learnopengl.com

번역 사이트

heinleinsgame.tistory.com/22?category=757483

 

[Learn OpenGL 번역] 4-2. 모델 불러오기 - Mesh

Mesh 모델 불러오기/Mesh Assimp를 사용하여 응용 프로그램에 많은 여러가지 모델들을 불러올 수 있습니다. 하지만 불러온 모델은 Assimp 데이터 구조의 형식으로 저장됩니다. 우리가 최종적으로 원하

heinleinsgame.tistory.com

 

Mesh

Assimp를 사용하여 응용 프로그램에 여러가지 모델들을 불러올 수 있다.

하지만 불러온 모델은 Assimp 데이터 구조의 형식으로 저장된다.

우리가 최종적으로 원하는 것은 이 데이터를 OpenGL이 이해할 수 있는 포맷으로 변환시켜 오브젝트를

렌더링할 수 있도록 해주는 것이다.

이 전의 강좌에서 mesh는 그려질 수 있는 하나의 독립체라는 것을 배웠다.

이제 우리만의 Mesh 클래스를 직접 만들어보자.

 

Mesh가 최소한 어떠한 데이터들을 가지고 있어야 하는지 정하기 위해 지금까지 배웠던 것들을 생각해보면

mesh는 최소 위치 벡터, 법선 벡터, 텍스처 좌표 벡터를 포함하고 있는 vertex들의 모음을 가지고 있어야 한다.

또한 mesh는 인덱스를 사용하여 그리기 위한 index들을 포함할 수 있으며 텍스처 형태의 material 데이터도

포함할 수 있다.

 

이제 mesh 클래스에 대한 최소한의 요구사항을 설정하였으니 OpenGL에 Vertex를 정의할 수 있다.

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

이제 각각의 vertex attribute들을 찾는 데 사용할 수 있는 필요한 벡터들을 Vertex Struct에 저장할 수 있다.

Vertex struct와는 별도로 Texture struct에 텍스터 데이터를 저장할 수도 있다.

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

텍스처의 id와 타입(예를 들어 diffuse 또는 specular 텍스처)를 저장한다.

vertex와 텍스처에 대해 실제로 이해했다면 이제 mesh 클래스의 구조를 정의할 수 있다.

 

class Mesh {
    public:
        /*  Mesh 데이터  */
        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:
        /*  렌더 데이터  */
        unsigned int VAO, VBO, EBO;
        /*  함수         */
        void setupMesh();
};  

클래스가 그렇게 복잡하지 않다는 것을 확인할 수 있는데,

생성자에서 mesh의 필수적인 모든 데이터를 매개변수로 넘겨준다.

setupMesh 함수에서 버퍼들을 초기화하고 마지막으로 Draw 함수를 통해 mesh를 그린다.

Draw 함수에 shader를 매개변수로 전달해야함을 유념해야한다.

shader를 전달함으로써 해당 메쉬를 그리기 전에 여러가지 uniform들을 설정할 수 있다.

(sampler들을 텍스처 유닛에 연결하거나 uniform 변수들을 전달하는 등의)

 

생성자의 구현은 간단하다.

간단히 클래스의 public 변수들을 해당 파라미터 변수로 설정해준다.

또한 생성자 내부에서 setupMesh 함수를 호출한다.

Mesh(vector<Vertex> vertices, vector<unsigned int> indices, vector<Texture> textures)
: vertices(vertices)
, indices(indices)
, textures(textures)
{
	// 초기화 리스트를 사용하는 쪽으로 코드를 변경했다.

    setupMesh();
}

여기에 특별한 것은 없다. 이제 setupMesh 함수를 알아보자.

 

Initialization

이 생성자 덕분에 렌더링에 사용할 수 있는 mesh 데이터의 목록을 가질 수 있다.

적절한 버퍼들을 설정하고 vertex attribute pointer를 통해 vertex shader layout을 지정해줘야 한다.

기존에도 계속 해왔던 것이므로 이제는 익숙할 것이다.

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 배열을 생성한다면 struct의 변수들이 순차적으로 정렬되어 array buffer에 필요한 float(실제로는 byte)

배열로 변환한다.

예를 들어, Vertex struct를 채워 넣으면 해당 메모리의 레이아웃은 다음과 같다.

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 함수에 파라미터로 들어갈 값들로 완벽하게 변환될 수 있다.

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

물론 sizeof 연산자는 적절한 바이트 크기를 위해 struct에 사용할 수 있다.

이는 32바이트(8 * 4바이트)이다.

Struct의 또다른 유용한 사용법은 offsetof(s, m)이라고 불리는 전처리기 지시문이다.

이 것의 첫 번째 파라미터는 struct이고 두 번째 파라미터는 이 struct의 변수 이름이다.

이 매크로는 struct의 시작지점으로부터 입력된 변수까지의 바이트 offset을 리턴한다.

이는 glVertexAttribPointer 함수의 offset 파라미터를 정의하기에 완벽하다.

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

이 offset은 이제 offsetof 매크로를 사용하여 정의되었습니다.

이 경우에서는 법선 벡터의 바이트 offset을 12바이트(3 * 4바이트)로 설정한다.

stride 파라미터는 Vertex struct의 크기로 설정해야 함을 유념해야 한다.

 

이런식으로 struct를 사용하는 것은 읽기 좋은 코드를 만들 뿐만 아니라 구조를 쉽게 확장할 수 있도록 해준다.

또 다른 Vertex attribute를 원한다면 간단히 struct에 추가하기만 하면 더이상 수정할 부분이 없어 간편하다.

 

Rendering

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

mesh를 실제로 렌더링하기 전에 먼저 glDrawElements 함수를 호출하기 전에 적절한 텍스처를 바인딩해야 한다.

하지만 이는 마냥 쉽지만은 않다.

이 mesh가 몇 개의 텍스처를 가지고 있는지 어떠한 타입의 텍스처를 가지고 있는지 모르기 때문이다.

이런 상황에서는 shader에 텍스처 유닛과 sampler를 설정하기 위해 네이밍 표준을 적용한다.

각 diffuse 텍스처는 texture_diffuseN이라고 이름을 붙이고 각 specular 텍스처는 texture_specularN이라고 이름을 붙인다.

여기에서 N은 1부터 텍스처 sampler에 허용하는 최댓값 사이의 어떠한 숫자이다.

3개의 diffuse 텍스처와 2개의 specular 텍스처를 가지고 있다고 가정하면.

이들의 텍스처 sampler는 다음과 같이 네이밍된다.

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

 

이 네이밍 표준으로 인해 shader에서 텍스처 sampler를 있는 만큼 모두 정의할 수 있다.

그리고 mesh가 실제로 텍스처들을 많이 가지고 있다고 하면 그들의 이름이 무엇인지 알 수 있다.

해당 네이밍 표준으로 인해 또한 하나의 mesh에 많은 양의 텍스처들을 처리할 수 있고

개발자는 간단히 적당한 sampler들을 정의해주기만 하면 이들을 마음껏 사용할 수 있다.

최종 드로잉 코드는 다음과 같다.

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); // 바인딩하기 전에 적절한 텍스처 유닛 활성화
        // 텍스처 넘버(diffuse_textureN 에서 N) 구하기
        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);

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

먼저 텍스처 타입마다 N값을 계산하고 적절한 uniform 이름을 얻기 위해 이 N값을 텍스처의 타입 문자열에 결합시킨다.

다음 적절한 sampler를 위치시키고 현재 활성화된 텍스처 유닛에 부합되는 위치 값을 주어주고

텍스처를 바인딩한다.

이 것이 Draw 함수에서 shader가 필요한 이유이다.

또한 "material."문자열을 최종 uniform 이름에 추가하였다.

일반적으로 텍스처를 material struct에 저장하기 때문이다.(이는 구현에 따라 다를 수도 있기는 하다.)

 

이 mesh 클래스는 이전에 다루었던 여러 주제에 대해 깔끔하게 추상화되어 있다.

다음에는 여러 mesh 오브젝트들로 이루어진 컨테이너 모델을 만들어 보고 실제로 Assimp의 로드 인터페이스를

구현해본다.

 

 

 

 

'공부한거 > OpenGL' 카테고리의 다른 글

OpenGL Advanced 5-1 Depth testing  (0) 2020.12.08
OpenGL Model Loading 4-3 Model  (0) 2020.12.07
OpenGL Model Loading 4-1 Assimp  (0) 2020.12.07
OpenGL Lighting 3-6 Multiple Lights  (0) 2020.12.06
OpenGL Advanced 5-6 Cubemaps  (0) 2020.11.26