화면에 빨강·초록·파랑이 섞인 삼각형을 그린다고 생각해 봅시다. C++에서 준비한 것은 위치와 색이 들어 있는 정점 세 개뿐입니다. 아직 삼각형 내부의 픽셀 색은 어디에도 저장되어 있지 않습니다. GPU는 어떻게 이 세 점으로 면 전체를 채울까요?
이 글에서는 정점 하나가 화면의 색이 될 때까지 따라갑니다. 아래 흐름도에서 버퍼는 재료, 셰이더는 재료를 처리하는 프로그램, 렌더 타겟은 결과를 쓰는 곳으로 읽어 주세요. 삼각형을 처음 그릴 때는 IA → VS → Rasterizer → PS → OM 경로를 사용합니다. 테셀레이션과 Geometry Shader는 그 사이에 추가할 수 있는 단계입니다.
flowchart LR
A[정점과 인덱스] --> I[Input Assembler]
I --> V[Vertex Shader]
V --> R[Clipping · 원근 나눗셈 · Rasterizer]
R --> P[Pixel Shader]
P --> O[깊이·스텐실·블렌딩]
O --> T[Render Target]Mermaid
복사
파이프라인이란?
파이프라인은 하나의 작업을 여러 단계로 나누어 처리하는 구조입니다. 그래픽스에서는 앞 단계의 출력이 다음 단계의 입력이 됩니다. 서로 다른 정점·도형·픽셀의 처리가 겹쳐 진행될 수 있지만, 모든 단계가 하나의 클록 사이클 안에 끝나야 한다는 뜻은 아닙니다.
렌더링 파이프라인(레스터라이제이션)
[Input Assembler]
3차원 모델 하나를 3차원 세상에 나타내기 위해서는 가장 먼저 해줘야 할것은 무언인가?
모델의 표면은 정점(vertex)과 그 정점들을 연결하는 도형으로 표현합니다. 폴리곤은 단순한 점의 집합이 아니라 정점과 변으로 둘러싸인 다각형이며, 이 강의에서는 삼각형을 기본 단위로 사용합니다.
주로 게임에서는 삼각형을 가지고 3D 폴리곤을 정의하는데, 이떄 이 정점데이터들을 운반하는 자료구조를 우리는 Vertex Buffer라고 합니다.
정점 버퍼와 같이 등장하는 용어로 Index buffer라는 것이 있다.
인덱스 버퍼는 쉽게 생각하면 정점들의 인덱스를 저장하고 있는 버퍼라고 할수 있는데
사각형을 예를 들면 사각형을 (삼각형 기반으로) 그리기 위해서는 2*3 = 6개의 정점이 필요하다.
사각형의 정점 4개를 정점 버퍼에 저장하고 인덱스 버퍼에 0, 1, 2, 0, 2, 3처럼 삼각형을 구성하는 정점 번호를 저장하면 정점을 재사용할 수 있습니다. 한 붓 그리기와는 다른 개념이며, 인덱스 없이도 Draw로 그릴 수 있습니다.
추가적으로 인덱스 버퍼를 사용하는데, 그것도 결국 메모리를 사용하니까 똑같다고 생각 할 수도 있습니다.
정점은 위치데이터 말고도, 색깔, 법선, 텍스처좌표(UV), 애니메이션에 필요한 정보등등 여러가지 프로그래머가 원하는 데이터를 추가하여 사용하기 떄문에 단순히 정수만 저장하는 인덱스버퍼가 훨씬 메모리적으로 효율적이다.
다시 정점버퍼로 돌아와서
정점버퍼는 그냥 정점들의 연속적인 메모리 구조에 불과 하기때문에 실제로 GPU에서는
이러한 정점들을 이용하여 어떤 도형을 만들어야 하는지 정보가 필요하다.
해당 도형정보를 Primitve Topology 라고 한다.
결론적으로 Input Assembler 는 이러한 정점들의 데이터를 읽고, 삼각형과 같은 도형으로 조립하는 단계의 일을 한다고 생각하면된다.
[Vertex Shader]
정점 셰이더는 입력 정점 하나를 받아 출력 정점 하나를 만듭니다. 가장 중요한 출력은 클립 공간 위치인 SV_Position입니다. 우리가 저장한 모델의 좌표를 곧바로 화면 좌표로 이해하는 것이 아니라, 모델의 배치와 카메라를 반영하여 화면에 투영할 준비를 하는 것입니다.
예를 들어 같은 나무 모델을 왼쪽과 오른쪽에 한 그루씩 그린다면 정점 배열 두 벌을 만들 필요가 없습니다. 같은 로컬 정점에 서로 다른 World 행렬을 적용합니다. 이어서 View 행렬로 카메라를 기준으로 위치를 표현하고, Projection 행렬로 원근과 시야 범위를 반영합니다. 처음 삼각형을 그리는 예제에서는 입력 자체를 클립 공간으로 정해 이 변환을 생략할 수도 있습니다.
[월드공간 변환]
Local Space(오브젝트 공간)는 각 모델이 자신의 원점과 축을 기준으로 정의된 좌표 공간입니다. World 행렬을 적용하면 여러 모델이 공통된 World Space에 배치됩니다.
[카메라 공간 변환]
월드 공간은 모든 물체가 함께 사용하는 지도와 같습니다. View 변환은 그 지도의 위치를 카메라의 위치와 방향을 기준으로 다시 표현합니다. 이것이 뷰 공간입니다. 뷰 공간이라는 좌표계와 카메라가 볼 수 있는 영역은 서로 다릅니다. 카메라 뒤의 물체도 뷰 공간 좌표로 표현할 수 있지만 화면에 보이지는 않습니다.
월드 공간의 모든 물체를 카메라 공간으로 변화하게 된다면 효율적으로 여러가지 효과나 렌더링등을 진행할수 있다.
원근 카메라가 담는 범위는 시야각 FOV, 가로/세로 비율 Aspect, 가까운 경계 Near, 먼 경계 Far로 정합니다. 실제 카메라에도 렌즈와 화면 비율에 따른 시야가 있듯이, 이 설정은 어떤 구도로 장면을 보여줄지 결정합니다. Near와 Far 사이의 잘린 피라미드 모양 영역을 절두체(view frustum)라고 부릅니다.
[View Frustrum]
카메라가 담는 범위 밖의 부분은 최종 화면에 필요하지 않습니다. 삼각형 전체가 범위 밖이면 제거하고, 경계에 걸쳐 있으면 안쪽 부분만 남기는 과정이 클리핑입니다. 삼각형의 중심이 화면 밖이라고 전체를 지워 버리면 가장자리에서 물체가 갑자기 사라지므로, 경계와 교차하는 부분을 잘라야 합니다. 이 판정은 View 변환 그 자체가 아니라 이후 클립 좌표를 기준으로 이루어집니다.
[투영 변환]
카메라 변환에서 월드의 모둔 물체를 카메라 공간으로 재배치하고, 이제 카메라 시점에서 세상을 바라볼수 있게 되었습니다. 우리가 카메라를 통해서 바라보는 가상의 공간 현실세계처럼 3차원이지만 최적으로 우리가 바라봐야할 공간은 모니터세상인 (2차원) 공간의 세계가 되어야 합니다. 3차원 공간을 어떻게 2차원으로 표현 할 수 있을까요?
3차원 세상은 2차원 평면에 표혀하는 방법은 다행이도 이미 화가들이 개발되었기 떄문에 우리도 이러한 방법을 쓰면된다. (원근법)
Projection 행렬을 곱한 결과는 (x, y, z, w) 네 성분의 클립 좌표입니다. 아직 모니터의 픽셀 위치는 아닙니다. 먼저 클립 범위와 비교하고, 이후 xyz를 w로 나누어 NDC를 만듭니다. 화면의 가로·세로 위치뿐 아니라 깊이 검사에 쓸 z 정보도 계속 필요하므로, 단순히 z를 버려 2차원으로 만드는 과정으로 이해하면 안 됩니다.
원근 카메라에서는 같은 크기의 물체라도 카메라에서 멀수록 w로 나눈 뒤의 화면상 크기가 작아집니다. NDC의 가시 범위를 정해 두면 다양한 카메라 설정도 같은 기준으로 화면에 옮길 수 있습니다. 아래의 원근 나눗셈 절에서 이 연결을 수치로 확인하겠습니다.
[Tesselator 단계]
테셀레이터는 3단계로 구성되어 있다. HullShader → Tesselation → DomainShader 단계를 거친다.
테셀레이션을 사용할 때는 정점 셰이더가 준비한 제어점(control point)을 Hull Shader로 전달합니다. 어디에서 공간 변환을 할지는 셰이더 설계에 달려 있습니다. 흔히 마지막 표면 위치를 계산하는 Domain Shader에서 클립 공간 위치를 완성합니다.
Hullshader 는 폴리곤을 어떻게 분활 할 것인가? 폴리곤을 얼마나 분활 할 것인가? 를 결정하는 단계이다. 아래 그림은 테셀레이션 적용 전과 후의 그림이다.
[적용전]
[적용후]
테셀레이터는 다시 말해 다각형을 겹치지 않고 작게 만들어 빈틈을 없애 게임등에서 사물이나 인물등을 실제에 보다 가깝게 표현할수 있게 도와주는 기술이다.
DomainShader는 테셀레이터가 출력한 정점마다 한 번씩 함수(셰이딩 언어) 호출을 해주게 된다. 테셀레이션이 활성화 되면 기존의 정점쉐이더에서 수행한 것들을 도메인 셰이더에서 수행하게 된다. 예를들어 공간변환(월드 → 뷰 → 투영)이 될수 있다.
아래처럼 가까운 표면을 더 세밀하게 나누면 곡면의 윤곽과 변위를 표현할 수 있습니다. 대신 생성되는 정점과 처리 비용이 늘어납니다. 미리 만든 여러 정밀도의 모델을 선택하는 LOD와도 비교하여, 필요한 화면 품질에 맞는 방법을 선택합니다. 첫 삼각형 실습에는 이 단계가 필요하지 않습니다.
[Geometry Shader]
Geometry Shader는 점·선·삼각형 같은 도형을 입력받아 도형을 내보내거나 생략할 수 있습니다. 예를 들어 점 하나로 화면을 향하는 사각형을 만들 수 있습니다. 저장할 정점 데이터를 줄일 가능성은 있지만 GPU에서 도형을 생성하는 비용도 있으므로, 사용한다고 자동으로 빨라지는 것은 아닙니다.
[Rasterization]
정점처리 단계를 지난 정점은 다음 단계인 레스터라이저 단계로 넘어갑니다.
우선 정점들은 삼각형으로 묶여있는데 이 시점부터는 하나의 독자적 도형으로 처리가 된다.
우선 화면에 그려질 2차원 삼각형의 세 정점이 결정되면 다음과 같은 일이 일어납니다.
1.
래스터라이저가 도형의 커버리지를 계산하고 픽셀 셰이더에 필요한 입력을 만듭니다. 실제 셰이더 호출 수는 MSAA, 보조 호출, 조기 깊이 검사 등의 영향을 받으므로 항상 화면 픽셀 수와 일치하지는 않습니다.
2.
삼각형의 세 정점에 할당 되었던 여러 데이터(pos, uv, normal, color)가 보간되어 삼각형 내부에 각 픽셀셰이더로 넘어옵니다.
래스터라이저는 셰이더 코드로 알고리즘 자체를 바꾸는 단계는 아니지만, culling·fill mode·scissor·viewport 등 상태를 설정할 수 있습니다. 기본 연산은 클리핑, 원근 나눗셈, 화면 좌표 변환과 커버리지 계산입니다. Microsoft: Rasterizer 설정
대표적 레스터라이제이션의 역할을 나열해 보자면
1.
클리핑
2.
원근 나눗셈(perpective division)
3.
뒷면제거 (backface culling)
4.
스캔변환 (ndc scan transform)
5.
뷰포트 변환
[클리핑]
클리핑은 투영변환 이후의 클립공간 볼륨 바깥에 놓인 폴리곤들을 잘라내는 작업을 말합니다. 이전 부터 언급되었던 이 작업이 바로 이 레스터라이저 단계에서 일어납니다.
[원근 나눗셈]
예를 들어 클립 좌표가 (1, 0, 1, 2)인 점을 생각해 봅시다. w가 2이므로 NDC는 (0.5, 0, 0.5)입니다. x가 1이라고 화면 오른쪽 끝에 놓이는 것이 아니라, w로 나눈 결과인 0.5 위치에 놓입니다.
원근 나눗셈은 클립 좌표 (x_c, y_c, z_c, w_c)의 x·y·z를 클립 좌표의 w로 나누어 NDC를 만드는 과정입니다. 일반적인 원근 투영에서 w는 뷰 공간 z와 관련되지만, 투영 후 z 자체로 나누는 것은 아닙니다. 결과는 (x_c/w_c, y_c/w_c, z_c/w_c)이며, Direct3D의 가시 NDC 범위는 x·y가 [-1, 1], z가 [0, 1]입니다.
clip → (clip.xyz / clip.w) → NDC → viewport → 화면 좌표
가로 800, 세로 600인 뷰포트가 왼쪽 위 (0, 0)에서 시작한다면, NDC (0.5, 0)은 화면 (600, 300)으로 옮겨집니다. x는 -1에서 1까지를 0에서 800까지로, y는 위쪽이 양수인 좌표를 아래쪽으로 증가하는 화면 좌표로 바꾼 결과입니다. z=0.5는 버리지 않고 깊이 검사에 사용합니다.
[뒷면 제거]
뒷면 제거는 삼각형이 화면에 투영된 뒤의 정점 회전 방향(winding)과 rasterizer의 CullMode·FrontCounterClockwise 설정으로 앞면·뒷면을 판정하는 기능입니다. 다른 물체에 가려졌는지는 별도의 깊이 검사 문제입니다.
[뷰포트 변환]
컴퓨터 화면상의 윈도우 스크린 공간을 갖는데 이 스크린 공간 내에 2차원 이미지가 그려질 뷰포트가 정의되는데 NDC공간의 물체들을 스크린 공간으로 이전시키는변환을 뷰포트 변환이라고 합니다.
[스캔 변환]
스캔 변환은 화면의 삼각형이 어느 샘플을 덮는지 결정합니다. 정점 속성은 보간되어 픽셀 셰이더로 전달되며, HLSL의 기본 선형 보간은 원근 보정을 포함합니다. noperspective, nointerpolation 등으로 보간 방식을 지정할 수 있습니다.
[Pixel Shader]
정점 세 개에 빨강·초록·파랑을 넣어 두면, 래스터라이저가 삼각형 안의 위치에 맞게 색을 보간해서 픽셀 셰이더에 넘깁니다. 세 정점의 w가 같고 정확히 무게중심에 있는 점이라면 각 색의 비중이 1/3이므로 RGB는 (1/3, 1/3, 1/3)이 됩니다. 셰이더가 아래처럼 입력 색을 그대로 반환하기만 해도 색이 자연스럽게 섞이는 이유입니다.
// HLSL: 보간된 색을 그대로 렌더 타겟에 출력하는 픽셀 셰이더
float4 main(float4 position : SV_Position,
float4 color : COLOR) : SV_Target
{
return color;
}
C++
복사
COLOR는 정점 셰이더가 넘긴 사용자 데이터, SV_Position은 시스템이 다루는 위치 정보, SV_Target은 출력할 렌더 타겟 색을 뜻합니다. 이 함수의 반환값을 float4(1, 1, 1, 1)로 바꾸면 정점 색과 관계없이 흰 삼각형이 나옵니다. 도형의 위치는 그대로인데 내부 색만 바뀐다면, 정점 처리와 픽셀 처리가 서로 다른 일을 한다는 것을 직접 확인한 것입니다.
[Output merger]
깊이 - 스텐실 테스트와 블렌딩이 일어나서 최종적인 화면(텍스처)에 물체를 그려준다.
[Compute shader]
Compute Shader는 삼각형을 래스터화하는 대신 Dispatch로 지정한 작업들을 병렬 실행합니다. 예를 들어 이미지의 각 픽셀 밝기를 계산하거나 파티클 위치를 갱신할 수 있습니다. Direct3D의 Compute Shader를 배우기 위해 CUDA나 OpenCL을 함께 사용할 필요는 없습니다. 결과를 어떤 버퍼나 텍스처에 쓰고, 다음 작업이 언제 읽을 수 있는지를 관리해야 한다는 점이 중요합니다.





























