본문 바로가기
<LIBRARY>/OPENGL

5. GLM 라이브러리 적용하기

by CodeGrimie 2021. 1. 19.

처음부터 수학 함수들을 직접 만들면서 OpenGL을 공부하는 것도 하나의 방법이지만

많은 사람들이 그래픽 프로그래밍을 제대로 맛보기도 전에 포기하는 경우가 더 많다고 한다.

 

외국 포럼에서도 직접 수함 함수를 구현하는 것보다도 그냥 빠르게 결과물을 보고

동작원리를 이해하는 것이 OpenGL에는 더 도움이 된다는 의견이 압도적으로 많다.

 

물론 OpenGL만 공부한다고 했을 때 경우를 가정했을 때라는 전제 조건이 붙는다.

그렇지 않다고 하더라도 나쁘지 않다는 의견도 많다.

결국 동작원리만 안다면 웬만한 코드들은 구현에 시간만 조금 들뿐이니까.

 

그러니 우리도 GLM을 사용해서 그래픽 프로그래밍을 좀 더 재밌고 편리하게 해 보자.

GLM 이란?

GLM(OpenGL Mathematics)는 이름 그대로 OpenGL에서 사용할 수 있는 수학 라이브러리다.

CPP로 작성되어 있고 GLSL(OpenGL Shading Language)에서도 사용할 수 있다.

 

세상 똑똑한 천재(박사)들이 만들어서 성능까지 좋기 때문에 OpenGL을 공부할 때 빼놓을 수 없는 라이브러리다.

GLM 내려받기

GLM은 굉장히 작은 라이브러리다.

문서를 포함한 잡다구리(?)들을 제외하고 실제 사용하는 라이브러리 크기3MB도 되지 않는다.

이 정도면 와이파이가 잠깐 연결되는 열악한 환경에서도 내려받을 용기가 생기는 크기다.

 

GLM 공식 릴리즈 저장소에서 내려받을 수 있다.

 

zip 파일을 내려받으면 된다.

GLM 설치하기

GLM을 내려받아서 압축을 풀면 여러 가지 폴더가 보이는데 glm 빼고 모두 필요 없는 것들이다.

그러니 glm 빼고 싹 다 지운다.

 

▼ 이전

▼ 이후

이제 0. OpenGL 개발환경 설정하기 때와 비슷한 흐름으로 진행된다.

glm-0.9.9.8을 GLM으로 변경해서 우리가 GLFW, GLEW 라이브러리를 둔 라이브러리 폴더로 복사한다.

 

▼ GLFW, GLEW, GLM 삼총사가 모두 모였다.

이제 비주얼 스튜디오에서 추가 포함 위치를 설정해줘야 한다.

이것도 0. OpenGL 개발환경 설정하기 와 다르지 않아서 어렵지 않다.

 

▼ 추가 포함 위치 설정

GLFW와 GLEW 경우와 달리 링커를 따로 설정해주지 않아도 된다.

솔직히 어떤 경우 링커를 설정해주고 안 해주는지 잘 모르겠다.

 

정상적으로 GLM이 포함되었는지 확인해보자.

바로 직전의 4. 삼각형 움직이기 코드에서 헤더 부분에 GLM 라이브러리의 헤더를 하나 추가해서 확인한다.

 

▼ 빨간 줄이 안 뜨면 정상적으로 포함된 것이다.

#include <cstdio>
#include <clocale>
#include <cstdlib>
#include <cstring>

#include <GL/glew.h>
#include <GLFW/glfw3.h>
//---------------------------------------------
// GLM 헤더 추가
#include <glm/glm.hpp>
//---------------------------------------------

const GLint WIDTH = 720, HEIGHT = 480;

4. 삼각형 움직이기 를 GLM 버전으로 변환하기

이제 이번 수정에 필요한 GLM 헤더들을 포함시켜 보자.

#include <GL/glew.h>
#include <GLFW/glfw3.h>

GLFW와 GLEW랑 삼총사이니 이 둘 바로 아래에 포함시키는 게 나중에 관리하기에도 편할 것이다.

 

▼ 필요한 헤더 파일 추가

