ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • OpenGL 4.5 강좌 - (5) VAO, VBO
    OpenGL 2021. 1. 31. 18:57

    지난 시간에는 화면을 단색으로 채우는 방법에 대해 알아봤습니다. 그런데 화면에 도형을 그리려면 어떻게 해야 할까요? 그걸 하려면 새로운 개념을 더 알아야 합니다.

    2차원 도형이든 3차원 도형이든간에 컴퓨터가 그것을 그리려면 우선 어떤 형식으로 표현되어야겠죠? OpenGL에서는 모든 도형을 삼각형으로 쪼개서 표현합니다. 왜냐하면 어떤 도형이든 삼각형으로 쪼개서 근사할 수 있기 때문이죠. OpenGL에서는 모든 도형의 기본 단위인 이 삼각형을 Primitive (기초요소) 라고 부릅니다.

    삼각형은 3개의 꼭짓점 (Vertex) 으로 나타낼 수 있습니다. 이 점 세개를 선으로 연결하고, 그 선 내부를 채우면 삼각형이 되는 것이죠. 각 꼭짓점은 여러 가지의 속성 (attribute)을 가질 수 있습니다. 예를 들어 각 점의 위치 정보, 색상 정보 또는 텍스쳐 좌표 정보, 노말 (normal, 법선 벡터) 정보 등이죠. 뒤의 두 개가 무엇인지는 차차 알아보도록 합시다

    이러한 꼭짓점 정보들을 담고 있는 배열 객체를 VBO (Vertex Buffer Object, 꼭짓점 버퍼 객체) 라고 합니다. 그런데 이 VBO에 담겨 있는 정보는 형식이 정해져 있지 않기 때문에 OpenGL에서는 VBO만 가지고는 이 정보를 삼각형들로 어떻게 해석해야 할지를 모릅니다. 따라서 VBO의 데이터를 어떻게 해석해야 하는지 대한 메타정보를 가지고 있는 객체를 VAO (Vertex Array Object, 꼭짓점 배열 객체) 라고 부릅니다.

    이렇게만 말하면 구체적으로 이해가 안 될 것이니 예시를 들어 봅시다.

    VBO 만들기

    아래와 같은 삼각형 두 개를 화면에 그리고 싶다고 해 봅시다.

    [그림 1] 화면에 이렇게 나오도록 만들어 봅시다.

    이 삼각형 두 개는 6개의 꼭짓점으로 이루어져 있습니다. 각 삼각형마다 꼭짓점에 시계 반대 방향으로 증가하도록 순서를 부여합시다. 이 순서가 왜 중요한지는 다음 강에서 설명하겠습니다.

    [그림 2] 각 꼭짓점에 순서를 부여한 상태

    이 꼭짓점들의 위치를 표현하려면 좌표계가 있어야겠죠?

    [그림 3] 각 꼭짓점의 위치를 좌표로 표현함.

    이제 이 꼭짓점들을 배열로 표현하려면 어떻게 해야 될까요? 우선 좌표를 담을 수 있는 구조체가 있어야겠죠? 점은 2차원 위치벡터로 표현할 수 있으므로 Vec2d라고 부르겠습니다.

    // 2차원 (위치)벡터를 담을 수 있는 구조체
    struct Vec2d {
        float x;
        float y;
    };

    또 삼각형의 색상을 담을 수 있는 구조체도 있어야겠습니다. OpenGL에서 색상은 흔히 0.0 ~ 1.0 사이의 값을 가지는 RGB로 표현되므로 float 3개를 가지도록 만들고 이름은 Color라고 부르겠습니다.

    // 색상 정보를 담을 수 있는 구조체
    struct Color {
        float r;
        float g;
        float b;
    };

    한 꼭짓점은 두 개의 속성, 즉 위치와 색상을 가집니다. 따라서 꼭짓점을 표현하는 구조체 Vertex를 아래와 같이 구성하겠습니다.

    // 꼭짓점 정보를 담을 수 있는 구조체
    struct Vertex {
        Vec2d position;
        Color color;
    };

    이 구조체를 이용해서 점들을 배열에 넣어 봅시다.

    // 삼각형 데이터
    const Color pink_color = {1.0f, 0.765f, 0.765f};
    const Color green_color = {0.545f, 1.0f, 0.345f};
    
    const Vertex vertices[6] = {
        // 핑크색 삼각형
        Vertex{Vec2d{2.0f, 8.0f}, pink_color},
        Vertex{Vec2d{1.0f, 5.0f}, pink_color},
        Vertex{Vec2d{5.0f, 6.0f}, pink_color},
    
        // 연두색 삼각형
        Vertex{Vec2d{4.0f, 4.0f}, green_color},
        Vertex{Vec2d{6.0f, 1.0f}, green_color},
        Vertex{Vec2d{8.0f, 3.0f}, green_color},
    };

    자, 이제 이 정보를 이용해서 VBO를 만들어 보겠습니다. VBO 객체를 만드는 OpenGL 함수는 glCreateBuffers(GLsizei num, GLuint* buffers)입니다. 첫번째 인자는 VBO를 몇 개 만들 것인지, 그리고 두번째 인자로는 생성된 VBO들의 번호를 저장할 배열의 시작 주소를 넘겨 주면 됩니다. 우리는 VBO를 하나만 만들 것이므로 num에는 1을, buffers에는 GLuint타입의 변수의 주소를 넘겨주면 되겠습니다.

    // VBO 생성
    GLuint my_vbo;
    glCreateBuffers(1, &my_vbo);

    이렇게 호출하고 난 후, my_vbo에는 새로 생성된 VBO의 번호가 저장될 것입니다. 여기서 중요한 것은 my_vbo는 실제 VBO 객체 그 자체가 아니라, 메모리 어딘가에 저장되어 있는 VBO 객체를 가리키는 정수 값일 뿐입니다. 즉 타입은 GLuint이라는 정수 타입임에도, 포인터같이 쓰인다는 것을 명심해야 합니다. 따라서 이 VBO 객체가 가지는 메모리를 해제하기 위해서는 glDeleteBuffers(GLsizei num, GLuint* buffers)함수를 호출해 줘야 합니다. 인자의 의미는 glCreateBuffers()함수와 같습니다.

    // VBO 리소스 해제
    glDeleteBuffers(1, &my_vbo);

    그럼 my_vbo에 아까 만든 데이터 (vertices 배열) 를 전달해 줘야 합니다. VBO에 데이터를 전달할 떄 쓰는 함수는 glNamedBufferData(GLuint buffer, GLsizeiptr size, const void *data, GLenum usage)입니다. 복잡해 보이니 표로 딱 정리합시다.

    이름
    gl 모든 OpenGL 함수 앞에 붙는 접두사
    Named "이름 붙은"이라는 뜻으로, VBO의 번호(Name)를 인자로 받는다는 의미
    Buffer 버퍼 (VBO를 가리킴)
    Data 데이터를 설정한다는 의미
    ( 함수 인자 리스트 시작
    GLuint buffer, VBO의 번호
    GLsizeiptr size, VBO를 채울 데이터의 크기 (바이트 단위)
    const void *data, VBO를 채울 데이터의 시작 주소.
    GLenum usage VBO를 어떻게 사용할 것인지를 의미. 자주 쓰는 값으로는 GL_STATIC_DRAW (STATIC은 한 번 데이터를 설정하고 수정하지 않는다는 의미, DRAW는 OpenGL에서 이 데이터를 읽는 용도로 사용한다는 의미이다) 가 있다.
    ) 함수 인자 리스트 끝

    첫번째 인자는 my_vbo를, 두번째 인자는 vertices 배열의 바이트 단위의 크기, 세번째 인자는 vertices 배열의 시작 주소, 네번쨰는 GL_STATIC_DRAW를 넘겨 주면 된다는 것을 알 수 있겠습니다.

    // VBO에 데이터 전달
    glNamedBufferData(my_vbo, sizeof(vertices), vertices, GL_STATIC_DRAW);

    이 함수 호출은 그래픽 카드로 데이터를 복사하는 작업이니, 실행 시간이 오래 걸립니다. 따라서 매 프레임마다 호출할 수 없고, 미리 만들어 놓아야 합니다. 또 하나 명심해야 할 점은 GL_STATIC_DRAW를 사용할 경우, 한번 설정한 데이터는 변경할 수 없다는 것입니다. 따라서 데이터를 수정할 예정이라면, GL_DYNAMIC_DRAW를 사용해야 합니다.

    VAO 만들기

    이렇게 데이터를 전달하는 방식으로 알 수 있듯이, VBO는 꼭짓점 데이터를 단순한 바이트 배열 또는 메모리 한 뭉태기로 볼 뿐, 내부 구조에 대해서는 알지 못합니다. 따라서 VBO의 데이터를 OpenGL에서 해석하고 사용하기 위해서는 VAO를 사용해야 합니다.

    우선 VAO 생성과 파괴는 VBO의 것과 유사합니다. 생성은 glCreateVertexArrays(), 파괴는 glDeleteVertexArrays()를 이용하면 됩니다.

    // VAO 생성
    GLuint my_vao;
    glCreateVertexArrays(1, &my_vao);
    // VAO 리소스 해제
    glDeleteVertexArrays(1, &my_vao);

    VAO의 구조는 꽤 복잡합니다. 일단 이번 강에 필요한 기능들만 간추려서 그림으로 표현해 봤습니다.

    [그림 4] VAO의 기본적인 구조

    위 그림에서 알 수 있듯이 VAO에는 속성 (attribute)(왼쪽)과 VBO 바인딩 (VBO binding)(오른쪽) 두 종류의 슬롯이 있습니다. '속성 (attribute)'은 꼭짓점의 속성을 의미하는 것으로, 0번 속성은 위치, 1번 속성은 색상.. 과 같은 식으로 할당할 수 있습니다. 각 VBO 바인딩 슬롯에는 VBO를 연결(바인딩)할 수 있습니다. 각 속성에는 최대 1개의 VBO 바인딩을 연결하고, 그 VBO 바인딩에 연결된 VBO를 어떻게 해석해야 하는지에 대한 형식 (format) 정보를 설정할 수 있습니다.

    이렇게만 말하면 이해가 어려우니 VAO를 실제로 만들어 봅시다.

    VBO를 VAO의 슬롯에 바인딩하기 위해서는 glVertexArrayVertexBuffer(GLuint vaobj, GLuint bindingindex, GLuint buffer, GLintptr offset, GLsizei stride) 함수를 사용합니다. vaobj에는 VAO의 번호, bindingindex에는 몇 번째 VBO 바인딩 슬롯에 바인딩할 것인지, buffer는 바인딩할 VBO의 번호, offset (시작 위치) 은 VBO의 몇 번째 바이트부터 사용할 것인지, 그리고 stride ("보폭") 는 꼭짓점 n의 데이터와 꼭짓점 n+1의 데이터 사이의 간격, 즉 VBO의 한 원소의 크기가 몇 바이트인지를 나타냅니다.

    offsetstride의 개념이 아직 잘 와닿지 않으신다면, 아래 그림을 봐 주세요.

    [그림 5] VBO 내부의 데이터 구조

    우리는 my_vao의 0번째 바인딩에다가 my_vbo를 바인딩할 것이고, Vertex vertices[6] 배열을 그대로 VBO에 복사했으니 offset은 0, strideVertex 구조체의 크기가 되겠죠?

    // VAO의 0번 VBO로 my_vbo를 바인딩할 것임
    const int MY_VBO_BINDING = 0;
    
    // my_vbo를 my_vao의 0번 VBO binding에 바인딩하기
    glVertexArrayVertexBuffer(my_vao, MY_VBO_BINDING, my_vbo, 0, sizeof(Vertex));

    이 함수를 호출하고 난 후에는 그림이 아래와 같이 됩니다.

    0번 VBO 바인딩에 my_vbo가 연결되었다.

    0번 (MY_VBO_BINDING번) VBO 바인딩이 활성화 (초록색) 되고, 그곳에 my_vbo가 연결되었습니다.

    이제 이 VBO 바인딩을 속성(attribute)으로 연결해 봅시다. 0번 속성에는 위치 (position), 1번 속성에는 색상 (color)를 연결할 것입니다. 속성과 VBO 바인딩을 연결하기 전, 우선 속성을 활성화 (enable) 해야 합니다. 활성화는 glEnableVertexArrayAttrib() 함수를 통해 할 수 있습니다. 두번째 인자에 몇 번째 속성을 활성화할 것인지를 넘겨줍니다.

    // 0번 attribute로 position을 설정할 것임
    const int POSITION_ATTRIBUTE_INDEX = 0;
    
    // my_vao의 0번 attribute 활성화
    glEnableVertexArrayAttrib(my_vao, POSITION_ATTRIBUTE_INDEX);

    이렇게 하면 아래와 같이 0번 (POSITION_ATTRIBUTE_INDEX번) 속성 (attribute) 이 활성화됩니다.

    0번 속성이 활성화되었다.

    속성과 VBO 바인딩을 연결하려면 glVertexArrayAttribBinding() 함수를 사용합니다. 두번째 인자로 속성의 번호, 세번째 인자로 VBO 바인딩의 번호를 넘겨주면 그 두 개가 연결됩니다.

    // my_vao의 0번 attribute로 my_vbo의 데이터를 바인딩
    glVertexArrayAttribBinding(my_vao, POSITION_ATTRIBUTE_INDEX, MY_VBO_BINDING);

    이 함수를 호출하고 난 후에는 그림이 아래와 같이 됩니다.

    0번 속성과 0번 VBO 바인딩이 서로 연결되었다.

    여기서 끝이 아닙니다. 0번 속성에서 my_vbo를 사용하라는 명령은 내렸지만, VAO에서 아직도 my_vbo형식 (format) 을 모르고 있기 때문입니다. 이 형식을 지정해주기 위해 glVertexArrayAttribFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset) 함수를 사용합니다. 복잡하니까 다시 표로 정리해 봅시다.

    이름
    gl 모든 OpenGL 함수 앞에 붙는 접두사
    VertexArray VAO (Vertex Array Object) 를 의미함.
    Attrib 속성 (Attribute) 을 의미함.
    Format 형식 (Format) 을 의미함.
    ( 함수 인자 리스트 시작
    GLuint vaobj, VAO의 번호
    GLuint attribindex, 몇 번째 VAO 속성의 포맷을 설정할 것인지.
    GLint size, 이 속성에 꼭짓점 하나에 값이 몇 개 있는지를 나타냄. 1, 2, 3, 4 중의 하나여야 함.
    예를 들어 2차원 위치 데이터 (x, y)라면 2, RGB 색상 데이터라면 3. [그림 5]를 참조
    GLenum type, 이 속성의 값의 타입 (GL_FLOAT, GL_DOUBLE 등)
    GLboolean normalized, GL_FALSE로 통일
    GLuint relativeoffset 각 꼭짓점 데이터의 시작점에서 몇 바이트를 들어가야 속성 값이 나오는지를 나타냄.
    [그림 5]를 참조
    )  

    우리는 my_vao의 0번 속성에 2개의 float형 원소 (x, y)를 가지는 position을 설정하려고 하므로, vaobj = my_vao, attribindex = 0, size = 2, type = GL_FLOAT, relativeoffsetposition 멤버가 struct Vertex 구조체 내에서 가지는 위치가 되겠습니다. 이 오프셋은 positionVertex의 첫 멤버이기 때문에 0이지만, 나중에 Vertex에 새로운 멤버가 추가될 경우 이 값이 바뀔 수 있기 때문에 하드코딩하지 않고 offsetof라는 C 표준 매크로를 사용하겠습니다. offsetof(S, m)S라는 구조체에서 m이라는 멤버가 가지는 오프셋 값을 나타냅니다.

    // my_vao의 0번 attribute에 바인딩된 vbo (my_vbo)를 어떻게 해석할지(format)를 설정
    glVertexArrayAttribFormat(my_vao,                      // VAO 번호
                              POSITION_ATTRIBUTE_INDEX,    // VAO attribute 번호
                              2,                           // 벡터의 원소 개수
                              GL_FLOAT,                    // 원소의 타입
                              GL_FALSE,                    // normalized
                              offsetof(Vertex, position)); // Vertex 내의 position 멤버의 위치

    여기까지 했다면 1번 속성에 color를 설정하는 것도 감이 잡힐 것입니다. 이 부분은 설명 없이 코드만 보여드릴 테니 각 함수 인자가 어떤 의미를 가지는지 복습해 보세요.

    // 1번 attribute로 color를 설정할 것임
    const int COLOR_ATTRIBUTE_INDEX = 1;
    
    // my_vao의 1번 attribute 활성화
    glEnableVertexArrayAttrib(my_vao, COLOR_ATTRIBUTE_INDEX);
    
    // my_vao의 1번 attribute로 my_vbo의 데이터를 바인딩
    glVertexArrayAttribBinding(my_vao, COLOR_ATTRIBUTE_INDEX, MY_VBO_BINDING);
    
    // my_vao의 1번 attribute에 바인딩된 vbo (my_vbo)를 어떻게 해석할지(format)를 설정
    glVertexArrayAttribFormat(my_vao,                     // VAO 번호
                                COLOR_ATTRIBUTE_INDEX,    // VAO attribute 번호
                                3,                        // 벡터의 원소 개수
                                GL_FLOAT,                 // 원소의 타입
                                GL_FALSE,                 // normalized
                                offsetof(Vertex, color)); // Vertex 내의 color 데이터의 위치
    

    여기까지 하면 아래 그림과 같이 VAO의 0번과 1번 속성에 각각 my_vbopositioncolor가 배정됩니다. 이로서 VAO가 완성되었습니다.

    완성된 VAO의 모습

    다음 강에서는 이 VAO를 사용해서 실제로 화면에 삼각형을 그려보도록 하겠습니다!

    여기까지 전체 코드 (main.cpp):

    #include <iostream>
    
    #define GLFW_INCLUDE_NONE
    #include <GLFW/glfw3.h>
    #include <glad/glad.h>
    
    // 2차원 (위치)벡터를 담을 수 있는 구조체
    struct Vec2d {
        float x;
        float y;
    };
    
    // 색상 정보를 담을 수 있는 구조체
    struct Color {
        float r;
        float g;
        float b;
    };
    
    // 꼭짓점 정보를 담을 수 있는 구조체
    struct Vertex {
        Vec2d position;
        Color color;
    };
    
    int main() {
    
        // GLFW 초기화
        if (!glfwInit()) {
            std::cerr << "GLFW Initialization Failed!\n";
            return -1;
        }
    
        // OpenGL 4.5 Core Profile 버전 설정
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 5);
        glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    
        // 창 띄우기
        GLFWwindow* window = glfwCreateWindow(640, 480, "My Title", NULL, NULL);
        if (!window) {
            std::cerr << "Create Window Failed!\n";
            glfwTerminate();
            return -1;
        }
    
        // 창에 OpenGL Context 설정하기
        glfwMakeContextCurrent(window);
        gladLoadGL();
    
        // V-Sync 설정하기 (Screen Tearing 방지)
        glfwSwapInterval(1);    
    
        // 삼각형 데이터
        const Color pink_color = {1.0f, 0.765f, 0.765f};
        const Color green_color = {0.545f, 1.0f, 0.345f};
    
        const Vertex vertices[6] = {
            // 핑크색 삼각형
            Vertex{Vec2d{2.0f, 8.0f}, pink_color},
            Vertex{Vec2d{1.0f, 5.0f}, pink_color},
            Vertex{Vec2d{5.0f, 6.0f}, pink_color},
    
            // 연두색 삼각형
            Vertex{Vec2d{4.0f, 4.0f}, green_color},
            Vertex{Vec2d{6.0f, 1.0f}, green_color},
            Vertex{Vec2d{8.0f, 3.0f}, green_color},
        };
    
        // VBO 생성
        GLuint my_vbo;
        glCreateBuffers(1, &my_vbo);
    
        // VBO에 데이터 전달
        glNamedBufferData(my_vbo, sizeof(vertices), vertices, GL_STATIC_DRAW);
    
        // VAO 생성
        GLuint my_vao;
        glCreateVertexArrays(1, &my_vao);
    
        // VAO의 0번 VBO로 my_vbo를 바인딩할 것임
        const int MY_VBO_BINDING = 0;
    
        // my_vbo를 my_vao의 0번 VBO binding에 바인딩하기
        glVertexArrayVertexBuffer(my_vao, MY_VBO_BINDING, my_vbo, 0, sizeof(Vertex));
    
        // 0번 attribute로 position을 설정할 것임
        const int POSITION_ATTRIBUTE_INDEX = 0;
    
        // my_vao의 0번 attribute 활성화
        glEnableVertexArrayAttrib(my_vao, POSITION_ATTRIBUTE_INDEX);
    
        // my_vao의 0번 attribute로 my_vbo의 데이터를 바인딩
        glVertexArrayAttribBinding(my_vao, POSITION_ATTRIBUTE_INDEX, MY_VBO_BINDING);
    
        // my_vao의 0번 attribute에 바인딩된 vbo (my_vbo)를 어떻게 해석할지(format)를 설정
        glVertexArrayAttribFormat(my_vao,                       // VAO 번호
                                  POSITION_ATTRIBUTE_INDEX,     // VAO attribute 번호
                                  2,                            // 벡터의 원소 개수
                                  GL_FLOAT,                     // 원소의 타입
                                  GL_FALSE,                     // normalized
                                  offsetof(Vertex, position));  // Vertex 구조체 내의 position 데이터의 위치
    
        // 1번 attribute로 color를 설정할 것임
        const int COLOR_ATTRIBUTE_INDEX = 1;
    
        // my_vao의 1번 attribute 활성화
        glEnableVertexArrayAttrib(my_vao, COLOR_ATTRIBUTE_INDEX);
    
        // my_vao의 1번 attribute로 my_vbo의 데이터를 바인딩
        glVertexArrayAttribBinding(my_vao, COLOR_ATTRIBUTE_INDEX, MY_VBO_BINDING);
    
        // my_vao의 1번 attribute에 바인딩된 vbo (my_vbo)를 어떻게 해석할지(format)를 설정
        glVertexArrayAttribFormat(my_vao,                       // VAO 번호
                                  COLOR_ATTRIBUTE_INDEX,        // VAO attribute 번호
                                  3,                            // 벡터의 원소 개수
                                  GL_FLOAT,                     // 원소의 타입
                                  GL_FALSE,                     // normalized
                                  offsetof(Vertex, color));     // Vertex 구조체 내의 color 데이터의 위치
    
        // 렌더 루프
        while (!glfwWindowShouldClose(window)) {
            // 화면 노란색으로 채우기
            GLfloat color[4] = {1.0, 1.0, 0.0, 1.0};
            glClearNamedFramebufferfv(0, GL_COLOR, 0, &color[0]);
    
            // Front Buffer와 Back Buffer Swap하기
            glfwSwapBuffers(window);
    
            // 사용자 입력 받아오기
            glfwPollEvents();
        }
    
        // VAO와 VBO 리소스 해제
        glDeleteVertexArrays(1, &my_vao);
        glDeleteBuffers(1, &my_vbo);
    
        // 창 리소스 해제
        glfwDestroyWindow(window);
    
        // GLFW 리소스 해제
        glfwTerminate();
        return 0;
    }
    

    댓글

Designed by Tistory.