Rendering worlds with a handful of formulas.
게임 속 산맥은 손으로 그린 게 아닙니다 — 노이즈 함수가 빚어냈죠. 구름은 워리·펄린 노이즈, 바다는 사인의 합과 거스너 파, 3D 미로는 레이 캐스팅이 그립니다. 그래픽 밖에서도 — 실력을 숫자로 바꾸는 ELO 레이팅까지. 게임이라는 가짜 세계가 사실은 몇 개의 공식으로 서 있다는 이야기, 직접 만지며 봅니다.
산맥의 높낮이를 만들려면 "부드럽게 울퉁불퉁한 무작위 값"이 필요합니다. 그냥 난수를 뿌리면 지직거리는 노이즈일 뿐이라 지형이 안 되죠. 1983년 켄 펄린이 〈트론〉 작업 중 만든 펄린 노이즈가 답이었습니다(1997년 아카데미 기술상까지 받았죠).
핵심 아이디어: 격자의 각 꼭짓점에 무작위 방향(그래디언트) 화살표를 심고, 임의의 점에서는 네 꼭짓점의 화살표와 "얼마나 방향이 맞는지"를 부드러운 곡선(\(6t^5-15t^4+10t^3\))으로 섞습니다. 값이 격자마다 \(0\)을 지나며 이어져 매끄러운 언덕이 되죠. 여기에 주파수를 2배씩, 진폭을 절반씩 겹쳐 쌓으면(옥타브, fBm) 큰 산에 잔봉우리가 얹힌 진짜 지형이 나옵니다:
\[ \text{fBm}(p) \;=\; \sum_{o=0}^{n-1} \tfrac{1}{2^{o}}\,\text{noise}(2^{o}p). \]구름도 노이즈지만 결이 다릅니다. 뭉게구름은 펄린 fBm에 절댓값을 씌운 "난류(turbulence)"로 몽글몽글하게, 양떼구름·비늘구름은 워리 노이즈로 세포처럼 만듭니다.
워리 노이즈(1996, 스티븐 워리)는 발상이 귀여워요 — 공간에 점(특징점)을 흩뿌리고, 각 위치에서 가장 가까운 점까지의 거리를 값으로 씁니다. 점 근처는 \(0\), 점들 사이는 크고 — 결과는 보로노이 세포 무늬죠. 물결·비늘· 세포·크랙 텍스처가 전부 이 하나에서 나옵니다.
바다의 첫 근사는 사인파입니다. 하지만 사인파 하나는 너무 매끈해요. 그래서 진폭·파장·방향이 다른 사인파를 여러 개 더합니다(푸리에의 정신) — 이러면 제법 바다다워지죠:
\[ y(x,t) \;=\; \sum_i A_i \sin(k_i x - \omega_i t),\qquad \omega_i=\sqrt{g\,k_i}\ (\text{심해 분산관계}). \]그런데 진짜 파도의 마루는 뾰족하고 골은 넓적합니다 — 사인은 위아래 대칭이라 이걸 못 내죠. 해법이 거스너 파(트로코이드 파): 물 입자가 위아래로만 움직이는 게 아니라 원을 그리게 만들어, 마루로 물을 몰아 뾰족한 봉우리를 세웁니다:
\[ x = x_0 - \sum_i Q_i A_i \sin(k_i x_0-\omega_i t),\qquad y = \sum_i A_i \cos(k_i x_0-\omega_i t). \]1992년 〈울펜슈타인 3D〉는 진짜 3D 엔진이 없던 시절, 2D 격자 지도만으로 3D를 그려 냈습니다. 비결이 레이 캐스팅 — 화면의 세로줄 하나하나마다 플레이어 눈에서 광선을 한 줄기씩 쏘아, 벽에 처음 닿는 거리를 재는 것입니다. 가까운 벽은 높게, 먼 벽은 낮게 그리면(벽 높이 \(\propto 1/d\), \(d\)=거리) 원근이 생기죠.
격자에서는 광선이 격자선을 넘는 순간만 검사하면 되므로(DDA 알고리즘) 아주 빠릅니다. 한 가지 함정 — 모든 광선의 거리를 그냥 쓰면 화면 가장자리가 볼록해지는 어안 왜곡이 생겨서, 시선 방향으로 사영한 "수직 거리"(\(d\cos\theta\))를 써야 벽이 평평해집니다.
3D 회전을 \(x,y,z\) 축 회전각(오일러 각)으로 다루면 두 축이 겹쳐 자유도를 잃는 짐벌락에 빠집니다. 게임은 대신 쿼터니언 \(q=w+xi+yj+zk\)(사원수, 4차원 단위수)로 회전을 담아요. 두 회전 \(q_A,q_B\) 사이를 부드럽게 이을 땐 slerp(구면 선형보간) — 4차원 구 위의 최단 호를 등속으로 따라갑니다:
\[ \text{slerp}(q_A,q_B;t)=\frac{\sin((1-t)\Omega)}{\sin\Omega}q_A+\frac{\sin(t\Omega)}{\sin\Omega}q_B,\quad \cos\Omega=q_A\!\cdot q_B. \]카메라 전환, 캐릭터 애니메이션 블렌딩이 전부 이겁니다. (쿼터니언이 왜 회전인지는 벡터를 곱한다는 것의 로터에서 다뤘어요.)
애니메이션 커브, 카메라 경로, 폰트 윤곽, 포토샵 펜툴 — 전부 베지어 곡선입니다. 제어점 몇 개로 곡선을 정의하고, 드 카스텔조 알고리즘으로 그 위의 점을 찾죠: 제어점들을 잇는 선분을 비율 \(t\)로 나눈 점들을 잡고, 그 점들을 다시 \(t\)로 나누고… 반복하면 한 점으로 수렴합니다. 그 점이 곡선 위의 점이에요.
"두 도형이 겹쳤나?"는 물리엔진의 핵심 질문. 볼록 도형이라면 분리축 정리(SAT)가 답합니다 — 두 도형을 어떤 축(방향)에 그림자로 사영했을 때 그림자가 안 겹치는 축이 하나라도 있으면, 두 도형은 안 겹친다. 검사할 축은 각 변의 법선들뿐이고요. 모든 축에서 겹치면 충돌 — 그중 가장 얕게 겹친 축이 서로를 밀어낼 방향(최소 이동 벡터)입니다.
깃발·망토·머리카락·밧줄은 질점을 막대(거리 제약)로 이은 것으로 흉내 냅니다. 움직임은 베를레 적분 — 속도를 따로 저장하지 않고 "이전 위치"만으로 관성을 냅니다(\(x_{\text{new}}=2x-x_{\text{old}}+a\,\Delta t^2\)). 그다음 막대 길이를 강제로 되돌리는 보정을 몇 번 반복하면 — 놀랍도록 안정적인 천이 됩니다. 구현이 짧고 안 터져서 게임의 단골이에요.
색 단계가 부족하면 하늘 그라데이션에 줄무늬(밴딩)가 집니다. 디더링은 픽셀을 규칙적으로 섞어(베이어 행렬) 눈이 중간색으로 착각하게 만들어 줄무늬를 지워요. 게다가 모니터는 밝기를 비선형으로 내보내서(감마 \(\approx2.2\)), 빛·색 계산은 선형 공간에서 하고 마지막에 감마로 되돌려야 정확합니다 — 안 그러면 반투명· 블러·조명이 미묘하게 틀리죠.
레벨을 손으로 안 짜고 알고리즘이 만듭니다(PCG). 미로는 격자를 그래프로 보고 깊이우선 탐색이나 프림 알고리즘으로 벽을 허물어 만들고, 던전은 공간을 반씩 쪼개는 BSP로 방을 배치하며, 물결 함수 붕괴(WFC)는 "이 타일 옆엔 이 타일만"이라는 이웃 규칙을 만족하는 배치를 제약충족으로 풉니다(스도쿠의 사촌이죠). 아래는 DFS로 미로가 자라나는 모습:
게임의 "무작위"는 사실 의사난수입니다(LCG·xorshift). 시드가 같으면 똑같이 재현돼서 — 리플레이·스피드런· 맵 시드가 가능하죠. 카드를 섞을 땐 피셔–예이츠라야 모든 순열이 균등하고(순진하게 "아무 둘이나 계속 바꾸기"는 편향됩니다). 그리고 가챠의 천장(pity) — 확률 \(p\)에 \(N\)번째 보장을 붙이면, 기대 뽑기 수가 \(1/p\)보다 확 줄어 최악의 불운을 막아 줍니다.
수학은 화면만 그리는 게 아닙니다. 대전 게임의 실력 매칭도 공식이 합니다. 체스에서 온 ELO 레이팅은 "레이팅 차이가 승률을 정한다"는 한 줄에서 출발해요 — \(400\)점 앞서면 이길 확률이 약 \(91\%\):
\[ E_A \;=\; \frac{1}{1+10^{\,(R_B-R_A)/400}} \quad(\text{A의 기대 승률}). \]경기 뒤엔 실제 결과와 기대의 차이만큼 점수를 주고받습니다 — \(R_A \leftarrow R_A + K\,(S_A - E_A)\) (\(S_A\): 이기면 \(1\), 지면 \(0\); \(K\): 변동폭). 기대보다 잘하면 오르고 못하면 내리며, 오래 두면 레이팅 차이가 실제 실력 차이로 수렴합니다.
노이즈 함수가 산과 구름을, 삼각함수의 합과 트로코이드가 바다를, 광선 하나가 3D 미로를, 로그 한 줄이 실력의 서열을 그렸습니다. 거기에 쿼터니언이 카메라를 돌리고, 베지어가 길을 긋고, 분리축이 충돌을 잡고, 베를레가 천을 흔들고, 디더링이 색을 속이고, DFS가 미로를 짓고, 천장이 확률을 다독였죠. 게임이라는 가짜 세계는 — 알고 보면 수십 개의 공식이 매 프레임 계산되는 무대였던 겁니다.
여기 쓰인 수학의 뿌리가 더 궁금하다면 — 사인의 합으로 파형을 짓는 이야기는 수학이 울리는 소리, 회전·벡터의 대수는 벡터를 곱한다는 것, 게임을 "풀 수 있나"의 관점은 컴퓨터가 못 푸는 문제와 보드 게임 페이지로 이어집니다.