Skip to content

04. 좌표계와 WVP행렬


목차

  • OpenGL의 좌표계
  • 모델, 월드, 뷰, 클립 공간 간의 변환
  • glm 라이브러리 소개
  • 월드 행렬 // (트랜스폼)
  • 뷰 행렬
  • 투영 행렬 // (원근 나누기, [near, far] 범위 안의 z값을 [-1, 1]로 만들어야 함)
  • 2D 렌더러 구현하기 // (삼각형+사각형 크기조절가능 렌더링, 셰이더 유니폼 설명 및 색 바꾸기)

OpenGL의 좌표계

컴퓨터 그래픽스에서 좌표계(Coordinate System)이란,

3차원의 점이 표현되는 데카르트 좌표계를 뜻합니다.

보통의 수학 / 물리학에서 사용하는 좌표계는

Z축이 위를 향하고, Y축이 오른쪽, X축이 앞을 향합니다.

OpenGL에서 사용되는 좌표계는 무엇일까요?

바로 '왼손 좌표계'와 '오른손 좌표계' 입니다.

왼손의 엄지, 검지, 중지만을 편 상태로

검지로는 위를 향하고 엄지를 오른쪽을 향하면

Z축 중지가 뒤를 향하는 왼손 좌표계가 됩니다.

또 그걸 오른손으로 해보면

중지가 앞을 향하는 오른손 좌표계가 됩니다.

4_0.png

모델, 월드, 뷰, 클립 공간 간의 변환

OpenGL에서 스크린에 무언가를 그리기 위해서는,

저희가 가진 정점들을 적절한 공간(=좌표계)의 정점으로

변환해줄 필요가 있습니다.

OpenGL에서 차례로 변환되는 좌표계를 순서대로 나열하면 다음과 같습니다.

좌표계(공간) 이름 좌표계 방향 범위 설명
모델 공간(Model Space) 직접 정의(ex: Blender 프리셋 등) 실수 범위(3차원) 3D 모델의 정점들이 표현되는 좌표계. 사용한 3D 모델링 툴에 따라 다를 수 있음
월드 공간(World Space) (대개) 오른손 좌표계 실수 범위(3차원) 여러 3D 모델이 배치되는 공간(=월드)을 기준으로 하는 좌표계
뷰 공간(View Space) 오른손 좌표계 실수 범위(3차원) 월드 안에서 위치와 회전을 갖는 카메라를 기준으로 하는 좌표계
클립 공간(Clip Space) 왼손 좌표계 -w <= (x, y, z) <= w w를 기준으로 절두체 클리핑을 하기 위한 좌표계
정규화된 장치 공간(Normalized Device Coordinates) 왼손 좌표계 -1 <= (x, y, z) <= 1 클립 공간에서 원근 나누기가 수행된 이후의 좌표계
윈도우 공간(Window Space) 왼쪽 하단을 원점으로 X축이 오른쪽, Y축이 위 [0, 0] <= (x, y) <= [viewport size], 0 <= z뎁스값 <= 1 실제 스크린에 대응되는 좌표계

2. 삼각형 그리기에서 나오는 순서도를 보면 알 수 있다시피,

공간 변환 과정은 '사용자가 해야 하는 변환' 과 'OpenGL이 담당하는 변환'으로 나눠집니다.

모델 공간 -> 클립 공간 까지가 사용자가 해야 하는 과정이고,

이후는 OpenGL이 담당하게 되죠.

다시 말해서 사용자가 어떻게든 정점 셰이더에서 gl_Position 을 통해 '클립 공간의 정점'을 제공한다면

렌더링이 정상적으로 진행된다는 뜻입니다.


그러면 사용자 단에서 해야 하는 공간 변환은 어떻게 이루어질까요?

3D 모델의 정점(이건 모델 공간의 점이겠죠?)을 버퍼에 저장했다고 가정하면, 이후 과정은 이렇게 됩니다.

Point_c = P * V * W * Point_m
  1. 모델 공간의 정점에 월드 행렬을 곱해서 월드 공간의 정점으로 만든다.
  2. 월드 공간의 정점에 뷰 행렬을 곱해서 뷰 공간의 정점으로 만든다.
  3. 뷰 공간의 정점에 투영 행렬을 곱해서 클립 공간의 정점으로 만든다.

이 3개의 과정은 정점 셰이더 안에서 다음처럼 수행됩니다.

void main() {

    // 모델 공간의 정점을 정의합니다.
    //     aPos는 버퍼에 기록했던 3차원 점입니다.
    //     (행렬과의 곱셈을 위해 동차좌표로 변환해서
    //         w = 1인 4차원 벡터를 사용해야 합니다)
    vec4 Model_Point = vec4(aPos, 1);

    // 정점 셰이더에는 gl_Position 이라는
    //     미리 정의된 변수가 있습니다.
    //     클립 공간의 정점을 제공하려면 이 변수에 값을 쓰면 됩니다.
    gl_Position = Projection_Matrix * View_Matrix * World_Matrix * Model_Point;
}

Warning

위에서 Projection * View * World 차례로 곱해 가는 순서는, 저희가 정점 셰이더에 제공했던
행렬의 형태가 "열 기반(Column-Major) 행렬" 이란 것에서 연유합니다.
GLSL에서는 곱셈의 순서가 오른쪽 -> 왼쪽으로 적용되고,
올바른 결합을 위해서 열 기반에서는 P-V-W * point 순서를,
행 기반에서는 point * W-V-P 순서를 사용합니다.
(열 / 행 기반을 전환하려면 GLSL의 transpose 함수(전치)를 사용할 수 있습니다)

위처럼 정점 셰이더 내에서 모델 공간 -> 클립 공간으로 변환해도 되지만,

미리 행렬을 곱한 후 이를 정점 셰이더에 넘겨 한 번만 곱해도 됩니다.

즉 CPU에서 Proj * View * World 를 미리 계산한 행렬을

WVP 행렬이라 합니다.

또한 위 순서에서 인접한 공간끼리는 특정 행렬과 그 역을 사용하여 상호 변환이 가능합니다.

아래 순서도를 보면 이해가 수월할 겁니다.

4_1.png

glm 라이브러리 소개

