OpenGL/공부

[OpenGL] Model Matrix (Translate, Rotate, Scale)

ciel45 2024. 5. 28. 16:56

이번엔 지난 번에 만들었던 삼각형을 조작해볼 것이다.

기본적으로 물체에 대한 조작은 선형 변환으로 이루어진다.

 

위키백과에 선형 변환을 검색하면 임의의 벡터에 대하여... 임의의 스칼라에 대하여... 이런 식으로 길게 설명이 되어있는데, 그냥 단순히 말하자면 앞에 행렬을 곱하는 것이다.

 

물체가 가지는 벡터(버텍스의 위치)에 행렬들을 곱해서, 그 벡터를 변화시키는 것이 선형 변환이라고 할 수 있다.

 

지금까지 삼각형을 그릴 때  vShader 내부에서 gl_Position = vec4(pos,  1.f); 를 사용하여 각 버텍스의 좌표를 그대로 넣어줬었다.

 

이번엔 이 vec4(pos,  1.f)의 앞에 적절한 행렬을 곱하여, 삼각형을 움직이고, 회전시키고, 크기를 조정해볼 것이다.

그림으로 나타내면 다음과 같다.

 

T, R, S는 각각 4x4 행렬이고, 행렬의 곱셈 법칙에 따라 이 3개를 곱한 행렬도 4x4 행렬이다.

따라서 T * R * S를 하나의 선형 변환으로 볼 수 있고, 이를 Model Matrix라고 부른다.

 

이 T, R, S를 만드는 방법은, 편하게 이미 glm 함수로 만들어져있다.

glm은 OpenGL Math Library로, 다양한 행렬, 벡터와 이에 대한 연산들을 사용하기 위해 필수이다.

 

glm::translate(), glm::rotate(), glm::scale()이 그것이다.

 

 

다만.. 수학적으로 어떻게 만들어지는지 파악하고 있을 필요도 물론 있다.

행렬의 내부 구조와 그에 따른 계산 결과는 다음과 같다.

Translate
Scale

대망의 Rotation은 다음과 같다.

Rotation

왜 이렇게 복잡하냐면, 3차원 공간에서 회전을 정의하려면, 좌표계에 새로운 축을 꽂고, 그 축을 중심으로 돌리는 수 밖에 없기 때문이다.

 

각 행렬의 원리를 쉽게 이해하려면 다음과 같이 생각하면 된다.

x축 단위를 (1, 0, 0), y축 단위를 (0, 1, 0), z축 단위를 (0, 0, 1)로 보았을 때,

  • 1열 : 새로운 x축 단위
  • 2열 : 새로운 y축 단위
  • 3열 : 새로운 z축 단위
  • 4열 : 새로운 원점 위치

 

아무튼, 이제 Model Matrix를 다음과 같은 방식으로 만들 수 있다.

glm::mat4 identityMatrix(1.f);
glm::mat4 T = glm::translate(identityMatrix, glm::vec3(0.f, 0.2f, -1.f));
glm::mat4 R = glm::rotate(identityMatrix, rotationOffset, glm::vec3(0.f, 1.f, 0.f));
glm::mat4 S = glm::scale(identityMatrix, glm::vec3(0.2f));

glm::mat4 model = T * R * S; // Model Matrix

 

만든 행렬을 쉐이더 안에 넣어주기 위해 Vertex Shader 안에 uniform 변수를 추가하였다.

uniform mat4 model;

uniform 변수는 렌더링 파이프라인 내부 글로벌 변수와 같다.

 

Model Matrix는 각 버텍스마다 달라질 필요가 없기 때문에, uniform으로 선언한 것이다.

 

이제 저 model 변수로 앞서 만든 행렬을 넣어주면 된다.

// 소스코드 중 일부
GLuint uniformModel;
uniformModel = glGetUniformLocation(shader, "model"); // 쉐이더 내 model 변수 위치 가져오기
// 쉐이더 내 model로 Model Matrix를 넣어주기
glUniformMatrix4fv(uniformModel, 1, GL_FALSE, glm::value_ptr(model));

 

 

전체 소스코드는 다음과 같다.

더보기

 

#include <stdio.h>
#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 WIDTH = 800, HEIGHT = 600;

GLuint VAO, VBO, shader, uniformModel;

GLfloat translateOffset = -1;
GLfloat rotateOffset = 0;
GLfloat scaleOffset = 0;

// Vertex Shader
static const char* vShader = "        \n\
#version 330                          \n\
                                      \n\
layout (location = 0) in vec3 pos;    \n\
                                      \n\
uniform mat4 model;                   \n\
                                      \n\
void main()                           \n\
{                                     \n\
    gl_Position = model * vec4(pos, 1.f);     \n\
}";

// Fragment Shader
static const char* fShader = "        \n\
#version 330                          \n\
                                      \n\
out vec4 color;                       \n\
                                      \n\
void main()                           \n\
{                                     \n\
    color = vec4(0.f, 1.f, 0.f, 1.f);  \n\
}";

