한양과학기술고등학교

에듀테크 프로젝트 대시보드

삼각형 래스터화 & 무게중심 좌표

삼각형 래스터화 & 무게중심 좌표

3D 게임 화면에 보이는 모든 것은 결국 수많은 삼각형이다. GPU는 그 삼각형들을 사각형 픽셀 격자로 바꿔 칠해야 한다 — 이 변환이 래스터화(rasterization)다. 핵심 질문은 두 가지다. ① 어떤 픽셀이 삼각형 안에 있나? ② 그 픽셀의 색·깊이·텍스처 좌표는 세 꼭짓점 값 중 무엇을 얼마나 섞은 것인가? 두 질문의 답이 모두 무게중심 좌표(barycentric coordinates) 하나에서 나온다. 아래에서 꼭짓점을 직접 끌어 보며 픽셀이 채워지는 과정을 관찰해 보자.

1. 꼭짓점을 끌어 보자 — 삼각형이 픽셀이 되는 순간

세 꼭짓점 A(빨강)·B(초록)·C(파랑) 를 마우스(터치)로 끌어 보라. 각 픽셀의 중심점이 삼각형 안에 들어오면 칠해지고, 색은 세 꼭짓점 색을 무게중심 비율로 섞은 값이다. 픽셀 위에 마우스를 올리면 그 픽셀의 (α, β, γ) 무게가 표시된다.

A B C 격자(픽셀)
…
픽셀 크기를 6px까지 줄이면 계단(에일리어싱)이 눈에 덜 띄고 매끄러워 보인다 — 실제 4K 모니터가 그렇다. 반대로 44px로 키우면 GPU가 실제로 무슨 일을 하는지가 또렷이 보인다.

2. "안/밖" 판정 — 엣지 함수 3개의 부호

변 A→B 위의 점 P에 대해 EAB(P) = (B−A) × (P−A) (2D 외적, 스칼라 하나)를 계산한다. 이 값의 부호가 P가 그 변의 왼쪽인지 오른쪽인지를 말해 준다. 세 변 AB·BC·CA 의 엣지 함수가 모두 같은 부호(≥ 0)면 P는 삼각형 안에 있다. 곱셈·뺄셈뿐이라 GPU가 픽셀마다 병렬로 순식간에 처리한다.

EAB(P) = (Bx−Ax)(Py−Ay) − (By−Ay)(Px−Ax)
P가 삼각형 안  ⇔  EAB(P) ≥ 0  그리고  EBC(P) ≥ 0  그리고  ECA(P) ≥ 0
세 엣지 함수의 합 = 2 × (삼각형 넓이)  →  나누면 그게 바로 무게중심 좌표
엣지 함수는 한 픽셀 옆으로 갈 때 일정한 값만큼만 증가한다(선형). 그래서 실제 GPU는 왼쪽 위에서 한 번만 계산하고, 오른쪽으로 +dY, 아래로 −dX 씩 덧셈만 하며 스캔한다 — 곱셈조차 거의 안 한다.

3. 색·깊이는 어떻게? — 무게중심 좌표

삼각형 안의 점 P는 세 꼭짓점의 가중 평균으로 유일하게 쓸 수 있다: P = αA + βB + γC,  α + β + γ = 1. 이때 α = (P·B·C 넓이) / (A·B·C 전체 넓이) — 즉 P가 A에서 멀어질수록 α가 작아진다. 이 세 무게를 색·깊이·법선·텍스처 좌표에 똑같이 적용하면 꼭짓점 값이 면 전체로 부드럽게 번진다. 데모의 무지개 그라데이션이 바로 이것이다.

α = Area(P,B,C) / Area(A,B,C)    β = Area(A,P,C) / Area    γ = Area(A,B,P) / Area
color(P) = α·color(A) + β·color(B) + γ·color(C)
α·β·γ 중 하나라도 음수 → P는 삼각형 밖 (버림)
정점 셰이더

3D 좌표를 화면 좌표로 변환하고, 꼭짓점마다 색·UV·법선을 계산한다.

래스터화

삼각형의 바운딩 박스 안 픽셀만 훑으며 엣지 함수로 안/밖을 판정, 안이면 (α,β,γ) 를 넘긴다.

