티스토리 뷰

[그래픽스] Hello Triangle - OpenGL에서 삼각형 띄워보기

0. 개요

opengl(GLEW/GLFW)을 이용하여 간단하게 빨간 삼각형을 띄워본다.
이 글에서 커버하는 지식 범위는 다음과 같다.

  • 무식한 방식으로의 vertex/fragment shader 생성 및 컴파일
  • VBO/VAO 다루기

완성된 코드를 바로 보고 싶다면 아래의 repo를 바로 참고해도 된다.

https://github.com/9ru9ru/open-gl-course/blob/ch1-tello-triangle-fix-tab/runtime/section02/FirstTriangleRunner.cpp

1. 삼각형을 띄우기 위한 work-flow 개략

다음과 같은 work-flow로 삼각형을 띄워보자.

  1. GLFW 초기화 및 셋업 / GLEW 초기화 및 셋업 / 창 관련 설정
  2. vertex 데이터로 삼각형 생성 후 context에 binding.
  3. 작성한 vertex/fragment 셰이더 추가 및 컴파일
  4. 렌더링 메인 루프 실행

2. 코드 작성

우선 전체 코드는 다음과 같다.(tab이 뭔가 이상하게 적용되었을 수 있다. 복붙이 필요하면 repo 확인을 추천)


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

const GLint WIDTH = 800, HEIGHT = 600;
GLuint VAO, VBO, shaderProgram;

// Create Vertex Shader in GLSL.
static const char* vShader =
    "#version 330\n"
    "layout (location = 0) in vec3 pos;\n"
    "void main()\n"
    "{\n"
    "   gl_Position = vec4(pos.x, pos.y, pos.z, 1.0);\n"
    "}\n";

// Create Fragment Shader in GLSL.
static const char* fShader =
    "#version 330\n"
    "out vec4 color;\n"
    "void main()\n"
    "{\n"
    "   color = vec4(1.0, 0.0, 0.0, 1.0);\n"
    "}\n";

// Add Shader to Program.
// Shader에 대해 1. source 지정, 2. compile, 3. targetProgram에 attach.
void FirstTriangleRunner::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 FirstTriangleRunner::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;
    }

}

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

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

    // 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);

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

// return NULL if failed.
GLFWwindow* FirstTriangleRunner::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;
    }

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

    return mainWindow;
}

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

    CreateTriangle();
    CompileShaders();
    glUseProgram(shaderProgram);
    glBindVertexArray(VAO);
    // 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);

        glDrawArrays(GL_TRIANGLES, 0, 3);

        glfwSwapBuffers(mainWindow);
    }
    glBindVertexArray(0);
    glUseProgram(0);
    return true;
}

위의 GLEW/GLFW 준비를 위한 코드는 기존에 설명한 바 있으니 삼각형을 만드는 데 관련있는 로직에 대한 설명만 작성한다.
차근차근 가보자.

2.1. Vertex Data로 삼각형 생성 후 Context에 binding하기

삼각형을 생성하는 CreateTriangle 함수의 전체는 다음과 같다.

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

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

    // 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);

    // VBO unbind
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    // VAO unbind
    glBindVertexArray(0);
}

한 부분씩 설명하겠다.

vertex input 준비

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

삼각형을 구성할 세 vertex의 좌표를 NDC 기준으로 선언한다.

화면에 꽉 찬 삼각형으로 구성했다.

NDC(Normalized Device Coordinates)란?

NDC란 View Plane(모니터에 실제 뜨는 화면)에 대해 정규화된 좌표계다.
3D Object 등도 결국 모니터에 뜰 때는 여러 변환 과정(Projection 등)을 거쳐 2D로 변환될 텐데, 이 때 최종적으로 표시되는 좌표계를 정규화한 것이 NDC다.
따라서 이 좌표계에서는 해상도에 따른 픽셀 좌표 차이를 고민할 필요가 없다.
정규화되어 있으므로 -1 ~ +1 사이의 값을 가진다.

VAO 및 VBO를 통해 vertex data 저장

우선 해당 data를 vertex shader 쪽으로 보내기 위해서는 다음 방법을 구성해야 한다.

  • GPU에 메모리를 생성하여 vertex data 저장.
  • opengl이 메모리를 해석하는 방식 구성.
  • 그래픽 카드로 데이터를 전송하는 방법을 지정.

그래서 opengl에서는 VBO를 통해 이 메모리 관리 작업들을 진행할 수 있다.
VBO를 사용하면 다음과 같은 장점이 있다.

  • 한 번에 GPU에 대량의 vertex batch를 보낼 수 있다.(느린 작업이기 때문에 한 번에 하는 게 좋음.)
  • GPU에 대량의 data를 보관할 수 있다.

