<-- home

3차원 물체를 그리기 위한 랜더링 파이프라인 요약

3차원 그래픽스에서 가장 핵심적인 내용으로 랜더링 파이프라인을 뺴놓을 수 없을 것입니다.
DirectX와 같은 그래픽 라이브러리를 다루고 셰이더 프로그래밍을 하는 것은 모두 이 랜더링 파이프라인의 각 단계들을 제어하는 것이라고 할 수 있습니다. 지금까지 제가 공부했던 내용을 복습도 할 겸 이 기나긴 여정을 쉽고 재미있게 이해할 수 있는 포스트를 작성하려 합니다.

그럼 지금부터 저와 함께 신나는 여정을 떠나볼까요?

출발!

Start

랜더링 파이프라인이란 폴리곤으로 구성된 3차원 장면 하나를 입력으로 받아 이 폴리곤들을 각각 2차원 형태로 바꾸고, 2차원 폴리곤 내부를 차지하는 픽셀(pixel)의 색깔을 결정하여 3차원 장면의 최종 영상을 2D인 모니터 평면 위에 출력하는 과정을 말합니다.
우선 랜더링 파이프라인은 3차원 그래픽스 이론에서 다음과 같이

  • 정점처리
  • 래스터화
  • 프래그먼트 처리
  • 출력 병합

크게 4 단계로 나눠지는데, DirectX11에서는 이와 같은 단계를 좀 더 세부적으로
Input Assembly -> Vertex Shader -> Hull Shader -> Tessellation -> Domain Shader -> Geometry Shader -> Rasterizer -> Pixel Shader -> Output Merger 단계로 나누어 제어할 수 있습니다.

각각의 DX 파이프라인을 3차원 그래픽스 랜더링 파이프라인 과정과 연관시킨다면

  • 정점처리 : Vertex Shader -> Hull Shader -> Tessellation -> Domain Shader -> Geometry Shader
  • 래스터화 : Rasterizer
  • 프레그먼트 처리 : Pixel Shader
  • 출력 병합 : Output Merger

으로 구별 할 수 있고, 각각의 단계가 하는 일에 대해서 모델 하나가 출력되는 과정을 예를 들어 설명하려고 합니다.

1. 입력 조립 단계

3차원 모델 하나를 3차원 세상에 나타내기 위해서는 가장 먼저 무엇을 해야 할까요?
우선 모델의 정보를 GPU로 전달해야 합니다.
여기서 모델은 삼각형(폴리곤)의 집합을 의미하고, 삼각형은 정점(Vertex) 3개의 집합이기 때문에 실제로 dx와 같은 랜더링 라이브러리에서는 모델의 최소단위인 정점들의 집합을 GPU로 전달하게 되는데요, 이 때 이 정점들을 운반하는 자료구조를 정점 버퍼 라고 합니다.

여기서 잠깐

정점 버퍼와 같이 등장하는 용어로 인덱스 버퍼(Index Buffer)라는 것이 있다. 이 인덱스 버퍼는 쉽게 생각하면 정점들의 인덱스를 저장하고 있는 버퍼라 할 수 있는데 사각형을 예를 들어 생각 해보자. 사각형 하나를 그리기 위해서 이것을 최소 단위의 도형(삼각형)으로 쪼개면 삼각형 두 개가 생긴다. 그렇다면 이 삼각형 두 개를 그리기 위해서는 당연히 2*3 =6 개의 정점이 필요 할 것이다. 그런데 실제로 사각형은 몇 개의 점으로 구성되어 있는가?

Start

4개의 점으로 구성되어 있다.
6개의 정점으로 사각형을 표현한다면 2개의 점이 중복되어 메모리 낭비가 된다고 할 수 있다.만약 사각형 하나가 아니라 사각형 10,000개로 구성된 모델이라고 가정하면 20000개의 정점이 낭비되는 것이고 이것은 엄청낭 손해이다! 또한 단순한 메모리의 낭비일 뿐만 아니라 같은 정점에 대한 작업을 GPU에서 여러번 처리해야 하기 때문에 불필요한 계산량까지 늘어난다.