#include <cstdio>
#include <clocale>
#include <cstdlib>
#include <cstring>

#include <GL/glew.h>
#include <GLFW/glfw3.h>
//---------------------------------------------
// 필요 GLM 헤더 추가
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
//---------------------------------------------

const GLint WIDTH = 720, HEIGHT = 480;

기본적으로 GLM은 행렬(Matrix) 연산으로 작동한다.

각각의 헤더가 어떤 기능을 추가해주는지 간단하게 알아보자.

 

glm/glm.hpp는 GLM의 가장 기본이 되는 헤더로

GLM에서 사용할 수 있는 행렬을 비롯한 여러 자료형들을 가지고 있다.

 

glm/gtc/matrix_transform.hpp는 행렬 변환에 사용되는 수학 함수들을 가지고 있다.

우리는 삼각형을 좌우로 움직이는 것뿐이지만 이것도 행렬 연산으로 작동하기 때문에 이 헤더 파일을 포함한다.

 

glm/gtc/type_ptr.hpp는 GLM 자료형들의 메모리와 관련된 기능을 가지고 있다.

 

이제 정말 몇 줄의 코드를 변경해서 GLM을 사용하여 똑같은 결과를 내보자.

먼저 우리가 사용하던 uniformXMove를 uniformModel로 바꾼다.

 

▼ uniformXMove를 uniformModel로 바꿔준다.

//---------------------------------------------
// uniformXMove -> uniformModel
GLuint VAO, VBO, shader, uniformModel;
//---------------------------------------------

변수형이 GLuint로 똑같지만 굳이 바꾸는 이유는 좀 더 직관적으로 이해하기 위해서다.

GLM에서는 모델 좌표계에 해당하는 변수에 Model이라고 붙이는 걸 권장한다.

 

모델 좌표계는 하나의 메쉬(면, 모델)가 각각의 좌표를 가진다고 가정하는 좌표계다.

우리가 그린 삼각형도 하나의 모델이기 때문에 모델 좌표계를 사용한다.

 

▼ 모델 좌표에 관해서는 아래의 사이트에서 자세히 설명하고 있다.

learnopengl.com/Getting-started/Coordinate-Systems

 

LearnOpenGL - Coordinate Systems

Coordinate Systems Getting-started/Coordinate-Systems In the last chapter we learned how we can use matrices to our advantage by transforming all vertices with transformation matrices. OpenGL expects all the vertices, that we want to become visible, to be

learnopengl.com

아무튼 uniformModel로 변수명을 바꿈으로써 모델 좌표계 변수라는 것을 알 수 있게 되었다.

 

이제 정점 쉐이더를 수정한다.

앞서서 작성했던 uniform float xMove; 은 단순한 float 형이기 때문에 GLM과 연산할 수 없다.

이걸 mat4(Matrix4x4)로 바로 위에서 선언한 uniformModel과 연동할 변수로 바꿔준다.

 

▼ GLM에서 제공하는 mat4로 변경한다.

// 정점 쉐이더
static const char* vShader = R"(
#version 330

layout (location = 0) in vec3 pos;

//---------------------------------------------
// model로 수정
uniform mat4 model;
//---------------------------------------------

void main()
{
    //---------------------------------------------
    // model을 곱해준다.
    gl_Position = model * vec4(0.5 * pos.x, 0.5 * pos.y, pos.z, 1.0);
    //---------------------------------------------
})";

추가적으로 gl_Position = model * vec4(0.5 * pos.x, 0.5 * pos.y, pos.z, 1.0); 로 변경되어 있음을 알 수 있다.

객체의 위치 값에 직접적으로 더하는 게 아니라 이동 행렬을 곱해서 이동하게 하는 것이다.

 

쉐이더가 수정되었기 때문에 CompileShader() 함수도 수정해줘야 한다.

uniformXMove = glGetUniformLocation(shader, "xMove"); 에서 두 변수명이 모두 변경되었기 때문에

우리가 수정한 uniformModelmodel로 변경해주기만 하면 된다.

 

▼ CompileShader에서 uniformModel과 쉐이더의 model을 연결한다.