이쯤에서 새롭게 사용할 라이브러리 하나를 소개합니다.

glm 은 OpenGL 등의 그래픽스 API 프로그래밍에 특화된 수학 라이브러리로써,

벡터, 행렬, 쿼터니언 등의 정의와 그 연산을 지원합니다.

또한 이동 행렬, 회전 행렬 등의 부가적인 행렬 함수도 방대하게 지원하기 때문에

수학 라이브러리를 직접 만들지 않는 이상

OpenGL 프로그래밍에서 거의 필수적으로 사용되는 라이브러리 중 하나입니다.

glm을 설치하려면 깃허브 리포 내의 glm 폴더를 복사해 프로젝트에 추가하기만 하면 됩니다.

(glm은 Header-only 라이브러리입니다)

그러면 glm을 사용하여 여러 행렬 클래스를 만들어보도록 하겠습니다.

이 글에서는 우선 2D 렌더링 전용 클래스만을 제작할 것입니다.

3D 에서 쓸 수 있는 월드 / 뷰 / 투영 행렬 클래스는 6. 3D로의 확장 을 참고하세요.

glm을 사용하려면, 다음과 같이

GLM_ENABLE_EXPERIMENTAL 매크로와 함께 glm.hppext.hpp를 포함시켜 주세요.

(단일 소스 파일인 경우만 매크로 쓰시고, 아니라면 전역 전처리기에 추가하세요)

#define GLM_ENABLE_EXPERIMENTAL
#include <glm/glm.hpp>
#include <glm/ext.hpp>

월드 행렬

모델 공간의 정점에 월드 행렬을 곱하면 월드 공간의 정점이 나온다는 것을 알았습니다.

그렇다면 월드 행렬은 어떻게 만들 수 있을까요?

월드 행렬은

  • 물체의 이동
  • 물체의 회전
  • 물체의 스케일
  • (+기울어짐 등..)

같은 '물체의 변형 정보' 를 담는 4 x 4 행렬입니다.

다시 말해서,

  • 물체의 이동을 나타내는 '이동 행렬'
  • 물체의 회전을 나타내는 '회전 행렬'
  • 물체의 스케일을 나타내는 '스케일 행렬'

등의 '여러 행렬의 곱'으로 나타낼 수 있습니다.

이제 클래스 구현을 해볼까요?

class Transform2D final {
private:
    glm::vec2 m_position{ 0.0f, 0.0f };
    float     m_rotation{ 0.0f };
    glm::vec2 m_scale{ 1.0f, 1.0f };
private:
    glm::mat4 m_matrix{ 1.0f };

먼저, 2차원 트랜스폼 정보를 담을 클래스이므로

  • 2차원 위치(X, Y)
  • 회전(=Z축)값 라디안
  • 2차원 스케일(X, Y)
  • 해당 정보를 캐싱해둘 4x4 행렬

이렇게 구성됩니다.

    ...

public:
    Transform2D() { 
        this->_update_matrix(); 
    }
    Transform2D(const glm::vec2& pos, const float& rot, const glm::vec2& scl)
        : m_position(pos), m_rotation(rot), m_scale(scl) {
        this->_update_matrix();
    }
    ~Transform2D() = default;
public:
    const glm::mat4& get_matrix() const { return m_matrix; }
public:
    void set_position(const glm::vec2& value) { m_position = value; this->_update_matrix(); }
    void set_rotation(const float& value){ m_rotation = value; this->_update_matrix(); }
    void set_scale(const glm::vec2& value){ m_scale = value; this->_update_matrix(); }
public:
    const glm::vec2& get_position() const { return m_position; }
    const float& get_rotation() const { return m_rotation; }
    const glm::vec2& get_scale() const { return m_scale; }

    ...

생성자 / 소멸자와 기본적인 set / get 함수를 만들어 주세요.

주의할 점은, 생성자와 setter가 불리는 순간에는

_update_matrix 를 호출하여 m_matrix 를 최신화해야 합니다.

_update_matrix 메서드는 다음과 같이 생겼습니다.

private:
    void _update_matrix() {
        m_matrix = glm::mat4{ 1.0f };

        // 꼭 Y축을 넘길 때 반전해주는 것을 잊지 마세요.
        m_matrix = glm::translate(m_matrix, { m_position.x, -m_position.y, 0.0f });
        m_matrix = glm::rotate(m_matrix, m_rotation, {0.0f, 0.0f, 1.0f});
        m_matrix = glm::scale(m_matrix, { m_scale.x, m_scale.y, 1.0f });
    }
};

먼저 반드시 행렬을 '단위행렬로 초기화' 하고,

다음과 같은 순서를 사용하여 변형 정보를 누적해 가야 합니다.

이번에도 아까와 마찬가지로, 반드시 올바른 순서로 곱해야 합니다. $$ W = T * R * S $$ 여러 행렬을 곱해서 하나로 만들 때, 적용되는 순서는 오른쪽에서 왼쪽입니다.

따라서 해당 순서를 다시 표현하면

  1. 물체의 스케일을 조정한다.
  2. 물체를 회전시킨다.
  3. 물체를 이동시킨다.

입니다.

왜 이런 순서가 나올까요?

저희가 기대하는 '스케일', '회전' 속성이 바뀐다는 것은

'물체의 원점' 을 기준으로 물체가 늘어나거나 돌아가는 상황입니다.

따라서 물체의 원점을 이동시키는 translation 은 가장 마지막에 적용해야 합니다.

또 rotation을 적용한 후에 scaling을 적용한다면 이미 X, Y축이 회전한 후이기에

기괴하게 스케일링이 적용됩니다.

따라서 scaling -> rotation -> translation 순으로 적용되도록 하는 것입니다.


각 glm 함수를 살펴봅시다.

먼저 glm::translate 의 경우, 인자로 받은 행렬을 특정 3차원 벡터만큼 이동시킨 새 행렬을 반환합니다.

지금은 2차원만 다루기에, Z축은 0으로 둡니다.

여기서 m_position.y 를 그대로 쓰지 않고 반전해서 넣는 이유는,

OpenGL의 월드 공간 좌표계는 Y축이 위를 향하는데 반해서

