티스토리 뷰

1. Indexed Draw의 컨셉

Indexed Drawing의 컨셉은 간단하다.

다음의 도형을 하나하나 그리려면 다음과 같이 3 * 3 = 9개의 vertex data 저장이 필요할 것이다.
그 다음 [0,1,2], [3,4,5], [6,7,8] 이렇게 primitive들을 구성하여 그릴 수 있을 것이다.

하지만 실제로 vertex는 6개이기 때문에 9개의 vertex에 대한 데이터를 저장하는 것은 낭비가 된다.
이는 자연스럽게 그릴 데이터가 커지면 그 낭비 또한 커지게 되는 결과가 나타난다.
따라서 정말 필요한 6개의 vertex data만 저장하고 그를 통해 도형을 그릴 방법이 필요하다.

이건 매우 간단한 아이디어로 해결할 수 있다.
그냥 각 6개의 vertex에 대해 index를 붙인 다음, 도형을 그릴 때 index number들만 call하여 그리면 된다.
예를 들어 위 도형의 vertex에 다음과 같이 index를 붙여보자.

그 다음 이 index를 이용하여 다음과 같이 그릴 순서 배열을 구성하면 된다.

[0, 4, 3,
 4, 1, 5,
 3, 5, 2]

이렇게 vertex에 index를 붙여 그리는 방법을 Indexed Draw 방식이라 부르고, 위의 순서 배열을 Index Buffer로 사용하게 된다.

Draw의 두 방식 - Ordered Draw와 Indexed Draw-에 대한 자세한 차이를 알고 싶다면 아래 글을 참고하는 것도 좋다.

https://gamedev.stackexchange.com/questions/133208/difference-in-gldrawarrays-and-gldrawelements

2. Indexed Draw OpenGL에서 구현하기

한 번 다음과 같은 사면체를 indexed draw로 그려보자.
완성된 코드를 바로 보고 싶은 사람은 아래의 repo를 참고하면 된다.

repo: https://github.com/9ru9ru/open-gl-course/blob/ch1-indexed-draw/runtime/section02/IndexedDrawRunner.cpp

2.1. Vertex 및 Index buffer 구성하기

우선 아래와 같이 vertex 정보를 구성한다.

    // NDC 좌표계에서 삼각형을 구성하는 vertex들의 위치.
    GLfloat vertices[] = {
        -1.0f, -1.0f, 0.0f,
        0.0f, -1.0f, 1.0f,
        1.0f, -1.0f, 0.0f,
        0.0f, 1.0f, 0.0f
    };

또한 어떻게 그릴 지에 대한 index 정보를 담은 indices 배열을 구성하자.
4개 중 3개씩 뽑아 면을 하나씩 구성하면 총 4개의 면이 나올 것이다.

    unsigned int indices[] = {
        0, 3, 1,
        1, 3, 2,
        2, 3, 0,
        0, 1, 2
    };

이제 이 index 정보들로 index buffer object를 구성하자.
이것 역시 Buffer Object이므로 VBO를 만들듯 버퍼 생성 - 버퍼 바인딩 - 버퍼 데이터 채우기 순서로 진행하면 된다.
이 때 target parameter에 GL_ELEMENT_ARRAY_BUFFER를 사용해준다.

