절두체 컬링(Frustum Culling)

 

- 3차원 월드에는 대단히 많은 폴리곤과 오브젝트가 있지만, 이들 중에서 실제로

 카메라의 시야범위에 포함되는 것들만 렌더링하고, 나머지 것들은 렌더링 하지 않는 기법을 말한다.
- 절두체 컬링은 3D 렌더링 기법에 있어서 가장 중요한 속도 증가 기법 중 하나이다.

 

 

 

절두체란?

 

- 투영행렬을 계산할 때 범위의 모양이 삼각뿔의 머리를 잘라놓은 것 같다고 해서 절두체라고 부른다.

- 절두체는 총 6개의 평면으로 이루어져 있다.

 

 

절두체를 이루는 6개의 평면

근평면 : 카메라와 수직하며 제일 가까운 곳의 시야 범위를 나타내는 평면

원평면 : 카메라와 수직하며 제일 먼 곳의 시야 범위를 나타내는 평면

좌평면 : 카메라의 좌측 시야 범위를 나타내는 평면

우평면 : 카메라의 우측 시야 범위를 나타내는 평면

상평면 : 카메라의 상단 시야 범위를 나타내는 평면

하평면 : 카메라의 하단 시야 범위를 나타내는 평면

 

6개의 평면 내부와 외부를 판단하기 위해 평면의 방정식이라는 수학적 도구를 사용한다.

 

위에서 바라 본 절두체의 모습

 

 

프러스텀 컬링 원리

 

절두체 내부에 점이 포함되는지 판단하기 위해서는 6개의 평면 방정식에 점의 좌표를 대입해

모든 평면 방정식의 결과값이 양수(+)이면 절두체 내부에 포함되어 있는 것이다.

 

절두체와 점과의 포함관계

<왼손, 오른손 좌표계>

 

공간 좌표에서 z축을 결정하기 위한 방법이다. 

 

좌표계를 표현할 땐 보통 아래의 그림과 같은 왼손 좌표계와 오른손 좌표계 두 가지 방법을 이용한다.

게임 캐릭터를 씬 상에 불러와서 좌표값을 초기화 시켜보면 사용자는 캐릭터의 앞 또는 뒷모습을 보게된다.

 

왼손 좌표계에서는 관찰자(Player)가 오브젝트의 뒷면, 오른손 좌표계에서는 오브젝트의 정면을 본다.

 

유니티는 왼손 좌표계를 사용하므로 카메라에 비치는 것은 오브젝트의 뒷면,

언리얼은 유니티와 같은 왼손 좌표계를 사용하지만 방향이 다르다.

언리얼 엔진에서의 Forward Vector는 ( 1, 0, 0 ), Right Vector는 ( 0, 1, 0 )이다.

 

여기서 X, Y, Z축의 좌표축을 나타내는 기즈모라는 아이콘이 있는데,

이것은 공간 좌표계를 생각하면 된다.

 

또는 플레밍의 왼손법칙을 생각하면 쉽다.

 

 

전류 : X방향

힘이 : Y방향

자기장 : Z방향

 

툴에서 사용하는 좌표계는 아래와 같다.

 

왼손 좌표계 : DirectX, UnrealEngine, UnityEngine

오른손 좌표계 : 그래픽 라이브러리 / OpenGL, 3ds max

선 그리기를 할 때 그릴 선이 어디 위치에 있는지 구하려면 실수 연산이 필요한데

이를 정수연산으로만 끝낼 수 없을까에서 찾아본 결과 "브레젠험알고리즘"이란 것을 발견하였다.

 

Bresenham의 선 알고리즘 :

-> 실수 연산이 필요 없고 정수 연산만으로 처리되는 속도가 매우 빠른 래스터 방식

-> 컴퓨터 그래픽에서 선을 긋는 알고리즘.

https://translate.google.co.kr/translate?hl=ko&sl=en&u=https://en.wikipedia.org/wiki/Bresenham%2527s_line_algorithm&prev=search


