OpenGL Draw Triangle 객체 삼각형 그리기 (Vertex Attribute VAO(Vertex Array Object) 버텍스(정점) 속성과 버텍스(정점) 배열) [Learn OpenGL 쉬운 번역]

728x90
반응형
※ 본 포스팅은 learnopengl.com을 번역 및 가감한 포스팅이며, learnopengl에서는 번역 작업 참여를 적극 장려하고 있습니다!
아래 링크에서 원문을 확인할 수 있습니다.

 

Linking Vertex Attributes

vertex shader는 vertex attribute 형태로 원하는 입력을 지정을 할 수 있습니다. 어떤 데이터가 들어가는지 직접 지정해야합니다. 그리고 opengl이 데이터를 해석하는 방법도 지정해야합니다.

 

vertex buffer data가 가진 형식을 알아봅시다.

  • position 데이터는 32bit(4 byte) float 값
  • position 데이터 3개로 구성
  • 3개 값들 사이에 공간 없이 tightly packed 밀집되어있음. 
  • 데이터 첫 번째 값은 buffer의 시작 부분에 위치

이러한 형식을 가졌기 때문에 vertex data를 vertex attribute 별로 어떻게 다르게 해석해야할지 glVertexAttribPointer로 알려줄 수 있습니다. 

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

glVertexAttribPointer함수의 매개변수가 많네요. 순서대로 알아봅시다.

 

  1. 첫 번째는 vertex attribute의 위치입니다. vertex shader에서 layout (location = 0)이 기억나시나요? 위치를 0으로 설정했기 때문에 이 위치에 저장하기 위해 0을 지정합니다.
  2. vertex attribute의 size입니다. vec3 이므로 3으로 지정합니다.
  3. 데이터 유형입니다. GLSL의 vector type은 float이므로 GL_FLOAT으로 지정합니다.
  4. 데이터의 정규화 여부입니다. 이걸 GL_TRUE로 하는 경우는 int나 byte 데이터를 넘겨주고 GLSL 안에서 float으로 변환할 때 사용하고 변환하면 0~1(signed data는 -1~1)로 정규화됩니다. 지금은 그런 상황이 아니니 GL_FALSE로 합니다.
  5. 여기는 stride 라고 하는데, 연속된 attribute가 있으면 얼마나의 크기만큼 이동해야 데이터를 읽을 수 있는지 그 size를 말합니다. 지금 position 데이터는 각각 float 3개의 크기만큼 넘어가야 다음 vertex attribute를 읽을 수 있기 때문에 float size의 3배로 지정합니다.
    배열이 딱 달라붙어 밀접한 상황이라면 값을 0으로 설정해서 자동으로 결정해줄 수 도 있습니다. (attribute 사이에 공간이 없다는 가정입니다). attribute가 다양해지면 stride를 작성하는 다른 방법들을 알게 됩니다.
  6. offset 값입니다. 데이터는 buffer의 처음부터 읽으므로 당연히 0인데, 타입이 void*로 되어있어서 casting 해주는 겁니다.
vertex attribute는 데이터를 VBO 메모리에서 가져옵니다. glVertexAttribPointer를 호출하면 GL_ARRAY_BUFFER에 binding된 VBO 중에서 어떤 걸 골라 데이터를 가져올지 결정합니다. 이제 vertex attribute 0을 찾으면 해당 vertex data를 찾아 가져올 수 있습니다.

vertex data를 읽는 방법을 정했습니다. 이제 glEnableVertexAttribArray를 이용하여 vertex attribute의 위치를 지정해서 vertex attribute를 활성화합니다. 아까 vertex attribute의 위치는 0번이었으니 0번을 활성화하면 됩니다.

// 0. copy our vertices array in a buffer for OpenGL to use
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 1. then set the vertex attributes pointers
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);  
// 2. use our shader program when we want to render an object
glUseProgram(shaderProgram);
// 3. now draw the object 
someOpenGLFunctionThatDrawsOurTriangle();

하나하나 object를 그릴 때마다 이렇게 많은 작업을 하면 쉽지 않습니다. object가 조금만 많아져도 코드는 복잡하겠죠. 그래서 여러 attribute를 모두 하나의 object에 저장하고 이걸 binding 해서 쓸 수 있으면 좋겠죠?

 

Vertex Array Object

vertex array object VAO는 VBO처럼 binding이 가능한 object 형태입니다. 여기에 binding하는 것은 vertex attribute입니다. VAO를 이용하면 한 번 호출로 여러 vertex attribute를 구성할 수 있습니다. VAO를 이용하면 vertex data를 전환할 때 VAO를 다르게 binding하기만 하면 되겠죠. attribute를 설정할 때의 구성들이 VAO에 다 담겨있으니까요.

 