일단 VBO만 있어도 이론 상 삼각형을 띄울 수 있으나 state 및 attribute의 관리 용이성을 위해 VAO를 선언하자.

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

이제 VBO를 만들고 vertex data를 넣어보자.
이전 챕터에서 선언한 것과 같이 생성과 context에의 binding을 진행한다.

// VBO 생성 및 Bind.
glGenBuffers(1, &VBO); //size & buffer pointer.
glBindBuffer(GL_ARRAY_BUFFER, VBO); // buffer type & buffer obj.

특히 glBindBuffer에서 GL_ARRAY_BUFFER는 buffer type으로 opengl에는 buffer type이 많기 때문에 따로 지정해주는 것이다.
이렇게 버퍼 생성 및 context binding이 끝났으면 우리가 GL_ARRAY_BUFFER 를 target으로 하는 모든 buffer call은 VBO를 구성하는 데 사용된다.

이제 버퍼를 채우기 위해 glBufferData 를 사용할 수 있다.

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

glBufferData는 buffer object에 데이터를 할당하는 데 사용되는 함수다.
(기존에 할당된 데이터가 있었다면 기존 데이터는 지워진다.)

GL_ARRAY_BUFFER target으로 지정된 buffer에 vertex 데이터만큼의 size를 확보하고 vertex data를 쓴다.
꽤 자주 사용되는 함수이므로 다음 API 읽어보면 좋다.

https://registry.khronos.org/OpenGL-Refpages/gl4/html/glBufferData.xhtml

Vertex Attribute 선언 및 Link

이전 글에서 설명했듯이 VBO 데이터 덩어리는 그 해석 방법이 없는 단순 데이터 쪼가리일 뿐이다.
xyz로 나열된건지 uv나 color값이 따로 지정된건지 모른다.
따라서 그걸 opengl에 이해시켜주기 위해 vertex의 속성을 나타내는 Vertex Attribute의 선언 및 링크가 필요하다.
우선 현재의 데이터 나열은 아래와 같이 해석해야 할 것이다.

  • offset은 없다.(시작 시 위치 차이)
  • stride는 12다.(한 종류의 데이터가 다시 등장할 떄의 보폭)
  • xyz순서로 compact하게 나온다.

이 Attribute 정보를 선언하려면 다음과 같이 코드를 작성한다.

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

glVertexAttribPointer는 opengl이 어떻게 buffer data를 해석해야 하는지 알려주는 함수로서 파라미터는 다음 순서로 적는다.

  • GLuint index: vertex attribute의 index. 현재 0으로 선언.
  • GLint size: vertex attribute의 수, xyz로 3개.
  • GLenum type: buffer에 저장된 데이터의 type. 현재 vertex는 GL_FLOAT로 선언.
  • GLboolean normalized: 정규화를 해야하는지의 여부, 0과 1 밖에 있는 값이 전부 여기로 매핑된다.
  • GLsizei stride: 보폭, 0으로 적으면 알아서 해준다.
  • const void * pointer: buffer에서 data가 시작하는 위치의 offset에 대한 pointer. 0으로 시작하므로 그 위치에 대한 값을 적어주면 된다. 0으로 적어도 알아서 형 변환 될거니까 그냥 그렇게 적어도 된다. 정석은 (**void***)0

만약 glVertexAttribPointer을 정석으로 적어본다면 다음과 같이 적을 수 있다.

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

VBO VAO Unbind

이제 VBO와 VAO를 핸들하여 데이터를 삽입하고 attribute를 링크 및 활성화했으니 Context로부터 Unbind 해줘야 한다. pointer 0으로 해주면 된다.

    // VBO unbind
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    // VAO unbind
    glBindVertexArray(0);

2.2. Vertex/Fragment Shader 작성 및 추가, 컴파일

이제 vertex에 대한 데이터 준비는 완료됐다.
어떻게 렌더링할 것인지를 알려줘야 한다.
그것이 vertex fragment shader의 stage다.

생각해보면 각각의 역할은 다음과 같다.

  • vertex shader: vertex 위치에 대한 연산 처리.
  • fragment(pixel) shader: pixel의 출력 color 결정.

우리는 삼각형을 띄우기 위해 다음과 같이 역할을 만들어 볼 것이다.

  • vertex shader: 입력된 vertex의 좌표를 그대로 출력, 단 현재 z가 비어있으므로 고정된 1 값으로 지정.
  • fragment shader: 빨간색으로 지정.

opengl 프레임워크에서 Shader를 이용하기 위해서는 다음과 같은 단계가 필요하다.

  • Shader Code를 작성한다.
  • Shader를 생성하고 Shader Code를 source로 지정.
  • Shader를 컴파일한다.
  • Shader를 Shader Program에 붙인다.
  • Shader Program을 Link한다.
  • Shader Program을 Validate한다.
  • Shader Program을 Use한다.

