10 — 래스터화 (Rasterization)

출처: SSU 컴퓨터 그래픽스 슬라이드 — SSU_Intro2CG_10_Rendering_rasterization.pdf 주차: 9주차 — 기말 시험범위 시작 선행: 07 — 3D 기하변환·뷰잉, 09 — 은면의 제거

3차원 삼각형을 화면의 픽셀로 바꾸는 과정. "이 픽셀이 삼각형 안에 있나?""그 픽셀의 색/깊이는 얼마인가?" 두 질문을 edge function무게중심좌표(barycentric coordinates) 로 푸는 것이 이 장의 핵심입니다.


1. 렌더링의 두 갈래

출처: 슬라이드 — Two Popular Methods

3D 장면을 2D 이미지로 만드는 대표적인 두 방법:

방법중심순회 단위특징
Rasterization객체 중심 (Object-centric)삼각형마다 → 그 안의 픽셀빠름, 실시간 GPU
Ray Tracing이미지 중심 (Image-centric)픽셀마다 → 광선을 쏨사실적, 느림

이 장은 래스터화. 즉 "삼각형을 하나씩 꺼내서, 그 삼각형이 덮는 픽셀들을 칠한다" 는 객체 중심 루프입니다.

1.1 폴리곤 메시 (Polygon Mesh)

렌더링의 입력 형상은 정점(vertex)·에지(edge)·면(face) 으로 표면을 정의한 폴리곤 메시입니다.

  • Face-vertex mesh: 면이 자신을 이루는 정점 인덱스를 참조하는 가장 흔한 표현.
  • Blender 등에서 모델링 후 .obj 같은 포맷으로 내보냄.

2. 래스터화의 큰 그림

출처: 슬라이드 — Rasterization Overview / Naïve Algorithm

두 단계로 요약됩니다:

  1. 투영(Projection): 삼각형의 정점들을 원근투영으로 화면에 투영.
  2. 포함 판정(Coverage): 각 픽셀이 투영된 2D 삼각형 안에 들어가는지 판정 → 들어가면 색을 칠함.

2.1 Naïve 알고리즘

C
for (each triangle in scene) {
  // STEP 1: 정점을 원근투영
  Vec2f v0 = perspectiveProject(triangle[i].v0);
  Vec2f v1 = perspectiveProject(triangle[i].v1);
  Vec2f v2 = perspectiveProject(triangle[i].v2);
  for (each pixel in image) {          // ← 모든 픽셀을 도는 게 낭비
    if (pixelContainedIn2DTriangle(v0, v1, v2, x, y)) {
      image(x, y) = triangle[i].color;
    }
  }
}

2.2 최적화: 바운딩 박스 (Bounding Box) ⭐️

삼각형마다 이미지 전체 픽셀을 도는 것은 낭비. 투영된 삼각형의 바운딩 박스 안 픽셀만 순회합니다.

세 정점의 최소/최대 좌표로 박스를 잡고, ymin..ymax × xmin..xmax 만 검사 → 큰 화면에서 대부분의 빈 픽셀 검사를 건너뜁니다.

정점을 드래그해 바운딩박스가 어떻게 잡히고, 박스 안에서 edge function 으로 어떤 픽셀이 내부 판정되는지(그리고 검사했지만 바깥인 픽셀과, 아예 검사조차 안 하는 픽셀의 차이) 직접 보세요.

래스터화 — 바운딩박스 + Edge Function 내부 판별

drag

삼각형 꼭지점을 드래그하세요. 바운딩박스 안 픽셀만 edge function 검사를 수행합니다. 세 edge 부호가 모두 같은 픽셀만 채워집니다.

V0V1V2
전체 픽셀
216
검사 픽셀 (bbox)
88
채워진 픽셀
35
절약 (미검사)
128
바운딩박스 최적화: 삼각형마다 화면 전체(216픽셀)가 아니라 바운딩박스 안 88픽셀만 검사 → 128픽셀 검사를 건너뜁니다. edge function 세 부호가 모두 같으면 내부( 35픽셀 커버).
내부 — edge 3개 부호 동일 (커버됨)
bbox 안 검사, 외부 판정
bbox 밖 — 검사 생략
바운딩박스
삼각형 엣지

2.3 한 픽셀에 두 삼각형이 겹칠 때 — Z-Buffering ⭐️