저희가 쓸, '2D 게임 개발 표준 좌표계' 에서는 왼쪽 상단을 원점으로 Y축이 아래를 향하기 때문입니다.

이러한 Renderer-Specific 코드는 OpenGL 개발에만 사용할 거라면 문제되지 않지만,

다른 렌더링 API를 쓸 때는 좌표계 통일 시스템을 구현해주어야 할 겁니다.


glm::rotate 의 경우, 인자로 받은 행렬을 '특정 축' 을 기준으로 '특정 라디안' 만큼 회전시킨 새 행렬을 반환합니다.

저희는 지금 Z축 회전만을 사용하기에, (0, 0, 1) 을 넘겨야 합니다.


glm::scale 은 인자로 받은 행렬을 특정 3차원 벡터만큼 스케일링한 새 행렬을 반환합니다.

'스케일링'이므로 기본값이 0이 아닌 1이란 것에 주의해 주세요.

마찬가지로 X, Y 스케일만을 사용하고, Z축은 1로 넘깁니다.

뷰 행렬

뷰 행렬이란, 말 그대로 바라보는 시점에 대한 정보를 담는 행렬입니다.

카메라로 생각해 보면, 뷰 행렬에는

  • 카메라의 위치
  • 카메라의 회전
  • 카메라 줌 인, 아웃

같은 정보가 들어갑니다.

저 3개는 위에서 구현한 월드 행렬의 각 속성과 똑같은 의미를 가집니다.

그러나 뷰 행렬에서 특별히 조심해 주어야 하는 것은,

'카메라 기준' 위치이기 때문에

이동, 회전, 스케일링 행렬을 역으로 만들어 줘야 한다는 점입니다.

카메라가 어떤 물체를 바라보고 있는 상황을 생각해 봅시다.

카메라가 '카메라 기준' 오른쪽으로 움직이면,

물체는 '카메라 기준' 왼쪽으로 움직이게 됩니다.

회전과 스케일링도 마찬가지로, 카메라가 '카메라 기준' 시계방향으로 회전하면

물체는 '카메라 기준' 반시계방향으로 회전합니다.

요약하면, 뷰 행렬을 구성하기 위해서는

('카메라'는 그릴 수 있는 개념이 아니고, 결국 그려지는 것은 세계이기 때문에)

'카메라가 움직이는 것' 이 아닌, '세계 자체가 움직이는 것' 으로 해석하고

카메라 변형 정보의 역을 사용해야 한다는 것입니다. $$ V = (T * R * S)^{-1} = S^{-1} * R^{-1} * T^{-1} $$ 그러면 클래스를 구현해 봅시다.

class ViewMatrix2D final {
private:
    glm::vec2 m_position{ 0.0f, 0.0f };
    float     m_rotation{ 0.0f };
    glm::vec2 m_zoom{ 1.0f, 1.0f };
private:
    glm::mat4 m_matrix{1.0f};
    ...

뷰 행렬은 월드 행렬(=Transform)과 똑같은 속성으로 이루어집니다.

m_scale 대신 m_zoom 이라는 이름이 쓰인 것에 주의하세요.

    ...
public:
    ViewMatrix2D() { this->_update_matrix(); }
    ViewMatrix2D(const glm::vec2& pos, const float& rot, const glm::vec2& zoom)
        : m_position(pos), m_rotation(rot), m_zoom(zoom) {
        this->_update_matrix();
    }
    ~ViewMatrix2D() = default;
public:
    void set_position(const glm::vec2& value) { m_position = value; this->_update_matrix(); }
    void set_rotation(const float& value) { m_rotation = value; this->_update_matrix(); }
    void set_zoom(const glm::vec2& value) { m_zoom = value; this->_update_matrix(); }
public:
    const glm::vec2& get_position() const { return m_position; }
    const float& get_rotation() const { return m_rotation; }
    const glm::vec2& get_zoom() const { return m_zoom; }
    const glm::mat4& get_matrix() const { return m_matrix; }
    ...

_update_matrix 에서만 차이가 있습니다.

    ...
private:
    void _update_matrix() {
        m_matrix = glm::mat4{ 1.0f };

        m_matrix = glm::scale(m_matrix, { m_zoom.x, m_zoom.y, 1.0f });
        m_matrix = glm::rotate(m_matrix, -m_rotation, { 0.0f, 0.0f, 1.0f });
        m_matrix = glm::translate(m_matrix, { -m_position.x, m_position.y, 0.0f });
    }
};

언급한 대로 T-R-S 순이 아닌, S-R-T 순으로 곱해가며,

우선 이동 행렬의 경우, Transform2D 에서는 { m_position.x, -m_position.y } 라는 값을 썼었죠?

그 값에 음수를 한번 더 곱했기에(=반대로 움직였기에) 저렇게 됩니다.

또한 회전 행렬도 마찬가지로, m_rotation에 음수를 곱해 행렬을 만듭니다.

스케일링 행렬의 경우, 카메라의 '줌 인, 아웃' 기능으로 대체되었으므로

그대로 둬도 괜찮습니다.

투영 행렬

투영 행렬이란, 카메라를 기준으로 바라본 세계(공간의 점)를 '시야각, 원근 정보'에 따른 클립 공간으로 변환하는 행렬입니다.

(이곳의 이미지를 참고해 주세요.)

보통 투영을 설명할 때 '그림자'에 빗대곤 합니다.

어떤 벽 앞에 물체가 놓여있는 상황에서, 벽을 향해 빛을 쏴보면

물체의 형태에 대응되는 그림자가 생깁니다.

'3차원 물체의 형태를 어떤 관점에서 바라보아 2차원 형태를 만드는 것' 을 투영이라 합니다.

컴퓨터 그래픽스에서 투영 행렬에는 크게 2가지가 있습니다.

  • 원근 투영 : Perspective Projection
  • 직교 투영 : Orthographic Projection

원근 투영이란, 가까이 있는 건 크게, 멀리 있는 건 작게 보이는 원근법을 적용한 투영법이며,

직교 투영이란, 원근감 없이 가까이 있던 멀리 있던 정점 사이의 똑같은 비율을 유지시키는 투영법입니다.

전자의 경우, 뷰 공간의 정점과의 곱을 통해서

  • 절두체 크기
  • FOV(Field of View)
  • Near Plane
  • Far Plane

