OpenGL/공부

[OpenGL] ModelView Matrix (glm::lookAt)

ciel45 2024. 6. 1. 15:13

카메라에 대한 포스팅을 적기 전, 이 두가지 행렬에 대한 글을 적고자 한다.

 

ModelView Matrix는 물체의 버텍스의 좌표를 Object Space에서 View Space로 바꿔준다.

이는 물체 자체를 중심으로 하는 좌표계에서, 카메라를 원점으로하는 좌표계로 바꿔준다는 의미이다.

 

앞선 포스팅에서 T * R * S 로 Model Matrix를 만들어, 이를 통해 물체를 움직인 바가 있었다.

https://ciel45.tistory.com/89

 

[OpenGL] Model Matrix를 통한 물체 조작(Translate, Rotate, Scale)

이번엔 지난 번에 만들었던 삼각형을 조작해볼 것이다.기본적으로 물체에 대한 조작은 선형 변환으로 이루어진다. 위키백과에 선형 변환을 검색하면 임의의 벡터에 대하여... 임의의 스칼라에

ciel45.tistory.com

 

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

좌측이 Model Matrix를 곱하기 전, 우측이 Model Matrix를 곱한 후이다.

 

Model Matrix를 곱함으로써, 오브젝트를 World Space 내 임의의 위치에 배치할 수 있다.

 

 

하지만 실제 대부분의 그래픽스 프로그램에서는, 카메라가 존재한다.

물체의 위치는 카메라에서 본 위치로 변환되어야 한다. 이를 그림으로 나타내면 다음과 같다.

원점은 물체 기준(우측)도, 월드 기준(중앙)도 아닌 카메라 기준(좌측)이 되어야 한다.

 

생각해보면, 카메라도 원점에 있던 것이 Translate, Rotation을 통해 저 위치에 배치된 것이다.

(카메라의 Scale은 의미가 없으므로 생략)

그러므로 카메라를 움직인 행렬도 T * R로 표현될 수 있다.

그리고 그 행렬을 View Matrix라고 칭할 것이다.

 

이제 잘 생각해보면,

(World Space 기준 좌표) = Model Matrix * (Object Space 기준 좌표)

(World Space 기준 좌표) = View Matrix * (Camera Space 기준 좌표) 

 

좌항이 같으므로 식을 정리해보자면,

Model Matrix * (Object Space 기준 좌표) = View Matrix * (Camera Space 기준 좌표)

 

이제 양변에 View Matrix의 역행렬을 곱해서 최종적으로 Camera Space 기준 좌표를 얻어낼 수 있다.

(Camera Space 기준 좌표) = (View Matrix)^-1 * Model Matrix * (Object Space 기준 좌표)

 

(View Matrix)^-1 * Model Matrix를 바로 ModelView Matrix라고 부르며,

View Matrix를 V로, Model Matrix를 M으로 하여 수식으로 예쁘게 나타내면 다음과 같다.

ModelView Matrix

 

M은 이미 구해놓았으므로, V^-1을 구하면 된다.

 

결론부터 말하자면, V = T * R이므로 V^-1은 행렬의 곱의 역행렬 공식(?)에 따라 다음과 같다.

 

 

역행렬의 기하학적 의미를 생각해보면, 원본 행렬이 하는 일을 반대로 하는 행렬을 의미한다.

따라서 T^-1은 T의 반대 역할, 즉 카메라가 원점에서 움직여온 translate의 반대 방향 translate를 의미한다.

예를들어, T가 (1, -2, 3)만큼 이동시키는 행렬이었다면, T^-1은 (-1, 2, -3)만큼 이동시키는 행렬이 된다.

 

그리고 역행렬의 수학적 의미를 살펴보면, 역행렬과 원본 행렬을 곱하면 Identity Matrix가 된다.

R^-1은 R과 곱했을 시 Identity Matrix를 만드는 행렬을 의미하는데,

 

R의 각 은 각각 x축 단위, y축 단위, z축 단위, 원점의 위치를 나타낸다고 했었다. (상단 Model Matrix 글 참조)

그렇다면 R^-1의 각 x축 단위, y축 단위, z축 단위, 원점의 위치를 나타내도록 되어야 한다.

 

왜냐하면 행렬의 곱셈 방식을 살펴보면, R * R^-1을 했을 때 R의 각 열이 R^-1의 각 행과 곱해지게 된다.

 

R^-1이 그렇게 되어야 R * R^-1을 했을 때 행렬의 곱셈 법칙에 의해 x축끼리, y축끼리, z축끼리 곱해져서 Identity Matrix가 만들어진다.

 