이 과정이 다소 복잡하지만 절차가 헷갈리는 거야 계속 보다보면 익숙해지는 것이고, 결국 어떤 구조를 만들 것인지를 이해하는 게 중요한 것 같다.

우리는 위의 구조처럼 fShader, vShader를 만들고 컴파일하여 그것들을 부착한 ShaderProgram을 Use하는 방식으로 Shader를 사용할 것이다.

Vertex Shader 및 Fragment Shader의 작성

아래의 목적을 이루는 셰이더를 작성해보자.

  • vertex shader: 입력된 vertex의 좌표를 그대로 출력, 단 현재 z가 비어있으므로 고정된 1 값으로 지정.
  • fragment shader: 삼각형 내부를 빨간색으로 지정.

Vertex Shader의 작성

우선 Vertex Shader는 아래와 같이 작성할 수 있다.

// Create Vertex Shader in GLSL.
static const char* vShader =
    "#version 330\n"
    "layout (location = 0) in vec3 pos;\n"
    "void main()\n"
    "{\n"
    "   gl_Position = vec4(pos.x, pos.y, pos.z, 1.0);\n"
    "}\n";

string화를 제외한 raw code는 아래와 같다.

#version 330
layout (location = 0) in vec3 pos;
void main()
{
   gl_Position = vec4(pos.x, pos.y, pos.z, 1.0);
};

하나하나의 의미를 살펴보자.

#version 330

이 부분은 opengl 버전명시

layout (location = 0) in vec3 aPos;

이 부분은 Vertex Attribute를 선언하는 부분.

  • layout (location = 0)버텍스 속성의 슬롯(위치)을 0번으로 지정
  • in vec3 aPos;버텍스 셰이더로 전달되는 입력 변수 선언 (3D 좌표값을 받음)

이제 gl_Position에 지정함으로서 vertex 값 연산을 완료한다.
우리는 별다른 연산 없이 z에 1만 갖다 박을거니까 그렇게 하면 된다.

void main()
{
   gl_Position = vec4(pos.x, pos.y, pos.z, 1.0);
};

Fragment Shader의 작성

fragment Shader는 아래와 같이 작성할 수 있다.

// Create Fragment Shader in GLSL.
static const char* fShader =
    "#version 330\n"
    "out vec4 color;\n"
    "void main()\n"
    "{\n"
    "   color = vec4(1.0, 0.0, 0.0, 1.0);\n"
    "}\n";

string화를 제외한 raw code는 아래와 같다.

#version 330
out vec4 color;
void main()
{
   color = vec4(1.0, 0.0, 0.0, 1.0);
}

version 커맨드는 기존과 같은 역할이므로 따로 설명하지 않는다.

out vec4 color;

frament shader는 1개의 vec4 output을 필요로 한다.
따라서 out 키워드를 통해 선언해준다. 변수명은 상관 없다.

void main()
{
   color = vec4(1.0, 0.0, 0.0, 1.0);
}

이 부분을 통해 컬러를 빨간색으로 지정해준다.

Shader 객체 생성 및 컴파일, ShaderProgram에의 부착.

작성한 GLSL rawcode를 source로 하는 Shader 객체를 생성하고 컴파일, Shader Program에 부착해보자.
이 과정을 하나의 함수로 묶어보면 다음과 같다.

// Add Shader to Program.
// Shader에 대해 1. source 지정, 2. compile, 3. targetProgram에 attach.
void FirstTriangleRunner::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;
}

parameter로 다음을 받는다.

  • GLuint targetProgram: 타겟 ShaderProgram.
  • const char* rawShaderCode: 아까 위에서 작성한 raw한 shader code.
  • GLenum shaderType: 만들 셰이더 타입, vertex나 frag 등.

이제 shader object를 생성하고 compile을 진행해보자.

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);

하나씩 살펴보자.

우선 이 코드를 통해 shader object를 생성한다.

GLuint targetShader = glCreateShader(shaderType);

또한 code와 codeLength 배열을 준비하여 source code를 연결해준다.

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

GLint codeLength[1];
codeLength[0] = strlen(rawShaderCode);
glShaderSource(targetShader, 1, code, codeLength);

source code 연결 이후 compile시킨다.

glCompileShader(targetShader);

그 다음의 코드들은 compile 정보를 로깅하고 compile error를 출력하는 부분이다.

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;
}

별 에러가 없을 시 다음 부분에서 shader를 shaderProgram 객체에 붙인다.

glAttachShader(targetProgram, targetShader);

셰이더 최종 완성

앞선 빌드업 이후 셰이더 프로그램을 최종적으로 완성하는 부분.