void CompileShader()
{
    shader = glCreateProgram();

    if (shader == NULL)
    {
        printf("Error Creating Shader Program!\n");
        return;
    }

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

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

    // 쉐이더 프로그램 연결
    glLinkProgram(shader);
    glGetProgramiv(shader, GL_LINK_STATUS, &result);
    if (!result)
    {
        glGetProgramInfoLog(shader, sizeof(eLog), NULL, eLog);
        printf("Error Linking Program: '%s'\n", eLog);
        return;
    }

    // 쉐이더 프로그램 검증
    glValidateProgram(shader);
    glGetProgramiv(shader, GL_VALIDATE_STATUS, &result);
    if (!result)
    {
        glGetProgramInfoLog(shader, sizeof(eLog), NULL, eLog);
        printf("Error Validating Program: '%s'\n", eLog);
        return;
    }

    //---------------------------------------------
    // unifomModel과 쉐이더의 model을 연결한다.
    uniformModel = glGetUniformLocation(shader, "model");
    //---------------------------------------------
}

마지막으로 GLFW 순환문에서 값을 넣어주는 과정만 해주면 된다.

 

이제 float형으로 바로 더하는 방식이 아니기 때문에glUniform1f(uniformXMove, triOffset); 은 의미가 없다.

행렬 연산을 통해서 삼각형이 움직이기 때문에 별도의 행렬을 저장할 model을 선언해서 저장한다.

 

triOffsety에도 추가로 들어있다.

// 연두색 화면 그리기
glClearColor(0.4f, 0.6f, 0.2f, 1.0f);
// OpenGL 배경색상 초기화
glClear(GL_COLOR_BUFFER_BIT);

// Shader 적용
glUseProgram(shader);

//---------------------------------------------
// mat4 model 초기화
glm::mat4 model = glm::mat4(1.0f);
// 우리가 원하는 값만큼 행렬 연산
model = glm::translate(model, glm::vec3(triOffset, triOffset, 0.0f));

// Mat4를 uniformModel로 변환한다.
glUniformMatrix4fv(uniformModel,1, GL_FALSE,glm::value_ptr(model));
//---------------------------------------------

// VBO에 있는 데이터 바인딩
glBindVertexArray(VBO);
// 데이터를 바탕으로 그리기
glDrawArrays(GL_TRIANGLES, 0, 3);
// 데이터 바인딩 해제
glBindVertexArray(0);

// Shader 해제
glUseProgram(0);

// GLFW 더블 버퍼링
glfwSwapBuffers(mainWindow);

간단히 요약하면 우리가 움직인 값의 행렬을 만들어서 기존의 삼각형 위치 행렬과 연산한다.

정점 쉐이더에서 gl_Position = model * vec4(0.5 * pos.x, 0.5 * pos.y, pos.z, 1.0); 를 생각해보자.

 

▼ 실행화면

삼각형이 오른쪽 위에서 왼쪽 아래 대각선으로 움직이고 있다.

 

위 코드의 model = glm::translate(model, glm::vec3(triOffset, triOffset, 0.0f)); 에서

y축도 같이 움직이도록 수정을 했기 때문이다.

 

이게 행렬을 사용하는 장점 중 하나다.

 

이전의 방법이었다면 우리는 y축을 움직이기 위해서 별도의 uniform을 할당해줘야 했을 것이다.

하지만 이제 변동 사항은 이 glm::translate()를 통해서 수정하고 model만 던져 주면 된다.

 

▼ 전체 코드

#include <cstdio>
#include <clocale>
#include <cstdlib>
#include <cstring>

#include <GL/glew.h>
#include <GLFW/glfw3.h>
//---------------------------------------------
// 필요 GLM 헤더 추가
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
//---------------------------------------------

const GLint WIDTH = 720, HEIGHT = 480;

//---------------------------------------------
// uniformXMove -> uniformModel
GLuint VAO, VBO, shader, uniformModel;
//---------------------------------------------

// 방향값(왼쪽, 오른쪽)
bool direction = true;
// 삼각형의 차이값
float triOffset = 0.0f;
// 삼각형의 최대 차이값
float triMaxOffset = 1.0f;
// 삼각형의 변화값
float triIncrement = 0.01f;