이러한 중복되는 정점을 해결하기 위한 방법이 바로 Index Buffer이다. 위의 사각형의 예를 들면 정점 버퍼는 Vertex Bufer[] = { P0,P1,P2,P3} 이렇게 4개의 데이터만 갖고, 인덱스 버퍼를 추가로 생성하여 Index Buffer[] = {0,1,2,1,3,2,} 요런식으로 정점을 어떤 순서로 그려야 하는지 알려주는 목록을 제공하면 위에서 설명한 두 가지 문제를 해결할 수 있다. 인덱스 버퍼를 생성하는데에도 메모리가 드는데 이것은 낭비가 아니냐고 생각 할 수도 있겠지만

  1. 인덱스버퍼는 그냥 정수이기 때문에 위치,색깔,법선,UV등등의 많은 데이터를 갖는 정점 구조체보다 훨씬 메모리를 적게 차지하기 때문에 별 문제가 되지 않는데다가
  2. 중복되는 정점에 대해서 중복된 GPU 계산을 하지 않는다는 메리트가 굉장히 크기 때문에 인덱스 버퍼에 소요되는 메모리는 걱정하지 않아도 된다.

Start

[정점 버퍼]

자 다시 이제 정점버퍼로 돌아와서 설명하겠습니다.
정점 버퍼는 그냥 정점들을 연속적인 메모리에 저장하는 자료구조에 불과하기 때문에 실제로 GPU에서는 이러한 정점들을 이용하여 어떤 도형을 만들어야 할 지 정보가 필요합니다. 예를 들면 이 정점을 두 개씩 엮어서 선분을 구성할 수도 있고, 세 개씩 엮어서 삼각형을 구성할 수도 있죠. 이러한 기본도형을 형성하는 방식을 알려주는데 쓰이는 수단으로 Direct3D에서는 Primitive Topology라는 형태를 이용합니다. 이 프리미티브 토폴로지는 해석하자면 “위상 구조”라 할 수 있는데 DX11 에서는 point list, line list, triangle strip, triangle list 등이 있습니다. 여기서 Triangle list를 선택하여 기본도형을 삼각형으로 선택했다고 가정 해 봅시다. 그러면 이제 모델 하나를 형성하기 위해서는 GPU에서 수많은 삼각형들을 이어 붙이기만 하면 되는 거죠.
입력 조립기 단계(Input Assemble) 에서는 이런 정점들을 읽어서 삼각형과 같은 기본 도형으로 조립하는 일을 합니다. 그래서 입력 조립 단계죠.

2. 정점 처리 단계

이렇게 조립된 기본 도형은 다시 가장 작은 단위인 정점 단위로 GPU에서 다뤄지게 되는데요 DX에서는 우선 (버텍스 셰이더)Vertex Shader 단계로 입력됩니다. 여기서 정점 셰이더는 정점 하나를 입력받아 정점 하나를 출력하는 함수로 생각하면 됩니다. 이 정점 셰이더에서 변환, 조명, 매핑등의 여러 가지 특수 효과를 수행할 수 있는데요, 만약 랜더링 과정을 크게 변환->색칠 이렇게 두 가지 관점으로 나눠서 본다면 정점 셰이더에서 주로 주목해야 할 부분은 바로 변환 과정 입니다.

변환과정은 [오브젝트 공간] ->월드 변환 -> [월드 공간] -> 뷰 변환 -> [카메라 공간] -> 투영 변환 -> [클립 공간] 을 거치게 되는데 각각의 과정에 대해서 아주 간략하게 설명한 뒤 Vertex Shader에 대해 이어서 설명하도록 하겠습니다.

2.1 월드 변환

우선 지역공간(local space)라고도 불리는 오브젝트 공간은 3차원 세상에서 표현될 각각의 오브젝트들이 정의된 공간인데요, 하나의 물체가 자신만의 공간에서 고정 불변의 좌표를 가지는 것이라고 생각할 수 있습니다. 그런데 이러한 오브젝트 공간의 물체는 다른 오브젝트의 공간과는 전혀 관계가 없기 때문에 3차원 세상에 표현하고자 하는 모든 오브젝트들은 하나의 단일 공간으로 모아질 필요가 있습니다. 지금 우리가 보는 모든 물체와 사람들이 같은 세상에 존재하듯이 3차원 세상을 표현하고자 하려면 당연히 모든 오브젝트들이 하나의 단일 공간에 있어야 하는거죠. 이 때 모든 대상들이 모아질 공간을 월드 공간이라 하고, 각각의 대상을 월드 공간에 모으는 과정을 월드 변환이라 합니다.

2.2 카메라 변환