void FirstTriangleRunner::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;
    }

}

우선 아래 부분을 이용하여 Shader Program Object를 만들어준다.

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

glCreateProgram은 Program Object를 만들어주는 함수다.
Program Object는 shader가 붙을 수 있는 객체를 의미한다.(이제 정체를 알았다..!)
해당 부분 이후 바로 위에서 만들었던 함수를 호출하여 vertex shader와 fragment shader object를 만들고 Program Object에 붙이자.

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

그 이후 Program Object를 링크하고 제대로 되지 않은 경우 로그를 확인한다.

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;
}

참고: Program Object의 링크 과정이란
→ 여러 개의 셰이더(예: 버텍스 셰이더, 프래그먼트 셰이더 등)를 하나의 실행 가능한 GPU 프로그램으로 결합하는 과정

이제 아래의 glValidateProgram을 통해 유효한지 확인하고 셰이더의 준비를 끝낸다.
→ 정확히는 현재 opengl state에서 program에 존재하는 executable이 실행가능한 지 확인하는 과정.

// 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;
}

2.3. 렌더링 메인 루프 실행

좋아 이제 위에서

  • vertex input 관련 준비
  • 셰이더 준비

했으니 렌더링만 하면 된다.
앞뒤 맥락 파악 위해 렌더링 루프를 포함한 Run 메서드를 보면 다음과 같다.

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

    CreateTriangle();
    CompileShaders();

    // 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);

        glUseProgram(shaderProgram);

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

        glBindVertexArray(0);
        glUseProgram(0);
        glfwSwapBuffers(mainWindow);
    }

    return true;
}

우선 뭐 아래 부분은 GLEW/GLFW 관련 셋업 부분이다.

GLFWwindow* mainWindow = ReadyGlfwGlewEnv();
if (!mainWindow)
{
    return false;
}

(ReadyGlfwGlewEnv()가 궁금하면 위의 repo링크를 타면 된다.)

이제 아까 만들어둔 삼각형 생성 및 셰이더 프로그램 생성을 진행한다.
둘 중 뭘 먼저 실행하든 상관 없다.

CreateTriangle();
CompileShaders();

이제 메인 루프를 실행하자.
메인 루프의 전체는 다음과 같다.

// 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);

    glUseProgram(shaderProgram);

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

    glBindVertexArray(0);
    glUseProgram(0);
    glfwSwapBuffers(mainWindow);
}

아래 부분을 통해 Shader Program Object의 사용을 선언해준다.

glUseProgram(shaderProgram);

이제 아까 만든 VBO가 binding되어있는 VAO object를 현재 context에 binding시킨다.

glBindVertexArray(VAO);

이제 그릴 건 명확히 삼각형이다.
VAO는 context에 binding되어있으므로 이제 그냥 그려달라고 말하면 된다.

glDrawArrays(GL_TRIANGLES, 0, 3);

glDrawArrays 함수는 현재 bind된 VBO의 vertex들을 그려주는 함수로, parameter 구성은 다음과 같다.

  • GLenum mode: 그릴 때의 primitive를 뭘로 할건지(삼각형, 선, quad 등)
  • GLint first: 현재 array에서 start index.
  • GLsizei count: 몇 개의 vertex를 render할 건지.

이후 아래 부분을 통해 현재 bind된 object들을 context에서 unbind 해준다.
(매 프레임 돌아가므로 매 프레임 bind - unbind가 진행될 것이다..!)

glBindVertexArray(0);
glUseProgram(0);

이제 이전에 그려졌던 buffer와 현재 buffer를 swap해서 현재 buffer에 담긴 내용을 창에 render하자.

glfwSwapBuffers(mainWindow);

이럼 렌더링이 끝난다. 실행을 눌러보면 다음과 같이 결과가 뜬다… 짠!

참고: 여기서 눈치챈 사람도 있겠지만.. bind unbind는 아래 코드와 같이 rendering loop 전후로 빼놔도 프로그램은 별 이상 없이 실행된다.(그리고 아마 이게 맞는 방향일거다.)
하지만 조금 더 렌더링 로직을 뭉치기 위해 루프 안에 두었다..! 비난 방지용으로 적어둔다.

Refs

https://www.udemy.com/course/graphics-with-modern-opengl/
https://learnopengl.com/Getting-started/Hello-Triangle
https://registry.khronos.org/OpenGL-Refpages/gl4/html/glValidateProgram.xhtml
https://www.khronos.org/opengl/wiki/Shader_Compilation#Program_linking
https://registry.khronos.org/OpenGL-Refpages/gl4/html/glBufferData.xhtml
https://registry.khronos.org/OpenGL-Refpages/gl4/html/glVertexAttribPointer.xhtml

댓글