
1. 서론
게임을 실행하면 캐릭터, 지형, 건물 같은 오브젝트가 화면에 나타난다. 하지만 실행 전의 모델은 완성된 이미지가 아니라 저장장치에 있는 데이터 파일일 뿐이다. 그 안에는 정점 위치, 법선, UV, 인덱스, 재질 이름 같은 숫자와 속성 정보가 들어 있다.
v -1.3143 15.0686 -1.6458 // 정점 위치
v -1.4575 15.1137 -1.5916
...
vn -0.3819 -0.2594 -0.8871 // 법선 위치
vn -0.4376 -0.2949 -0.8495
...
s 1
f 1//1 2//2 3//3 4//4 // 하나의 면을 구성하는 정점들
f 4//4 3//3 5//5 6//6
...
즉 모델 파일은 화면에 바로 그릴 수 있는 그림이 아니라, GPU가 처리할 수 있도록 해석되어야 하는 원본 데이터다.
일반적으로 CPU는 모델 파일을 읽어 정점 배열과 인덱스 배열을 준비하고, 렌더러는 이 데이터를 GPU가 사용할 버퍼 리소스로 업로드한다. 정점 데이터가 담긴 영역은 Vertex Buffer, 정점을 어떤 순서로 참조할지가 담긴 영역은 Index Buffer가 된다.
하지만 GPU 메모리에 버퍼가 올라갔다고 바로 화면에 그려지는 것은 아니다. 버퍼는 여전히 연속된 바이트이므로 어느 부분이 위치이고, 어느 부분이 UV인지, 어떤 세 점을 하나의 삼각형으로 볼지 정해야 한다. 그리고 모델을 장면에 배치하고, 카메라 기준으로 변환하고, 삼각형을 픽셀 후보로 바꾸고, 색과 깊이를 계산해야 한다.
렌더링 파이프라인은 이 과정을 여러 단계로 나누어 처리하는 구조다. Direct3D 11의 기본 흐름은 다음과 같다.
2. Input Assembler(IA)
Input Assembler는 Vertex Buffer의 연속된 바이트를 Input Layout에 따라 위치, 법선, UV 같은 정점 속성으로 읽는다. Index Buffer와 Primitive Topology도 함께 사용해 어떤 정점들이 점, 선, 삼각형을 이루는지 준비한다.
CPU가 모델 파일을 읽어 정점 버퍼와 인덱스 버퍼를 만들었더라도, GPU는 그 바이트들이 어떤 의미인지 자동으로 알 수 없다. 이를 해석하기 위해 Input Layout, Index Buffer, Primitive Topology가 필요하다.
1. Input Layout: 정점 레코드 설명서
정점은 위치 하나만 가진 데이터가 아니다. 표면 방향을 나타내는 법선, 텍스처를 읽을 위치인 UV, 정점 색, 애니메이션용 뼈 인덱스와 가중치 등이 함께 들어갈 수 있다.
Input Layout은 버퍼 안의 바이트를 셰이더 입력과 연결한다. 예를 들어 앞의 12바이트는 'POSITION', 다음 12바이트는 'NORMAL', 다음 8바이트는 'TEXCOORD'처럼 해석하도록 알려준다.
2. Index Buffer: 정점 재사용을 위한 순서 버퍼
Vertex Buffer만 있어도 물체를 그릴 수는 있다. 하지만 이웃한 삼각형은 보통 정점을 공유한다. 같은 정점 데이터를 여러 번 저장하는 대신, 고유 정점은 한 번만 저장하고 Index Buffer로 어떤 정점을 사용할지 가리키면 메모리 사용량과 정점 처리량을 줄일 수 있다.


예를 들어 사각형은 위처럼 두 개의 삼각형으로 표현할 수 있다. 정점을 중복 저장하면 여섯 개의 정점이 필요하지만, 아래처럼 인덱스 버퍼를 사용하면 고유 정점 네 개와 인덱스 여섯 개로 같은 사각형을 만들 수 있다.