월드 변환이 완료되어 모든 물체가 월드 공간에 모아지면 이제 우리가 원하는 시점에서 물체들을 관찰할 수 있어야 합니다. 이 때 관찰자로써 가상의 카메라가 필요하고, 이 카메라(관찰자)가 볼 수 있는 영역의 공간을 카메라 공간 혹은 뷰 공간이라고 합니다. 월드 공간의 모든 물체를 카메라 공간으로 변환하게되면 보다 효율적인 랜더링 알고리즘을 설계할 수 있기 때문에 월드 공간의 모든 물체는 카메라 공간으로 변환되는데 이를 카메라 변환이라고 부릅니다.

그런데 여기서 잠깐 카메라가 바라보는 세상에 대해 생각해 봅시다. 가상의 카메라는 컴퓨터의 성능의 한계 때문에 실제 세상의 카메라와는 다르게 시야가 제한되어 있을 수밖에 없습니다. 이 떄 이 시야는 fovy(시야각), aspect(종횡비)에 의해 결정되는데 이러한 시야의 가시 영역을 뷰 볼륨(view volume)라고 부르고, 이렇게 생성된 뷰 볼륨은 n(근평면), f(원평면)에 의해 절단되어 View Frustum(절두체)의 영역으로 다시 정의됩니다.

Start

[view volume]

Start

[view frustum]

카메라 공간 내의 물체는 절두체 공간 밖에 있는 물체는 그리지 않게 되는데, 우리가 살고 있는 3차원의 카메라의 세상은 이렇지 않지만 이는 계산상의 효율성을 위해 어쩔 수 없이 도입된 개념입니다. 만약 물체가 절두체의 경계에 걸치게 되면 어떻게 되냐고요? 하! 아주 예리한 질문이었습니다. 만약 물체(폴리곤)이 절두체의 경계에 걸치게 되면 바깥쪽에 있는 부분은 잘려져 버려지게 되는데요 이를 클리핑이라고 합니다. 이 클리핑은 카메라 변환에서 일어나지 않고 나중에 클립 공간에서 수행되는데 이에 대해서는 조금 있다가 설명하도록 하겠습니다.

2.3 투영 변환

자 카메라 변환에서 월드의 모든 물체를 카메라 공간으로 이동시켰고, 이제 카메라 시점에서 세상을 바라 볼 수 있게 되었는데요. 그런데 우리가 카메라를 통해 바라보는 가상의 세상은 현실 세계처럼 3차원 세상이지만 실제로 우리가 바라볼 모니터는 2차원의 평면입니다. 3차원 공간을 어떻게 2차원 공간에 표현할 수 있을까요???

사람이 그림을 그릴 때를 생각해 보세요. 어떻게 그리죠? 원근법에 따라서.

“멀리 있는 물체일수록 작게”, “멀리있는 물체일수록 소실점에 가깝게”

3차원 세상을 2차원 평면에 표현하는 방법은 다행히 이미 화가들에 의해 개발되었기 때문에 우리도 이러한 방법을 따르면 됩니다.

Start

투영 변환은 이러한 원근법을 구현하기 위해 카메라 공간에서 정의된 절두체를 축에 나란한 직육면체 볼륨으로 변경하여 카메라 공간의 모든 물체를 3차원 클립 공간으로 변환하는 것을 의미합니다. 여기서 투영 변환이라는 이름과는 달리 3차원 카메라 공간의 물체를 2차원 평면으로 투영하는 것이 아니라 또 다른 3차원 물체로 ‘변형’함을 주목합시다. 이러한 투영 변환을 거친 물체들을 관찰해보면 절두체 뒤쪽에 있던 영역의 폴리곤은 상대적으로 작아지는 것을 볼 수 있는데 우리가 원했던 원근법이 적용된 것이라고 볼 수 있고요. 다시 정리하자면 투영변환은 3차원 공간 내에서 원근법을 실현한 것입니다.

그리고 하나 더 생각해야 할 것이 원근법을 3차원 공간에서 실현하기 위해 직육면체 볼륨으로 물체들을 변환시켰는데 여기서 부가적으로 얻게되는 이점이 있습니다. 바로 나중에 설명할 클리핑(clipping)에 관한 건데요. 아까 클리핑은 뷰 프러스트럼 바깥쪽의 폴리곤들을 잘라 내는 것이라고 설명하였는데 현재 투영 변환을 거치면서 절두체 공간이 직육면체의 클립핑 공간으로 변형되었죠? 절두체 도형과 직육면체 도형 중에 클리핑을 하기에 더 쉬운 도형이 뭐인지는 안봐도 비디오죠? 투영 변환을 통해 모든 물체가 클립 공간으로 변환되면서 클리핑하기가 더 쉬워졌습니다!!