x축, y축, z축은 서로 직교하므로, 만약 서로 다른 축이 곱해진다면 벡터의 내적 법칙에 의해 그 값은 1이 아닌 0이 되어, Identity Matrix가 만들어질 수 없기 때문이다.

 

 

그렇다면 그 R^-1을 만드는 방법은 간단하다. R을 전치(transpose)시키면 되는 것이다.

이렇게 R^-1과 T^-1을 각각 구하여, V^-1을 구할 수 있게 되었다.

 

-------------------------------------------------------------------------------------------------------------------------

 

 

사실 V^-1을 구하는 건 이미 OpenGL 함수로 만들어져있다.

굳이 저런 계산을 안해도 된다.

 

glm::lookAt(position, target, up) 을 사용하면 된다. 이 함수가 V^-1을 바로 뱉어준다.

position은 카메라의 위치, target은 카메라가 보는 포인트, up은 월드의 위 방향(0, 1, 0)을 넘겨주면 된다.

 

그런데 매개변수를 저렇게 받는 이유가 무엇일까?

 

원래 회전은 Pitch, Yaw, Roll 3가지로 나타내질 수 있다. 그런데, 이 3가지가 혼합된다면 순서에 따라 결과가 달라진다.

사람이 고개를 끄덕하고 갸웃하는 것, 갸웃하고 끄덕하는 것의 결과가 다르다는 것을 생각해보면 쉽게 알 수 있다.

 

그래서 순서에 구애받지 않게 저렇게 매개변수를 받는 것이다.

 

뜬금없이 up을 넘겨주는 이유는 다음과 같다.

일단 View Matrix를 구한다는 것은, 카메라의 좌표계를 구하는 것과 같다.

position과 target을 통해, 카메라의 front를 얻어낼 수 있다. 

 

OpenGL에서, 카메라의 front는 z축의 반대 방향과 같다.

OpenGL에서 카메라가 보는 방향은 -Z 방향이다.

이는 OpenGL이 오른손 법칙을 따르기 때문이다.

 

눈 앞에 x축, y축으로 이루어진 2차원 좌표계가 그려진다고 상상해보자.

x축, y축 순으로 오른손의 네 손가락으로 감아쥐었을 때, 엄지가 향하는 방향이 z축의 방향이다. 

 

참고로 DirectX는 반대로 왼손법칙을 따른다. 

 

우리는 -z를 얻어냈지만, x축과 y축은 아직 모른다.

그래서 일단 월드의 위 방향에 해당하는, (0, 1, 0)을 대충 up으로 넘겨주었다. (어설픈 y축)

 

이 up은 카메라의 y축에 비해 조금 뒤틀려있을 것이다. 카메라가 기울어져있다면 up은 똑바른 위 방향이 아닐 것이기 때문.

 

그렇다면 우리는 어설픈 y축과, 올바른 z축을 가지고 있다.

이제 벡터의 외적을 활용할 시간이다. 

벡터의 외적. 마찬가지로 오른손 법칙을 따른다.

 

핵심은, 두 벡터의 외적은 두 벡터를 포함하는 평면의 법선 벡터를 구한다는 것이다.

 

따라서 (어설픈 y축) X (올바른 z축)을 한다면? 올바른 x축을 얻어낼 수 있다.

그리고 다시 (올바른 z축) X (올바른 x축)을 한다면? 올바른 y축을 얻을 수 있다!

 

이런 과정을 통해 glm::lookAt 함수는 position, front, up만으로 View Matrix를 만들 수 있는 것이다.

 

 

 

결론적으로, glm::lookAt의 리턴값 (View Matrix)^-1을 이전에 구한 Model Matrix와 곱하면?

ModelView Matrix가 완성된다.

 

 

코드로 나타내면 다음과 같다.

// Camera 클래스 멤버 함수.
glm::mat4 Camera::calculateViewMatrix()
{
	return glm::lookAt(position, position+front, worldUp);
}
//------------------------------------------------------------------

// Model Matrix
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;

// View Matrix (역행렬)
glm::mat4 view = camera.calculateViewMatrix();

// ModelView Matrix
glm::mat4 modelView = view * model

 

다소 복잡한 내용을 다뤘는데, 사실 이런거 몰라도 glm::lookAt에 파라미터만 올바르게 넣어주면 잘 작동한다.

그래도 이러한 이론적 배경을 아는 것이 장기적으로는 확실히 도움이 될 것이라고 생각한다.