같은 픽셀을 여러 삼각형이 덮으면 카메라에 더 가까운 것이 이겨야 합니다. 픽셀마다 깊이값(거리) 을 저장하는 z-buffer(depth-buffer) 를 둡니다.

C
float buffer[W*H];
for (i in pixels) buffer[i] = INFINITY;   // 무한대로 초기화
 
for (each triangle) {
  // 투영 + 바운딩박스 계산 ...
  for (y...) for (x...) {
    float z;
    if (pixelContainedIn2DTriangle(v0,v1,v2, x,y, z)) {
      if (z < zbuffer(x,y)) {     // 더 가까우면 갱신
        zbuffer(x,y) = z;
        image(x,y)   = triangle.color;
      }
    }
  }
}

은면 제거의 이미지공간법 (09장)이 래스터화 루프 안에 자연스럽게 들어가는 지점.

2.4 한 장의 그림에 필요한 것들

  • 이미지 버퍼 (색의 2D 배열)
  • 깊이 버퍼 (float 의 2D 배열)
  • 삼각형들 (장면 형상)
  • 정점을 캔버스로 투영하는 함수, 투영된 삼각형을 래스터화하는 함수
  • 이미지 버퍼를 디스크로 저장하는 코드

3. 투영 단계 (The Projection Stage)

출처: 슬라이드 — Projecting Vertices / Screen → NDC → Raster

3.1 화면 공간으로의 원근분할 (Perspective divide)

카메라 공간 3D 점을 z 좌표로 나눠 2D 화면 좌표를 얻습니다.

3.2 z 좌표를 버리지 마라 ⭐️

화면 좌표만 남기면 가시성(visibility) 문제를 풀 수 없습니다. 투영 후에도 정점의 카메라 공간 z 를 별도로 보관해야 합니다 (보통 양수 -z_camera 로 저장해 이후 계산을 단순화).

"Screen-space is also three-dimensional" — 화면 공간도 사실 3차원이며, x·y 만 원근 왜곡된 채로 깊이 z 가 함께 살아 있습니다 (Ed Catmull, 1974).

3.3 좌표계 사슬: Screen → NDC → Raster

GPU 관례로 화면 좌표를 정규화 장치좌표(NDC, 각 축 [-1, 1])로 재매핑한 뒤, 픽셀 격자(raster space)로 옮깁니다.

raster space 는 y 축이 아래로 향하므로 (1 - y_NDC) 로 뒤집습니다.


4. 래스터화 단계 — Edge Function ⭐️

출처: 슬라이드 — The Edge Function (Juan Pineda, 1988)

"이 픽셀이 삼각형 안에 있나?" 를 푸는 고전적이고 지금도 쓰이는 방법.

4.1 정의

에지(V0→V1)에 대해 점 P 의 위치를 부호로 판정:

→ P 가 에지의 오른쪽 → P 가 직선 위 → P 가 왼쪽

세 에지에 대해 부호가 모두 같으면 점은 삼각형 안에 있습니다.

4.2 기하학적 의미 — 외적 (Cross Product)

edge function 은 두 벡터의 외적과 닮았습니다. 그 크기는 두 벡터가 만드는 평행사변형의 넓이이고, 따라서 삼각형 넓이의 2배가 됩니다.

수식만 보면 와닿지 않습니다. 아래에서 변 A→B 와 점 P 를 드래그해 보세요. edge function이 곧 두 벡터가 만드는 평행사변형 넓이(= 밑변 × 높이) 이고, 부호가 직선의 어느 쪽인지 알려준다는 걸 눈으로 확인할 수 있습니다.

Edge Function = 부호 있는 넓이 (외적)

drag

방향이 있는 변 A→B 와 점 P 를 드래그하세요. edge function 값은 두 벡터 (B−A), (P−A) 가 만드는 평행사변형의 넓이(= 외적의 크기)이고, 부호는 P가 직선 A→B 의 어느 쪽에 있는지를 말합니다. P가 직선을 넘어가면 부호가 뒤집히고 넓이는 0을 지납니다.