// 정점 쉐이더
static const char* vShader = R"(
#version 330

layout (location = 0) in vec3 pos;

//---------------------------------------------
// model로 수정
uniform mat4 model;
//---------------------------------------------

void main()
{
    //---------------------------------------------
    // model을 곱해준다.
    gl_Position = model * vec4(0.5 * pos.x, 0.5 * pos.y, pos.z, 1.0);
    //---------------------------------------------
})";

// 조각 쉐이더
static const char* fShader = R"(
#version 330

out vec4 colour;

void main()
{
    colour = vec4(1.0, 1.0, 1.0, 1.0);
})";

void CreateTriangle()
{
    GLfloat vertices[] = {
        -1.0f, -1.0f, 0.0f,
        1.0f, -1.0f, 0.0f,
        0.0f, 1.0f, 0.0f
    };

    // OpenGL 정점 배열 생성기를 사용해서 VAO를 생성
    glGenVertexArrays(1, &VAO);
    // 우리가 생성한 VAO를 현재 수정 가능하도록 연결한다.
    glBindVertexArray(VAO);

    // OpenGL 정점 배열 생성기를 사용해서 VBO를 생성
    glGenBuffers(1, &VBO);
    // 우리가 생성한 VBO를 현재 수정 가능하도록 연결한다.
    glBindBuffer(GL_ARRAY_BUFFER, VBO);

    // 우리가 만든 삼각형 정점 좌표를 VBO에 저장한다.
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    // VAO에 이 VAO를 어떻게 해석해야 할 지 알려줍니다.
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);

    // VAO 사용 허용
    glEnableVertexAttribArray(0);
    // VBO 수정 종료 및 연결 초기화
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // 수정이 완료 되면 연결을 끊기 위해 초기값으로 연결한다.
    glBindVertexArray(0);
}

void AddShader(GLuint theProgram, const char* shaderCode, GLenum shaderType)
{
    // 쉐이더 생성
    GLuint theShader = glCreateShader(shaderType);

    // 쉐이더 코드를 저장할 배열 생성
    const GLchar* theCode[1];
    theCode[0] = shaderCode;

    // 쉐이더 코드 길이를 저장할 배열 생성
    GLint codeLength[1];
    codeLength[0] = strlen(shaderCode);

    // 쉐이더에 우리가 작성한 쉐이더 코드를 저장한다.
    glShaderSource(theShader, 1, theCode, codeLength);
    // 쉐이더 컴파일
    glCompileShader(theShader);

    // 에러 검출을 위한 변수 선언
    GLint result = 0;
    GLchar eLog[1024] = { 0 };

    // 쉐이더 컴파일 정상완료 여부 저장
    glGetShaderiv(theShader, GL_COMPILE_STATUS, &result);
    if (!result)
    {
        // 쉐이더 오류 로그를 저장하고 출력합니다.
        glGetShaderInfoLog(theShader, sizeof(eLog), NULL, eLog);
        printf("Error Compiling the %d shader: '%s'\n", shaderType, eLog);
        return;
    }

    // 쉐이더 프로그램에 쉐이더를 등록합니다.
    glAttachShader(theProgram, theShader);
}

void CompileShader()
{
    shader = glCreateProgram();

    if (shader == NULL)
    {
        printf("Error Creating Shader Program!\n");
        return;
    }

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

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

    // 쉐이더 프로그램 연결
    glLinkProgram(shader);
    glGetProgramiv(shader, GL_LINK_STATUS, &result);
    if (!result)
    {
        glGetProgramInfoLog(shader, sizeof(eLog), NULL, eLog);
        printf("Error Linking Program: '%s'\n", eLog);
        return;
    }

    // 쉐이더 프로그램 검증
    glValidateProgram(shader);
    glGetProgramiv(shader, GL_VALIDATE_STATUS, &result);
    if (!result)
    {
        glGetProgramInfoLog(shader, sizeof(eLog), NULL, eLog);
        printf("Error Validating Program: '%s'\n", eLog);
        return;
    }

    //---------------------------------------------
    // unifomModel과 쉐이더의 model을 연결한다.
    uniformModel = glGetUniformLocation(shader, "model");
    //---------------------------------------------
}

