그래픽스(DirectX)

[DirectX11] 렌더링 파이프라인

gamzachips 2023. 5. 16.

렌더링 파이프라인

가상의 카메라가 보는 것에 기반하여 2D 이미지를 생성해내는 데 필요한 일련의 전 과정을 렌더링 파이프라인(rendering pipeline)이라고 한다. 
몇몇 단계는 input으로서 GPU 리소스에 접근하고 몇몇 단계에서는 GPU리소스를 write하기도 한다. 
하지만 대부분의 단계에서는 GPU 리소스를 사용하지 않고, 이전 단계의 output을 input으로 받는다.
 

 

1. Input Assembler(IA) 단계

Input Assembler 단계는 메모리로부터 기하학적 데이터(정점, 인덱스)를 읽고
삼각형, 선 등의 기하학적 형태로 조립하는 단계이다. 
 
 

1) Vertex Buffer

수학적으로 정점(vertex)은 삼각형의 두 변이 만나는 곳, 선의 끝점, 단일 점 자체를 의미한다. 
그러나 Direct3D에서 정점은 공간상의 위치 뿐만 아니라
normal vector나 texture 좌표 등 추가적인 데이터를 포함할 수 있다. 
 
정점은 vertex buffer라 하는 Direct3D의 자료구조에서 렌더링 파이프라인에 바운딩된다. 
vertex buffer는 단순히 연속적인 메모리에 정점의 리스트를 저장하고 있다. 
 
 

2) Primitive Topology

정점들이 모여서 기하학적 형태를 형성하는 방식을 primitive topology를 지정하여 알려주어야 한다. 
 

_deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

 
Triangle list 방식은 모든 세 정점이 독립적인 삼각형을 형성하는 방식이다.
3n개의 정점이 있으면 n개의 삼각형을 이룬다. 
 
 

3) Index Buffer

3D물체는 기본적으로 삼각형으로 형성하는데 삼각형들은 같은 정점을 많이 공유하게 된다. 
중복되는 정점이 늘어나면 메모리가 낭비되고 하드웨어의 처리량도 늘어나므로 정점이 중복되지 않게 하는 것이 좋다. 
 
primitive topology 방식을 triangle strip 방식으로 지정하면 문제 해결에 도움이 될 수 있지만,
삼각형이 이어져있지 않아도 되는 triangle list가 더 유연한 방식이다. 
 
따라서 이러한 정점 중복 문제를 인덱스를 사용하여 해결한다.
 
정점 리스트인덱스 리스트를 만들고, 정점 리스트에는 유일한 정점들만 담으며
인덱스 리스트에는 삼각형들을 형성하는 방식을 나타내기 위해 정점 리스트의 인덱스를 담는다. 
 

Vertex v[4] = {v0, v1, v2, v3};

 

UINT indexList[6] = 
{ 
    v0, v1, v2, //삼각형1
    v0, v2, v3 //삼각형2
};

인덱스를 사용하지 않으면 6개의 정점이 필요하지만 인덱스를 이용해 정점 4개만을 이용하여 사각형을 정의하였다. 
 
 
정점은 큰 메모리를 가질 수 있는 반면 인덱스는 단순히 정수이므로 메모리를 적게 차지하며 
정점 캐시 순서가 좋으면 하드웨어가 정점을 중복으로 많이 처리할 필요가 없다. 
 
 
 

2. Vertex Shader(VS) 단계

 
vertex shader 단계는 input으로 정점을 받아서 연산 후 output으로 정점을 반환하는 단계이다. 
좌표 변환, 빛, displacement mappipng 등 많은 것들이 여기서 행해진다.
vertex shader 단계의 함수를 구현하면 GPU가 각 정점에 대해 빠르게 실행한다. 
 
 

1) 로컬 공간와 월드 공간

로컬 좌표계(로컬 공간)에서 물체를 이루는 정점이 정의되고, 글로벌 씬 좌표계(월드 공간)에 물체가 배치된다.
따라서 로컬 공간에서의 원점과 축을 월드 공간에 대해 상대적으로 표현해야 한다. 
 
이는 좌표 변환에 의해서 수행되는데, 로컬 좌표계에의 상대적인 좌표를 월드 좌표계로 변환하는 것을 월드 변환(world transform)이라 한다. 그리고 이에 대응하는 행렬을 월드 행렬(world matrix)이라 한다. 

월드 행렬은 W = SRT로 구할 수 있다.
S는 로컬 공간을 월드 공간으로 크기 변경을 하기 위한 scale 행렬
R은 월드 공간에 상대적으로 로컬 공간의 방향을 회전하기 위한 rotation 행렬
T는 월드 공간에 상대적으로 로컬의 원점을 정의하기 위한 translation 행렬이다.
 
SRT로 구한 W 행렬의 각 행에는 월드 공간에 상대적인 로컬 공간의 x축,y축,z축,원점의 좌표가 저장되어있다. 
 
 

2) 뷰 공간

2D 이미지를 얻기 위해서는 가상 카메라가 배치되어야 한다. 
카메라의 로컬 좌표계를 view space, eye space, camera space라고 한다. 
월드 공간에서 뷰 공간으로 좌표변환하는 것을 view transform이라 하고
이에 대응하는 행렬을 view matrix라고 한다.
 
뷰 공간에서 월드 공간으로 변환하는 행렬이 W이므로, 월드 공간에서 뷰 공간으로 가는 행렬은 W-1이다. 
카메라는 위치나 방향만 바꾸므로 W=RT라고 하면, 이를 이용해 뷰 행렬을 구할 수 있다. 
 