+hABP
E(A,B,P)
26800
P의 위치
양(+) 쪽
밑변 |B−A|
204.0
높이 h = |E|/밑변
131.4
평행사변형 넓이 |E|
26800
삼각형 넓이 = |E|/2
13400
방향 변 A→B (밑변)
E>0 : 왼쪽(양) 반평면
E<0 : 오른쪽(음) 반평면
높이 h (밑변까지 수직거리)
핵심 직관 3가지: ① edge function = 두 벡터의 외적 → 그 크기가 평행사변형 넓이 |E| = 밑변 × 높이. ② 삼각형 넓이는 그 절반 |E|/2. ③ 부호가 직선의 어느 쪽인지를 알려주므로, 삼각형 세 변에 대해 부호가 모두 같으면 P는 내부 — 이것이 래스터화의 내부 판정입니다.
C
bool edgeFunction(const Vec2f &a, const Vec2f &b, const Vec2f &c) {
  return ((c.x-a.x)*(b.y-a.y) - (c.y-a.y)*(b.x-a.x) >= 0);
}
bool inside = true;
inside &= edgeFunction(V0, V1, p);
inside &= edgeFunction(V1, V2, p);
inside &= edgeFunction(V2, V0, p);   // 세 부호가 모두 같으면 내부

4.3 와인딩 순서 (Winding Order) 가 중요

정점을 시계/반시계 어느 방향으로 도느냐에 따라 edge function 의 부호 기준이 통째로 뒤집힙니다. 일관된 와인딩 순서를 전제로 "모두 양수면 내부" 같은 규칙을 세워야 합니다.


5. 무게중심좌표 (Barycentric Coordinates) ⭐️⭐️

출처: 슬라이드 — Barycentric Coordinates / Vertex Attribute Interpolation

"픽셀의 색·노멀·UV 는 얼마인가?" 를 푸는 정점 속성 보간의 도구. 정점 사이를 선형 보간하는 가중치입니다.

5.1 정의

삼각형 위 임의의 점 P 를 세 정점의 가중합으로 표현:

세 가중치의 합은 항상 1. 같은 가중치로 색·노멀·UV 도 보간합니다.

5.2 어떻게 구하나 — 부분삼각형 넓이의 비

λ 는 P 가 만드는 부분삼각형 넓이 ÷ 전체 삼각형 넓이. edge function 이 곧 (넓이 × 2)이므로 그대로 재사용됩니다.

C
float area = edgeFunction(v0, v1, v2);   // 전체 넓이 × 2
float w0   = edgeFunction(v1, v2, p);    // V0 맞은편 부분삼각형
float w1   = edgeFunction(v2, v0, p);    // V1 맞은편
float w2   = edgeFunction(v0, v1, p);    // V2 맞은편
if (w0>=0 && w1>=0 && w2>=0) {           // 내부 판정과 보간을 동시에!
  w0 /= area; w1 /= area; w2 /= area;    // λ0, λ1, λ2
  // color = w0*C0 + w1*C1 + w2*C2 ...
}

핵심: edge function 하나로 내부 판정무게중심좌표동시에 얻습니다. 아래에서 직접 확인하세요 — P 를 옮기면 세 부호와 λ, 그리고 보간된 색이 실시간으로 바뀝니다.

Edge Function & Barycentric Coordinates

drag

삼각형의 세 꼭지점(V0, V1, V2)과 점 P 를 드래그하세요. 세 edge function 부호가 모두 같으면 P 는 삼각형 내부이고, 세 부분삼각형 넓이의 비가 곧 무게중심좌표 (λ0, λ1, λ2) 입니다.

V0V1V2P
E(V1,V2,P) → w0
9450
E(V2,V0,P) → w1
9450
E(V0,V1,P) → w2
10800
inside?
YES
Barycentric
λ0 = 0.32
λ1 = 0.32
λ2 = 0.36
Σ = 1.00
V0 / w0 = area(V1,V2,P)
V1 / w1 = area(V2,V0,P)
V2 / w2 = area(V0,V1,P)

그런데 λ 가 "왜" 그 값인지 기하학적으로 보려면, 부분삼각형 넓이의 비로 이해하는 게 가장 직관적입니다. 아래에서 각 부분삼각형의 실제 넓이 숫자와 λ 비율 막대, 그리고 로 재구성한 점(점선 고리)이 항상 P에 겹치는 것을 확인하세요.

무게중심좌표 = 부분삼각형 넓이의 비

drag