2.4 다시 Vertex Shader로…

지금까지 설명한 월드 변환, 카메라 변환, 투영 변환이 정점 처리 과정 중에서도 변환 과정이었습니다. 변환 과정은 주로 정점 셰이더에서 수행되는데 만약 테셀레이션이나 기하 셰이더 단계가 추가되면 정점 셰이더에서의 변환 과정은 생략되고 테셀레이션이나 기하 셰이더에서 대신 수행하기도 합니다. 기하셰이더와 테셀레이션은 각각 shader4.0, shader5.0 이상부터 지원하는 정점 변환 단계의 파이프라인으로써 간략하게 설명하자면 기하 셰이더는 정점 셰이더로부터 전달받은 정점 하나를 이용하여 새로운 프리미티브와 기하 도형을 만들 수 있는 파이프라인이고, 테셀레이션 단계는 입력조립기로부터 전달받은 패치(patch)의 제어점(control point)으로부터 더 많은 기하구조를 추가할 수 있는 단계입니다. 정점 셰이더와 더불어 이 두 단계에서 수행되는 과정이 정점 처리 단계입니다. 이 두 파이프라인에 각각에 대해서 설명하기엔 이 포스팅의 수준을 벗어나는 내용이므로 나중에 따로 포스팅하도록 하고 이제 레스터화 단계로 넘어가도록 하겠습니다.

3. 레스터화 단계

정점 처리 단계를 지난 정점은 이제 랜더링 파이프라인의 다음 단계인 레스터화 단계로 들어갑니다. 우선 정점들은 삼각형 등과 같은 프리미티브(primitive)로 묶여지는데, 이 시점 이후 부터는 프리미티브가 독자적인 개체로 처리됩니다. 삼각형을 예를 들어서 설명하겠습니다. 우선 화면에 그려질 2차원 삼각형의 세 정점 좌표가 결정되면 다음과 같은 일이 일어납니다.

  1. 이 삼각형이 포함하는 모든 픽셀마다 프레그먼트가 생성된다.
  2. 삼각형의 세 정점에 할당되었던 여러 데이터(position, color, uv …)등은 보간(Interpolation)되어 삼각형 내부의 각각의 프레그먼트에 할당된다.

Direct3D에서는 이러한 과정을 통틀어서 레스터라이저(resterizer)라고 부릅니다. 이 레스터라이저는 고정 파이프라인 단계로 프로그래밍이 불가능하여 하드웨어 자체 알고리즘을 통해 동작하며,

  • 클리핑(Clipping)
  • 원근 나눗셈(perspective division)
  • 뒷면 제거(back-face culling)
  • 뷰포트 변환(view-port transform)
  • 스캔 변환(sacn transform)

등의 요소로 구성됩니다. 이제 각각의 요소들에 대해 살펴보도록 하겠습니다.

3.1 클리핑(clipping)

클리핑은 투영변환 이후의 클립공간볼륨 바깥에 놓인 폴리곤을 잘라 내는 작업을 말합니다. 이전부터 몇 번 언급되었던 이 작업은 바로 이 레스터화 단계에서 일어납니다.

3.2 원근 나눗셈(perspective division)

Start

현재 단계에서 투영변환을 통해 원근법이 적용된 3차원 물체들은 직육면체 볼륨의 3차원 클리핑 공간에서 정의되어 있습니다. 그러나 우리가 최종적으로 필요한 것은 모니터 화면에 출력될 2차원 사각 영역이죠. 그렇다면 3차원 공간을 어떻게 2차원 공간으로 변환시킬 수 있을까요?

단순히 생각하면 그냥 차원을 줄이면 됩니다. 바로 z좌표로 모든 성분을 나눠버리는 것이지요. 실제 z값은 투영변환행렬을 곱한 후 동차좌표계의 (x,y,z,w)에서 w성분에 저장되어 있기 때문에 투영변환 이후 w에 저장된 값으로 좌표를 나누는 연산을 마치면 원근법 구현이 완료되고 이를 원근 나눗셈이라 부릅니다. 원근 나눗셈이 적용된 이후에는 (x,y,z,w)의 동차 좌표계에서 (x,y,z)의 카테시안 좌표계로 변화게 되는데요 이 때 이를 NDC(normalized device coordinates) 공간이라 부릅니다. 여기서 정규화라는 이름이 붙는 이후는 이 좌표의 xy범위가 [-1,1]이고 z범위가 [0,1]이기 때문입니다.