Core OpenGL VAO를 사용해서 vertex input을 처리하는 방법을 알 수 있습니다. VAO를 binding해야지 비로소 opengl에서 그림을 그립니다.

 

VAO의 구성을 봅시다.

  • glEnableVertexAttibArray 혹은 glDiableVertexAttribArray 호출정보
  • glVertexAttribPointer로 구성한 vertex attribute 정보
  • glVertexAttribPointer 호출로 vertex attribute와 연결된 VBO정보

VAO 생성 방법도 VBO와 비슷합니다.

unsigned int VAO;
glGenVertexArrays(1, &VAO);

VAO는 glBindVertexArray로 binding합니다. 그 다음에 VAO가 가리킬 VBO의 attribute point를 binding해서 구성하고 VAO를 unbinding한 후 나중에 사용하는 방식입니다. 

// ..:: 초기화 코드 (한 번만 수행 (객체가 자주 변경되지 않는 한)) :: ..
// 1. 정점 배열 객체 바인딩
glBindVertexArray(VAO);
// 2. OpenGL이 사용할 버퍼에 정점 배열 복사
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 정점 속성 포인터 설정
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

...

// ..:: 그리기 코드 (렌더 루프 내) :: ..
// 4. 객체 그리기
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();

여기까지 하면 충분합니다. 보통 object를 여러 개 그리기 위해서 VAO들을 모두 생성해서 구성해주고, 나중에 그릴 object에 대한 VAO를 하나씩 꺼내서 사용합니다. 사용하면 다시 unbinding합니다. 

 

The triangle we've all been waiting for

object를 그리려면 shader program으로 shader를 활성화하고 VAO를 binding해서 VBO와 vertex attribute 세팅을 binding한 다음 glDrawArrays 함수를 사용해서 그립니다. 

glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);

glDrawArrays는 첫 번째 인수로 opengl primitive 유형을 정하는데, 여기서는 삼각형을 그리므로 GL_TRIANGLES로 지정합니다. 두 번째는 vertex array의 시작 index입니다. 여기서는 시작이 0입니다. 마지막 인수는 vertex의 개수를 넣습니다. 삼각형 한 개만 그리려고 하니 3을 넣습니다. 

 

코드를 컴파일해서 실행해보고, 오류가 생긴다면 지금까지 과정을 확인해보세요.

전체 코드는 아래와 같습니다.

#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);

// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

const char *vertexShaderSource = "#version 330 core\n"
    "layout (location = 0) in vec3 aPos;\n"
    "void main()\n"
    "{\n"
    "   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
    "}\0";
const char *fragmentShaderSource = "#version 330 core\n"
    "out vec4 FragColor;\n"
    "void main()\n"
    "{\n"
    "   FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
    "}\n\0";

int main()
{
    // glfw: initialize and configure
    // ------------------------------
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

    // glfw window creation
    // --------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    // glad: load all OpenGL function pointers
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }


    // build and compile our shader program
    // ------------------------------------
    // vertex shader
    unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    glCompileShader(vertexShader);
    // check for shader compile errors
    int success;
    char infoLog[512];
    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
    }
    // fragment shader
    unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);
    // check for shader compile errors
    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
    }
    // link shaders
    unsigned int shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);
    // check for linking errors
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
    }
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    // set up vertex data (and buffer(s)) and configure vertex attributes
    // ------------------------------------------------------------------
    float vertices[] = {
        -0.5f, -0.5f, 0.0f, // left  
         0.5f, -0.5f, 0.0f, // right 
         0.0f,  0.5f, 0.0f  // top   
    }; 

    unsigned int VBO, VAO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    // bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
    glBindVertexArray(VAO);

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    // note that this is allowed, the call to glVertexAttribPointer registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbind
    glBindBuffer(GL_ARRAY_BUFFER, 0); 

    // You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other
    // VAOs requires a call to glBindVertexArray anyways so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary.
    glBindVertexArray(0); 


    // uncomment this call to draw in wireframe polygons.
    //glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    {
        // input
        // -----
        processInput(window);

        // render
        // ------
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // draw our first triangle
        glUseProgram(shaderProgram);
        glBindVertexArray(VAO); // seeing as we only have a single VAO there's no need to bind it every time, but we'll do so to keep things a bit more organized
        glDrawArrays(GL_TRIANGLES, 0, 3);
        // glBindVertexArray(0); // no need to unbind it every time 
 
        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // optional: de-allocate all resources once they've outlived their purpose:
    // ------------------------------------------------------------------------
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteProgram(shaderProgram);

    // glfw: terminate, clearing all previously allocated GLFW resources.
    // ------------------------------------------------------------------
    glfwTerminate();
    return 0;
}

// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // make sure the viewport matches the new window dimensions; note that width and 
    // height will be significantly larger than specified on retina displays.
    glViewport(0, 0, width, height);
}

 

728x90
반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유