P 가 삼각형을 세 개의 부분삼각형으로 나눕니다. 각 꼭지점의 무게 λᵢ 그 꼭지점 맞은편 부분삼각형 넓이 ÷ 전체 넓이 입니다. P를 꼭지점으로 끌면 그 λ→1, 변 위로 끌면 맞은편 λ→0, 밖으로 끌면 λ가 음수가 됩니다.

587555756000V0V1V2P
λ00.337
넓이 5875 / 17450 (맞은편 V1V2P)
λ10.319
넓이 5575 / 17450 (맞은편 V2V0P)
λ20.344
넓이 6000 / 17450 (맞은편 V0V1P)
Σλ = 1.000
P 내부
두 가지로 읽기 — ① 넓이 비: 각 부분삼각형 넓이를 전체로 나누면 λ. 세 넓이의 합은 항상 전체 넓이라 Σλ = 1. ② 가중평균: P = λ0·V0 + λ1·V1 + λ2·V2 — 점선 고리(재구성점)가 항상 P에 정확히 겹치는 것으로 확인됩니다. 같은 λ로 색·노멀·UV 도 보간합니다.

5.3 선형 보간의 기초

두 점 사이의 값은 거리에 반비례하는 가중평균. 예를 들어 C0 와 C1 을 7등분한 위치에서 C0 쪽으로 4칸 떨어졌다면:

무게중심좌표는 이것을 삼각형(2D)으로 확장한 것입니다.


6. 깊이 보간과 원근 보정 ⭐️

출처: 슬라이드 — Depth Interpolation / Perspective Correct Interpolation

6.1 z 를 그냥 보간하면 틀린다

P 의 깊이를 무게중심좌표로 z 를 직접 보간하면 틀립니다. 원근투영은 직선은 보존하지만 거리(비율)는 보존하지 않기 때문입니다.

6.2 해법: z 의 역수를 보간

정점 z 의 역수를 무게중심좌표로 선형 보간합니다:

6.3 원근 보정 보간 (Perspective-Correct Interpolation)

색·노멀·UV 같은 정점 속성도 같은 이유로 그냥 보간하면 왜곡됩니다 (무게중심좌표는 2D 화면에서 계산했으므로). 올바른 절차:

  1. 각 정점 속성을 그 정점의 z 로 나눈다.
  2. 무게중심좌표로 선형 보간한다.
  3. 결과에 픽셀의 깊이 Z 를 곱한다.

아래에서 깊이비(zFar/zNear) 슬라이더를 키워 보세요. 왼쪽(화면 공간 선형 보간)은 체커가 일그러지고, 오른쪽(원근 보정)은 멀어질수록 칸이 올바르게 작아집니다.

원근 보정 보간 — Affine vs Perspective-Correct

live

체커보드 바닥면이 깊이 방향으로 멀어질 때, 화면 공간에서 선형 보간(Affine)하면 칸 간격이 균일하게 보이지만 실제로는 틀린 원근입니다. 원근 보정은 u/z·1/z를 보간한 뒤 나눠서 복원합니다. 깊이 비율을 크게 할수록 두 방식의 차이가 뚜렷해집니다.

4.0×
Affine 보간 (틀림)
칸 간격이 균일 — 원근이 무시됨
Perspective-Correct (정답)
먼 쪽 칸이 촘촘 — 올바른 원근감
Affine 오류
선형 보간 u/v
보정 핵심
u/z, 1/z 보간
복원 공식
u = (u/z)÷(1/z)
원근 투영의 핵심 성질: 직선은 보존되지만 비율(거리)은 보존되지 않습니다. 화면 좌표에서 텍스처 좌표를 선형 보간하면 먼 쪽에 더 많은 텍셀이 배정되어 화면에 칸이 균일하게 퍼집니다(PS1 워핑 현상). 해법은 u/z1/z를 보간한 뒤 z를 곱해 복원하는 것입니다. 이렇게 하면 비스듬한 면에서도 텍스처가 찌그러지지 않습니다.

텍스처가 비스듬한 면에서 찌그러지지 않게 하는 바로 그 보정. 텍스처 매핑(13장)에서 다시 만납니다.


📝 Knowledge Check: 래스터화

Quiz

Rasterization

Q 1 / 8

래스터화와 레이트레이싱의 가장 본질적인 차이는?


🔗 다음 학습