// Index Buffer Object 생성 및 Bind.
glGenBuffers(1, &IBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

이 과정을 담은 CreateTriangle 함수 전체는 다음과 같다.

void IndexedDrawRunner::CreateTriangle()
{

    unsigned int indices[] = {
        0, 3, 1,
        1, 3, 2,
        2, 3, 0,
        0, 1, 2
    };
    // NDC 좌표계에서 삼각형을 구성하는 vertex들의 위치.
    GLfloat vertices[] = {
        -1.0f, -1.0f, 0.0f,
        0.0f, -1.0f, 1.0f,
        1.0f, -1.0f, 0.0f,
        0.0f, 1.0f, 0.0f
    };

    // VAO 생성 및 Bind - 이 밑에 생성되는 VBO들을 VAO에 자동으로 Bind.
    glGenVertexArrays(1, &VAO);
    glBindVertexArray(VAO);

    // Index Buffer Object 생성 및 Bind.
    glGenBuffers(1, &IBO);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    // VBO 생성 및 Bind.
    glGenBuffers(1, &VBO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);

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

    // Attribute 선언 및 활성화
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(0);

    // Index Buffer Object binding 다시 정리해주기
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    // VBO binding 다시 정리해주기
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    // VAO binding 다시 정리해주기
    glBindVertexArray(0);
}

2.2. IndexBuffer 이용한 Drawing - GLDrawElements

이제 필요한 것들을 준비했으니 Draw 하면 된다.
기존에는 glDrawArrays를 통해 context에 binding된 VBO를 그렸다.
이제는 glDrawElements 를 통해 VAO context에 binding된 VBO를 IBO에 저장된 index buffer대로 그려주면 된다.

해당 부분을 포함한 렌더링 루프는 다음과 같이 구성한다.

while (!glfwWindowShouldClose(mainWindow))
{
    // Get + handle user input events.
    glfwPollEvents();

    // Clear window.
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT); // clear color buffer and depth buffer.

    // glDrawArrays(GL_TRIANGLES, 0, 3);
    glDrawElements(GL_TRIANGLES, 3 * 4, GL_UNSIGNED_INT, 0);

    glfwSwapBuffers(mainWindow);
}

2.3. y축으로 Rotate 시켜 모든 면이 제대로 렌더링 되었는 지 확인하기

하지만 이렇게만 구성하면 하나의 면만 보일 것이다.
당연하다. z축으로 길게 있는 면은 가려지니까.
그래서 y축을 기점으로 회전해가며 3개의 면을 볼 수 있게 코드 구성을 변경해보자.

우선 vertex shader 및 fragment shader는 xyz위치에 따라 다른 rgb 값을 갖도록 다음과 같이 구성해보자.
vertex shader에서 위치 값의 vec3를 저장해 fragment shader로 넘겨주고 그것을 color로 띄우는 형식이다.

const char* IndexedDrawRunner::vShader =
    "#version 330\n"
    "layout (location = 0) in vec3 pos;\n"
    "uniform mat4 model;\n"
    "out vec4 vCol;\n"
    "void main()\n"
    "{\n"
    "   vCol = vec4(clamp(pos, 0.0f, 1.0f), 1.0f);\n"
    "   gl_Position = model * vec4(pos * 0.4f, 1.0);\n"
    "}\n";
const char* IndexedDrawRunner::fShader =
    "#version 330\n"
    "out vec4 color;\n"
    "in vec4 vCol;\n"
    "void main()\n"
    "{\n"
    "   color = vCol;\n"
    "}\n";

또한 Rotate 가능하도록 Rotate 하는 코드를 추가해준다.
Rotate는 행렬을 이용한 간단한 선형 변환 연산으로 구현한다.

// RotateTriangle
void IndexedDrawRunner::RotateTriangle()
{
    currentAngle += 0.50f;
    if (currentAngle >= 360.0f)
    {
        currentAngle -= 360.0f;
    }
    glm::mat4 translateMatrix(1.0f); // identity matrix.
    translateMatrix = glm::rotate(translateMatrix, currentAngle * toRads, glm::vec3(0.0f, 1.0f, 0.0f)); // 세 번째 파라미터는 회전축. y-axis
    glUniformMatrix4fv(uniformModel, 1, GL_FALSE, glm::value_ptr(translateMatrix));
}

또한 Z축의 depth test 또한 가능하도록 변경해야 회전 시 뒷 쪽에 있는 면이 제대로 렌더링 될 것이다.
그것을 위해 우선 Glfw 및 Glew 환경을 준비할 때 depth test를 enable 시켜준다.

glEnable(GL_DEPTH_TEST); // enable depth testing.

이 다음에는 Render Loop에서 color buffer 만 비우지 말고 depth buffer도 함께 비워주도록 하자.

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // clear color buffer and depth buffer.

2.4. 전체 구성

이제 전체 코드를 구성해보면 다음과 같다.