으로 정의되는 절두체(Frustrum) 을 사용하여,

해당 절두체의 near ~ far 범위 안의 적절한 정점이라면

원근 정보를 담은 4차원 벡터(4번째 요소 w에 원근 정보가 들어감)를 얻을 수 있습니다.

후자의 경우, 곱을 통해서

  • 절두체 크기
  • Near / Far Plane

로 정의되는 직육면체 형태(아래 그림의 오른쪽)의 절두체를 사용하여

원근 정보가 없는 4차원 벡터(이 경우, 벡터의 4번째 요소 w는 1이 됨)를 얻을 수 있습니다.

4_2.png

위는 뷰 공간을 기준으로 본, 원근 투영과 직교 투영을 시각화한 그림입니다.

원근 투영의 경우, 카메라에서 뻗어져 나오는 투영선이 점점 확대되지만

직교 투영의 경우, 투영선이 평행하게 유지된다는 점을 주목해주세요.

Note

'원근 정보'를 4차원 벡터의 w로 나타낸 이유는 무엇일까요?
그래픽스에서 '3차원의 정점을 2차원으로 나타낸다' 라고 하는 것은,
(x', y') = (x / z, y / z) 와 같이, 카메라로부터 얼마나 떨어져 있는지를 나타낸
z값으로 x, y 좌표를 나누는 개념으로 설명됩니다.
그러나 행렬의 선형 변환만으로는 '변수로 나누기'같은 비선형 연산을 표현할 수 없기 때문에(+, -, *만이 선형 연산),
투영 행렬을 곱해서 원근 정보를 w라는 공간에 빼 두고,
행렬의 곱으로 가능한 변환이 모두 끝난 뒤에야
GPU 측에서 클립 공간의 (x, y, z) 를 w로 나눠 NDC 공간의 (x, y, z) 를 만듭니다.
이것을 원근 나누기라고 합니다.
클립 공간 (x, y, z)의 범위는 [-w, w] 이고, 이를 w로 나누면 범위가 [-1, 1] 이 되는 것을 기억하세요.

이제 클래스를 만들어 봅시다.

2차원 투영 행렬에는 '직교 투영'이 사용되기에

절두체 크기, Near / Far Plane 값이 필요합니다.

class ProjectionMatrix2D final {
private:
    glm::uvec2 m_frustrum_size{ 1280, 720 };
    float m_near = -1.0f;
    float m_far = 1.0f;
private:
    glm::mat4 m_matrix{ 1.0f };
    ...

마찬가지로 setter / getter를 만들어 주세요.

    ...
public:
    ProjectionMatrix2D() {
        this->_update_matrix();
    }
    ProjectionMatrix2D(const glm::uvec2& frustrum_size, const float& proj_near, const float& proj_far) 
        : m_frustrum_size(frustrum_size), m_near(proj_near), m_far(proj_far)
    {
        this->_update_matrix();
    }
    ~ProjectionMatrix2D() = default;
public:
    void set_viewport_size(const glm::uvec2& value) { m_frustrum_size = value; this->_update_matrix(); }
    void set_near(const float& value) { m_near = value; this->_update_matrix(); }
    void set_far(const float& value) { m_far = value; this->_update_matrix(); }
public:
    const glm::uvec2& get_frustrum_size() const { return m_frustrum_size; }
    const float& get_near() const { return m_near; }
    const float& get_far() const { return m_far; }
public:
    const glm::mat4& get_matrix() const { return m_matrix; }
    ...

glm::ortho 함수를 통해서 직교 투영 행렬을 생성할 수 있습니다.

절두체 크기를 반으로 나눠서, 창의 정 중앙이 원점이 되도록

left / right, bottom / top 인자를 넣는 것에 주의하세요.

    ...
private:
    void _update_matrix() {
        const float fwidth = static_cast<float>(m_frustrum_size.x);
        const float fheight = static_cast<float>(m_frustrum_size.y);
        const float width_half = (fwidth / 2.0f);
        const float height_half = (fheight / 2.0f);

        m_matrix = glm::ortho(-width_half, width_half, -height_half, height_half, m_near, m_far);
    }
};

2D 렌더러 구현하기

축하드립니다. 여기까지 오셨다면 자유로운 2D 렌더링 세계를 구현하실 준비가 되셨습니다.

단색 도형이 움직이고, 돌아가고 커지는 것 뿐이지만

카메라도 구현이 가능하고, 추후에 텍스쳐를 넣는다면

웬만한 2D 게임 제작 라이브러리 뺨치는 자작 프레임워크가 완성됩니다.

그러면 시작해 봅시다.

제일 먼저 할 것은, 전에 작성했던 삼각형 코드를 약간 수정하는 겁니다.

이번엔 사각형을 그리기 위해서,

정점 데이터와 인덱스 데이터를 다음과 같이 수정하세요.

    // 정점 데이터를 준비합니다.
    std::vector<float> vertices_data = {
        -0.5f, 0.5f,  // 첫 번째 정점 (사각형의 topleft)
        0.5f, 0.5f,   // 두 번째 정점 (사각형의 topright)
        0.5f, -0.5f,  // 세 번째 정점 (사각형의 bottomright)
        -0.5f, -0.5f  // 네 번째 정점 (사각형의 bottomleft)
    };

    // 인덱스 데이터를 준비합니다.
    std::vector<uint32_t> indices_data = {
        // 반시계방향으로!!
        0, 3, 1,
        1, 3, 2
    };

삼각형때는 정점이 3개였기에 그냥 순서대로 이으면 됐지만,

사각형에서는 인덱스의 잇는 순서도 중요합니다.

OpenGL에서 삼각형을 잇는 방향의 기본값은 '반시계방향' 입니다.

(glFrontFace을 호출해서 '면을 앞을 향하게 잇는 방향'을 GL_CW 또는 GL_CCW 로 전환할 수 있습니다)

따라서 다음 그림처럼 데이터를 구성해야 합니다.

4_3.png


다음은 정점 셰이더를 수정해야 합니다.

저희가 지금까지 만들어온 Transform2D, ViewMatrix2D, ProjectionMatrix2D를 정점 셰이더에 전달하고