뷰 행렬 구하는 과정

더보기

 

Q는 카메라의 위치이고, 카메라가 향하는 타겟 지점을 T라고 하자. 

카메라가 바라보는 look 벡터 w는 

 

 

 

월드 공간의 up 방향벡터 (0,1,0)을 j라고 하면, 카메라의 right 벡터 u는 

 

 

카메라의 up 벡터 v는 w x u (단위벡터이므로 normalized)

 

 

로 구해진다. 

 
 
 

3) 투영과 동차 클립 공간

절두체와 투영

카메라가 바라보는 공간의 부피는 절두체(frustum)로 정의되며,
절두체 안에 있는 3D 기하도형을 2D 투영창에 투영한다. 
원근 투영(perspective projection)에서 물체의 깊이가 깊을수록 투영된 크기가 감소한다. 
 
정점으로부터 카메라의 원점까지의 선을 정점의 투영 선(vertex's line of projection)이라고 부르며, 
3D의 정점 v를 투영선이 2D 투영 평면에 접하는 지점 v'으로 변환하는 것을 원근 투영 변환이라고 한다. 
그리고 v'v의 투영이라고 한다.
 
절두체는 가까운(near) 평면 n, 먼(far) 평면 f, 수직 시야각 α, 종횡비 r 로 정의된다.
n과 f는 xy평면에 평행하므로 원점으로부터 z축을 따라 거리를 특정할 수 있다. 
수평 시야각 β는 수직 시야각 α와 종횡비 r에 의해 결정된다. 
 
 

 
종횡비는  r = w(투영 창 너비) / h (투영 창 높이)로 정의된다. 
아래에서 정규화를 편하게 하기 위해서 h를 2로 설정한다. 즉 h = 2, w = 2r이 된다. 
 
투영 창이 후면 버퍼로 매핑되므로 투영 창과 후면 버퍼의 종횡비를 같게 하는 것이 좋다. 
종횡비가 같지 않으면 크기 조정이 불균일하게 일어나므로 매핑 과정에서 왜곡이 발생할 수 있다. 
 
 
점 (x,y,z)가 주어졌을 때 이것이 평면 z = d에 투영된 점 (x',y',d)을 구해보자. 

 
 

Normalized Device Coordinates(NDC)

투영 창의 크기가 종횡비 r 값에 의존하는데, 하드웨어가 나중에 투영 창의 크기를 가지고 작업을 해야 하기 때문에 하드웨어에 종횡비를 알려줘야 하는 문제가 있다. 
따라서 이러한 의존성을 없애기 위해서 x좌표의 크기를 [-1,1]로 정규화 해야 한다. (y는 이미 [-1,1] 이고, z는 아직 정규화되지 않았다)

이렇게 매핑한 x,y좌표를 normalized device coordinates (NDC) 라고 한다.
 
이제 투영 창의 높이와 너비가 2로 같아졌기 때문에 종횡비를 하드웨어에 알려주지 않아도 되지만, 항상 NPC 공간에 투영된 좌표를 알려주어야 한다. 
 
따라서 투영 변환을 행렬로 표현해놓자. 
 
원근 투영 변환 행렬 구하는 과정

더보기

x,y를 x', y'에 투영하는 식은 비선형이므로 행렬로 바로 표현할 수 없다. 따라서 비선형적인 부분인 z로 나누는 부분은 분리하여 나중에 처리하자. 이를 위해 w 좌표에 z를 기록해놓는다. 

A,B는 밑에서 z좌표를 정규화하는 데 사용할 상수이다. 

 

 

w=z으로 나눈다. 

 

 

 

마지막으로 z를 정규화한다. 깊이 버퍼링 알고리즘을 위해 깊이값을 [0,1] 범위로 넘겨주어야 하기 때문이다. 

[n,f]를 [0,1] 범위에 매핑하는 함수 g(z)를 구성해보자. 위에서 z좌표를 아래와 같이 변환하였다. 따라서 A와 B를 채우면 된다. 

n이 0에, f가 1에 매핑되어야 하므로 다음과 같은 조건식을 얻을 수 있다. 

 

두 식을 풀이하여 A와 B를 n과 f를 이용해 표현하면 아래와 같이 구해지므로 g(z)에 대입하여 g(z)를 완성할 수 있다.  

 

 

최종적으로 원근 투영 행렬은 다음과 같이 구해진다.  

 
 

 
 
 

3. Rasterizer(RS) 단계

Rasterizer 단계에서는 주로 투영된 3D 삼각형으로부터 픽셀들의 색상을 계산한다.
 
 
 

4. Pixel Shader(PS) 단계

Pixel Shader 단계에서는 우리가 작성한 프로그램이  GPU에서 실행된다.
Pixel shader는 각 픽셀마다 실행되고 색상을 계산하기 위해서 인풋으로서 보간된 정점 속성들을 사용한다. 
단순히 상수 색상을 리턴할 수도 있고, 라이팅이나 반사, 그림자 효과같은 더 복잡한 작업을 할 수도 있다. 
 
 
 

5. Output Merger(OM) 단계

Pixel shader단계에서 넘어온 각 픽셀들은 Ouput Merger 단계에 들어온다. 
이때 depth , stencil 테스트에서 일부 픽셀들은 거절되고, 거절되지 않은 것들은 후면 버퍼에 쓰인다. 
 
OM단계에서 블렌딩을 하기도 한다. 픽셀이 현재 후면 버퍼에 있는 것을 완전히 덮어쓰는 대신에 섞일 수 있다. 
투명 처리와 같은 효과들이 블렌딩으로 구현된다. 
 

댓글