int main()
{
    // 로케일 국가 한국 지정
    _wsetlocale(LC_ALL, L"Korean");

    // GLFW 초기화
    if (glfwInit() == GLFW_FALSE)
    {
        wprintf(L"GLFW 초기화 실패\n");
        glfwTerminate();
        return (1);
    }

    // OpenGL 버전 지정
    // OpenGL MAJOR.MINOR 방식으로 표현
    // 이번엔 3.3을 사용한다.
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);

    // OpenGL 코어 프로필 설정
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    // OpenGL 상위호환 활성화
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);

    // GLFW 윈도우 생성
    GLFWwindow* mainWindow = glfwCreateWindow(WIDTH, HEIGHT, "OpenGL TRIANGLE", NULL, NULL);
    if (mainWindow == NULL)
    {
        wprintf(L"GLFW 윈도우 생성이 실패했습니다.\n");
        glfwTerminate();
        return (1);
    }

    // 버퍼 가로, 버퍼 세로 선언
    int bufferWidth, bufferHeight;
    // mainWindow로부터 버퍼 가로 크기와 버퍼 세로 크기를 받아온다.
    glfwGetFramebufferSize(mainWindow, &bufferWidth, &bufferHeight);

    glfwMakeContextCurrent(mainWindow);

    // GLEW의 모든 기능 활성화
    glewExperimental = GL_TRUE;
    if (glewInit() != GLEW_OK)
    {
        wprintf(L"GLEW 초기화가 실패했습니다.\n");
        // mainWindow 삭제
        glfwDestroyWindow(mainWindow);
        glfwTerminate();
        return (1);
    }

    CreateTriangle();
    CompileShader();

    // OpenGL Viewport 생성
    glViewport(0, 0, bufferWidth, bufferHeight);

    // GLFW가 종료되지 않는 한 계속 도는 순환문
    while (glfwWindowShouldClose(mainWindow) == GLFW_FALSE)
    {
        // GLFW 이벤트 입력
        glfwPollEvents();

        // 방향이 오른쪽인지 왼쪽인지
        if (direction == true)
        {
            triOffset += triIncrement;
        }
        else
        {
            triOffset -= triIncrement;
        }

        // 최대 차이값을 넘기게 되면 방향 전환
        if (abs(triOffset) >= triMaxOffset)
        {
            direction = !direction;
        }


        // 연두색 화면 그리기
        glClearColor(0.4f, 0.6f, 0.2f, 1.0f);
        // OpenGL 배경색상 초기화
        glClear(GL_COLOR_BUFFER_BIT);

        // Shader 적용
        glUseProgram(shader);

        //---------------------------------------------
        // mat4 model 초기화
        glm::mat4 model = glm::mat4(1.0f);
        // 우리가 원하는 값만큼 행렬 연산
        model = glm::translate(model, glm::vec3(triOffset, triOffset, 0.0f));

        // Mat4를 uniformModel로 변환한다.
        glUniformMatrix4fv(uniformModel,1, GL_FALSE,glm::value_ptr(model));
        //---------------------------------------------

        // VBO에 있는 데이터 바인딩
        glBindVertexArray(VBO);
        // 데이터를 바탕으로 그리기
        glDrawArrays(GL_TRIANGLES, 0, 3);
        // 데이터 바인딩 해제
        glBindVertexArray(0);

        // Shader 해제
        glUseProgram(0);

        // GLFW 더블 버퍼링
        glfwSwapBuffers(mainWindow);
    }

    return (0);
}

'<LIBRARY> > OPENGL' 카테고리의 다른 글

6. 삼각형 회전 시키기  (0) 2021.01.25
4. 삼각형 움직이기  (0) 2021.01.17
3. 삼각형 그리기(2)  (0) 2021.01.17
2. 삼각형 그리기 (1)  (0) 2021.01.14
1. 기본 코드 작성하기  (0) 2021.01.11

댓글