정점 셰이더 내에서 gl_Position에 클립 공간의 정점을 넘기기 위해서는-

먼저 '유니폼' 을 추가해 줍시다.

layout (location = 0) in vec2 aPos; 

// 4개의 유니폼을 추가합니다.
layout (location = 0) uniform vec2 uSize = vec2(64, 64);
layout (location = 1) uniform mat4 uProj;
layout (location = 2) uniform mat4 uView;
layout (location = 3) uniform mat4 uWorld;

유니폼이란, CPU에서 GPU로 데이터를 업로드하고,

셰이더 내에서 전역으로 읽을 수 있는 저장공간을 말합니다.

셰이더 코드 내에서 uniform 키워드를 사용해서

layout (location = [유니폼 위치 인덱스]) uniform [유니폼 타입] [유니폼 이름] = [유니폼 기본값];

이 형태로 유니폼을 선언할 수 있습니다.

저희는 uSize, uProj, uView, uWorld 라는 4개의 유니폼을 정의했고,

각각 '사각형의 크기', '투영 행렬', '뷰 행렬', '월드 행렬' 을 뜻합니다.

이 행렬을 적용하기 위해선, main함수를 다음과 같이 재작성해주세요.

void main() {
    vec2 pos = (aPos * uSize);

    gl_Position = uProj * uView * uWorld * vec4(pos, 0, 1);
}

위에서 봤던 형태와 똑같이 곱해줍니다.

단, uSize는 [-0.5, 0.5] 라는 범위의 aPos에 곱해져서

[-(width * 0.5), +(width * 0.5)] 형태로 정점 사이의 거리를 키워 주는 역할을 합니다.


프래그먼트 셰이더 또한 수정합니다.

다음과 같이 uColor라는 유니폼을 추가해 주세요.

해당 유니폼은 사각형의 색을 결정합니다.

...
layout (location = 4) uniform vec4 uColor = vec4(1, 1, 1, 1);

void main() {
    FragColor = uColor;
}

Warning

유니폼 uColor의 인덱스가 4인 것이 보이시나요?
유니폼은 '전역 데이터 공간' 이기 때문에,
정점 셰이더 <-> 프래그먼트 셰이더는 물론
같은 Program 안에 링크된 모든 셰이더들 내에서 공유됩니다.
따라서 정점 셰이더에서 4개의 유니폼 인덱스를 먼저 써 버렸으므로,
프래그먼트 셰이더에서는 겹치지 않도록 인덱스를 4부터 써야 합니다.


이제 CPU 코드로 돌아와서.,

프로그램의 링킹이 끝난 뒤 부터 코드를 추가해 갑시다.

glGetUniformLocation을 통해서, '해당 유니폼이 몇 번째 위치(인덱스)인지' 를 알 수 있습니다.

    // 유니폼 location을 저장해 둬야 합니다.
    //      layout (location = N) 에서, N의 값과 같습니다.
    //      즉, (0, 1, 2, 3, 4) 가 됩니다.
    GLint uniform_location_size = glGetUniformLocation(program, "uSize");
    GLint uniform_location_proj = glGetUniformLocation(program, "uProj");
    GLint uniform_location_view = glGetUniformLocation(program, "uView");
    GLint uniform_location_world = glGetUniformLocation(program, "uWorld");
    GLint uniform_location_color = glGetUniformLocation(program, "uColor");

저희는 셰이더 코드 내에 layout (location = N) 을 추가하여

명시적으로 유니폼의 인덱스를 지정했지만,

그렇지 않았을 경우 위와 같이 GLSL 컴파일러가 자동으로 지정한

각 유니폼의 location을 저장해 두어야 합니다.

명시적으로 지정했다면 저 값이 아닌 0, 1, 2 같은 인덱스 리터럴을 사용해도 무관합니다.

다음은 저희가 정의한 클래스를 사용할 때입니다.

    ProjectionMatrix2D proj_mat{ {1280, 720}, -1.0f, 1.0f};

    ViewMatrix2D view_mat{};
    view_mat.set_position({ 640, 360 });

    Transform2D world_mat{};
    world_mat.set_position({ 640, 360 });

투영 행렬, 뷰 행렬, 월드 행렬을 각각 정의하고

창 크기인 1280x720 에 맞게

  • 투영 행렬의 절두체 크기를 정하고
  • 뷰 행렬을 창 크기의 절반만큼 옮기고 (=카메라가 중앙으로 오도록)
  • 월드 행렬을 창 크기의 절반만큼 옮기는 (=사각형 또한 중앙으로 오도록)

작업을 해줍니다.


렌더링 루프 안에서는 다음과 같이 수정합니다.

            glBindVertexArray(VA);
            glUseProgram(program);

            // GPU로 유니폼에 정보를 업로드합니다.

            // uSize를 100x100으로 정해 업로드합니다.
            glProgramUniform2f(program, uniform_location_size, 100.0f, 100.0f);
            // 투영 / 뷰 / 월드 행렬을 업로드합니다.
            glProgramUniformMatrix4fv(program, uniform_location_proj, 1, GL_FALSE, glm::value_ptr(proj_mat.get_matrix()));
            glProgramUniformMatrix4fv(program, uniform_location_view, 1, GL_FALSE, glm::value_ptr(view_mat.get_matrix()));
            glProgramUniformMatrix4fv(program, uniform_location_world, 1, GL_FALSE, glm::value_ptr(world_mat.get_matrix()));
            // 색상을 빨간색 (1, 0, 0) 으로 정합니다. 알파값은 1로 고정합니다.
            glProgramUniform4f(program, uniform_location_color, 1.0f, 0.0f, 0.0f, 1.0f);

            // glDrawElements 는 인덱스 버퍼를 사용한다는 가정 하에 쓰이는 렌더 콜입니다.
            // 첫번째 인자 : 프리미티브를 지정합니다. 이 경우에서는 GL_TRIANGLES, 즉 삼각형을 그립니다.
            // 두번째 인자 : 인덱스의 개수를 지정합니다. 아까 인덱스 데이터의 개수는 3개였습니다.
            // 세번째 인자 : 인덱스 데이터의 형태를 지정합니다. 아까 인덱스 데이터의 형은 uint32_t 형이었습니다.
            // 네번째 인자 : 인덱스 데이터의 오프셋을 지정합니다. 보통은 0으로 둡니다.
            glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