실수연산이 정수연산에 비해서 매우 느린 것도 이유가 되고,

화면에 그려질 때도 정수단위의 픽셀에 그려지기 때문에 정수 연산만으로 처리하는 것이 더 좋다고 한다.

 

x의 변화량이 y의 변화량 보다 크다면

 

위와 같은 그림으로 그려지게 될 텐데 xi와 xi + 1까지는 y값이 더 가깝기 때문에 yi에 점이 그려지고

xi + 2때는 y값이 yi + 1에 더 가깝기 때문에 yi + 1에 그려져야 나름 부드럽게 보이는 선이 만들어지게 된다.

 

 

 

방정식에 의해 나온 y값이 정수값인 yi와 yi + 1중 어디에 가까운지 판단하는 것 부터 생각해보면 된다.

위에 그림처럼 d1과 d2값을 구해 더 차이가 적은 쪽을 선택하면 된다.

 

직선의 방정식 : mx + b ( m는 기울기, b는 y절편 ) m = dy / dx

 

d1 = m(xi + 1) + b - yi

d2 = (yi + 1) - m(xi + 1) - b

 

계산 한 번으로 어디에 가까운지 판단하기 위해서 위에 두 식을 합쳐야 한다.

d1 - d2를 구해 이 값이 0보다 작다면 d1이 작다는 의미로 판단할 수 있고,

0보다 크다면 d2가 더 작다는 것을 알 수 있다.

 

d1 - d2 = m(xi + 1) + b - yi - (( yi + 1 ) - m(xi + 1) - b )

           = m(xi + 1) + b - yi - ( yi + 1 ) + s ( xi + 1)  + b

           = 2m(xi + 1 ) + 2b - 2yi - 1

 

식을 x와 y값을 이용해서만 계산할 수 있게 하기위해 m = dy / dx를 넣어 정리해준다.

 

d1 - d2 = 2(dy / dx) (xi + 1) + 2b - 2yi - 1

 

해당 값의 부호만 판단하면 되기 때문에 굳이 비용이 많이 드는 나누기 연산을 할 필요가 없다.

 

따라서  dx를 양변에 곱해준다.

 

dx(d1 - d2) = 2(dy)xi + 2dy + 2b(dx) - 2(dx)yi - dx

b , dx, dy값은 변화하지 않는 상수이므로 한 번만 계산하면 된다.

위의 식을 다시 이쁘게 정리 해보면,

 

dx( d1 - d2 ) = 2(dy)xi - 2(dx)yi + 2dy - dx + 2b(dx)

                 = 2(dy)xi - 2(dx)yi + 2dy + dx(2b - 1)

 

2dy + dx(2b - 1)로 인해 dx, dy, b의 값을 알 수 있기 때문에 계산을 하고 한 변수에 넣어 두면 된다.

 

식에서도 보기 쉽게 2dy + dx(2b - 1)를 A로 표현하겠다.

 

브레젠험 알고리즘은 이전 값을 이용하여 다음 값을 찾는 형식으로 루프를 돌게 되어있다.

이전 값을 Pi라 하고 그 이후의 값을 Pi + 1이라고 하자.

 

Pi = 2(dy)xi - 2(dx)yi + A

Pi+1 = 2(dy)xi + 1 - 2(dx)yi + 1 + A

 

이전 Pi값에 계속 값을 더해주어서 다음 값을 찾는 방법을 이용하기 때문에 더해줘야 되는 값을 구하기 위해

Pi + 1에서 Pi를 빼보겠다.

 

Pi + 1 - Pi - 2(dy)xi+1 - 2(dx)yi + 1 + A - 2(dy)x + 2(dx)y - A

= 2(dy)(xi + 1 - xi) - 2(dx)(yi+1 - yi)

 

지금은 무조건 한 칸씩 옆으로 가니까 dx가 dy보다 크다고 가정을 한 상태에서 식을 세우고 있기 때문에