픽셀 셰이더

보간된 UV로 텍스처를 읽고 조명을 계산해 최종 색을 낸다. 깊이 버퍼로 앞뒤를 가린다.

원근 보정

3D에서는 α/w 를 보간한 뒤 다시 나눠야(perspective-correct) 텍스처가 안 휜다. 2D 데모는 필요 없다.

4. 두 삼각형이 맞닿을 때 — Top-Left 규칙

사각형은 보통 삼각형 2장으로 만든다. 두 삼각형이 공유하는 변 위에 픽셀 중심이 딱 걸리면 어떻게 될까? 순진하게 ≥ 0 으로 판정하면 양쪽 삼각형이 모두 그 픽셀을 칠해 반투명 합성에서 이중으로 어두워진다. > 0 이면 아무도 안 칠해 검은 틈(crack)이 생긴다. 해법이 Top-Left 규칙: 경계 픽셀은 위쪽 변 또는 왼쪽 변에 속할 때만 그 삼각형이 칠한다. 각 픽셀이 정확히 한 번 칠해진다.

데모의 Top-Left 규칙 버튼을 끄면, 큰 삼각형 두 개를 붙였을 때 공유 변에서 픽셀이 어떻게 흔들리는지 볼 수 있다(무작위 삼각형을 여러 번 눌러 경계에 꼭짓점을 맞춰 보라). D3D·OpenGL·Vulkan 명세에 이 규칙이 못박혀 있다.

5. 삼각형 하나 래스터화하기 (JavaScript)

// 2D 외적 = 변 (b→c) 기준 점 p 의 부호 있는 면적 × 2 function edge(b, c, p) { return (c.x - b.x) * (p.y - b.y) - (c.y - b.y) * (p.x - b.x); } function rasterize(A, B, C, setPixel) { const area = edge(A, B, C); // 전체 넓이 × 2 if (area === 0) return; // 선으로 찌그러진 삼각형 // 바운딩 박스: 이 사각형 밖은 볼 필요도 없다 const minX = Math.floor(Math.min(A.x, B.x, C.x)); const maxX = Math.ceil (Math.max(A.x, B.x, C.x)); const minY = Math.floor(Math.min(A.y, B.y, C.y)); const maxY = Math.ceil (Math.max(A.y, B.y, C.y)); for (let y = minY; y <= maxY; y++) { for (let x = minX; x <= maxX; x++) { const p = { x: x + 0.5, y: y + 0.5 }; // 픽셀 중심 let w0 = edge(B, C, p); let w1 = edge(C, A, p); let w2 = edge(A, B, p); if (w0 < 0 || w1 < 0 || w2 < 0) continue; // 밖 const a = w0 / area, b = w1 / area, c = w2 / area; // (α,β,γ) setPixel(x, y, { r: a * A.r + b * B.r + c * C.r, g: a * A.g + b * B.g + c * C.g, b: a * A.b + b * B.b + c * C.b, }); } } }
바운딩 박스 + 엣지 함수 방식은 GPU 하드웨어가 실제로 쓰는 구조다(타일 단위 병렬화). 옛날 소프트웨어 렌더러는 대신 스캔라인(변을 따라 y마다 x 시작·끝을 구해 한 줄씩 채우기)을 썼다.

6. 놓치기 쉬운 점

감는 방향(winding)

꼭짓점이 시계/반시계 어느 쪽이냐에 따라 area 부호가 뒤집힌다. 엔진은 한 방향을 앞면으로 정하고 뒷면은 아예 버린다(back-face culling).

계단 현상

픽셀 중심 하나로 안/밖을 정하니 경계가 우툴두툴하다. MSAA는 픽셀당 표본을 4개로 늘려 커버리지를 4단계로 부드럽게 만든다.

아주 얇은 삼각형

화면에서 선처럼 보이면 픽셀 중심이 하나도 안 걸려 깜빡이며 사라진다. 멀리 있는 울타리·전선이 반짝이는 이유.

원근 보정 보간

3D에서 화면 공간 무게중심을 그냥 쓰면 벽에 붙인 텍스처가 고무처럼 휜다. 깊이 w 로 나눈 값을 보간해야 한다.