3. Primitive Topology: 점 연결 규칙
Primitive Topology는 정점들을 '어떤 도형으로 해석할지' 정한다. 같은 정점 배열이라도 점 목록, 선 목록, 삼각형 목록, 삼각형 스트립 등으로 다르게 해석될 수 있다.
일반적인 3D 메시에서는 삼각형이 기본 단위로 많이 사용된다. 삼각형은 세 점만으로 하나의 평면이 확정되고, 내부의 색, 법선, UV 같은 속성을 세 꼭짓점 값으로 안정적으로 보간할 수 있기 때문이다.

최종적으로 Input Assembler는 버퍼, Input Layout, 인덱스, 토폴로지를 함께 사용해 뒤 단계가 처리할 정점 스트림과 프리미티브를 만든다.
3. Vertex Shader(VS)
Vertex Shader는 Input Assembler에서 넘어온 정점을 하나씩 처리한다. 가장 중요한 역할은 정점 위치를 Local Space에서 Clip Space로 변환하고, 다음 단계에 필요한 속성을 전달하는 것이다.
모델 파일의 정점은 보통 '자기 모델의 원점'을 기준으로 저장된다. 캐릭터, 나무, 바위가 모두 각자의 로컬 좌표계를 가지고 있으므로, 이 값을 그대로 사용하면 장면 안의 올바른 위치에 배치할 수 없다.
그래서 정점은 여러 좌표 공간을 거쳐 변환된다.
- Local/Object Space: 모델 자체의 좌표계다.
- World Space: 모델의 위치, 회전, 크기를 적용해 장면 안에 배치한 좌표계다.
- View/Camera Space: 카메라를 기준으로 장면을 다시 표현한 좌표계다.
- Clip Space: 투영과 클리핑에 사용되는 4성분 동차좌표 공간이다.
Local에서 World로 갈 때는 모델의 이동, 회전, 크기 변환이 적용된다. World에서 View로 갈 때는 카메라 기준으로 장면 전체가 변환된다. Projection 변환을 적용하면 카메라가 볼 수 있는 공간이 Clip Space로 옮겨진다.
좌표 변경을 위해 행렬이 사용된다. 행렬은 좌표를 다른 기준으로 옮기는 규칙으로, 정점마다 같은 이동·회전·크기 변경을 반복해서 적용할 수 있으며, 여러 변환을 행렬 곱으로 묶어 하나의 변환으로 만들 수도 있다.
모델을 월드에 배치하고 카메라 기준으로 다시 표현하면 카메라가 무엇을 보고 있는지 알 수 있다. 그러나 카메라 앞의 공간은 여전히 3차원이고 모니터는 2차원이다. 길게 뻗은 길 위에 같은 크기의 나무를 여러 그루 놓으면 먼 나무일수록 작게 보여야 하는데, 3차원 좌표의 x와 y를 그대로 화면 좌표로 쓰면 이런 원근감이 생기지 않는다.
원근감은 원근 투영을 통해 구현한다. 원근 투영에서는 가까운 물체는 크게, 먼 물체는 작게 보이도록 계산된다. 이를 위해 정점 위치는 (x, y, z, w) 형태의 동차좌표로 표현되며, 이후 Rasterizer 단계에서 x, y, z를 w로 나누어 NDC 좌표를 만든다.