glProgramUniform * 형태의 함수를 사용하여

유니폼 데이터를 업로드할 수 있습니다.

예를 들어 2f 라는 접두사는 2개의 float, 즉 vec2 타입의 유니폼을 업로드할 때 쓰입니다.

Matrix4fv 의 경우에는, 1개의 4x4 행렬, 즉 mat4 타입입니다.

이때 인자를 2개 더 받게 되는데, 3번째 인자 1은 행렬의 개수를,

4번째 인자 GL_FALSE는 행렬의 전치 여부를 뜻합니다.

전자는 mat4[] 타입을 업로드할때, 후자는 행렬이 전치되어 있는 경우 사용합니다.

glm::value_ptr 을 통해서 glm::mat4 형을 포인터로 변환해서 넘기는 것에 주의하세요.

또한 삼각형 렌더링의 인덱스가 3개였던 것에 비해

사각형은 6개의 인덱스를 사용합니다.


이제 코드를 실행해 보세요.

창의 중앙에 빨간색 사각형이 그려지면 성공입니다.

이것만으로는 조금 심심하니,

아래처럼 사각형을 조종하는 코드를 추가해 보세요.

    while (glfwWindowShouldClose(window) == false) {
        glfwPollEvents();
        {
            float speed = 1.0f;
            if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) {
                world_mat.set_position(world_mat.get_position() - glm::vec2(speed, 0.0f));
            }
            if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) {
                world_mat.set_position(world_mat.get_position() + glm::vec2(speed, 0.0f));
            }
            if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) {
                world_mat.set_position(world_mat.get_position() - glm::vec2(0.0f, speed));
            }
            if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) {
                world_mat.set_position(world_mat.get_position() + glm::vec2(0.0f, speed));
            }
            if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS) {
                world_mat.set_rotation(world_mat.get_rotation() + 0.01f);
            }
            if (glfwGetKey(window, GLFW_KEY_E) == GLFW_PRESS) {
                world_mat.set_rotation(world_mat.get_rotation() - 0.01f);
            }
            if (glfwGetKey(window, GLFW_KEY_COMMA) == GLFW_PRESS) {
                world_mat.set_scale(world_mat.get_scale() - glm::vec2(0.01f));
            }
            if (glfwGetKey(window, GLFW_KEY_PERIOD) == GLFW_PRESS) {
                world_mat.set_scale(world_mat.get_scale() + glm::vec2(0.01f));
            }
        }

        ...

W, A, S, D 키를 이용하여 사각형을 움직이고,

Q, E 키를 이용하여 삼각형을 회전,

<, > 키를 이용하여 삼각형의 스케일링을 조정할 수 있습니다.

추가적으로 view_mat의 set_ * 인터페이스를 이용하여

카메라를 변형해 보세요.

이제 기본적인 2차원 세계에서 물체의 움직임과 카메라가 구현되었습니다.

4_4.gif

전체 코드는 밑을 참고해 주시고,

다음 글에서는 5. 텍스쳐의 사용 을 주제로, 텍스쳐 자원을 생성하고 샘플링하여 화면에 그려보도록 하겠습니다.

전체 코드 보기

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <cassert>

#define GLM_ENABLE_EXPERIMENTAL
#include <glm/glm.hpp>
#include <glm/ext.hpp>

#include <vector>

const char* s_vertex_shader_src =
R"""(// 모든 GLSL 코드는 #version [버전] [core(선택)] 이라는 전처리기로 시작합니다.
#version 460 core

// 아까 만들었던 정점 데이터가 하나하나씩 여기로 들어옵니다.
//     2차원 정점으로 만들었으니 vec2로 받아야 합니다.
layout (location = 0) in vec2 aPos; 

layout (location = 0) uniform vec2 uSize = vec2(64, 64);

layout (location = 1) uniform mat4 uProj;
layout (location = 2) uniform mat4 uView;
layout (location = 3) uniform mat4 uWorld;

// GLSL도 셰이더 1개당 진입점을 하나씩 가집니다.
// C++ 처럼 에러 코드를 반환하지는 않지만, 
//      버텍스 셰이더에서는 'gl_Position 이라는 내부 변수에 값을 쓰는 것' 이 핵심입니다.
//          gl_Position 은 버텍스 셰이더 내에서 읽고 쓰기가 가능한 vec4 타입의 변수이며,
//          이름 그대로 GPU에 넘겨질 정점을 뜻합니다. 왜 vec4이고 마지막에 1을 넣는지는 다다음 장을 참고해주세요.
void main() {
    // 저희는 2차원 정점, 즉 (X, Y) 데이터만 입력했기에 남은 Z축 데이터는 0으로 통일합니다.

    vec2 pos = (aPos * uSize);

    gl_Position = uProj * uView * uWorld * vec4(pos, 0, 1);
}
)""";

const char* s_fragment_shader_src =
R"""(#version 460 core

// 위의 코드에서 정점을 in 으로 받은 것과 다르게, 
// 이번에는 프래그먼트 셰이더가 'RGBA 색상을 출력' 한다는 것을 표현하기 위해 
// out 을 사용합니다.
// FragColor 라는 변수에 값을 쓴다는 것은, 최종적으로 화면에 그려질 픽셀의 색상을 결정하는 것과 같습니다.
layout (location = 0) out vec4 FragColor;

layout (location = 4) uniform vec4 uColor = vec4(1, 1, 1, 1);

void main() {
    FragColor = uColor;
}
)""";