//
// Created by ypp06 on 2025-02-02.
//

#include "IndexedDrawRunner.h"
#include <iostream>
#include <string.h>
#include <GL/glew.h>
#include <GLFW/glfw3.h>

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

// 전역 변수 정의
const GLint IndexedDrawRunner::WIDTH = 800;
const GLint IndexedDrawRunner::HEIGHT = 600;
GLuint IndexedDrawRunner::VAO = 0;
GLuint IndexedDrawRunner::VBO = 0;
GLuint IndexedDrawRunner::IBO = 0;
GLuint IndexedDrawRunner::uniformModel = 0;
GLuint IndexedDrawRunner::shaderProgram = 0;

// Shader 소스 코드 정의
const char* IndexedDrawRunner::vShader =
    "#version 330\n"
    "layout (location = 0) in vec3 pos;\n"
    "uniform mat4 model;\n"
    "out vec4 vCol;\n"
    "void main()\n"
    "{\n"
    "   vCol = vec4(clamp(pos, 0.0f, 1.0f), 1.0f);\n"
    "   gl_Position = model * vec4(pos * 0.4f, 1.0);\n"
    "}\n";

const char* IndexedDrawRunner::fShader =
    "#version 330\n"
    "out vec4 color;\n"
    "in vec4 vCol;\n"
    "void main()\n"
    "{\n"
    "   color = vCol;\n"
    "}\n";

const float IndexedDrawRunner::toRads = 3.14159265f / 180.0f;

// make currentAngle = 0;
IndexedDrawRunner::IndexedDrawRunner()
{
    currentAngle = 0.0f;
}

// Add Shader to Program.
// Shader에 대해 1. source 지정, 2. compile, 3. targetProgram에 attach.
void IndexedDrawRunner::AddShader(GLuint targetProgram, const char* rawShaderCode, GLenum shaderType)
{
    GLuint targetShader = glCreateShader(shaderType);

    const GLchar* code[1];
    code[0] = rawShaderCode;

    GLint codeLength[1];
    codeLength[0] = strlen(rawShaderCode);

    glShaderSource(targetShader, 1, code, codeLength);
    glCompileShader(targetShader);

    GLint result = 0;
    GLchar eLog[1024] = { 0 };

    glGetShaderiv(targetShader, GL_COMPILE_STATUS, &result);
    if (!result)
    {
        glGetShaderInfoLog(targetShader, sizeof(eLog), NULL, eLog);
        printf("Error compiling the %d shader: '%s'\n", shaderType, eLog);
        return;
    }

    glAttachShader(targetProgram, targetShader);
    return;
}

void IndexedDrawRunner::CompileShaders()
{
    shaderProgram = glCreateProgram();
    if (!shaderProgram)
    {
        printf("Error creating shader program!\n");
        return;
    }

    AddShader(shaderProgram, vShader, GL_VERTEX_SHADER);
    AddShader(shaderProgram, fShader, GL_FRAGMENT_SHADER);

    GLint result = 0;
    GLchar eLog[1024] = { 0 };

    glLinkProgram(shaderProgram);

    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &result);
    // 제대로 링크되지 않았다면.
    if (!result)
    {
        glGetProgramInfoLog(shaderProgram, sizeof(eLog), NULL, eLog);
        printf("Error linking program: '%s'\n", eLog);
        return;
    }

    // Validate program.
    glValidateProgram(shaderProgram);

    glGetProgramiv(shaderProgram, GL_VALIDATE_STATUS, &result);
    if (!result)
    {
        glGetProgramInfoLog(shaderProgram, sizeof(eLog), NULL, eLog);
        printf("Error validating program: '%s'\n", eLog);
        return;
    }
    uniformModel = glGetUniformLocation(shaderProgram, "model");
}

