DirectX/개념

[DX]##3. 원근 투영 ( Perspective Projection )

코딩하는상후니 2022. 12. 16. 18:44

 

 


 

 

*Clip Space

 
대표적인 투영 방법으로는  원근 투영   /   직교 투영 이 존재한다.
 
이번 단원에서 원근 투영에 대해서 알아보자.
 
 
 
 

*원근 투영 ( Perspective projection )

 
 
원근법에 대한 기본적인 방식은 먼 거리에 있는 물체가 더 작게 보이게 2D 화면에 나타내는 방식이다.
일반적인 예시로 원근법을 적용한 그림들을 쉽게 찾아볼 수 있다.
 

 
 
원근법을 적용한 그림들의 공통점은 '소실점의 존재' 이다.
이  소실점을 기준으로 일정 각도만큼의 직선들을 나란히 그리게되면
실제로 소실점에 가까운 부분들은 멀어보이게 되는 시각적 효과를 가져온다.
 
하지만, 3D 컴퓨터 환경에서 원근법을 적용하기 위해선 실제 그림에서 적용되는 방식과는 조금 다르다.
결론은 3D Rendering 에서는 소실점 개념은 필요치 않다.
 
우선, 한 가지 생각해볼 문제는 '우리가 바라보는 위치' 이다.
3D 공간을 표현하기 위한 우리의 세계에서는 '카메라가 우리의 눈' 이 된다.
 
또,
컴퓨터에 Rendering ( 랜더링 ) 되어진다는 의미는 해당 물체를 정점으로 표현하고
그 정점을 이으면서 나타나는 영역에 색을 입히게 되면서 우리 모니터에 보이게 된다.
 
정리하면,
우리의 눈인 카메라에 포착되는 장면 하나가 모니터 안 해상도로 표현되는 viewport 에 mapping ( 사상 ) 되는 과정이다.
( 모니터는 물리적인 화면 크기이고 viewport 는 모니터 안 창 크기를 말한다. 흔히 쓰이는 Web Browser 창 크기가 그것이다. )
 
정점을 통해 그려지는 컴퓨터는 정점의 위치만 알 수 있으면 해당 물체를 그릴 수 있다.
따라서, 소실점 개념이 필요없는 이유는 소실점을 기준으로 물체를 표현할 필요가 없기 때문이다.
 
3D 원근 투영을 위해서 우리는 '절두체' 공간을 만들어 원근 투영을 표현한다.
( 아래의 그림처럼 직각뿔의 위쪽을 뗀 모양이다. )

 

 
 
'절두체' 라는 특별한 공간을 이용해야하는 이유는 3D 원근 투영에 효과적이기 때문이다.
 
한가지 이유로는 '삼각비' 를 사용하기 때문이다.
물체에 포함된 하나의 정점이 화면 어디에 위치하는지 비율을 통해서 해당 위치를 구할 수 있는 식이 나온다.
 
이제 우리는 정점이 화면 어디에 위치하는지 알고 있다.
이 때 만약 해당 위치에 두 개 이상의 정점이 들어오게 된다면 우리는 어떤 정점을 그려야할까 ??
 
이를 구별하기 위해서 '깊이' 정보를 알아야한다.
절두체를 사용하는 또 한가지 이유는 이 깊이 범위를 지정하기 위함이다.
왜냐하면, 이론상 3D 공간에서의 깊이는 무한대이기 때문이다.
 
위 그림처럼 범위를 지정하기 위해 가까운 평면 ( near ) 과 먼 평면 ( far ) 를 둔다.
추가적인 이유는 나중에 나올 z 값 정규화을 위한 계산에서 거리의 시작점과 끝점을 정의해 mapping ( 사상 ) 하기 위함이다.

 
혼동되지 말아야할 사실은 여기는 'Clip Space' 라는 사실이다.
 
즉,
near 평면은 우리가 최종적으로 사상되어질 viewport 와는 다른 평면이다.
( 여기서 말하는 viewport 는 모니터 안에 표현될 2D Screen ( 창크기 )을 말한다. )
 
 
간단하게 생각하면,
'물체를 곧바로 viewport 에 해당하는 평면 크기에 맞춰넣으면 되지 않을까??' 라고 생각할 수도 있다.
다시 말해, Clip space 과정이 꼭 필요할까란 의문점이 들 수 있다.
 