3.3 뒷면 제거(back-face culling)

다음으로 레스터라이저에서 하는 기능으로 뒷면 제거가 있습니다. 뒷면 제거는 카메라에 등을 돌리고 있어 보이지 않는 폴리곤을 제거하는 작업입니다. 이런 보이지 않는 폴리곤을 주로 뒷면(back-face), 보이는 면을 앞면(front-face)라고 부릅니다. 원리는 간단한데 카메라의 시선벡터 V0가 있고, 폴리곤의 법선벡터 V1이 있다고 가정하면 이 둘이 이루는 각도가 예각인지 둔각인지에 따라 뒷면을 판별할 수 있습니다. 여기서 폴리곤의 법선벡터를 구할 때는 외적(cross product)를 사용하여 구하는데 이 때 외적은 순서에 따라 방향이 다르게 나올 수 있잖아요? Direct3D에서 등장하는 와인딩 오더(시계방향 혹은 반시계)는 이것을 정하는 방법인데 기본적으로 시계 방향으로 감긴 삼각형을 앞면으로, 반시계 방향으로 감긴 삼각형을 뒷면으로 간주하지만 이는 랜더 상태 설정에 따라 변경 가능합니다.

3.4 뷰포트 변환(view-port transform)

컴퓨터 화면 상의 윈도우는 스크린 공간(screen-space)을 갖는데요. 이 스크린 공간 내에 2차원 이미지가 그려질 뷰포트(view-port)가 정의되는데 NDC 공간의 물체들을 스크린 공간으로 이전시키는 변환을 뷰포트 변환이라 합니다.

3.5 스캔 변환(scan transform)

이제 레스터라이저의 마지막 단계인 스캔변환만이 남았습니다. 이전의 변환들은 자세한 사항을 몰라도 프로그래밍 하는데 있어 별다른 영향을 미치지 않지만 이 스캔변환은 랜더링 프로그램에서 직접적인 영향을 미치기 때문에 꽤 중요한 내용입니다. 우선 스캔변환이란 삼각형 하나가 내부에 차지하는 프레그먼트를 생성하는 과정입니다. 좀 더 자세하게 설명하면 개별 삼각형이 차지하는 스크린 공간의 픽셀 위치를 결정하고, 삼각형의 정점별 속성을 보간(interpolation)하여 이를 각 픽셀 위치에 할당하는 일을 수행합니다. 여기서 정점별 속성이란 프로그램마다 다르지면 대체로 position, uv, normal과 같은 값을을 의미합니다. 이때 보간 과정은 하드웨어가 수행하기 떄문에 그에 깔린 수학적 내용을 몰라도 지장은 없지만 대충 설명하자면 삼각형을 구성하는 각 변마다 기울기를 계산하여 모서리를 따라 선형보간(Linear Interpolation)하는 과정을 통해 계산됩니다.

4.프래그먼트 처리와 출력 병합

지금까지 한 내용을 다시 복습해보자면 응용프로그램으로 부터 정점버퍼를 입력받아 입력조립기에서 이 정점들을 프리미티브 단위로 조립하고, 각각의 정점은 정점 변환 단계를 거쳐 변환되어 레스터라이저로 넘어갑니다. 레스터화 단계에서는 클리핑을 통해 폴리곤을 잘라내고, 뒷면 제거를 통해 보이지 않을 면을 제거하고, 원근 투영 나눗셈을 하여 원근법을 구현하고, 뷰포트 변환을 통해 NDC상의 물체들을 스크린 공간으로 사상하고, 마지막으로 스캔변환을 통해 삼각형을 구성하는 정점별 속성을 보간하여 내부의 프레그먼트들에게 할당하였습니다. 이제는 각각의 프레그먼트의 색상을 결정하는 일만 남았습니다. 조명을 계산한다던가, 텍스쳐를 입힌다던가 하는 작업을 통해 프래그먼트를 색칠하는 단계를 바로 프래그먼트 처리 단계라 합니다. Direct3D의 세이더 파이프라인에서는 Pixel Shader 단계에서 이러한 일들을 수행합니다.

프래그먼트 처리 단계에서 화면에 그려질 모든 프레그먼트의 색상을 결정한 뒤에는 출력 병합 단계로 넘어갑니다. 출력 병합 단계에서는 깊이-스텐실 테스트와 블랜딩이 일어납니다.