게임 프로그래머의 만담, 게임코디 2nd
+5 투표
542 views 만담 (1.9kp)
수정됨 , 542 views

지난 강좌

1. OpenGL 윈도우 만들기

2. 삼각형 그리기

 


 

전체 소스 코드 : https://github.com/sunduk/opengl-3d-examples

 

3. 삼각형 컬러 넣기

 

인덱스 버퍼

이전 시간에 버텍스 버퍼를 통해 삼각형을 출력하는 프로그램을 만들어 봤습니다. 이번에는 인덱스 버퍼라는 개념에 대해 알아보겠습니다.

만약 삼각형 두개를 합쳐 사각형 하나를 만든다고 하면 버텍스는 총 6개가 필요할 것입니다. 그런데 이 중에서 면을 맞닿고 있는 두개의 버텍스는 완전히 같은 위치이므로 이 버텍스들은 없어도 무방한 것들 입니다. 이렇게 겹치는 버텍스들을 제거하고 각 버텍스의 위치를 가리키고 있는 인덱스 버퍼를 사용하여 폴리곤을 렌더링 할 수 있다면 성능이 훨씬 올라갈 것입니다.

// 인덱스 값을 보관할 배열.
std::vector<unsigned int> mIndices{};

// OpenGL에서 생성한 인덱스 버퍼를 보관할 ID.
unsigned int mElementBufferObjectId{};

OpenGL에서 인덱스 버퍼를 사용하려면 우선 인덱스값을 보관할 배열과 버퍼ID를 기억할 변수가 필요합니다. 기존에 사용했던 버텍스 버퍼도 삭제하면 안됩니다. 인덱스 버퍼는 버텍스 버퍼를 대체하는 개념이 아니라 최적화를 위해 추가된 개념이기 때문이죠.

void Example02::CreateTriangle()
{
    // NDC(Normalized Device Coordinate) 기준으로 삼각형의 버텍스 좌표를 설정한다.
    mVertices.push_back(glm::vec3(0.0f, 1.0f, 0.0f));       // center top.
    mVertices.push_back(glm::vec3(-1.0f, -1.0f, 0.0f));     // left bottom.
    mVertices.push_back(glm::vec3(1.0f, -1.0f, 0.0f));      // right bottom.

    // (1) 추가된 부분
    // 인덱스를 설정한다.
    mIndices.push_back(0);  // first vertex.
    mIndices.push_back(1);  // second vertex.
    mIndices.push_back(2);  // third vertex.
}

인덱스 배열에 값을 채워넣는 부분이 추가되었습니다. 삼각형을 구성하는 버텍스의 순서대로 인덱스를 만들어준다고 보면 됩니다.

 

oid Example02::CreateVertexBuffer()
{
    // 버텍스 버퍼와 인덱스 버퍼를 보관할 "vertex array" 오브젝트를 생성한다.
    glGenVertexArrays(1, &mVertexArrayObjectId);
    // Create vertex buffer.
    glGenBuffers(1, &mVertexBufferObjectId);
    
    // (1) 추가된 부분
    glGenBuffers(1, &mElementBufferObjectId);

    // Bind VAO.
    glBindVertexArray(mVertexArrayObjectId);
    {
        // 버텍스 데이터를 버텍스 버퍼에 올린다.
        glBindBuffer(GL_ARRAY_BUFFER, mVertexBufferObjectId);
        {
            glBufferData(GL_ARRAY_BUFFER, mVertices.size() * sizeof(glm::vec3), &mVertices[0], GL_STATIC_DRAW);
        }

        // (2) 추가된 부분
        // 인덱스 데이터를 인덱스 버퍼(element array buffer)에 올린다.
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mElementBufferObjectId);
        {
            glBufferData(GL_ELEMENT_ARRAY_BUFFER, mIndices.size() * sizeof(unsigned int), &mIndices[0], GL_STATIC_DRAW);
        }

        // 버텍스 속성(attribute)을 설정한다.
        glEnableVertexAttribArray(0);
        {
            glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), nullptr);
        }
    }

    // Unbind.
    glBindVertexArray(0);
}

인덱스 버퍼를 설정하고 OpenGL에 등록하는 코드 입니다.

(1) 버텍스 버퍼와 비슷하게 glGenBuffer함수로 인덱스 버퍼 오브젝트를 생성 하여 mElementBufferObjectId 변수에 ID값을 담아 놓습니다.