void IndexedDrawRunner::CreateTriangle()
{

    unsigned int indices[] = {
        0, 3, 1,
        1, 3, 2,
        2, 3, 0,
        0, 1, 2
    };
    // NDC 좌표계에서 삼각형을 구성하는 vertex들의 위치.
    GLfloat vertices[] = {
        -1.0f, -1.0f, 0.0f,
        0.0f, -1.0f, 1.0f,
        1.0f, -1.0f, 0.0f,
        0.0f, 1.0f, 0.0f
    };

    // VAO 생성 및 Bind - 이 밑에 생성되는 VBO들을 VAO에 자동으로 Bind.
    glGenVertexArrays(1, &VAO);
    glBindVertexArray(VAO);

    // Index Buffer Object 생성 및 Bind.
    glGenBuffers(1, &IBO);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    // VBO 생성 및 Bind.
    glGenBuffers(1, &VBO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);

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

    // Attribute 선언 및 활성화
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(0);

    // Index Buffer Object binding 다시 정리해주기
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    // VBO binding 다시 정리해주기
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    // VAO binding 다시 정리해주기
    glBindVertexArray(0);
}

// RotateTriangle
void IndexedDrawRunner::RotateTriangle()
{
    currentAngle += 0.50f;
    if (currentAngle >= 360.0f)
    {
        currentAngle -= 360.0f;
    }
    glm::mat4 translateMatrix(1.0f); // identity matrix.
    translateMatrix = glm::rotate(translateMatrix, currentAngle * toRads, glm::vec3(0.0f, 1.0f, 0.0f)); // 세 번째 파라미터는 회전축. y-axis
    glUniformMatrix4fv(uniformModel, 1, GL_FALSE, glm::value_ptr(translateMatrix));
}

// return NULL if failed.
GLFWwindow* IndexedDrawRunner::ReadyGlfwGlewEnv()
{
    // init GLFW.
    if(!glfwInit())
    {
        printf("GLFW init fail");
        glfwTerminate();
        return NULL;
    }

    // Setup GLFW window properties.
    // OpenGL Version.
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);

    // Core profile = No Backwards Compatible.
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    // Allow forward compatible.
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);

    GLFWwindow* mainWindow = glfwCreateWindow(WIDTH, HEIGHT, "Hello Window", NULL, NULL);
    if (!mainWindow)
    {
        printf("glfwCreateWindow failed!");
        glfwTerminate();
        return NULL;
    }

    // Get Buffer Size Info.
    int bufferWidth, bufferHeight;
    glfwGetFramebufferSize(mainWindow, &bufferWidth, &bufferHeight);

    // Set Context for GLEW to use.
    glfwMakeContextCurrent(mainWindow);

    // Allow modern extension feats.
    glewExperimental = GL_TRUE;

    if (glewInit() != GLEW_OK)
    {
        printf("glew init failed");
        glfwDestroyWindow(mainWindow); // 이 시점에서 window가 생성되었으니 지워야 한다.
        glfwTerminate();
        return NULL;
    }

    glEnable(GL_DEPTH_TEST); // enable depth testing.

    // Setup Viewport size.
    glViewport(0, 0, bufferWidth, bufferHeight);

    return mainWindow;
}

bool IndexedDrawRunner::Run()
{
    GLFWwindow* mainWindow = ReadyGlfwGlewEnv();
    if (!mainWindow)
    {
        return false;
    }

    CreateTriangle();
    CompileShaders();
    glUseProgram(shaderProgram);
    glBindVertexArray(VAO);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IBO);
    // loop til window closed.
    while (!glfwWindowShouldClose(mainWindow))
    {
        // Get + handle user input events.
        glfwPollEvents();

        // Clear window.
        glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // clear color buffer and depth buffer.

        RotateTriangle();

        // glDrawArrays(GL_TRIANGLES, 0, 3);
        glDrawElements(GL_TRIANGLES, 3 * 4, GL_UNSIGNED_INT, 0);
        glfwSwapBuffers(mainWindow);
    }
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
    glUseProgram(0);
    return true;
}

그럼 다음과 같이 회전하는 정사면체가 잘 보일 것이다.
index buffer를 이용한 다면체 렌더링이 성공했다.

Refs

https://www.udemy.com/course/graphics-with-modern-opengl/

https://www.youtube.com/watch?v=KG9ZXKaJWwY

댓글