class Transform2D final {
private:
    glm::vec2 m_position{ 0.0f, 0.0f };
    float     m_rotation{ 0.0f };
    glm::vec2 m_scale{ 1.0f, 1.0f };
private:
    glm::mat4 m_matrix{ 1.0f };
public:
    Transform2D() { 
        this->_update_matrix(); 
    }
    Transform2D(const glm::vec2& pos, const float& rot, const glm::vec2& scl)
        : m_position(pos), m_rotation(rot), m_scale(scl) {
        this->_update_matrix();
    }
    ~Transform2D() = default;
public:
    const glm::mat4& get_matrix() const { return m_matrix; }
public:
    void set_position(const glm::vec2& value) { m_position = value; this->_update_matrix(); }
    void set_rotation(const float& value){ m_rotation = value; this->_update_matrix(); }
    void set_scale(const glm::vec2& value){ m_scale = value; this->_update_matrix(); }
public:
    const glm::vec2& get_position() const { return m_position; }
    const float& get_rotation() const { return m_rotation; }
    const glm::vec2& get_scale() const { return m_scale; }
private:
    void _update_matrix() {
        m_matrix = glm::mat4{ 1.0f };
        m_matrix = glm::translate(m_matrix, { m_position.x, -m_position.y, 0.0f });
        m_matrix = glm::rotate(m_matrix, m_rotation, {0.0f, 0.0f, 1.0f});
        m_matrix = glm::scale(m_matrix, { m_scale.x, m_scale.y, 1.0f });
    }
};

class ViewMatrix2D final {
private:
    glm::vec2 m_position{ 0.0f, 0.0f };
    float     m_rotation{ 0.0f };
    glm::vec2 m_zoom{ 1.0f, 1.0f };
private:
    glm::mat4 m_matrix{1.0f};
public:
    ViewMatrix2D() { this->_update_matrix(); }
    ViewMatrix2D(const glm::vec2& pos, const float& rot, const glm::vec2& zoom)
        : m_position(pos), m_rotation(rot), m_zoom(zoom) {
        this->_update_matrix();
    }
    ~ViewMatrix2D() = default;
public:
    void set_position(const glm::vec2& value) { m_position = value; this->_update_matrix(); }
    void set_rotation(const float& value) { m_rotation = value; this->_update_matrix(); }
    void set_zoom(const glm::vec2& value) { m_zoom = value; this->_update_matrix(); }
public:
    const glm::vec2& get_position() const { return m_position; }
    const float& get_rotation() const { return m_rotation; }
    const glm::vec2& get_zoom() const { return m_zoom; }
    const glm::mat4& get_matrix() const { return m_matrix; }
private:
    void _update_matrix() {
        m_matrix = glm::mat4{ 1.0f };

        m_matrix = glm::scale(m_matrix, { m_zoom.x, m_zoom.y, 1.0f });
        m_matrix = glm::rotate(m_matrix, -m_rotation, { 0.0f, 0.0f, 1.0f });
        m_matrix = glm::translate(m_matrix, { -m_position.x, m_position.y, 0.0f });
    }
};

class ProjectionMatrix2D final {
private:
    glm::uvec2 m_frustrum_size{ 1280, 720 };
    float m_near = -1.0f;
    float m_far = 1.0f;
private:
    glm::mat4 m_matrix{ 1.0f };
public:
    ProjectionMatrix2D() {
        this->_update_matrix();
    }
    ProjectionMatrix2D(const glm::uvec2& frustrum_size, const float& proj_near, const float& proj_far) 
        : m_frustrum_size(frustrum_size), m_near(proj_near), m_far(proj_far)
    {
        this->_update_matrix();
    }
    ~ProjectionMatrix2D() = default;
public:
    void set_viewport_size(const glm::uvec2& value) { m_frustrum_size = value; this->_update_matrix(); }
    void set_near(const float& value) { m_near = value; this->_update_matrix(); }
    void set_far(const float& value) { m_far = value; this->_update_matrix(); }
public:
    const glm::uvec2& get_frustrum_size() const { return m_frustrum_size; }
    const float& get_near() const { return m_near; }
    const float& get_far() const { return m_far; }
public:
    const glm::mat4& get_matrix() const { return m_matrix; }
private:
    void _update_matrix() {
        const float fwidth = static_cast<float>(m_frustrum_size.x);
        const float fheight = static_cast<float>(m_frustrum_size.y);

        float width_half = (fwidth / 2.0f);
        float height_half = (fheight / 2.0f);
        m_matrix = glm::ortho(-width_half, width_half, -height_half, height_half, m_near, m_far);
    }
};