(2) OpenGL인덱스 버퍼에 인덱스 값들을 올리는 부분 입니다. 변수명이 다른 것을 제외 하면 버텍스 버퍼를 올리는 부분과 큰 차이는 없습니다.

 

마지막으로 버텍스 버퍼를 이용하여 렌더링 하였던 부분을 인덱스 버퍼를 이용하도록 변경 해 줘야 합니다.

void Example02::Render()
{
    // 렌더링에 적용할 셰이더 프로그램을 설정한다.
    glUseProgram(mDefaultShaderID);

    // Bind VAO.
    glBindVertexArray(mVertexArrayObjectId);
    {
        // 버텍스 버퍼를 이용한 삼각형 렌더링.
        //glDrawArrays(GL_TRIANGLES, 0, 3);

        // (1) 추가된 부분
        // 인덱스 버퍼(element buffer)를 이용한 삼각형 렌더링.
        glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(mIndices.size()), GL_UNSIGNED_INT, 0);
    }
    glBindVertexArray(0);
}

인덱스 버퍼를 쓸 때는 glDrawArrays대신 glDrawElements함수를 사용합니다. OpenGL에서는 엘리먼트 버퍼라고도 부르는데 인덱스 버퍼가 더 자주 쓰는 용어 같습니다.

여기까지 하고 실행 해 보면 결과는 똑같지만 내부적으로 인덱스 버퍼를 사용하여 렌더링 된 코드가 돌아가고 있는 것입니다. 여기서는 어차피 버텍스 세 개짜리 삼각형이라서 별 차이가 없습니다만 폴리곤이 더 복잡해질 수록 성능은 크게 차이가 날 것입니다.

 

버텍스 컬러 넣기

현재 우리가 만든 버텍스에는 위치 속성만 들어가 있습니다. 여기에 컬러값을 나타내는 벡터를 하나 추가하고 이것을 OpenGL 셰이더로 넘겨서 렌더링 해 보겠습니다.

이렇게 되면 버텍스에 위치, 컬러 두 가지 변수가 들어가야 하므로 이것을 모두 포함하는 구조체를 만들어 버텍스 배열에 집어 넣도록 하겠습니다.

// 버텍스 좌표와 컬러값을 보관 할 구조체.
struct Vertex
{
    glm::vec3 mPosition{ 0,0,0 };
    glm::vec3 mColor{ 0,0,0 };

    Vertex()
    {
    }

    Vertex(const glm::vec3& position, const glm::vec3& color)
    {
        mPosition = position;
        mColor = color;
    }
};


// 이전에 사용했던 버텍스 배열
//std::vector<glm::vec3> mVertices{};

// Vertex구조체를 사용하도록 변경된 버텍스 배열
std::vector<Vertex> mVertices{};

 

이 구조체에 맞게 각 버텍스마다 컬러값도 넣어 줍니다.

void Example03::CreateTriangle()
{
    glm::vec3 RED = glm::vec3(1, 0, 0);
    glm::vec3 GREEN = glm::vec3(0, 1, 0);
    glm::vec3 BLUE = glm::vec3(0, 0, 1);

    mVertices.push_back(Vertex(glm::vec3(0.0f, 1.0f, 0.0f), RED));       // center top.
    mVertices.push_back(Vertex(glm::vec3(-1.0f, -1.0f, 0.0f), GREEN));     // left bottom.
    mVertices.push_back(Vertex(glm::vec3(1.0f, -1.0f, 0.0f), BLUE));      // right bottom.
    ...
}

 

OpenGL에 버텍스 버퍼를 올리는 부분도 새로 변경된 구조체 크기에 맞춰 수정 해 줘야 합니다. 주석처리된 부분과 추가된 부분을 비교해보면 어떻게 바뀌었는지 쉽게 파악하실 수 있을겁니다.

