VAO의 사용 목적은 Vertex buffer가 여러 개 있을 때 각 버퍼마다 Layout(Attribute를 지정해서 버퍼 내에 어떤 정보가 어디에 얼만큼 위치하는지를 지정해두는 것)을 개별적으로 지정해둘 필요가 없게 하는 것이다.
즉 Binding된 Vertex Buffer가 변경될 때마다 매번 그 버퍼가 가진 Attribute들에 대해 glVertexAttribPointer()를 호출할 필요가 없다는 의미다.
glBindBuffer(buffer1);
glVertexAttribPointer(...);
...
glBindBuffer(buffer2);
glEnableVertexAttribArray(...);
이런식으로 할 필요가 없어진다는 의미다.
참고글:
https://stackoverflow.com/a/57258564/14134221
What are Vertex Array Objects?
I am just starting to learn OpenGL today from this tutorial: http://openglbook.com/the-book/ I got to chapter 2, where I draw a triangle, and I understand everything except VAOs (is this acronym OK...
stackoverflow.com
쉽게 말해, 점들의 좌표값 4개를 저장하고 있는, 사각형을 나타내는 어떤 버퍼가 있다고 가정해보자.
이 사각형 버퍼가 두 종류가 있다면, 즉 사각형 A와 사각형 B가 있다면,
우리는 사각형A와 사각형B에 개별적으로 레이아웃을 지정해주어야 할 것이다. (즉 A 버퍼는 꼭짓점 4개가 float으로 들어있고, B 버퍼에도 꼭짓점 4개가 float으로 들어있다는 걸 개별적으로 두번 지정해주어야 한다)
그러나 VAO를 사용하면 이 과정을 줄일 수 있다.
참고: opengl에서의 바인딩이란?
https://stackoverflow.com/questions/9758884/concept-behind-opengls-bind-functions/9758960
Concept behind OpenGL's 'Bind' functions
I am learning OpenGL from this tutorial. My question is about the specification in general, not about a specific function or topic. When seeing code like the following: glGenBuffers(1, &
stackoverflow.com
간단히 말해, OpenGL에게 지금 선택한 오브젝트가 무엇이다 라고 전해주는 것이다.
포토샵 같은걸 쓸 때 마우스로 그림을 그리면 현재 선택된 레이어에 그려지는 것처럼, 미리 무언가를 선택한 이후(OpenGL에서는 버퍼나 셰이더 등 다양한 게 될 수 있다) 행위를 해야 그 선택한 대상에 행위가 처리된다. (선택한 레이어에 그림이 그려진다)
// Include GLEW
#include <GL/glew.h>
// Include GLFW
#include <GLFW/glfw3.h>
#include <iostream>
#include <fstream>
#include <string>
#include <sstream>
#include <assert.h>
#define ASSERT(x) if (!(x)) assert(false)
#define GLCall(x) GLClearError();\
x;\
ASSERT(GLCheckError())
static void GLClearError()
{
while (glGetError() != GL_NO_ERROR);
}
static bool GLCheckError()
{
while (GLenum error = glGetError())
{
std::cout << "[OpenGL Error] ";
switch(error) {
case GL_INVALID_ENUM :
std::cout << "GL_INVALID_ENUM : An unacceptable value is specified for an enumerated argument.";
break;
case GL_INVALID_VALUE :
std::cout << "GL_INVALID_OPERATION : A numeric argument is out of range.";
break;
case GL_INVALID_OPERATION :
std::cout << "GL_INVALID_OPERATION : The specified operation is not allowed in the current state.";
break;
case GL_INVALID_FRAMEBUFFER_OPERATION :
std::cout << "GL_INVALID_FRAMEBUFFER_OPERATION : The framebuffer object is not complete.";
break;
case GL_OUT_OF_MEMORY :
std::cout << "GL_OUT_OF_MEMORY : There is not enough memory left to execute the command.";
break;
case GL_STACK_UNDERFLOW :
std::cout << "GL_STACK_UNDERFLOW : An attempt has been made to perform an operation that would cause an internal stack to underflow.";
break;
case GL_STACK_OVERFLOW :
std::cout << "GL_STACK_OVERFLOW : An attempt has been made to perform an operation that would cause an internal stack to overflow.";
break;
default :
std::cout << "Unrecognized error" << error;
}
std::cout << std::endl;
return false;
}
return true;
}
struct ShaderProgramSource
{
std::string VertexSource;
std::string FragmentSource;
};
static struct ShaderProgramSource ParseShader(const std::string& filepath)
{
enum class ShaderType
{
NONE = -1, VERTEX = 0, FRAGMENT = 1
};
std::ifstream stream(filepath);
std::string line;
std::stringstream ss[2];
ShaderType type = ShaderType::NONE;
while (getline(stream, line))
{
if (line.find("#shader") != std::string::npos)
{
if (line.find("vertex") != std::string::npos)
type = ShaderType::VERTEX;
else if (line.find("fragment") != std::string::npos)
type = ShaderType::FRAGMENT;
}
else
{
ss[(int)type] << line << '\n';
}
}
return { ss[0].str(), ss[1].str() };
}
static unsigned int CompileShader(unsigned int type, const std::string& source)
{
GLCall( unsigned int id = glCreateShader(type) );
const char* src = source.c_str();
GLCall( glShaderSource(id, 1, &src, nullptr) );
GLCall( glCompileShader(id) );
// Error handling
int result;
GLCall( glGetShaderiv(id, GL_COMPILE_STATUS, &result) );
std::cout << (type == GL_VERTEX_SHADER ? "vertex" : "fragment") << " shader compile status: " << result << std::endl;
if ( result == GL_FALSE )
{
int length;
GLCall( glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length) );
char* message = (char*) alloca(length * sizeof(char));
GLCall( glGetShaderInfoLog(id, length, &length, message) );
std::cout
<< "Failed to compile "
<< (type == GL_VERTEX_SHADER ? "vertex" : "fragment")
<< "shader"
<< std::endl;
std::cout << message << std::endl;
GLCall( glDeleteShader(id) );
return 0;
}
return id;
}
static unsigned int CreateShader(const std::string& vertexShader, const std::string& fragmentShader)
{
// create a shader program
unsigned int program = glCreateProgram();
unsigned int vs = CompileShader(GL_VERTEX_SHADER, vertexShader);
unsigned int fs = CompileShader(GL_FRAGMENT_SHADER, fragmentShader);
GLCall( glAttachShader(program, vs) );
GLCall( glAttachShader(program, fs) );
GLCall( glLinkProgram(program) );
GLint program_linked;
GLCall( glGetProgramiv(program, GL_LINK_STATUS, &program_linked) );
std::cout << "Program link status: " << program_linked << std::endl;
if (program_linked != GL_TRUE)
{
GLsizei log_length = 0;
GLchar message[1024];
GLCall( glGetProgramInfoLog(program, 1024, &log_length, message) );
std::cout << "Failed to link program" << std::endl;
std::cout << message << std::endl;
}
GLCall( glValidateProgram(program) );
GLCall( glDeleteShader(vs) );
GLCall( glDeleteShader(fs) );
return program;
}
int main( void )
{
// Initialise GLFW
if( !glfwInit() )
{
fprintf( stderr, "Failed to initialize GLFW\n" );
getchar();
return -1;
}
/*
OpenGL 3.3,
OPENGL_COMPAT_PROFILE은 VAO 하나를 기본으로 생성한 채로 시작한다.
OPENGL_CORE_PROFILE은 VAO 없이 시작한다.
때문에 OPENGL_CORE_PROFILE을 선택했다면glGenVertexArrays함수를 이용해
VAO 하나를 만들어주어야 한다.
*/
glfwWindowHint(GLFW_SAMPLES, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// Open a window and create its OpenGL context
GLFWwindow* window = glfwCreateWindow( 1024, 768, "Tutorial 02 - Red triangle", NULL, NULL);
if( window == NULL ){
fprintf( stderr, "Failed to open GLFW window. If you have an Intel GPU, they are not 3.3 compatible. Try the 2.1 version of the tutorials.\n" );
getchar();
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
// Initialize GLEW
glewExperimental = true; // Needed for core profile
if (glewInit() != GLEW_OK) {
fprintf(stderr, "Failed to initialize GLEW\n");
getchar();
glfwTerminate();
return -1;
}
std::cout << "Using GL Version: " << glGetString(GL_VERSION) << std::endl;
// Ensure we can capture the escape key being pressed below
glfwSetInputMode(window, GLFW_STICKY_KEYS, GL_TRUE);
// Dark blue background
glClearColor(0.0f, 0.0f, 0.4f, 0.0f);
float positions[] = {
-0.5f, -0.5f, // 0
0.5f, -0.5f, // 1
0.5f, 0.5f, // 2
-0.5f, 0.5f // 3
};
unsigned int indices[] = {
0, 1, 2,
2, 3, 0
};
/*
VAO를 생성하고 바인딩한다
위에서 OPENGL_CORE_PROFILE을 사용중이기 때문이다.
만약 여기서 VAO를 바인딩해주지 않는다면 그동안 사용해왔던 코드인 glEnableVertexAttribArray(0);를 호출할 때
에러가 발생한다. glEnableVertexAttribArray(0);는 기본적으로 opengl이 만들어준 0번 VAO에 AttribArray를 연결해준다는 의미인데,
CORE_PROFILE에서는 0번 VAO를 만들어주지 않기 때문이다.
*/
unsigned int vao;
GLCall( glGenVertexArrays(1, &vao) ); // VAO 1개를 uint vao id로 만들어준다.
GLCall( glBindVertexArray(vao) ); // 이렇게 만든 VAO를 bind해준다.
// Vertex buffer (혹은 VBO)를 생성한다
unsigned int buffer;
GLCall( glGenBuffers(1, &buffer) );
GLCall( glBindBuffer(GL_ARRAY_BUFFER, buffer) );
GLCall( glBufferData(GL_ARRAY_BUFFER, 4 * 2 * sizeof(float), positions, GL_STATIC_DRAW) );
// define vertex layout (레이아웃, 즉 vertex buffer의 Attrib 들을 설정한다)
/*
이 아랫줄 코드가 굉장히 중요하다.
이 코드가 Vertex Array와 Vertex Buffer을 연결해주는 역할을 한다.
glVertexAttribPointer의 첫 아규먼트인 0은 현재 바인딩된 Vertex Array의 0번 인덱스에
해당 Attrib Pointer가 가리키는 Vertex Buffer을 연결한다는 의미이다.
즉 만약 또 다른 Attrib Pointer을 연결해주려면
GLCall( glVertexAttribPointer(1, ... , Attrib pointer); 형식으로 작성하면 된다.
https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glVertexAttribPointer.xhtml
*/
GLCall( glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), 0) );// This links the attrib pointer wih the buffer at index 0 in the vertex array object
GLCall( glEnableVertexAttribArray(0) ); //https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=jungwan82&logNo=20108365931
// Create index buffer
unsigned int ibo;
GLCall( glGenBuffers(1, &ibo) );
GLCall( glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo) );
GLCall( glBufferData(GL_ELEMENT_ARRAY_BUFFER, 6 * sizeof(unsigned int), indices, GL_STATIC_DRAW) );
ShaderProgramSource source = ParseShader("res/shaders/Basic.shader");
std::cout << "VERTEX" << std::endl << source.VertexSource << std::endl;
std::cout << "FRAGMENT" << std::endl << source.FragmentSource << std::endl;
unsigned int shader = CreateShader(source.VertexSource, source.FragmentSource);
GLCall( glUseProgram(shader) );
GLCall( unsigned int u_Color = glGetUniformLocation(shader, "u_Color") );
ASSERT(u_Color != -1);
float red = 0.0f;
float step = 0.05f;
// 여기서 glBindVertexArray(0);으로 vao의 바인딩을 풀어준 것을 확인할 수 있다.
// 그러나 나중에 다시 vao를 바인딩하면 vao와 연결해두었던 attrib와 vertex buffer까지 알아서
// 바인딩되는 것을 알 수 있다.
GLCall( glUseProgram(0) );
GLCall( glBindBuffer(GL_ARRAY_BUFFER, 0) );
GLCall( glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0) );
GLCall( glBindVertexArray(0) );
do{
// Clear the screen
GLCall( glClear( GL_COLOR_BUFFER_BIT );
// set shader and set uniform color
GLCall( glUseProgram(shader) );
GLCall( glUniform4f(u_Color, red, 0.3, 0.8, 1.0) );
// Bind vertex buffer and attribute layout
// GLCall( glBindBuffer(GL_ARRAY_BUFFER, buffer) );
// GLCall( glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), 0) );
// GLCall( glEnableVertexAttribArray(0) );
// Instead of binding vertex buffer, attrib pointer, just bind Vertex Array Object
/*
이번 Vertex Array Object의 핵심이 되는 내용:
매 draw call 마다 vertex buffer, attrib pointer을 바인딩해줄 필요 없이
VAO 하나만 바인딩해주면 얘네 둘을 알아서 바인딩해준다.
*/
GLCall( glBindVertexArray(vao) );
// 인덱스 버퍼는 VAO에 포함되지 않으므로 따로 bind 해주어야함
GLCall( glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo) );
// Draw
GLCall( glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr)) );
// Swap buffers
glfwSwapBuffers(window);
glfwPollEvents();
// increment red
if (red < 0.0f || red > 1.0f)
step *= -1.0;
red += step;
} // Check if the ESC key was pressed or the window was closed
while( glfwGetKey(window, GLFW_KEY_ESCAPE ) != GLFW_PRESS &&
glfwWindowShouldClose(window) == 0 );
// VBO, VAO 삭제
GLCall( glDeleteBuffers(1, &buffer) );
GLCall( glDeleteVertexArrays(1, &vao) );
GLCall( glDeleteProgram(shader) );
// Close OpenGL window and terminate GLFW
glfwTerminate();
return 0;
}
VAO를 사용하는 것과 그냥 매번 Vertex Buffer와 Layout(즉 glVertexAttribPointer를 매번 호출)을 지정해주는 것중에 뭐가 더 좋을까? 상황에 따라 다르지만, 대부분의 상황에서 VAO를 사용하는 게 편의성과 성능면에서 우월하다. 실제로 VAO를 사용하는 것이 OpenGL이 권장하는 방식이기도 하다.
'computer graphics > OpenGL' 카테고리의 다른 글
[OpenGL] 9. Textures (0) | 2022.04.09 |
---|---|
[OpenGL] 8. 추상화(Abstraction)와 렌더러 (0) | 2022.04.02 |
[OpenGL] 6. Uniform (0) | 2022.02.27 |
[OpenGL] 5. Error Handling (1) (0) | 2022.02.24 |
[OpenGL] 4. Index Buffer (0) | 2022.02.19 |