우선 기준이 다르다. 우리가 쓰는 좌표계에서 ( 0,0 ) 은 중앙을 가리키지만,
viewport 의 ( 0,0 ) 은 왼쪽 위 끝을 가리킨다.
기준점을 바뀌면서 원근법을 적용할 수 있도록 한번에 사상할 수 있는 공식을 만드는 것이 생각처럼 쉽지 않아보인다.
즉,
하나의 행렬로 표현하지 못한다. 이것은 선형이 아니라는 점이다.
나중에 언급되겠지만 x,y 이외의 깊이를 나타내는 z 값은 모니터 화면에 선형이지 않다.
'깊이가 깊을수록 ( 멀리 있을수록 ) 화면에 어떻게 표현되어져야하는가'
이것이 원근법의 핵심이고 일반적인 행렬 계산으론 불가능하다는 것이다.
 
이러한 문제들을 해결하기 위해서
과정을 세분화하고 추가적인 과정을 넣어 원근법을 적용한 것이라고 볼 수 있다.
 
 
여기까지 우리는
선형 변환에서의 원근법 적용을 위해 일반적인 행렬 계산이 아닌 다른 추가적인 과정, 계산들이 필요하다는 것을 알 수 있다.
 
이제 우리가 해야할 일은 공간을 정의하는 일이다.
Clip space 에서 Screen Space 로 넘어가기 위해서 Clip space 에서 정점을 나타낼 수 있는 범위가 존재해야한다.
 
결론적으로,
일정 범위의 공간에 점들을 배치시키고 해당 공간의 점들을 viewport ( Screen Space ) 로 배치시키는 과정을 거쳐야한다.
이 때, 일정 범위의 공간에 점들을 배치시키는 작업을 '정규화' 라고 한다.
 
 
 
 
 
 
 
 

* 정규화

 
정규화 작업을 위한  가로, 세로, 깊이 범위는 얼마로 설정해야할까 ??
 
중요한 것은
우리가 정의하는 범위가 나중에 사상될 viewport 와 '비례' 한다는 사실이다.
 
x, y 를  '종횡비' 로 맞춰주기만 하면 된다.
보여질 viewport Screen 의 실제 크기를 보통 해상도 ( ex. 1920*980 ) 로 표현하는데
이 때, 종횡비 ( ex. r = 1920/980 ) 를 구해서 가로, 세로 비율을 맞춰준다.
 
즉,
우리가 계산하기 편하도록 x 는 [ -r, r ]  y 는 [ -1, 1 ] 범위 안에 넣어주면 된다.
따라서, Clip space 에서는 실제 viewport 의 크기는 중요하지 않다.
 
이 후,
Screen Space 에서 이 축소된 공간을 viewport 기준점으로 위치시키고
해상도의 크기에 비례시키면서 픽셀에 매칭하게 된다.
 
대신, z 값의 경우는 예외이다.
DirextX 에서는 깊이 z 는 [ 0, 1 ] 범위로 설정하길 원한다.
 
이렇게 각각의 ( x,y,z ) 좌표들을 최종적인 화면에 비례한 범위 내에 사상시키는 작업을 '정규화' 라고 한다.
 
 
우리의 목적은 범위 내의 좌표 값에 정규화를 적용할 행렬을 만드는 것이다.
투영행렬의 구현을 위해서 우리가 알아야할 변수들은 총 4가지이다.
 
@ n (near)  :  가까운 평면 거리
@  f (far)  :  먼 평면 거리
@ fov  :  수직 시야각
@  r  :  종횡비
 
종횡비는 후면 버퍼의 크기로 구할 수 있다. DX 에서 후면 버퍼는 RenderTarget class 에서 정의되어 있다.
이곳은 더블 버퍼링이 수행되어지며 보여질 화면 크기를 설정할 수 있다. 앞서 말한 viewport 가 존재하는 곳이다.
 
종횡비 ( r ) 이 필요한 이유는 앞에서도 설명했듯이, Clipspace 에서의 종횡비는 1 : 1 이기 때문이다.
만약, 종횡비가 2 : 1 이고 종횡비를 적용하지 않은 채 viewport 에 사상되어지면
가로의 픽셀하나가 2칸을 차지하게 되면서 그림이 퍼져보인다.
 
 
 
 
 
 
 
 
 

* 투영 행렬 구현

 
우선 z 값의 정규화는 나중에 살펴보도록 하고
near, far 사이 물체의 정점 ( x, y ) 을 정규화시켜  정점 ( x', y' )을 구하는 계산을 살펴보자.
 
 
 
 
 
 