void Example02::CreateVertexBuffer()
{
    glGenVertexArrays(1, &mVertexArrayObjectId);
    glGenBuffers(1, &mVertexBufferObjectId);
    glGenBuffers(1, &mElementBufferObjectId);

    // Bind VAO.
    glBindVertexArray(mVertexArrayObjectId);
    {
        // 버텍스 데이터를 버텍스 버퍼에 올린다.
        glBindBuffer(GL_ARRAY_BUFFER, mVertexBufferObjectId);
        {
            // (1)
            // 수정 전.
            //glBufferData(GL_ARRAY_BUFFER, mVertices.size() * sizeof(glm::vec3), &mVertices[0], GL_STATIC_DRAW);
            // 수정 후.
            glBufferData(GL_ARRAY_BUFFER, mVertices.size() * sizeof(Vertex), &mVertices[0], GL_STATIC_DRAW);
        }

        // 인덱스 데이터를 인덱스 버퍼(element array buffer)에 올린다.
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mElementBufferObjectId);
        {
            glBufferData(GL_ELEMENT_ARRAY_BUFFER, mIndices.size() * sizeof(unsigned int), &mIndices[0], GL_STATIC_DRAW);
        }

        // 버텍스 속성(attribute)을 설정한다.
        glEnableVertexAttribArray(0);
        {
            // (2)
            // 수정 전.
            //glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), nullptr);
            // 수정 후.
            glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), nullptr);
        }

        // (3)
        // 추가된 부분
        // 버텍스 컬러 속성을 설정한다.
        glEnableVertexAttribArray(1);
        {
            glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, mColor));
        }
    }

    glBindVertexArray(0);
}

 

이제 마지막으로 셰이더 코드를 수정 해 보겠습니다. 추가된 부분을 보면 버텍스 구조체에 맞게 컬러값을 갖고 있는 변수가 들어간 것을 볼 수 있습니다.

void Example03::CreateDefaultShader()
{
    // 버텍스 셰이더 코드.
    std::string vertexCode = "#version 330 core\n"
        "layout(location = 0) in vec3 position;\n"
        // 추가된 부분
        "layout(location = 1) in vec3 vertex_color;\n"
        "out vec3 VertexColor;\n"
        "void main()\n"
        "{\n"
        "   gl_Position = vec4(position, 1.0f);\n"
        // 추가된 부분
        "   VertexColor = vertex_color;\n"
        "}\n";

    // 프래그먼트 셰이더 코드.
    std::string fragmentCode = "#version 330 core\n"
        // 추가된 부분
        "in vec3 VertexColor;\n"
        "out vec4 Color;\n"
        "void main()\n"
        "{\n"
        //"   Color = vec4(1, 0, 0, 1); \n"
        "   Color = vec4(VertexColor, 1); \n"
        "}\n";
...
}

셰이더는 아직 우리에게 생소하지만 간단히 말씀드리면 GPU에서 폴리곤을 렌더링 하기 위해 필요한 코드라고 이해하시면 됩니다. 작동 순서는 버텍스 셰이더 -> 프래그먼트 셰이더 순으로 이루어 집니다.(물론 저 사이와 전후로 다른 과정들도 많이 존재합니다만 간단히 나타내면 이렇습니다.) 

프로그램 코드에서 버텍스 셰이더로 파라미터를 넘기면 그걸 받아 버텍스 셰이더가 작업을 하고 필요한 변수들을 프래그먼트 셰이더로 넘겨 줍니다. 프래그먼트 셰이더는 또 그걸 받아 각 픽셀의 색상을 결정하게 되죠. 이렇게 파이프가 연결된 것처럼 흘러가기 때문에 이러한 과정을 렌더링 파이프라인 이라고 부릅니다.

이렇게 셰이더까지 수정하고 실행 해 보면 아래처럼 알록달록한 삼각형이 렌더링 됩니다. 각 버텍스 컬러끼리 보간 처리가 되어 중간 컬러값 까지 알아서 렌더링 된 것을 볼 수 있습니다. 이 보간 처리는 GPU에서 자동으로 해주는 작업입니다.

 

전체 소스 코드 : Example03.h / cpp (https://github.com/sunduk/opengl-3d-examples)


 

이번 시간에는 중복되는 버텍스를 줄일 수 있는 인덱스 버퍼의 사용 방법과 각 버텍스마다 컬러 속성을 추가하는 방법에 대해 알아봤습니다. 버텍스 버퍼는 위치, 컬러 뿐만 아니라 나중에 배우게 될 노말 벡터, 텍스쳐 좌표등 필요한 것들을 커스텀하게 만들수 있습니다. 이렇게 구성된 버텍스 버퍼는 OpenGL 셰이더로 넘어가 우리가 원하는 형태로 렌더링 할 수 있게 됩니다.

다음번에는 이미지 파일을 폴리곤에 입히는 기법인 텍스쳐 매핑에 대해 배워보겠습니다.

>>> 다음 강좌 : 4. 텍스쳐 입히기

 

감사합니다.

 

만담에 대한 답글을 하려면 로그인 또는 가입해야합니다.

게임 프로그래머의 만담 커뮤니티, 게임코디 입니다

온종일 코드와 씨름하는 우리들의 외로움을 삭이고자 합니다