xi+1 - xi = 1 이 된다.

대신 y값은 이전과 같은 점일 수도 있고 한 칸 움직인 점일 수도 있다.

 

그렇기 때문에 위의 식에서 부터 두 가지 경우에 대한 식으로 정리할 수 있다.

 

1. yi+1 - yi = 0 일 때 : 2(dy) -> Pi + 1 = Pi + 2(dy)

2. yi+1 - yi = 1 일 때 : 2(dy) - 2(dx) -> Pi+1 = Pi + 2(dy) - 2(dx)

 

이를 통해 다음 P값을 구할 수 있는 식을 구하게 되었다. 

현재 상황의 경우에는 처음 점을 (x1, y1)으로 두고 그 다음 점을 (x1 + 1, y1 + 0.5)로 두고 구하면 된다.

x는 1씩 증가하지만 y는 어떻게 될 지 모르기 때문에 절반 값만 넣어주었다.

위의 점화식에 넣고 구해보면 최종적으로 처음 점은 P0 = 2(dy) - (dx)가 나오게 된다.

 

 

Clip Space

 

각 vertex shader 실행의 마지막에 OpenGL은 지정된 범위의 좌표를 받아들이고,

이 범위에서 벗어난 모든 좌표는clipped(자르다)된다.

clip된 좌표들은 폐기되고 남은 좌표들은 최종적으로 fragment가 되어 화면에 보이게 된다.

clip space에서 이름을 가져오는 위치이기도 하다.

 

눈에 보이는 모든 좌표들이 -1.0와 1.0 범위 안으로 지정하는 것은 직관적이지 않기 때문에

우리만의 좌표를 지정하고 작업을 수행한 후 그들을 다시 OpenGL이 원하는 NDC로 변환한다.

 

vertex 좌표를 view에서 clip-space로 변환하기 위해

우리는 좌표의 범위를 지정(예를들어 각 축에 대해 -1000에서 1000까지)하는 project matrix라고 불리는 것을 정의한다. 그런 다음 projection 행렬은 이 지정된 범위에 잇는 좌표들을 NDC(-1.0, 1.0)로 변환한다.

지정된 범위 밖에 있는 좌표들은 -1.0 ~ 1.0에 매핑되지 않으므로 clip된다.

projection 행렬에서 지정한 이 범위를 사용한다면 좌표 (1250, 500, 750)은 보이지 않게된다.

 x 좌표가 범위밖에 있어 NDC에서 1.0보다 높아지므로 clip되기 때문이다.

 

projection 행렬이 생성하는 viewing box는 절두체(frustum)라고 불리고

이 절두체 내부에 있는 각 좌표들은 유저의 화면에 나타나게된다.

지정된 범위에서 NDC(2D view-space 좌표로 쉽게 매핑가능함)로 변환하는 전체적인 과정은

projection(투영)이라고도 불린다.

 

projection 행렬이 3D 좌표를 2D에 매핑하기 쉬운 NDC로 투영하기 때문이다.

모든 vertex들이 clip space로 변환되었다면 perspective division이라고 불리는 마지막 작업이 수행된다.

여기에서 위치 벡터의 x, y, z 요소들을 벡터의 w 요소로 나누어진다.

perspective division은 4D clip space 좌표를 3D NDC로 변환하는 것이다.

이 단계는 vertex shader의 실행 마지막에 자동으로 수행된다.

 

이 단계 이후에는 결과 좌표들을 screen 좌표에 매핑하고 fragment로 변환하는 것이다.

projection 행렬은 view 좌표를 clip 좌표로 변환하기 위해 두개의 다른 형식을 받는다.

각 형식은 자신만의 고유한 절도체를 가진다.

우리는 정사영(orthographic) projection 행렬과 원근(perspective) projection 행렬 중에서 하나를 생성할 수 있게된다.

+ Recent posts