일단 투영행렬이란 무엇이고 왜 필요한지 알아보자
우리가 살고있는 세계에는 원근감이라는게 있다. 가까운 물체는 크고 먼 물체는 작게 보이는 것이다.
언리얼로 원근감이 있는 카메라와 없는 카메라를 비교해보자
왼쪽은 원근감이 없는 흔히말하는 오쏘그래픽인 카메라이고
오른쪽은 시야각이 90도인 투영행렬이 들어간 카메라다.
누가봐도 오른쪽이 원근감도 있고 우리가 사는 세계와 비슷하다.
왼쪽 같은 카메라를 사용하는 게임들도 있지만 원근감이 있는 게임을 만들려면 투영행렬이 필요한것이다.
그럼 이 투영행렬은 어떻게 만드냐.
원근감을 표현하는 카메라는 카메라로부터 시작하는 사각뿔의 앞부분이 잘린 시야영역을 사용한다. 그럼 상하 좌우 전후로 이루어진 절두체(FRUSTUM)이 나오게 된다.
출처 : https://relativity.net.au/gaming/java/Frustum.html
그 절두체를 X축 방향에서 보면 이와 같은 그림이 나온다.
그중 근 평면이 우리의 모니터 화면이고 카메라로 부터 근 평면까지의 거리를 초점거리라고 한다(Focal Length).
시야각은 카메라에 우리가 설정해준 FOV값이니 모니터 화면 높이의 절반 값을 안다면 삼각함수를 이용해 초점거리를 알 수 있다. 하지만 유저의 모니터가 전부 같은 해상도가 아닐테니 이를 정규화 시켜줄 필요가 있다.
그래서 아래와 같이 모니터 화면을 가로, 세로의 크기가 2인 사각형으로 정규화 시켜 작업하고 이를 NDC라고 한다.
이렇게 정규화 한다면 모니터절반의 높이는 1이므로 초점 거리를 구할 수 있다.
초점거리를 구하는 공식은 각도와 높이를 아니 탄젠트를 이용해 구한다.
시야각이 커질수록 탄젠트 값이 증가하고 초점거리는 그에 반비례해 줄어든다. 그래서 물체는 시야각이 커지면 화면에서 작아지고 반대로 시야각이 작아지면 화면에서 크게 보인다.
언리얼에서 비교해보자
시야거리를 구했다면 다음으로 절두체 공간 안에 있는 어떤 점을 모니터 화면에 투영한 위치를 구해보자. 아래 그림과 같이 투영행렬을 적용하기 전 뷰 좌표계를 통해 변한된 최종 점의 위치를 Pview라고 하고 투영된 위치를 Pndc라고 하자.
우리가 사용중인 투영 공간은 철저히 카메라를 기준으로 만든 공간이다. 이전 26상에서 유도한 뷰 변환 행렬은 월드가 사용하는 오른손 좌표계를 따른다. 오른손 좌표계는 카메라 뒤를 향하는 방향이 +z축을 가지므로 물체의 깊이값 z는 음의 값을 가진다.
그럼 이제 비례식을 이용해 Yndc의 값을 구할 수 있다.
Xndc값도 구할 수 있는데 주의할 점은 우리의 모니터는 정사각형이 아닌 직사각형이라는 것이다. 그러므로 종횡비를 맞추기 위해 (가로/세로)의 값을 추가로 곱해줘야 한다. 이 값은 a라고 두면 밑에 식이 나온다.
이렇게 화면의 x,y좌표를 구해 물체의 최종위치를 구해주면 된다.
근데 또 문제가 있다. 깊이 문제다. 주어지는 물체를 카메라와의 거리를 신경쓰지 않고 막 그리면 물체가 겹칠경우 무조건 맨 뒤에 그린 물체를 맨 앞으로 그리게 되는건데. 이 를 해결하려면 카메라와의 거리정보 즉 깊이 값을 어딘가 저장해 두고 찍을때 가까운 물체를 그려줘야하는 것이다.
출처 : https://www.tomdalling.com/blog/modern-opengl/03-matrices-depth-buffering-animation/
컴퓨터 그래픽에서는 이를 보관하는 메모리 공간을 뎁스버퍼 혹은 z-버퍼 라고 한다. 이는 깊이를 판별하는데도 쓰이고 셰이더에서도 다양한 시각적 효과를 줄 수 있다.
그러니 이 뎁스값도 정규화해 보관하는편이 나중에 값을 비교하기 편할 것이다. 그래서 ndc좌표계는 x,y,z로 3차원 으로 이루어진다.
참고(OpenGL의 NDC좌표는 -1~1 DirectX는 0~1까지 사용한다.)
이렇게 NDC에서 정규화된 해당 점의 z 값을 구한 후 좌표계의 z 값에 이를 세팅한다.
이제 우리가 구하려는 대상은 모두 정해졌다. 뷰 좌표계를 선형변환 하는 투영행렬은 이와같이 표현할 수 있다.
동차 좌표계의 w 성분을 나누어 데카르트 좌표계로 만드는 과정을 생각해보면 우변을 아래와 같이 표현할 수 있다.
NDC의 x와 y값은 앞서 구했으므로 이를 참고해 투영행렬의 첫 두줄은 아래와 같이 유도할 수 있다.
NDC의 x 좌표와 y 좌표의 값은 모두 뷰 좌표의 -z 값의 분모를 공통으로 가지고 있다. 이 값이 w’라고 가정한다면 w’로 나누어 데카르트 좌표가 되는 동차좌표계의 정의와 맞아 떨어지게 된다. 이를 가정해 행렬의 네 번째 행을 구해보자. 이것이 투영 변환 행렬을 푸는 첫 번째 단서다.
이제 마지막 세 번째 행을 구하자. NDC의 z 값은 결국 i 뷰좌표x + j 뷰좌표y + k * 뷰좌표z + l로 구해지게 되는데, 그림과 카메라의 상황을 잘 생각해보면 뷰 좌표계의 x와 y 값이 변경된다고 NDC의 z 값에 변동이 올리는 만무하다. 그래서 위의 행렬은 더 아래와 같이 간략화 시킬 수 있다.
이제 마지막으로 k와 l만 구하면 된다. 미지수가 두개이므로 2개의 해를 찾아 두개의 연립방정식을 만들면 k와 l을 구할 수 있다.
z가 근평면인 경우 뷰 좌표계의 z 값이 근평면일때 NDC의 z값은 -1이 된다. 근평면의 뷰좌표계 값을 -n이라고 하면 이를 적용한 행렬은 다음과 같다.
이걸 계산하면 아래의 수식이 나온다.
z가 원평면인경우엔 z가 원평면 -f일 때 ndc값은 1이 된다.
이를 이용해 또 수식이 나온다.
그럼 k와 l에 대한 연립방정식으로 풀면 밑의 식에 도달한다.
그래서 최종 투영 변환 행렬은 다음과 같이 유도된다.
소스코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
//투영행렬
Matrix4x4 Camera::GetProjectionMatrix(int InScreenSizeX, int InScreenSizeY) const
{
//Xndc값을 구하기 위한 종횡비
static float invA = (float)InScreenSizeY / (float)InScreenSizeX;
//근평면과의 거리
static float d = 1.f / tanf(Math::Deg2Rad(FOV) * 0.5f);
//근평면과 카메라의 거리
static float n = 5.5f;
//원평면과 카메라의 거리
static float f = 1000.f;
//k와 l을 구하기 위한값(곱하기보다 나눗셈이 비용이 크게 드니 미리 나눠둠)
float invNF = 1.f / (n - f);
float k = f * invNF;
float l = f * n * invNF;
return Matrix4x4(
Vector4::UnitX * invA * d,
Vector4::UnitY * d,
Vector4(0.f, 0.f, k, -1.f),
Vector4(0.f, 0.f, l, 0.f));
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
|
http://colorscripter.com/info#e" target="_blank" style="text-decoration:none;color:white">cs |
[출처] 29. 투영 변환 행렬 ( Projective Transformation Matrix )|작성자 이득우
프러스텀 컬링 (0) | 2020.01.13 |
---|---|
[ScanLine]삼각형 빠르게 칠하기 (0) | 2019.12.19 |
벡터 내적과 외적을 응용한 왼쪽과 오른쪽의 판별 (0) | 2019.12.19 |
내적을 사용한 시야 판별 (0) | 2019.12.19 |
평면의 방정식과 D의 의미 (0) | 2019.12.19 |