Vertex Shader의 출력은 아직 픽셀이 아니다. 이 단계의 결과는 Clip Space 위치와 UV, 법선, 색처럼 뒤 단계에서 사용할 정점 속성이다.
참고: 직교 투영
원근 투영에서는 거리에 따라 크기가 달라진다. 일반적인 3D 카메라가 이 방식을 사용한다. 직교 투영에서는 깊이에 따른 크기 변화가 없다. CAD, 에디터의 일부 뷰, 2D UI처럼 원근감이 필요하지 않은 장면에 적합하다. 두 방식은 투영 행렬이 다르지만 이후 파이프라인은 같은 구조를 따른다.
4. Optional Stages: Tessellation, Geometry Shader
Vertex Shader는 들어온 정점의 위치와 속성을 바꾸지만, 기본적으로 정점 수나 프리미티브의 형태까지 크게 바꾸지는 않는다. 그런데 오픈 월드 같이 맵이 넓은 게임을 하다 보면, 멀리 있는 산은 각지고 가까이 있는 지형은 세세하게 굴곡진 모습을 볼 수 있다. 또 불꽃이나 연기 같은 파티클, 바닥 위 잔디 가닥 같은 것들도 볼 수 있다. 이 같은 케이스에 대응하기 위해 기존 기하를 잘게 나누거나 새로운 형태로 변경하는 처리를 한다.
1. Tessellation: Hull Shader -> Tessellator -> Domain Shader
Tessellation은 패치라고 부르는 표면 조각을 더 작은 삼각형으로 나누는 과정이다. Hull Shader가 얼마나 나눌지 정하고, 고정 기능 Tessellator가 새 위치를 만들며, Domain Shader가 실제 정점 위치와 속성을 계산한다. 이를 이용하면 가까운 지형은 더 세밀하게 만들고, 먼 지형은 단순하게 유지하는 식의 처리가 가능하다.


