OpenGL/공부

[OpenGL] Projection Matrix (glm::perspective, glm::ortho)

ciel45 2024. 6. 1. 15:54

지난 포스팅에서 ModelView Matrix를 통해 물체의 버텍스의 좌표를 Object Space에서 View Space로 바꾸는 것까지 하였다.

 

이번엔 View Space 기준 좌표를 Clip Space 기준 좌표로 변환할 것이며, Projection Matrix가 이 역할을 해준다.

 

 

우선 배경 설명을 먼저 하자면, 그래픽스에서 카메라의 Projection은 주로 두 가지 타입으로 나뉜다.

하나는 Perspective, 하나는 Orthographic이다.

 

유니티 엔진을 다뤄보신 분들이라면 익숙할 것이다. Orthographic은 주로 2D 어플리케이션에서 사용되고, Perspective는 주로 3D 어플리케이션에서 사용된다.

 

간단히 말하자면 Perspective는 원근감을 반영한 것, Orthographic은 원근감을 반영하지 않은 것이다.

 

두 방식은 각기 다른 모양의 View Volume을 만든다.

Perspective의 경우 피라미드의 꼭대기를 반듯이 잘라낸 모양, Orthographic의 경우 직육면체 모양이 된다.

 

또한 같은 Perspective, Orthographic이더라도 FOV, 줌 등의 카메라 내부 파라미터에 의해 View Volume의 모양은 계속 달라진다.

 

그런데 하드웨어, 즉 GPU 입장에서는 이런 것들을 일일이 구별해가면서 렌더링할 여력이 없다. 그래서 이 View Volume을 내가 이해하기 편한 정육면체의 모양으로 받고 싶다.

 

그 역할을 해주는 것이 Projection Matrix이다. 

각기 다른 모양의 View Volume을 정육면체 모양의 Canonical View Volume으로 변환해준다. 

그리고 이 Canonical View Volume 기준 좌표계Clip Space라고 한다.

Canonical View Volume

 

View Space의 원점이 카메라의 위치였다면, Clip Space의 원점은 정육면체의 중심이다.

Clip Space 내 좌표의 범위는 항상 (-1, -1, -1) ~ (1, 1, 1) 이며,

 

이러한 일관성 덕분에 GPU 입장에서 버텍스의 위치를 다루는 것이 쉬워진다.

다시 한번 그림으로 보면 다음과 같다.

좌측 : View Space 기준 View Volume / 우측 : Clip Space 기준 View Volume

 

결과적으로 화면은 이렇게 보여진다.

 

이러한 Projection Matrix를 만드는 것은 복잡하지만, View Matrix 때와 마찬가지로 glm에서 이미 함수를 제공해주고 있다.

 

Perspective를 사용한다면 glm::perspective(float fovy, float aspect, float near, float far) 를,

Orthographic을 사용한다면 glm::ortho(float left, float right, float bottom, float top, float zNear, float zFar) 을 사용하면 된다.

 

파라미터들이 결정하는 것은, 정육면체로 바꾸기 전 원본 View Volume의 모양이다.

  • glm::perspective
    • fovy : FOV를 결정, 즉 View Volume의 위아래로 벌려진 정도를 결정
    • aspect : 비율을 결정
    • near : near clipping plane을 결정. near clipping plane보다 멀리 있는 사물만 보이게 된다.
    • far : far clipping plane을 결정. far clipping plane보다 가까이 있는 사물만 보이게 된다.
  • glm::ortho
    • left, right, bottom, top : View Volume의 상, 하, 좌, 우 clipping plane의 위치를 결정.
    • zNear : near clipping plane을 결정
    • zFar : far clipping plane을 결정

 

여기서 다시 생각해보면, Projection Matrix는 두 가지 의미를 가진다.

  • 소프트웨어적 측면 : 카메라의 내부 파라미터를 조작한다.
  • 하드웨어적 측면 : 카메라의 내부 파라미터를 조작했음에도 불구하고 단일한 공간으로 바꿔준다.

 

정리해보면 이제 Object Space를 Clip Space로 변환할 수 있게 되었고, 필요한 행렬은 다음과 같다.

 

앞으로는 이 행렬을 쉐이더에 PVM 이라는 uniform 변수로 선언하고 사용할 것이다.

버텍스마다 달라져야 할 값이 아니기 때문에 uniform이 적절하기 때문.

 

바뀐 Vertex Shader 코드 : 

// Vertex Shader
#version 330                          
                                      
layout (location = 0) in vec3 pos;
                                      
out vec4 vertexColor;                 

uniform mat4 PVM; // PVM 행렬을 담을 변수
                                      
void main()                          
{                                     
    gl_Position = PVM * vec4(pos, 1.f);          
    vertexColor = vec4(clamp(pos, 0.f, 1.f), 1.f);               
}

gl_Position으로 내보내는 각 버텍스의 좌표는, PVM을 통해 Object Space에서 Clip Space로 변환된 좌표가 될 것이다.

 

main.cpp 일부 :

GLuint uniformPVM = 0; //쉐이더 내 PVM의 위치를 받아올 변수

/* (생략) */

shaderList[0].UseShader();
uniformPVM = shaderList[0].GetPVMLocation(); // PVM 위치 저장

/* (생략) */

glm::mat4 identityMatrix(1.f);
glm::mat4 T = glm::translate(identityMatrix, glm::vec3(0.f, 0.f, -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(2.f));

glm::mat4 model = T * R * S;
glm::mat4 view = camera.calculateViewMatrix(); // glm::lookAt 함수의 리턴값을 받아온다.
glm::mat4 projection = glm::perspective(45.f, mainWindow.getBufferWidth() / mainWindow.getBufferHeight(), 0.1f, 100.f);

glm::mat4 PVM = projection * view * model; // PVM 행렬 구하기

/* (생략) */

glUniformMatrix4fv(uniformPVM, 1, GL_FALSE, glm::value_ptr(PVM)); // 쉐이더 내부로 넣어주기

 

주의 : projection * view * model의 순서는 바뀌어서는 안된다.

Object Space -> World Space -> View Space -> Clip Space 순서대로 맞게 변환되어야 하기 때문.

 

 

 

이제 진짜 카메라가 존재하여 3D 공간 상의 물체를 볼 수 있는 프로그램이 되었다.

 

다만 아직 할 일이 남아있다. 프로그램 상에서 키보드, 마우스 조작을 통해 카메라를 조작할 것인데, 이에 따라 계속하여 카메라의 좌표계를 다시 계산해주어야 하기 때문이다.

 

이는 다음 포스팅에서 다룰 것이다.