[DirectX 12 3D게임 입문] Chapter 5.6: 렌더링 파이프라인 - 정점 셰이더 단계
5.6 정점 셰이더 단계
정점 셰이더(vertex shader)를, 정점 하나를 받아서 정점 하나를 출력하는 함수로 생각해도 된다. 개념적으로, 하드웨어 안에서 다음과 같은 일이 일어난다고 할 수 있다.
for(UINT i = 0; i < numVertices; ++i)
outputVertex[i] = VertexShader( inputVertex[i] );
변환, 조명, 변위 매핑 등 수많은 특수 효과를 정점 셰이더에서 수행할 수 있다.
5.6.1 국소 공간과 세계 공간
물체의 기하구조를 장면 전역의 좌표계를 기준으로 직접 구축하는 것이 아니라 물체 자신의 국소 좌표계를 기준으로 구축하는 것이 더 편하다. 전자의 좌표계를 세계공간(world space)라고 하고 후자를 국소 공간(local space)라고 부른다. 국소 공간에서 3차원 모형의 정점들을 모두 정의했다면, 다음으로 할 일은 그것들을 세계 공간에 적절한 위치와 방향으로 배치하는 것이다. 이를 위해서는, 전역 공간 좌표계에 상대적인 국소 공간 좌표계의 원점 위치와 축 방향들을 지정하고, 그에 해당하는 좌표 변경 변환을 수행해야 한다.
국소 좌표계에 상대적인 좌표를 전역 장면 좌표계에 상대적인 좌표로 바꾸는 것을 세계 변환(world transform)이라고 부르고, 해당 행렬을 세계 행렬(world matrix)라고 부른다.
각 모형을 자신의 국소 좌표계에서 정의하는 데에는 여러 장점이 있다.
- 더 쉽다. 일반적으로 국소 공간에서는 물체의 중심이 원점과 일치하며, 물체가 주축 중 하나에 대칭이다.
- 한 물체가 여러 장면에서 재사용될 수 있는데, 각 장면마다 국소 좌표계를 해당 세계 좌표계로 적절히 변환하는 좌표 변경행렬을 정의해서 적용하는게 좋다.
- 한 물체를 하나의 장면 안에서 위치나 방향, 비례를 달리해서 여러 번 그리기도 한다. 인스턴스마다 물체의 정점 및 색인 자료를 중복해서 지정한다면 낭비가 심하다. 그보다는, 물체의 국소 공간을 기준으로 한 기하구조의 복사본 하나만 저장해 두고, 물체의 개별 인스턴스마다 세계 공간 안에서의 위치와 방향, 비례를 정의하는 세계 행렬을 적절히 다르게 설정해서 그리는 것이 효율적이다. 이런 기법을 인스턴싱이라고 부른다.
세계 행렬을 만들기 위해서는 국소 공간 원점 및 축들의 세계 공간에 상대적인 좌표를 알아야 한다. 그런데 그런 좌표들을 구하기 쉽지 않거나 직관적이지 않은 경우가 있다. 좀 더 흔한 접근방식은 W를 일련의 변환들의 형태로 정의하는 것이다. 예를 들어 W = SRT로 둘 수 있다. 비례행렬, 회전행렬, 이동행렬을 곱한 것을 세계 행렬로 사용한다.
5.6.2 시야 공간
3차원 장면의 2차원 이미지를 만들어 내려면 가상의 카메라를 배치해야 한다. 그 카메라는 세계에서 관찰자에게 보이는 영역을 결정한다. 그 영역이 바로 응용 프로그램이 2차원 이미지로 만들어서 화면에 표시할 영역이다. 그러한 가상 카메라에 국소 좌표계를 부여한다고 하자. 이 좌표계는 시점 공간이나 카메라 공간이라고도 하는 시야 공간(view space)을 정의한다. 카메라는 이 시야 공간의 원점에 놓여서 양의 z축을 바라본다. 세계 공간에서 시야 공간으로의 좌표 변경 변환을 시야 변환(view transform)이라고 부르고, 해당 변환 행렬을 시야 행렬(view matrix)이라고 부른다.
시야 공간에서 세계 공간으로의 좌표 변경 행렬은 다음과 같다.
세계 공간에서 시야 공간으로의 변환 행렬은 바로 W의 역행렬이다.
일반적으로 세계 좌표계와 시야 좌표계는 위치와 방향만 다르므로, W = RT라고 둘 수 있다.
결론적으로, 시야 행렬은 다음과 같이 정의된다.
Q가 카메라의 위치이고 T가 카메라가 바라보는 지점, j가 세계 공간의 위쪽을 가리키는 단위 벡터라고 하자. 카메라가 바라보는 방향은 다음과 같이 주어진다.
이 벡터는 카메라의 국소 z축에 해당한다. w의 오른쪽을 가리키는 단위벡터는 다음과 같이 주어진다.
이 벡터는 카메라의 국소 x축에 해당한다. 카메라의 국소 y축을 서술하는 벡터는 다음과 같이 주어진다.
이처럼, 카메라의 위치와 대상점, 그리고 세계 상향 벡터만 있으면 카메라를 서술하는 국소 좌표계를 유도할 수 있으며, 그것을 이용해서 시야 행렬을 구할 수 있다.
XMMATRIX XM_CALLCONV XMMatrixLookAtLH( // Outputs view matrix V
FXMVECTOR EyePosition, // Input camera position Q
FXMVECTOR FocusPosition, // Input target point T
FXMVECTOR UpDirection); // Input world up direction
일반적으로 세계 공간의 y축이 장면의 위쪽 방향에 해당하며, 그런 경우 보통의 카메라 설정에서 상향 벡터는 j = (0, 1, 0)이다.
XMVECTOR pos = XMVectorSet(5, 3, -10, 1.0f);
XMVECTOR target = XMVectorZero();
XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
XMMATRIX V = XMMatrixLookAtLH(pos, target, up);
5.6.3 투영과 동차 절단 공간
카메라에 보이는 공간은 하나의 절두체로 정의된다.
3차원의 환상을 만들어 내려면 투영을 반드시 평행선들이 하나의 소실점으로 수렴하는 방식으로, 그리고 물체의 3차원 깊이가 증가함에 따라 그 투영의 크기가 감소하는 방식으로 수행해야 한다. 원근 투영이 바로 그런 방식이다.
3차원 기하구조의 한 정점에서 시점으로의 직선을 정점의 투영선이라고 부른다. 원근 투영 변환은 하나의 3차원 정점 v를 그 투영선이 2차원 투영 평면과 만나는 점 v'로 변환하는 변환이다.
5.6.3.1 절두체의 정의
절두체를 네 가지 수량을 이용해서 정의할 수 있다. 원점과 가까운 평면 사이의 거리 n, 먼 평면 사이의 거리 f, 수직 시야각 a, 종횡비 r이다. 종횡비는 r = w/h인데, 여기서 w는 투영 창 너비이고 h는 투영 창의 높이이다. 이 이미지가 결국에는 후면 버퍼에 사상되므로, 투영 창의 종횡비를 후면 버퍼의 종횡비와 일치시키는 것이 바람직하다. 다르면 비균등 비례를 적용해야 해서, 이미지가 왜곡된다. 수평 시야각은 β로 표기된다. 이 시야각은 수직 시야각 α와 종횡비 r로 결정된다. 계산하기 쉽도록 투영 창의 높이를 2로 두기로 한다. 그러면 너비는 다음과 같이 주어진다.
d는 다음과 같이 구할 수 있다.
이렇게 투영 창 거리 d까지 구했다. 이제 수평 시야각을 구해 보자.
이를 정리하면, 투영 창의 높이가 2일 때 수직 시야각과 종횡비로부터 수평 시야각을 구하는 다음과 같은 공식이 나온다.
5.6.3.2 정점의 투영
위의 그림에서 닮은꼴 삼각형의 원리를 적용하면 다음을 알 수 있다.
이때 점 (x, y, z)가 절두체 안에 있을 필요충분조건은 다음과 같음을 주목하기 바란다.
5.6.3.3 정규화된 장치 좌표(NDC)
앞에서는 투영된 점의 좌표를 시야 공간에서 계산했다. 이 방식의 문제점은 투영 창의 크기가 종횡비에 의존한다는 것이다. 종횡비에 대한 의존성을 없앨 수 있다면 작업이 더 수월할 것이다. 해결책은, 투영된 점의 x성분을 다음과 같이 [-r, r] 구간에서 [-1, 1]으로 비례하는 것이다.
x, y 성분을 이렇게 사상한 후의 좌표를 정규화된 장치 좌표(normalized device coordinates, NDC)라고 부른다. 이 경우 점(x, y, z)가 절두체 안에 있을 필요충분조건은 다음과 같다.
시야 공간에서 NDC 공간으로의 이렇나 변환을 일종의 단위 변환으로 볼 수 있다. x 축에서 NDC의 한 단위는 시야 공간의 r단위와 같다(즉, 1ndc = r vs). 따라서, 시야 공간의 x단위를 NDC의 단위로 변환하고 싶다면 다음 공식을 사용하면 된다.
이제 투영 공식들을 다음과 같이 변경한다면, 투영된 x와 t의 NDC 좌표성분들을 직접 얻을 수 있다.
NDC좌표에서는 투영 창의 높이가 2이고 너비도 2이다. 투영 창의 크기가 고정되었으므로 하드웨어는 종횡비를 몰라도 된다.
5.6.3.4 투영 변환을 행렬로 표현
식 5.1은 비선형이므로 행렬 표현이 없다. 이를 해결하는 요령은 식을 선형인 부분과 비선형인 부분으로 나누는 것이다. 식에서 z로 나누기가 바로 비선형 부분이다. 투영 시 x와 y뿐만 아니라 z 성분도 정규화된다. 이는 투영 변환 단계에서 나누기에 사용할 원래의 z 성분이 더 이상 남아 있지 않음을 뜻한다. 따라서 반드시 투영 변환 전에 입력 z성분을 어딘가에 저장해 두어야 한다. 이를 위해, 동차 좌표의 w 성분에 입력 z 성분을 복사해 둔다.
상수 A와 B는 입력 z좌표를 정규화된 구간 [-1, 1]로 변환하는데 쓰인다. 임의의 점 (x, y, z, 1)을 곱하면 다음이 나온다.
이제 비선형 부분을 적용하면, 다시 말해서 각 좌표성분을 w = z로 나누면 투영 변환이 완성된다.
투영된 x 성분과 y 성분이 식 5.1과 일치함을 주목하기 바란다.
5.6.3.5 정규화된 깊이 값
3차원 z성분은 2차원으로 투영되어도 깊이 버퍼링 알고리즘을 위해서는 3차원 깊이 정보가 여전히 필요하다. Direct3D는 깊이 성분도 일정구간으로 정규화한다. 단 깊이 성분의 정규화 구간은 [0, 1]이다. 이러한 정규화를 위해서는 구간 [n, f]를 구간 [0, 1]로 사상하는 함수 g(z)가 필요하다. 그러한 함수는 순서를 보존하는 함수이어야 한다.
식 5.3에 의해 z 성분은 다음과 같은 변환을 거친다.
이제 다음 두 구속조건을 만족하는 A와 B를 선택해야 한다.
- 조건 1: g(n) = A + B/n = 0 (가까운 평면이 0으로 사상됨)
- 조건 2: g(f) = A +B/f = 1 (먼 평면이 1로 사상됨)
조건 1을 B에 대해 정리하면 B = -An이 나온다. 이를 조건 2에 대입해서 A에 대해 정리하면 다음이 나온다.
따라서
이다. g의 그래프는 순증가 함수이자 비선형 함수임을 보여준다. 순증가 함수는 곧 순서 보존 함수이다. 깊이 값들의 대부분이 치역의 작은 부분 집합에 몰려있다. 이는 깊이 버퍼 정밀도 문제로 이어진다.
이제 앞에서 구한 A와 B로 원근투영 행렬을 완성하면 다음이 나온다.
이 투영 행렬을 곱한 후의 기하구조를 가리켜 동차절단 공간(homogeneous clip space) 또는 투영 공간에 있다고 말한다. 원근 나누기를 수행한 후의 기하구조를 가리켜 정규화된 장치 좌표 (NDC) 공간에 있다고 말한다.
5.6.3.6 XMMatrixPerspectiveFovLH 함수
DirectXMath 라이브러리의 다음 함수를 이용해서 원근투영 행렬을 구축할 수 있다.
// Returns the projection matrix
XMMATRIX XM_CALLCONV XMMatrixPerspectiveFovLH(
float FovAngleY, // vertical field of view angle in radians
float Aspect, // aspect ratio = width / height
float NearZ, // distance to near plane
float FarZ); // distance to far plane
다음은 수직 시야각을 45도로 설정하고, 가까운 평면은 z = 1에, 그리고 먼 평면은 z = 1000에 둔 예이다.
XMMATRIX P = XMMatrixPerspectiveFovLH(0.25f*XM_PI,
AspectRatio(), 1.0f, 1000.0f);
종횡비는 다음과 같이 응용 프로그램 창의 너비와 높이에 부합하도록 계산한다.
float D3DApp::AspectRatio()const
{
return static_cast<float>(mClientWidth) / mClientHeight;
}