다만 지나치게 잘게 분할해 삼각형이 과도하게 많아지면 처리 비용이 늘어나 게임 플레이에 문제가 생길 수 있다. 그래서 실제 게임에서는 테셀레이션을 이용하기보단 동일한 에셋에 대해 정점이 많은 하이폴리곤 모델과 정점이 적은 로우폴리곤 모델을 각각 준비해 지원하는 경우가 많다.
2. Geometry Shader
Geometry Shader는 점, 선, 삼각형 같은 프리미티브 하나를 입력받아 다른 프리미티브를 출력할 수 있다. 입력을 그대로 넘길 수도 있고, 제거할 수도 있으며, 새로운 도형을 만들어낼 수도 있다.
Grass Shader Series
www.youtube.com
다만 일반적인 메시 렌더링에서는 이 단계들을 사용하지 않는 경우도 많다. 기본적인 흐름에서는 Vertex Shader의 결과가 Rasterizer로 바로 넘어간다고 이해해도 된다.
5. Rasterizer(RS)
Rasterizer는 Clip Space의 점, 선, 삼각형 정보를 화면의 픽셀 또는 샘플 후보로 바꾼다. 즉 정점 중심의 표현을 픽셀 중심의 표현으로 바꾸는 단계다.
Vertex Shader가 만든 결과는 여전히 삼각형 꼭짓점 정보다. 화면에 색을 칠하려면 이 삼각형이 모니터 같은 렌더 타깃의 어떤 위치를 덮는지 알아야 한다. Rasterizer는 이를 위해 클리핑, 원근 나눗셈, 뒷면 제거, 뷰포트 변환, 속성 보간을 수행한다.
1. Clipping
Clip Space에서는 가시 범위를 벗어난 도형을 제거한다. Direct3D의 기본 clip 조건은 다음과 같다.
-w <= x <= w
-w <= y <= w
0 <= z <= w
- 여기서 w는 투영 행렬을 거쳐 만들어진 clip 좌표의 w 성분이다. 일반적인 원근 투영에서는 View Space 깊이와 관련된 값이 들어간다.
- 삼각형이 가시 범위 밖에 완전히 있으면 제거되고, 일부만 걸쳐 있으면 경계와 만나는 새 정점을 만들어 보이는 부분만 남긴다.
2. Perspective Divide
클리핑이 끝나면 x, y, z 성분을 w로 나눈다. 이 결과가 NDC, 즉 Normalized Device Coordinates다. Direct3D에서 NDC의 x, y는 보통 [-1, 1] 범위에 있고, z는 [0, 1] 범위에 있다.
x_ndc = x_clip / w_clip
y_ndc = y_clip / w_clip
z_ndc = z_clip / w_clip
최종 화면은 2차원이지만 z값은 버리지 않는다. 이후 Output Merger 단계에서 깊이 테스트를 할 때 어느 표면이 더 가까운지 판단하는 데 사용된다.
3. Back-face Culling: 카메라 반대쪽 면 제거
닫힌 물체의 표면은 앞면과 뒷면으로 나눌 수 있다. Rasterizer는 화면에서 정점이 감기는 방향, 즉 와인딩 순서를 기준으로 앞면과 뒷면을 판정해 카메라 반대쪽을 향한 면을 그리지 않도록 제거한다.
4. Viewport Transform
뷰포트 변환은 해상도와 독립적인 이 NDC 좌표를 실제 렌더 타깃의 픽셀 좌표 범위로 옮긴다. 예를 들어 같은 NDC 장면이라도 1280×720 뷰포트와 1920×1080 뷰포트에 맞게 배치될 수 있다. Direct3D의 화면 좌표는 원점이 왼쪽 위이며, y는 아래 방향으로 증가한다.
5. Scan Conversion & Interpolation
마지막으로 Rasterizer는 화면 좌표상의 삼각형이 어떤 픽셀 또는 샘플을 덮는지 판정한다. 삼각형 안에 들어온 위치는 Pixel Shader가 실행될 후보가 된다.
이 후보 위치에는 UV, 법선, 색 같은 입력값도 필요하다. Rasterizer는 세 꼭짓점의 값을 삼각형 내부로 보간한다. 이때 투영으로 생긴 왜곡을 보정하기 위해 기본적으로 원근 보정 보간을 사용한다.
Rasterizer는 최종 색을 정하지 않는다. 대신 Pixel Shader가 계산할 화면 위치와 입력값을 준비한다.
6. Pixel Shader(PS)
Pixel Shader는 Rasterizer가 만든 후보 위치마다 실행되어 렌더 타깃에 보낼 값을 계산한다. 보간된 UV, 법선, 색, 텍스처, 재질, 조명 정보를 사용해 해당 위치의 표면이 어떤 색으로 보일지 결정한다.
Rasterizer는 어느 위치에서 계산할지만 정한다. 같은 흰색 벽이라도 빛을 정면으로 받으면 밝고, 그림자 속에 있으면 어둡다. 나무와 금속은 같은 빛을 받아도 다르게 보인다. 이런 차이를 계산하는 단계가 Pixel Shader다.
1. Texture, UV
나무 상자의 나뭇결이나 캐릭터 옷의 무늬를 삼각형마다 직접 색칠하려면 많은 데이터가 필요하다. 대신 표면에 붙일 2차원 이미지를 준비하고, 모델의 각 위치가 이미지의 어느 부분을 가리킬지 연결한다. 이 이미지가 텍스처(Texture)이고, 이미지 안의 위치를 나타내는 2차원 좌표가 UV다.
Rasterizer는 정점에 있던 UV를 삼각형 내부까지 보간하고, Pixel Shader는 그 UV를 사용해 텍스처 값을 읽는다. 텍스처를 어떻게 읽을지, 예를 들어 가장 가까운 텍셀(텍스처 이미지의 한 칸)을 고를지 주변 값을 섞을지, UV가 0~1 범위를 벗어났을 때 반복할지 고정할지는 Sampler 상태가 정한다.
참고: 유니티에서 Sampler State를 사용하는 방법
2. Normal, Lighting
법선은 표면이 어느 방향을 향하는지 나타낸다. 빛의 방향과 표면 법선의 관계에 따라 표면의 밝기가 달라진다. 가장 단순한 확산 조명에서는 법선과 빛 방향의 내적을 사용해 빛을 얼마나 받는지 계산할 수 있다.
모델의 삼각형을 늘리지 않고도 작은 홈과 돌기를 표현하고 싶다면 노멀 맵(Normal Map)을 사용할 수 있다. 노멀 맵을 사용하면 실제 삼각형을 더 늘리지 않고도 작은 홈과 돌기가 있는 것처럼 빛의 반응을 바꿀 수 있다. Pixel Shader가 노멀 맵에서 읽은 방향 정보를 조명 계산에 사용하면 표면이 더 세밀하게 보인다.