void CreateTriangle()
{
    GLfloat vertices[] = 
    {
    //    x     y    z
        -1.f, -1.f, 0.f,
        1.f, -1.f, 0.f,
        0.f, 1.f, 0.f
    };

    glGenVertexArrays(1, &VAO);
    glBindVertexArray(VAO);

    glGenBuffers(1, &VBO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // static draw : 넣은 값을 바꾸지 않겠다

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, 0);

    glBindVertexArray(0);
}

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

    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 errorLog[1024] = { 0 };
    glGetShaderiv(theShader, GL_COMPILE_STATUS, &result);
    if (!result)
    {
        glGetShaderInfoLog(theShader, sizeof(errorLog), NULL, errorLog);
        printf("Error compiling the %d shader : %s\n", shaderType, errorLog);
    }

    glAttachShader(theProgram, theShader); // 프로그램에 attach
}

void CompileShaders()
{
    shader = glCreateProgram();

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

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

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

    glLinkProgram(shader); // 각 쉐이더를 컴파일해서 프로그램에 attach했으므로, 링킹 수행
    glGetProgramiv(shader, GL_LINK_STATUS, &result);
    if (!result)
    {
        glGetProgramInfoLog(shader, sizeof(errorLog), NULL, errorLog);
        printf("Error linking program : %s\n", errorLog);
    }
    
    glValidateProgram(shader); // 현재 OpenGL context에 유효한지 확인
    glGetProgramiv(shader, GL_VALIDATE_STATUS, &result);
    if (!result)
    {
        glGetProgramInfoLog(shader, sizeof(errorLog), NULL, errorLog);
        printf("Error validating program : %s\n", errorLog);
    }

    uniformModel = glGetUniformLocation(shader, "model");
}

int main()
{
    // GLFW 초기화
    if (!glfwInit())
    {
        printf("GLFW 초기화 실패\n");
        glfwTerminate();
        return 1;
    }

    // GLFW 윈도우 속성 셋업
    // OpenGL 버전
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    // Core profile = 이전 버전 호환성 없음
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    // 앞으로의 호환성을 허용
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);

    GLFWwindow* mainWindow = glfwCreateWindow(WIDTH, HEIGHT, "Test", NULL, NULL);
    if (!mainWindow)
    {
        printf("GLFW 창 생성 실패\n");
        glfwTerminate();
        return 1;
    }

    // 프레임 버퍼 크기 정보를 가져온다
    int bufferWidth, bufferHeight;
    glfwGetFramebufferSize(mainWindow, &bufferWidth, &bufferHeight);

    // glew가 사용할 컨텍스트 설정
    glfwMakeContextCurrent(mainWindow);

    // 최신 확장 기능을 허용
    glewExperimental = GL_TRUE;

    if (glewInit() != GLEW_OK)
    {
        printf("GLEW 초기화 실패\n");
        glfwDestroyWindow(mainWindow);
        glfwTerminate();
        return 1;
    }

    // 뷰포트 생성
    glViewport(0, 0, bufferWidth, bufferHeight);

    CreateTriangle();
    CompileShaders();

    // 창이 닫힐 때까지 반복
    while (!glfwWindowShouldClose(mainWindow))
    {
        // 사용자 입력 이벤트 가져오고 처리
        glfwPollEvents();

        // 창 지우기
        glClearColor(0.f, 0.f, 0.f, 1.f);
        glClear(GL_COLOR_BUFFER_BIT);

        glUseProgram(shader);

        glm::mat4 identityMatrix(1.f);
        glm::mat4 T = glm::translate(identityMatrix, glm::vec3(0.f, 0.2f, -1.f));
        glm::mat4 R = glm::rotate(identityMatrix, rotationOffset, glm::vec3(0.f, 1.f, 0.f));
        glm::mat4 S = glm::scale(identityMatrix, glm::vec3(0.2f));

        glm::mat4 model = T * R * S; // Model Matrix

        glUniformMatrix4fv(uniformModel, 1, GL_FALSE, glm::value_ptr(model));

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

        glUseProgram(0);

        glfwSwapBuffers(mainWindow);

        translateOffset += 0.0001f;
        rotateOffset += 0.0001f;
        scaleOffset += 0.00001f;
    }
    return 0;
};

 

glm::mat4 T = glm::translate(identityMatrix, glm::vec3(0.f, 0.2f, -1.f));
glm::mat4 R = glm::rotate(identityMatrix, rotationOffset, glm::vec3(0.f, 1.f, 0.f));
glm::mat4 S = glm::scale(identityMatrix, glm::vec3(0.2f));

translateOffset += 0.0001f;
rotateOffset += 0.0001f;
scaleOffset += 0.00001f;

해당 코드를 통해 while문을 돌면서 물체의 위치, 회전, 스케일이 바뀌도록 하였다.

 

실행 결과 : 

 

 

이동, 회전, 스케일링이 잘 수행되고 있는 것을 볼 수 있다.