int main() {
    glfwInit();

    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    GLFWwindow* window = glfwCreateWindow(1280, 720, "Triangle", nullptr, nullptr);

    glfwMakeContextCurrent(window);
    assert(gladLoadGL() != 0);

    // 정점 데이터를 준비합니다.
    std::vector<float> vertices_data = {
        -0.5f, 0.5f,  // 첫 번째 정점 (사각형의 topleft)
        0.5f, 0.5f, // 두 번째 정점 (사각형의 topright)
        0.5f, -0.5f, // 세 번째 정점 (사각형의 bottomright)
        -0.5f, -0.5f // 네 번째 정점 (사각형의 bottomleft)
    };

    // 인덱스 데이터를 준비합니다.
    std::vector<uint32_t> indices_data = {
        // 반시계방향으로!!
        0, 3, 1,
        1, 3, 2
    };

    // 정점 배열(Vertex Array) 입니다.
    GLuint VA = 0;
    // 정점 버퍼(Vertex Buffer), 인덱스 버퍼(Index Buffer) 입니다.
    //      OpenGL 에서는 인덱스 버퍼를 공식적으로는 Element Buffer 라고 부릅니다. 똑같은 개념이니 주의하세요.
    GLuint VB = 0, EB = 0;

    // 정점 배열을 만듦니다.
    glCreateVertexArrays(1, &VA);

    // 정점 버퍼, 인덱스 버퍼를 만듦니다. 
    //      둘 다 똑같은 '버퍼' 이고, 사용처만 다릅니다.
    glCreateBuffers(1, &VB);
    glCreateBuffers(1, &EB);

    // 각 버퍼에 데이터를 업로드합니다.
    glNamedBufferData(VB, sizeof(float) * vertices_data.size(), vertices_data.data(), GL_STATIC_DRAW);
    glNamedBufferData(EB, sizeof(uint32_t) * indices_data.size(), indices_data.data(), GL_STATIC_DRAW);

    // 정점 배열에 정점 버퍼와 인덱스 버퍼를 연결
    // VB를 0번 바인딩에
    glVertexArrayVertexBuffer(VA, 0, VB, 0, sizeof(float) * 2);
    glVertexArrayElementBuffer(VA, EB);
    // 정점 배열의 0번째 속성을 활성화
    //      -> 0번째 속성은 2차원 벡터로, 위치 정보를 나타냅니다.
    glEnableVertexArrayAttrib(VA, 0);
    // 정점 배열의 0번째 속성을 0번 바인딩에 연결
    glVertexArrayAttribBinding(VA, 0, 0);
    // 정점 배열의 0번째 속성에 형태 명시
    glVertexArrayAttribFormat(VA, 0, 2, GL_FLOAT, GL_FALSE, 0);

    // 셰이더 프로그램 만들기
    GLuint program = glCreateProgram();

    // 각각 Vertex, Fragment 셰이더를 만듦니다.
    GLuint vertex_shader = glCreateShader(GL_VERTEX_SHADER);
    GLuint fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);

    GLint success = 0;

    glShaderSource(vertex_shader, 1, &s_vertex_shader_src, 0);
    glCompileShader(vertex_shader);
    glGetShaderiv(vertex_shader, GL_COMPILE_STATUS, &success);
    assert(success == 1);

    glShaderSource(fragment_shader, 1, &s_fragment_shader_src, 0);
    glCompileShader(fragment_shader);
    glGetShaderiv(fragment_shader, GL_COMPILE_STATUS, &success);
    assert(success == 1);

    glAttachShader(program, vertex_shader);
    glAttachShader(program, fragment_shader);

    glLinkProgram(program);

    glGetProgramiv(program, GL_LINK_STATUS, &success);
    assert(success == 1);

    // 유니폼 location을 저장해 둬야 합니다.
    //      layout (location = N) 에서, N의 값과 같습니다.
    //      즉, (0, 1, 2, 3, 4) 가 됩니다.
    GLint uniform_location_size = glGetUniformLocation(program, "uSize");
    GLint uniform_location_proj = glGetUniformLocation(program, "uProj");
    GLint uniform_location_view = glGetUniformLocation(program, "uView");
    GLint uniform_location_world = glGetUniformLocation(program, "uWorld");
    GLint uniform_location_color = glGetUniformLocation(program, "uColor");


    ProjectionMatrix2D proj_mat{ {1280, 720}, -1.0f, 1.0f};

    ViewMatrix2D view_mat{};
    view_mat.set_position({ 640, 360 });

    Transform2D world_mat{};
    world_mat.set_position({ 640, 360 });

    while (glfwWindowShouldClose(window) == false) {
        glfwPollEvents();
        {
            float speed = 1.0f;
            if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) {
                world_mat.set_position(world_mat.get_position() - glm::vec2(speed, 0.0f));
            }
            if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) {
                world_mat.set_position(world_mat.get_position() + glm::vec2(speed, 0.0f));
            }
            if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) {
                world_mat.set_position(world_mat.get_position() - glm::vec2(0.0f, speed));
            }
            if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) {
                world_mat.set_position(world_mat.get_position() + glm::vec2(0.0f, speed));
            }
            if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS) {
                world_mat.set_rotation(world_mat.get_rotation() + 0.01f);
            }
            if (glfwGetKey(window, GLFW_KEY_E) == GLFW_PRESS) {
                world_mat.set_rotation(world_mat.get_rotation() - 0.01f);
            }
            if (glfwGetKey(window, GLFW_KEY_COMMA) == GLFW_PRESS) {
                world_mat.set_scale(world_mat.get_scale() - glm::vec2(0.01f));
            }
            if (glfwGetKey(window, GLFW_KEY_PERIOD) == GLFW_PRESS) {
                world_mat.set_scale(world_mat.get_scale() + glm::vec2(0.01f));
            }
        }
        {
            // 화면 초기화 색상을 초록색으로 정하고, 컬러 버퍼와 깊이 버퍼를 초기화합니다.
            glClearColor(0.2f, 0.3f, 0.1f, 1.0f);
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

            // 정점 배열과 프로그램을 지정합니다.
            //     glDraw* 함수를 부르려면 이 두 코드가 꼭 필요합니다.
            glBindVertexArray(VA);
            glUseProgram(program);

            // GPU로 유니폼에 정보를 업로드합니다.

            // uSize를 100x100으로 정해 업로드합니다.
            glProgramUniform2f(program, uniform_location_size, 100.0f, 100.0f);
            // 투영 / 뷰 / 월드 행렬을 업로드합니다.
            glProgramUniformMatrix4fv(program, uniform_location_proj, 1, GL_FALSE, glm::value_ptr(proj_mat.get_matrix()));
            glProgramUniformMatrix4fv(program, uniform_location_view, 1, GL_FALSE, glm::value_ptr(view_mat.get_matrix()));
            glProgramUniformMatrix4fv(program, uniform_location_world, 1, GL_FALSE, glm::value_ptr(world_mat.get_matrix()));
            // 색상을 빨간색 (1, 0, 0) 으로 정합니다. 알파값은 1로 고정합니다.
            glProgramUniform4f(program, uniform_location_color, 1.0f, 0.0f, 0.0f, 1.0f);
            // glDrawElements 는 인덱스 버퍼를 사용한다는 가정 하에 쓰이는 렌더 콜입니다.
            // 첫번째 인자 : 프리미티브를 지정합니다. 이 경우에서는 GL_TRIANGLES, 즉 삼각형을 그립니다.
            // 두번째 인자 : 인덱스의 개수를 지정합니다. 아까 인덱스 데이터의 개수는 3개였습니다.
            // 세번째 인자 : 인덱스 데이터의 형태를 지정합니다. 아까 인덱스 데이터의 형은 uint32_t 형이었습니다.
            // 네번째 인자 : 인덱스 데이터의 오프셋을 지정합니다. 보통은 0으로 둡니다.
            glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
        }
        glfwSwapBuffers(window);
    }

    // 생성했던 자원들을 모두 해제합니다.
    glDeleteVertexArrays(1, &VA);

    glDeleteBuffers(1, &VB);
    glDeleteBuffers(1, &EB);

    glDetachShader(program, vertex_shader);
    glDetachShader(program, fragment_shader);

    glDeleteShader(vertex_shader);
    glDeleteShader(fragment_shader);

    glDeleteProgram(program);


    glfwDestroyWindow(window);

    glfwTerminate();
}