Pixel Shader는 보통 색을 출력하지만 반드시 최종 화면색만 출력하는 것은 아니다. 여러 렌더 타깃에 법선, 재질 값, 속도, 오브젝트 ID 같은 중간 결과를 기록하고, 이후 렌더링 패스에서 이를 다시 사용할 수도 있다.
- 렌더 타깃(Render Target)은 렌더링 결과를 기록하는 GPU의 2차원 이미지 리소스다. 화면에 표시할 백 버퍼가 렌더 타깃이 될 수도 있고, 다음 계산에서 사용할 중간 텍스처가 될 수도 있다.
7. Output Merger(OM)
Output Merger는 Pixel Shader가 계산한 값을 실제 렌더 타깃에 기록할지 결정한다. 새 출력값과 기존 버퍼 값을 비교하고, 설정된 규칙에 따라 버리거나 기록하거나 섞는다.
Pixel Shader가 색을 계산했다고 해서 그 값이 바로 화면에 반영되는 것은 아니다. 같은 위치에 이미 더 가까운 물체가 그려져 있을 수도 있고, 반투명 물체라면 기존 색과 섞어야 할 수도 있다. 이런 공통 규칙을 처리하는 단계가 Output Merger다.
1. Depth Buffer: 가장 가까운 표면
Depth Buffer는 화면의 각 위치에 지금까지 기록된 표면의 깊이를 저장하는 버퍼다. Direct3D의 일반적인 깊이 범위에서는 0에 가까울수록 Near 평면에 가깝고, 1에 가까울수록 Far 평면에 가깝다.
새 후보 픽셀이 들어오면 기존 깊이값과 비교한다. 새 값이 더 가까우면 통과해 색을 기록할 수 있고, 더 멀면 이미 그려진 표면 뒤에 있다고 보고 버릴 수 있다. 이 과정이 깊이 테스트다.
깊이 쓰기는 테스트를 통과한 새 깊이값을 Depth Buffer에 저장할지 정하는 설정이다. 불투명 물체는 보통 깊이 테스트와 깊이 쓰기를 모두 사용한다. 반투명 물체는 앞의 불투명 물체에는 가려져야 하므로 깊이 테스트는 사용하되, 다른 반투명 물체를 막지 않기 위해 깊이 쓰기를 끄는 경우가 많다.
2. Stencil Buffer: 위치별 정수 마스크
Stencil Buffer는 화면의 각 위치에 작은 정수 값을 저장한다. 깊이가 앞뒤 관계를 나타낸다면, 스텐실 값은 애플리케이션이 정한 마스크 정보로 사용할 수 있다.
예를 들어 특정 영역에만 효과를 적용하거나, 캐릭터 윤곽선, 거울, 포털 같은 효과를 만들 때 사용할 수 있다. 현재 스텐실 값과 기준값을 비교해 통과 여부를 정하고, 통과 또는 실패했을 때 값을 유지하거나 바꾸는 규칙도 설정할 수 있다.
3. Blending: 색 혼합
깊이와 스텐실 테스트를 통과했다고 해서 항상 새 색으로 기존 색을 덮어쓰는 것은 아니다. 유리, 연기, UI처럼 반투명한 표면은 기존 렌더 타깃 색과 새 색을 섞어야 한다.
Blending은 Pixel Shader가 출력한 source 색과 렌더 타깃에 이미 있던 destination 색을 설정된 공식에 따라 합치는 작업이다. 흔한 알파 블렌딩 공식은 다음과 같다.
C_out = C_src * a_src + C_dst * (1 - a_src)
예를 들어 새 색의 알파가 0.25라면 결과 색의 25%는 새 색에서, 75%는 기존 색에서 가져온다.
이 공식에서 알 수 있듯 어떤 색이 source이고 destination인지에 따라 결과가 달라진다. 그래서 일반적인 반투명 물체는 카메라에서 먼 것부터 가까운 것 순서로 그리는 경우가 많다. Output Merger는 투명 물체의 앞뒤를 자동으로 정렬하지 않고, 애플리케이션이 제출한 순서와 현재 블렌딩 설정에 따라 처리한다.
8. 최종 정리
Direct3D 11의 기본 렌더링 흐름은 다음과 같이 정리할 수 있다.
- Draw 준비
CPU가 모델 파일을 읽어 정점 버퍼, 인덱스 버퍼, 텍스처 같은 GPU 리소스를 만들고, 사용할 셰이더와 렌더링 상태를 바인딩한다. 이후 Draw 또는 DrawIndexed를 호출하면 파이프라인 처리가 시작된다. - Input Assembler
Vertex Buffer의 바이트를 Input Layout에 따라 정점 속성으로 해석한다. Index Buffer와 Primitive Topology를 사용해 어떤 정점들이 하나의 프리미티브를 이루는지 준비한다. - Vertex Shader
각 정점의 위치를 Local Space에서 World, View, Clip Space로 변환한다. UV, 법선, 색 같은 속성도 다음 단계로 전달한다. - Optional Stages
필요하면 Tessellation으로 표면을 더 작은 삼각형으로 나누거나, Geometry Shader로 프리미티브를 생성·변경·제거할 수 있다. 사용하지 않으면 건너뛴다. - Rasterizer
Clip Space의 프리미티브를 화면의 픽셀 또는 샘플 후보로 바꾼다. 클리핑, 원근 나눗셈, 뒷면 제거, 뷰포트 변환, 속성 보간을 수행한다. - Pixel Shader
Rasterizer가 만든 후보 위치마다 실행되어 색이나 중간 렌더링 값을 계산한다. 텍스처, UV, 법선, 재질, 조명 정보가 이 단계에서 주로 사용된다. - Output Merger
깊이 테스트, 스텐실 테스트, 블렌딩을 적용해 Pixel Shader의 출력값을 실제 렌더 타깃과 깊이·스텐실 버퍼에 기록할지 결정한다.
결국 렌더링 파이프라인은 “모델 파일의 숫자 데이터”를 “렌더 타깃에 기록되는 픽셀 결과”로 바꾸기 위한 단계적 처리 과정이다. 각 단계는 앞 단계의 출력을 입력으로 받아 더 화면에 가까운 형태로 변환한다.
Input Assembler가 버퍼를 정점과 삼각형으로 해석하고, Vertex Shader가 정점을 카메라와 투영 기준으로 변환하며, Rasterizer가 삼각형을 픽셀 후보로 바꾼다. Pixel Shader는 각 후보 위치의 출력값을 계산하고, Output Merger는 이 값을 실제 버퍼에 기록할지 최종 결정한다.
참고 자료
렌더링 파이프라인(Rasterization)
파이프라인이란?
www.yamyamcoding.com
그래픽 파이프라인 - Win32 apps
이 섹션에서는 Direct3D 11 프로그래밍 가능 파이프라인에 대해 설명합니다.
learn.microsoft.com
Codex | OpenAI의 AI 코딩 파트너
Codex는 에이전트 기반 개발 작업에 가장 최적화된 코딩 어시스턴트입니다. 기획과 기능 구현부터 리팩터링, 리뷰, 릴리스까지 작업 전반을 가속화하며 다양한 개발 도구와도 매끄럽게 연동됩니
openai.com



