지난 글에서는 DirectX를 시작하기 전 필요한 장치들의 기본 세팅 작업을 모두 마무리하였다.
이제 본격적으로 DirectX를 사용하여 렌더링을 해볼 것인데, 그 전에 렌더링 파이프라인이 무엇인지를 알고 넘어가야한다.
모니터의 출력되는 화면은 결국 2차원 평면이다. 따라서 3차원의 장면을 평면의 모니터에 출력하기 위해서는 일련의 단계들이 필요하다. 이를 렌더링 파이프라인이라고 부른다.
렌더링 파이프라인은 입력 조립기 단계부터 정점 쉐이더, 래스터라이저, 출력 병합기의 과정을 거쳐 진행된다. 이때 헐 쉐이더와 테셀레이터는 꼭 필요한 작업이 아닌 세부 기능을 넣어주는 작업들이므로 생략할 수 있다.
입력 조립기 단계에서는 메모리에서 기하 자료의 정보(정점, 인덱스)를 읽어서 기본 도형(삼각형, 선분 등)을 조립한다.
도형이 만들어질 때 도형의 각 변이 만나는 곳에는 정점이 생기게 된다. 선분의 경우 양 끝점이 정점이 되고, 하나의 점의 경우에는 그 점 자체가 정점이다.
삼각형의 경우에는 위와 같이 세 개의 정점이 생기게 된다.
모든 물체는 삼각형으로 나누어져 표현되게 된다. 따라 사각형은 삼각형을 두 개. 즉 정점 6개를 만들어 표현할 수 있다. 이때 두 정점이 겹쳐지며 중복되게 되는데, 이 경우 메모리의 요구량도 늘어날 뿐더러 하드웨어의 처리량이 증가하여 퍼포먼스의 영향을 미친다.
이런 삼각형 목록에서 중복 정점들을 제거하는데에 사용되는 것이 인덱스이다.
정점 버퍼 - P1, P2, P3, P4, P5, P6, P7
인덱스 버퍼 - 0,1,2 1,3,2 2,3,4 3,5,4 4,5,6
다음과 같은 삼각형 목록에서 정점은 겹치지 않게 사용하고 인덱스 목록을 위와 같이 구성한다면, 중복된 정점을 사용하지 않으며 효율적으로 삼각형을 연결할 수 있게 된다.
인덱스를 구성할 때에는 normal벡터의 방향을 주의해야한다. 인덱스를 시계방향 즉 1 -> 2 -> 3 순서로 구성할 경우 normal 벡터는 앞을 향하기에 삼각형이 우리 눈에 보이도록 렌더링된다. 하지만 인덱스를 반시계방향 즉 1 -> 3 -> 2 순서로 구성할 경우에 normal 벡터는 뒤를 향하게 되며 삼각형의 뒷면이 그려져 아무것도 보이지 않을것이다.
위에서 설명한대로 보통 삼각형으로 묶어서 도형을 구성하게 되지만, 다른 방법으로도 도형을 표현할 수 있다. 이처럼 정점과 인덱스를 어떠한 형태로 구성하여 도형을 구성할지를 정하는 것이 기본 도형 위상 구조이다.
기본 도형 위상 구조는 위와 같이 존재하며, 보통 D3D12_PRIMITIVE_TOPOLOGY_TRIANGLELIST
를 사용한다.
입력 조립기 단계에서 기본 도형들의 정보를 조립한 후에 정점들의 정보가 정점 쉐이더 단계로 넘어가게 된다.
정점 쉐이더에서 수행되는 작업은 프로그래머가 구현하여 GPU에서 수행되기에 아주 빠르게 동작한다.
화면에 그려지는 모든 정점들은 정점 쉐이더를 거쳐가게 되며, 다양한 특수효과들을 정점에 적용할 수 있다. 정점 쉐이더에 입력되는 정점 자료는 기본적인 텍스처, 변환 행렬, 라이트 정보 등 GPU 메모리에 담긴 다른 자료에도 접근 할 수 있다.
다양한 작업 중 우리가 구상한 3D세계를 제대로 렌더링 하기 위해서는 정점 쉐이더가 좌표계 변환 작업을 수행해줘야한다.
좌표계 변환이란 우리가 입력 조립기 단계에서 입력하여준 정점들의 지역 좌표를 실제 정점을 렌더링하는 월드 위에 좌표로 변환해주는 작업을 의미한다.
입력 좌표계에서 구성한 로컬좌표들을 월드좌표계에 적절하게 배피하기 위해서는 월드 변환 행렬을 구성하여 로컬 좌표에 곱해주는 과정이 필요하다. 이를 월드 변환이라고 부른다.
월드 좌표 정점 = 로컬 좌표 월드 변환 행렬(Scale Rotation * Position)
3D 좌표계는 각 모델들이 자신을 중심으로 하는 모델좌표계(로컬좌표계)와 월드의 중점을 중심으로 하는 월드좌표계로 나누어진다. Unity의 로컬, 월드와 동일한 개념이다.
스케일은 물체의 크기를 담고있는 행렬이다.
X, Y, Z의 크기 정보를 각각 (1, 1), (2, 2), (3, 3) 대각 행렬에 담아서 표현한다.
로테이션은 물체의 회전 정보를 담고 있는 행렬이다. 회전 행렬은 스케일과 달리 조금 복잡한데, X축과 Y축, 마지막 Z축의 회전 행렬을 각각 구하여 곱해주면 최종 회전 행렬이 완성되게 된다.
각각의 회전 행렬을 만들기 위해 삼각함수를 공부할 때 배웠던 코코싸사, 싸코코싸를 이용하면 된다.
우선 Z축의 회전 행렬을 만들어보자.
원래의 좌표가 (x, y, z)인 점 P가 있을 때, 이 점을 β만큼 회전한 점 P′의 좌표는 (x′, y′, z′)라고 하자. 원점에서 점 P까지 선을 이으면, 이 선분의 길이는 r이고 x과 이루는 어떤 각도 α를 가진다.
이때, 점 P의 좌표는 다음과 같이 표현할 수도 있다.
점 P를 β만큼 회전시킨 점 P′의 좌표 또한 다음과 같이 표현할 수 있다.
이를 삼각함수의 덧셈 정리를 활용하여 풀어내면 다음과 같다.
이를 행렬로 표현하면 다음과 같은 식이 된다.
즉, Z에 대한 회전 행렬은 다음과 같이 표현할 수 있다.
X축과 Y축을 기준으로 회전 행렬을 구한다는 것은 Z축을 기준으로 회전변환을 하는 식에서 x, y, z의 값을 알맞게 돌려주는 것과 같기에 동일한 방법으로 구하면 된다.
이러한 방법으로 X, Y, Z 세 축의 회전 행렬을 모두 구했다면, 구한 모든 회전 행렬을 곱해주어 물체의 최종 회전 행렬을 만들어낼 수 있다.
마지막인 포지션은 물체의 이동 정보를 담는 행렬이다. 포지션은 기본적으로 x, y, z 세 가지의 정보를 가지고 있다. 다만 3x3의 행렬은 오직 방향과 크기만을 나타내는 벡터 정보이기에 위치 행렬로 사용할 수 없다. 따라서 x, y, z 정보에 w를 추가한 4x4 행렬을 만들어줘야한다.
위에서 말했던 월드 변환 행렬을 구하는 방법은 다음과 같다.
월드 변환 행렬 = (Scale Rotation Position)
구해낸 행렬들을 모두 곱하여 월드 좌표 행렬을 구해준 후 로컬 정점에 곱해주어 정점을 원하는 월드 좌표로 렌더링 할 수 있다.
이제 물체는 월드의 정점을 기준으로 배치되었을 것이다. 다만 우리가 직접적으로 모니터에서 보는 화면은 월드가 기준이 아닌 카메라가 기준이 된다. 물체들을 월드 공간에서 카메라 공간으로 바꾸어주는 작업이 필요하다.
DirectX에서는 카메라 변환 행렬을 구하는 함수를 제공해주기 때문에 카메라의 Look 벡터, Right 벡터, Up 벡터만 구해준 뒤에 함수를 적절히 사용해주면 된다.
물체는 카메라가 움직이는 방향의 반대로 작용하게 되므로, 카메라의 위치 값을 각 카메라의 방향에 곱해주고, 음수로 만든 다음 변환 행렬을 만들어주어야 한다.
Unity에서 Perspective 카메라를 사용해보았다면 Near Plane, Far Plane이라는 단어가 익숙할 것이다. DirectX에서도 Near와 Far값을 카메라에 제공해주어 카메라에 가까울 수록 물체가 크게 멀수록 작게 보이게 만들어주어야 한다.
이러한 작업을 투영이라고 한다. 평행선들이 하나의 소실점으로 수렴하며, 깊이에 따라 투영의 크기가 감소하는 방식으로 수행하는데, 이를 원근 투영이라고 한다.
투영을 하기 위해서도 행렬이 필요하다.
우리가 하고 싶은 작업은 카메라를 기준으로 위치한 점 P를 투영평면 위에 점 T로 변경하는 것이다. 이러한 작업을 위해선 다음과 같은 투영 변환 행렬이 필요하다.
행렬에 사용된 변수는 아래와 같다.
n: 카메라에서 가장 가까운 절두체 면까지의 거리
f: 카메라에서 가장 먼 절두체 면까지의 거리
Width: 카메라에서 가장 가까운 절두체 면의 너비
Height: 카메라에서 가장 가까운 절두체 면의 높이
이제 이러한 투영 변환 행렬의 유도과정을 살펴보자. 로컬 월드 변환 행렬을 구할 때 이동 행렬을 구하는 방법과 비슷하게 시야 공간의 점 P(x, y, z)를 투영 평면상의 점 T(x, y, z)로 변환한다고 생각하여보자. 위에서 말하였듯 위치를 표현하기 위해서는 4차원 벡터로 표현되어야 하기에 식은 다음과 같아진다.
먼저, x좌표의 변환 과정을 살펴보자.
절두체공간에 정점 P가 그림처럼 위치한다고 할 때 점 P를 z=n상에 사영시키면, 사영된 점 T의 x좌표는 투영변환된 점의 X좌표가 된다.
사영된 점 T에 대한 X좌표는 닮음비를 이용해서 구할 수 있다.
투영변환에 의해 정점이 이동되는 공간은 (-1, -1, 0) ~ (1, 1, 0)의 공간이므로, [-1, 1]로 범위를 조정해주어야 한다.
정리하자면, X는 다음과 같다.
위의 수식중, 중간의 식이 최종변환된 X의 수식이고. 마지막 수식은 나중에 행렬을 구할 때 계산이 용이하도록 미리 X에 z를 곱해둔 식이다.
변환된 Y좌표 또한 똑같은 논리로 구해주면 된다. Z좌표의 변환은 좀 달라지게 되는데
위와 같은 식으로 Z를 표기할 때,
z=n일 경우 변환된 Z는 0이고, z=f일 경우 변환된 Z는 1이다. 따라서 A와 B를 구하여 대입시켜주면,
위와 같은 결과를 얻을 수 있다.
정리하자면,
이제 (x, y, z, 1)이 (zX, zY, zZ, z)가 되도록 행렬을 구성하자. ((X, Y, Z, 1)을 편의상 (zX, zY, zZ, z)로 바꿔놓고 있음을 유의)
이 때, right=-left이고, top=-bottom이므로, 행렬의 (3, 1)과 (3, 2)는 0이 된다. 또, right-left = Width, top - bottom = Height로 정의하면, 구하고자 하였던 투영 변환 행렬을 구할 수 있다.
시야 절두체 안에 정점들이 투영되도록 하는 작업을 마무리 하였다. 다만 시야 절두체 바깥에 있는 정점들은 렌더링할 이유가 없기에 폐기해주어야한다. 6개의 평면으로 이루어져 있는 시야 절두체의 각 평면의 양의 공간에 있는 부분은 렌더링하고 음의 공간에 있는 부분은 폐기한다.
해당 연산은 하드웨어가 수행해준다.
래스터화 단계의 주요 작업 내용은 투영된 삼각형들의 픽셀 생상들을 계산하는 것이다.
원근 투영 변환 이후, 정규화된 점들의 x, y 성분은 백 버퍼의 직사각형 영역으로 변환된다. 이 영역이 뷰표트이다.
뷰표트 변환을 마치면 x, y 성분은 픽셀 단위의 값으로 바뀌게 된다.
인덱스를 설명할 때 시계 방향이면 전면이 렌더링 되고, 시계 반대방향이면 후면이 렌더링된다고 하였다. 이때 카메라는 어쩌피 보이지 않는 후면을 렌더링할 이유가 없다. 따라 렌더링 파이프라인에서는 후면을 골라 폐기하는 과정이 진행되게 되고, 이를 후면 선별이라고 한다.
뷰포트 변환 이후 삼각형의 각 정점들에 대한 보간을 진행한다. 각 정점의 색이 존재한다면, 삼각형에서 비어있는 각 픽셀들에 대해서 색 보간이 진행된다.
프로그래머가 구현하고 GPU가 실행한다. 화면에 그려지는 각각의 모든 픽셀들이 이 쉐이더를 거쳐가게 되며, 조명, 반사, 그림자등의 작업을 수행할 수 있다.
픽셀 쉐이더가 생성한 픽셀들을 출력 병합기 단계에서 입력받아 최종적으로 출력이 진행되게 된다.