-
OpenGL 4.5 강좌 - (5) VAO, VBOOpenGL 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의 한 원소의 크기가 몇 바이트인지를 나타냅니다.offset
과stride
의 개념이 아직 잘 와닿지 않으신다면, 아래 그림을 봐 주세요.[그림 5] VBO 내부의 데이터 구조 우리는
my_vao
의 0번째 바인딩에다가my_vbo
를 바인딩할 것이고,Vertex vertices[6]
배열을 그대로 VBO에 복사했으니offset
은 0,stride
는Vertex
구조체의 크기가 되겠죠?// 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
,relativeoffset
은position
멤버가struct Vertex
구조체 내에서 가지는 위치가 되겠습니다. 이 오프셋은position
이Vertex
의 첫 멤버이기 때문에 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_vbo
의position
과color
가 배정됩니다. 이로서 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; }
'OpenGL' 카테고리의 다른 글
OpenGL 4.5 강좌 - (4) Framebuffer, 더블 버퍼링, V-Sync (0) 2021.01.31 OpenGL 4.5 강좌 - (3) 창 띄우기 (0) 2021.01.31 OpenGL 4.5 강좌 - (2) 개발환경 셋팅하기 (0) 2021.01.31 OpenGL 4.5 강좌 - (1) OpenGL 소개: C언어로 화면에 그림 그리기 (2) 2021.01.31