* NDC ( Normalized Device Coordinate )

 
앞서 말한 최종 viewport 에 비례한 식으로 사상되어진 후의
좌표 ( x', y', z' ) 를 NDC 좌표라고 부른다.
 
먼저 투영되어지는 x', y' 값을 계산할 것이다.
아래 그림을 보자.

- 프랭크 D.루나. ( DX11 을 활용한 3D 게임 프로그래밍 입문 ),류광(역)

 
한 가지 알 수 있는 사실은
수평 시야각 ( B ) 를 수직 시야각 ( A ), 종횡비 ( r ) 로 구할 수 있다는 것이다.
 
이를 통해 우리는
수평 시야각은 종횡비에 의존적이란 사실을 알 수 있다.
왜냐하면, viewport 크기에 비례되기 위함이다.
보여지는 카메라 화면이 그대로 viewport 에 사상되기 때문에 우리는 한 축의 각도만 설정할 수 있다.
 
수평 시야각 = 40도, 수직 시야각 = 20도 이렇게 따로 설정할 수 없다는 소리이다.
수직 시야각이 정해지면 수평시야각은 자연스레 종횡비에 맞춰져 설정된다.
 
 
위 그림에서 표현하듯 투영창까지의 길이를 d 라고 놓을 때,
수평 시야각 B 는 수직 시야각 A 로 어떻게 표현되는지 계산해보자.
 
우리가 가진 정보로 tan 공식을 쓰게 되면
tan ( a/2 ) = 1 / d, tan ( b/2 ) = r / d 가 되고
 
즉, 수평시야각 b 의 d 거리는
' d = tan ( b/2 ) = r * tan ( a/ 2 ) ' 로 표현할 수 있다.
 
 
 
 
 

* 삼각비를 이용한 투영점 계산

 

- 프랭크 D.루나. ( DX11 을 활용한 3D 게임 프로그래밍 입문 ),류광(역)

 

실제 투영되는 점의 위치를 계산해보자.
 
결국 우리가 원하는 것은 x', y' 의 위치를 식으로 표현하고 싶은 것이다.
이 때, 우리는 닮은꼴 삼각형 tan 개념을 이용할 수 있다.
 
y' / d = y / z 이다.
이전에 우리는 y 축 기준에서의 d 를 구했다.
따라서 정리하면 y' = y / z * tan( a/2 ) 가 된다.
 
x' 도 마찬가지로 x 축 기준으로 d = r * tan ( a/2 ) 이므로
x' = x / r * z * tan ( a/2 ) 가 되겠다.
 
 
최종적으로 화면에 그려질 x, y 의 좌표는 아래의 식이다.
 
- 프랭크 D.루나. ( DX11 을 활용한 3D 게임 프로그래밍 입문 ), 류광(역)

 

결과적으로 x' y' z 의 범위는 아래와 같다. ( z 는 아직 정규화되지 않았다. )
 
-r <=  x'  <= r
-1 <=  y'  <= 1
n <=  z  <= f
 
 
 
 
 
 
 

*발생하는 문제점

 
최종 목적은 ( x,y,z,1 ) 좌표를 정규화하는 투영행렬을 만드는 것이다.
이제 z 값을 정규화해야하는 작업이 남아있다.
 
하지만, 위에서 나타낸 x', y' 를 구하는 식 자체가 행렬로 표현될 수 없다.
 
- 프랭크 D.루나. ( DX11 을 활용한 3D 게임 프로그래밍 입문 ),류광(역)

 

 
앞서 잠깐 언급되었던 z 값을 화면에 표현하는 것은 선형이지 않다고 했다.
이것의 의미는 행렬로 표현되지 못하기 때문이라고 했다.
 
더불어, 위에서 구한 식 x', y' 를 구하는 식에도 z 값으로 나눠야하는 영향이 미치고 있다.
우리의 최종 목적은 투영행렬을 구하는 것을 다시 상기하자.
 
우리가 ( x, y, z, 1 ) * projection Matrix 의 결과값으로
위에서 표현된 식 x' = x / r z tan ( @/ 2 ),  y' = y / z tan ( @/2 ) 으로 나올 수 있는  projection Matrix 을 구할 수 있을까 ??
 
"구할 수 없다."
( 우리가 알고 있는 행렬 계산법으로는 도출할 수 없다. )
 
어떻게 이 문제를 해결할 수 있을까 ??
 
 
 
 
 

* 해결방법

 
위 참고 그림에도 나와있듯이, 비선형 부분을 해결하기위해서는 z 를 나누는 부분을 분리하는 것이다.
따라서, 최종적으로 벡터의 w 자리에 z 값을 저장할 수 있는 행렬을 만들어야 한다.
 
- 프랭크 D.루나. ( DX11 을 활용한 3D 게임 프로그래밍 입문 ), 류광(역)
 
 
그림에서 보다시피,
z 값을 저장할 수 있도록 m34 부분의 값이 '1' 인것을 볼 수 있다.
 
나는 여기서 왜 B 부분이 필요한지 이해할 수 없었는데
우선 행렬의 B 부분이 0 으로 표현된다면 단지 A 값만으로 나타나기 때문에 식으로 표현될 수 없다.
 
또한, 우리는 near 와 far 범위를 설정하기 때문에 near (n), far (f) 의 식으로 표현하기 위해서도
2개의 미지수 A, B 부분이 필요하다.
 
왜냐하면,
우리는 z 값을 [ 0,1 ] 로 표현해야하는 정규화 과정이 필요하다는 것을 알고 있기 때문에
g ( z ) = A + B/z 일 때, g ( n ) = 0,  g ( f ) = 1 이란 사실을 알고 있다.
이 사실을 통해서 A, B 부분을 n, f 로 표현할 수 있다.
 
 
참고로, DirectX 에서는
w 값을 나누어주는 과정은 우리가 직접하지 않아도
하드웨어에서 계산되어지고 이 때, NDC 로 변환된다.
 
이후, viewport 좌표로 mapping 되고 나면
해당 x,y 값은 픽셀 단위의 값이 된다.
 
이 때,픽셀 쉐이더로 넘어가면서 투영된 삼각형의 정점과 정점 사이의 픽셀을
Rasterizor ( 레스터라이저 ) 에서 보간해주게 된다.
 
 
 
 
 
 
 
 

* z 값의 정규화 [ 0,1 ]

 
투영행렬을 곱한 후 w 에 저장되어있던 z 로 나눌 때, z 값은 A + B/z 가 된다.
 
우리의 목적은 z 값을 [ 0, 1 ] 사이로 사상해야한다.
near 평면을 정의함으로써 깊이가 '0' 인 좌표를 나타낼 수 있다.
또, 1 의 범위를 나타내기 위해 far 평면을 정의한다.
 
한마디로,
우리가 범위만 설정하면 해당 z 값이 해당 범위 어디에 위치하는지 매칭시킬 수 있다는 말이다.
범위 안에 존재하는 점을 다른 범위에 비례해서 재배치한다고 생각하면 된다.
 
아래 그림을 참고해 계산해보자.
 

- 프랭크 D.루나. ( DX11 을 활용한 3D 게임 프로그래밍 입문 ),  류광(역)

 
g( z ) = A + B / z 와 g(n) = 0, g(f) = 1 인 사실만으로 A, B 를 구할 수 있고 이로써 우리는 투영행렬을 만들 수 있다.
 
 
 
 
 

* Clip Space  / NDC 구분

 
투영행렬을 곱해 Clip Space 에 온 좌표는 NDC 좌표일까 ??
 
Clip Space 에 존재하는 좌표는 모두 NDC 라고 하기엔 애매한 부분이 있다.
앞서 구한 카메라 행렬까지 선형 변환된 좌표를 투영행렬을 곱했을 때 Clip space 에 존재한다고 볼 수 있다.
이 때, 좌표는 NDC  좌표일까 ??
=> 그럴수도 있고 아닐 수도 있다.
 
w 에 저장된 z 값으로 나누는 행위는 DirextX 에서 Viewport 로 넘어가기 전에 수행되는데
이 때, z 값으로 나누는 행위가 발생하지 않았을 땐 NDC 라고 할 수 없다.
 
즉,
'z 값으로 모두 나눠져있는 상태' 일 때 정규화 과정이 수행된 것이며
이 때,비로소 'NDC' 라고 말할 수 있다.
 
 
 
 
 

p.s

(수정_221216) 이전 원근 투영글을 재작성한 글.

 

 

 

 

참고 자료

 

 

- 프랭크 D.루나. ( DirectX11 을 활용한 3D 게임 프로그래밍 입문 ),  류광(역)