테마 선택

📚 지식 노트

AI·하드웨어·주변기기·게임·컴퓨터 시스템·이론 컴퓨터 과학·수학·개발과 운영·보안·응용 분야에 대해 알아 두면 좋은 내용을 정리한 노트다. 위에서 분야를 고르고 아래에서 노트를 고른다.

렌더링·빛

빛과 반사, 계단현상, 모션 블러처럼 화면 한 장을 그리는 방식과 그 30년 흐름이다.

목차 (13)
  1. 셰이더: 픽셀마다 따로 도는 작은 함수
  2. 한 프레임의 해부: 패스를 차례로 쌓는다
  3. 빛 30년: 라이트맵에서 패스 트레이싱까지
  4. Nanite와 Lumen: 언리얼 엔진 5가 바꾼 두 가지
  5. 거울: 평면 반사와 화면 공간 반사
  6. 포탈: 옮긴 카메라로 그린다
  7. 창문 속 가짜 방: 인테리어 매핑
  8. 삼각형 없이 그리는 모양: 거리 함수와 스피어 트레이싱
  9. 계단 현상 지우기: 슈퍼샘플링에서 DLSS까지
  10. 모션 블러: 셔터 각과 속도 버퍼
  11. 풀 수백만 가닥: GPU 인스턴싱과 버텍스 셰이더
  12. 폰은 화면을 타일로 쪼개 그린다: 타일 기반 렌더링
  13. 3D로 그리는 도트: 픽셀 스냅과 정사영

셰이더: 픽셀마다 따로 도는 작은 함수

GPU에게 그림 그리기는 같은 함수를 수백만 번 부르는 일이다. 1920 × 1080 화면은 약 207만 픽셀이라 60fps면 1초에 1억 2천만 번 넘게 색을 정해야 한다. 이 함수를 셰이더(shader)라 하고, 호출끼리 서로 묻지 않으니 GPU는 수천 개의 코어에 나눠 동시에 돌린다(GPU 노트).

  • 버텍스 셰이더: 정점마다 한 번 돈다. 모델 좌표에 행렬을 곱해 화면 좌표로 바꾸는 것이 기본 일이고, 아래 풀 절처럼 정점을 휘는 일도 여기서 한다.
  • 래스터화(rasterization): 셰이더가 아니라 고정 하드웨어다. 삼각형이 덮는 픽셀을 찾고, 세 정점이 내보낸 값(UV, 노멀, 색)을 픽셀 자리에 맞게 섞어(보간) 넘긴다.
  • 픽셀 셰이더(pixel shader, OpenGL에서는 fragment shader): 픽셀마다 한 번 돈다. 받은 값과 텍스처로 색 하나를 돌려줄 뿐, 옆 픽셀이 무슨 색을 냈는지는 볼 수 없다.

입력은 두 가지다. 모든 픽셀이 똑같이 받는 상수(유니폼, uniform: 시간, 광원 위치, 행렬)와 픽셀마다 다른 보간 값이다. uv.x를 그대로 빨강 값으로 내면 화면이 왼쪽 검정에서 오른쪽 빨강으로 번지고, 시간 유니폼을 sin에 넣어 밝기로 쓰면 화면 전체가 함께 숨 쉰다. 셰이더는 "이 좌표의 색은?"에만 답하는 함수라 무늬를 붓질 순서가 아니라 좌표의 식으로 생각해야 한다. 원을 그리려면 원을 그리는 대신 "중심까지 거리가 반지름보다 작으면 흰색"이라고 적는다. 이 사고방식을 작은 예제로 하나씩 연습하는 온라인 책 The Book of Shaders가 출발점으로 좋다.

완전히 따로 도는 것은 아니다. GPU는 픽셀을 2 × 2 묶음(쿼드)으로 함께 돌려 옆 픽셀과의 값 차이(미분)를 얻고, 그것으로 텍스처를 얼마나 줄여 읽을지(밉맵 단계)를 고른다. 그래서 삼각형이 한 픽셀만 덮어도 묶음의 넷이 다 돌고, 픽셀보다 작은 삼각형이 많으면 계산이 헛돈다. 또 수십 개 스레드가 한 명령을 같은 박자로 실행하므로 (워프·웨이브프론트), 이웃 픽셀끼리 if의 갈래가 다르면 두 갈래를 모두 돌고 결과만 골라 쓴다.

셰이더는 게임을 만들 때가 아니라 대개 플레이어의 PC에서 마지막 컴파일을 거친다. Direct3D 12·Vulkan은 셰이더와 블렌드·깊이 설정 같은 그리기 상태를 묶은 파이프라인 상태 객체(PSO)를 통째로 만들게 하고, 드라이버가 그 GPU에 맞는 기계어로 바꾼다. 재질, 정점 형식, 패스마다 조합이 따로 생겨 수가 많고, 그리는 순간에 처음 만들면 그 프레임이 멈칫한다. 언리얼 엔진은 두 가지로 막는다. 테스트 플레이로 실제 쓰인 PSO 목록을 모아 게임과 함께 배포하는 번들 캐시, 그리고 물체를 불러오는 즉시 쓸 수 있는 PSO를 추려 뒤에서 미리 컴파일하는 PSO 프리캐싱이다. 플레이어 쪽에서 보이는 끊김과 재는 법은 게임 성능 노트에 있다.

한 프레임의 해부: 패스를 차례로 쌓는다

패스는 프레임을 만드는 작업 묶음이고, 그 안에서 정점 처리·래스터화 같은 GPU 단계가 여러 번 실행된다. 브라우저·편집기와의 비교, 렌더 그래프, CPU·GPU 병렬 실행은 컴퓨터 그래픽스 노트의 렌더링 파이프라인으로 이어진다.

게임 화면 한 장은 한 번에 그려지지 않는다. 그래픽 디버거(RenderDoc 등)로 한 프레임을 잡아 보면 수백~수천 번의 그리기 호출이 몇 개의 패스(pass)로 묶여 차례로 돌고, 앞 패스가 남긴 텍스처를 뒤 패스가 읽는다. 디퍼드 렌더링을 쓰는 게임의 대표적인 순서는 이렇다.

한 프레임의 패스 순서: 깊이 프리패스, 그림자 맵, G-버퍼, 조명, 반투명 포워드, 후처리, UI가 차례로 그려지고, 앞쪽 불투명 패스는 깊이 버퍼를, 조명부터 후처리까지는 HDR 색 버퍼를, UI는 톤 매핑을 마친 화면 색을 쓴다한 프레임 = 패스 여러 개를 차례로깊이프리패스그림자광원 깊이G-버퍼재질 적기조명빛 더하기반투명포워드후처리TAA·블룸UI글자·HUD불투명: 깊이를 쓰고 비교HDR 색 버퍼(1보다 밝은 값)화면 색끝에서 톤 매핑G-버퍼까지는 "어디에 무엇이 있나"만 적고, 빛 계산은 조명 패스에서 한 번에 한다.반투명은 G-버퍼에 한 겹만 담기지 않아 따로 그린다 · 순서와 구성은 게임마다 다르다
디퍼드 렌더링 한 프레임의 대표적인 패스 순서. 불투명 물체는 깊이와 재질만 먼저 적고, 빛은 조명 패스에서 한 번에 더하며, 화면 범위로 누르는 톤 매핑과 UI는 맨 끝이다.
  1. 깊이 프리패스(depth pre-pass): 불투명 물체를 색 없이 깊이만 먼저 적는다. 뒤 패스는 깊이가 같은 픽셀만 칠하므로 나중에 가려질 픽셀에 비싼 계산을 하지 않는다(겹쳐 칠하기, overdraw를 줄인다).
  2. 섀도 맵: 광원마다 광원 자리에서 깊이를 찍는다(아래 빛 30년 절). 해는 거리별 캐스케이드 몇 장, 점광원은 여섯 방향 큐브로 찍는다.
  3. G-버퍼: 불투명 물체의 재질을 화면 크기 텍스처 몇 장에 나눠 적는다. 기본색, 노멀, 거칠기·금속성 같은 값이다. 빛은 아직 계산하지 않는다.
  4. 조명: 픽셀마다 G-버퍼를 읽어 광원별 빛을 더한다. 결과는 1보다 밝은 값도 담는 HDR(high dynamic range) 버퍼에 쌓는다. 한낮의 햇빛과 실내 전등 아래는 밝기가 수백 배 넘게 차이 난다.
  5. 반투명: 유리·연기·파티클은 한 픽셀에 여러 겹이라 G-버퍼 한 칸에 담기지 않는다. 먼 것부터 정렬해 포워드 방식으로 위에 섞는다.
  6. 후처리: TAA나 업스케일, 피사계 심도, 모션 블러, 밝은 곳이 번지는 블룸(bloom)을 거쳐 마지막에 톤 매핑(tone mapping)으로 HDR 값을 모니터가 낼 수 있는 범위로 누른다.
  7. UI: 글자와 HUD는 블러나 톤 매핑을 받지 않도록 맨 끝에 그린다.

순서에는 이유가 있다. 블룸은 HDR 값이 살아 있을 때 해야 진짜로 눈부신 곳만 번진다. 톤 매핑을 먼저 하면 햇빛과 흰 벽이 같은 1로 잘려 둘 다 번지거나 둘 다 안 번진다. UI를 후처리 앞에 그리면 글자가 모션 블러로 흐르고 TAA로 떨린다.

모든 게임이 이 틀은 아니다. 2016년 둠은 포워드 렌더링을 바탕에 두고, 화면을 깊이까지 작은 칸(클러스터)으로 나눠 칸마다 닿는 광원 목록을 만든 뒤 픽셀이 제 칸의 목록만 돌게 했다. 재질을 G-버퍼 몇 장에 맞춰 넣지 않아도 되고 반투명도 같은 조명으로 비출 수 있다. 실제 게임의 한 프레임을 패스별 중간 결과로 뜯어 본 분석 글로는 GTA V, DOOM (2016), 메탈기어 솔리드 V 편이 좋고, 디퍼드 셰이딩을 유니티에서 직접 짜 보려면 Catlike Coding의 렌더링 튜토리얼 13편이 있다.

빛 30년: 라이트맵에서 패스 트레이싱까지

현실의 빛은 광원에서 나와 벽과 바닥에 여러 번 튕긴 뒤 눈에 들어온다. 이것을 화면의 픽셀마다 다 따라가면 한 프레임(16.7ms)에 끝나지 않는다. 그래서 게임 조명의 역사는 "무엇을 포기하고, 그 계산을 언제 하느냐"를 고르는 역사였다.

게임 조명 30년의 흐름: 구역마다 밝기 숫자 하나를 두던 1993년에서 라이트맵, 섀도 볼륨, SSAO, 격자 전파, 레이 트레이싱, 패스 트레이싱으로 이어지며, 미리 굽는 방식은 사라지지 않고 게임 중 계산이 그 위에 쌓였다1993구역 밝기둠1996라이트맵퀘이크2004섀도 볼륨둠 32007SSAO크라이시스2009격자 전파LPV2018레이 트레이싱DXR2023패스 트레이싱사이버펑크 2077만들 때 굽기(라이트맵·프로브)게임 중 직접광·그림자게임 중 화면·격자 간접광게임 중 레이막대는 쓰이기 시작한 때부터 지금까지 · 옛 방식도 새 방식과 섞여 쓰인다
게임 조명의 대표 기법과, 그 계산을 언제 하느냐의 변화. 미리 굽는 방식은 사라지지 않았고 게임 중 계산이 그 위에 한 겹씩 쌓였다.
  • 구역 밝기: 1993년 둠은 맵의 구역(섹터)마다 밝기 숫자 하나만 갖고, 멀수록 어둡게 칠했다. 광원의 위치를 계산하지 않으니 빛 자국도 그림자도 없다.
  • 라이트맵(lightmap): 1996년 퀘이크는 벽을 굵은 격자로 나누고, 맵을 만들 때 칸마다 광원까지 직선을 그어 가렸는지 검사해 밝기를 텍스처에 저장했다. 이렇게 미리 계산해 두는 일을 베이킹(baking)이라 한다. 다음 세대에는 빛을 받은 면이 다시 빛을 내는 간접광까지 라디오시티(radiosity)로 구웠다.
  • 프로브(light probe): 구운 빛은 벽과 바닥에만 붙어 있어 움직이는 캐릭터가 받을 값이 없다. 공간에 점을 띄엄띄엄 찍고 점마다 여러 방향에서 오는 간접광을 저장해, 캐릭터가 가까운 점의 값을 읽게 했다. 하프라이프 2(2004)의 앰비언트 큐브가 대표적이다.

2000년대에는 직접광과 그림자를 게임 중에 계산하기 시작했다. 둠 3(2004)의 섀도 볼륨(shadow volume)은 물체의 윤곽을 빛 반대쪽으로 늘린 입체 안을 그늘로 쳐서 가장자리가 칼같이 정확했지만 비쌌다. 그 뒤 표준이 된 섀도 맵(shadow map, 1978년 논문)은 광원 자리에서 깊이만 한 장 찍어 두고, 픽셀마다 광원까지의 거리가 그 깊이보다 멀면 그늘로 본다. 한 장으로 넓은 곳을 덮으면 그림자가 계단이 되므로 가까운 곳과 먼 곳을 나눠 찍는 캐스케이드(cascaded shadow map)가 흔하다. 조명 수가 많아지자 디퍼드 렌더링(deferred rendering)이 퍼졌다. 픽셀마다 위치·노멀·색을 먼저 버퍼에 적어 두고, 광원은 자기가 닿는 픽셀만 칠하는 방식이다.

간접광은 오랫동안 굽거나 어림했다. SSAO(screen space ambient occlusion)는 이미 그린 화면의 깊이만 보고 구석과 틈을 어둡게 하고, LPV(light propagation volumes)는 빛을 3차원 격자에 넣어 이웃 칸으로 퍼뜨린다. 둘 다 싸지만 화면 밖은 모르고, 격자 칸이 벽보다 굵으면 빛이 얇은 벽 너머로 샌다.

레이 트레이싱(ray tracing)은 픽셀에서 광선을 쏴 처음 맞는 곳을 찾고, 거기서 다시 광원과 반사 방향으로 쏜다. 비싼 일은 "처음 맞는 곳 찾기"라서, 물체를 상자로 묶고 그 상자를 또 묶은 계층(BVH, bounding volume hierarchy)을 만들어 광선이 스치지 않는 상자는 통째로 건너뛴다. 2018년 DirectX Raytracing과 이 탐색을 맡는 하드웨어(GPU 노트의 RT 코어)가 나오면서 게임은 화면은 하던 대로 그리고 그림자·반사·간접광에만 광선을 쓰는 하이브리드로 옮겨 갔다. 그래도 비싸서 광선은 절반이나 4분의 1 해상도로 쏘고 나머지를 채운다.

패스 트레이싱(path tracing)은 효과마다 따로 구하던 것을 한 방법으로 묶는다. 광선이 맞은 곳에서 무작위 방향으로 계속 튕겨 광원에 닿은 길의 밝기를 평균한다. 무작위라 노이즈가 끼고, 오차는 샘플 수 N에 대해 1/√N로 줄어 절반으로 줄이려면 샘플이 네 배 든다. 게임은 픽셀당 한두 개만 쏘고 앞 프레임과 이웃 픽셀의 값을 모으는 디노이저(denoiser)로 메운다. 이웃이 찾은 좋은 샘플을 빌리는 ReSTIR(2020) 같은 기법이 이를 받쳐 주고, 2023년 사이버펑크 2077이 화면 전체를 패스 트레이싱으로 그리는 모드를 냈다(기술 분석 영상). 대가는 시간 누적에서 오는 반응 지연과 잔상(고스팅)이다.

방식계산하는 때잘하는 것한계
라이트맵·프로브만들 때게임 중에는 읽기만 해서 싸다. 간접광이 곱다빛을 바꿀 수 없다. 굽는 시간과 메모리가 든다
섀도 맵·디퍼드게임 중움직이는 빛과 그림자, 많은 광원간접광이 없다. 그림자 해상도 한계
SSAO·SSR·LPV게임 중싸게 간접광 느낌을 낸다화면 밖을 모른다. 격자가 굵으면 빛이 샌다
하이브리드 레이 트레이싱게임 중화면 밖까지 비치는 반사, 정확한 그림자비싸서 낮은 해상도로 쏘고 채운다
패스 트레이싱게임 중모든 빛을 한 방법으로노이즈, 디노이저의 잔상과 지연

흐름을 한 줄로 줄이면, 광선을 쏘는 계산 자체는 1990년대에도 맵을 구울 때 이미 했다. 바뀐 것은 그 계산을 하는 때다. 그렇다고 베이킹이 끝난 것도 아니어서, 프레임을 고르게 지켜야 하는 게임은 지금도 미리 계산한 조명을 많이 쓰고 둘을 함께 넣는 게임도 있다.

Nanite와 Lumen: 언리얼 엔진 5가 바꾼 두 가지

언리얼 엔진 5를 쓴 게임 이야기에 거의 빠지지 않는 두 이름이다. 둘 다 위에서 본 오래된 숙제, 곧 LOD를 사람이 만들고 조명을 미리 굽는 일을 게임 중 계산으로 바꾼다.

  • Nanite(가상화 형상): 메시를 불러올 때 삼각형 묶음(클러스터)의 계층으로 쪼개 두고, 화면에 보이는 크기에 맞는 묶음만 골라 그린다. LOD를 따로 만들지 않아도 되고, 필요한 묶음만 그때그때 읽어 메모리에 올리므로 빠른 SSD를 권한다. 반투명 재질은 지원하지 않는다(Nanite 문서).
  • Lumen(동적 전역 조명과 반사): 간접광과 반사를 게임 중에 계산한다. 기본은 물체마다 만든 거리장(SDF)에 광선을 쏘는 소프트웨어 레이 트레이싱이라 RT 코어 없이도 돌고, 지원하는 GPU에서는 하드웨어 레이 트레이싱으로 바꿀 수 있다. 맞은 곳의 빛은 표면 캐시(surface cache)에 모아 다시 쓴다. Lumen을 켜면 구운 라이트맵은 꺼진다(Lumen 문서).

미리 굽던 계산을 게임 중으로 옮긴 만큼 GPU 비용이 든다. Lumen 문서도 최종 수집 품질, 광선 거리 같은 설정을 GPU 비용과 화질을 맞바꾸는 손잡이로 설명한다. 컬링과 LOD의 기본은 컴퓨터 그래픽스 노트에 있다.

거울: 평면 반사와 화면 공간 반사

게임 거울에 얼굴이 반쪽만 비치거나 거울 방에서 프레임이 떨어지는 데에는 이유가 있다. 거울에 비쳐야 할 것은 대개 카메라 뒤에 있고, 렌더러는 화면에 그린 픽셀만 갖고 있다. 화면에 없는 것을 거울에 넣으려면 장면을 한 번 더 그려야 한다.

평면 반사(planar reflection)는 카메라를 거울면에 대해 대칭인 자리로 옮겨 장면을 렌더 타깃(그림을 그려 넣을 수 있는 텍스처)에 찍고, 그 그림을 거울면에 화면 좌표로 붙인다. 평면이 법선 n과 거리 d로 n·x = d일 때 점 p는 p − 2(n·p − d)n으로 옮겨진다. 이 변환은 좌우를 뒤집으므로 삼각형의 감기 방향이 반대가 되어, 뒷면 컬링을 뒤집거나 그림을 뒤집어 읽어야 한다. 접은 카메라는 거울 뒤에 서 있어서 거울 뒤의 물체가 찍혀 거울 속에 떠 보이는데, 거울면을 클립 평면으로 삼아 그 뒤를 잘라 낸다.

위에서 본 거울과 포탈: 거울은 카메라를 거울면에 대칭인 자리로 접어 방을 한 번 더 찍고, 포탈은 문 A에 대한 카메라의 상대 위치와 방향을 그대로 문 B로 옮겨 가상 카메라를 세운다거울: 면에 대해 접는다거울면물체카메라접은 카메라대칭 자리에서 찍은 그림을 뒤집어 붙인다포탈: 다른 문으로 옮긴다문 A문 B카메라가상 카메라문 A 기준 위치·방향을 문 B 기준으로
위에서 본 가상 카메라. 거울은 카메라를 거울면에 대해 접은 자리에서 방을 한 번 더 찍고, 포탈은 문 A에 대한 카메라의 상대 위치를 그대로 문 B에 옮긴다.

장면을 두 번 그리니 비용도 그만큼 든다. 그래서 반사 쪽은 해상도를 낮추고, 두 프레임에 한 번만 찍거나, 멀리 있는 물체는 낮은 LOD로 그리고, 거울이 화면에 안 보이면 아예 찍지 않는다. 옛 게임은 이 비용을 꼼수로 피했다. 슈퍼 마리오 64의 거울 방은 거울 뒤에 방을 대칭으로 지어 두고 마리오 사본만 하나 더 그렸고, 듀크 뉴켐 3D의 Build 엔진은 시점을 거울 벽에 대해 접어 그린 뒤 그 화면 영역을 좌우로 뒤집었다.

화면 공간 반사(SSR, screen space reflection)는 다시 그리지 않는다. 반사면의 픽셀마다 반사 방향으로 화면 위를 걸어 가며 깊이 버퍼와 부딪히는 곳을 찾아 그 색을 가져온다. 싸지만 화면에 없는 것은 비출 수 없다. 거울을 정면으로 보면 반사 방향이 카메라 뒤쪽이라 거의 모든 픽셀이 실패하고, 그 자리는 미리 찍어 둔 반사 프로브(큐브맵)로 메운다. 프로브에는 캐릭터가 없으니 화면에 보이던 몸 일부만 조각처럼 비친다. 1인칭 게임은 내 몸이 화면에 거의 없어서 거울에 비출 전신 모델을 따로 세워야 한다.

방식재료비용약점
평면 반사접은 카메라로 장면을 한 번 더장면 하나만큼평평한 면에만, 거울마다 한 번씩
화면 공간 반사이미 그린 화면의 색·깊이작다화면 밖·가려진 면은 못 비춘다
반사 프로브(큐브맵)미리 찍어 둔 주변 사진매우 작다자리가 고정이라 어긋나고 움직이는 것은 없다
레이 트레이싱 반사장면 전체에 광선크다하드웨어와 디노이저가 필요하다

요즘 엔진은 싼 화면 공간 반사를 먼저 쓰고, 실패한 곳만 프로브나 광선으로 메우는 식으로 섞는다.

포탈: 옮긴 카메라로 그린다

포탈 게임의 문 너머로 다른 방이 보이고 걸어 들어가면 거기서 나오지만, 레벨의 두 곳이 정말로 이어져 있지는 않다. 보이는 것은 그림이고 지나가는 것은 순간이동이다. 두 흉내가 같은 변환 하나를 쓸 때만 이어진 것처럼 느껴진다.

그림 쪽은 거울과 재료가 같다. 다만 대칭으로 접지 않고 옮긴다. 카메라 변환을 문 A 기준 좌표로 바꾸고(문 A 변환의 역행렬을 곱하고), 그 값을 문 B 기준으로 되돌리면 가상 카메라가 나온다. 들어간 면과 나오는 면이 마주 보게 반 바퀴 돌리는 회전을 사이에 끼우는 것이 보통이다. 회전과 이동뿐이라 거울과 달리 좌우가 뒤집히지 않는다. 가상 카메라와 문 B 사이에 낀 벽이 찍히지 않도록, 투영 행렬을 고쳐 근평면을 문 면에 비스듬히 맞추는 방법(oblique near plane, 2005년 논문)을 쓴다. 그린 결과는 텍스처로 문 면에 붙이거나, 스텐실 버퍼로 문 모양 안에만 다시 그린다.

몸이 지나가는 순간에는 세 가지가 함께 일어나야 끊기지 않는다.

  • 위치와 방향뿐 아니라 속도도 같은 변환으로 돌린다. 안 돌리면 출구에서 엉뚱한 방향으로 튀어 나간다.
  • 몸이 문 면에 걸친 동안 반대편에 복제본을 세워, 어느 쪽에서 봐도 몸이 잘려 사라지지 않게 한다.
  • 문 자리의 벽 충돌을 열어 둔다. 안 열면 그림은 있는데 눈앞에서 막힌다.

문 속에 또 문이 보이면 재귀가 된다. 단계마다 장면을 한 번 더 그리니 비용이 곱으로 쌓여, 보통 몇 단계에서 끊고 가장 안쪽 한 겹은 지난 프레임의 그림이나 단색으로 때운다. 화면에 안 보이는 문은 아예 찍지 않는다.

창문 속 가짜 방: 인테리어 매핑

도시 게임의 빌딩 창 수천 개마다 방을 모델로 지으면 삼각형이 감당할 수 없게 는다. 유리에 방 사진을 붙이면 정면에서는 그럴듯해도 비스듬히 볼 때 납작한 그림이 들통난다. 인테리어 매핑(interior mapping, 2008년 논문)은 방을 짓지 않고 셰이더가 유리의 픽셀마다 방을 계산해 그린다.

인테리어 매핑의 단면: 건물 앞면은 평평한 판이고 창 뒤의 방은 셰이더 속 숫자 상자다. 유리 픽셀마다 카메라 광선을 상자 안으로 늘여 천장, 안쪽 벽, 바닥 가운데 먼저 닿는 면의 그림을 읽는다위층 칸(같은 규칙으로 되풀이)건물 앞면(평평한 판)창카메라천장안쪽 벽바닥창 뒤에 진짜 방은 없다 — 광선이 처음 만난 면 자리의 그림(큐브맵)을 읽을 뿐이다
옆에서 자른 건물 앞면. 유리 픽셀마다 카메라 광선을 셰이더 속 상자 안으로 늘여, 천장·안쪽 벽·바닥 가운데 먼저 닿는 면을 고른다.
  1. 건물 앞면을 층 높이와 방 폭의 칸으로 나누고, 칸마다 뒤에 일정한 깊이의 상자가 있다고 친다. 이 상자는 모델이 아니라 셰이더 속 숫자다.
  2. 유리 픽셀마다 카메라에서 그 픽셀로 가는 광선을 상자 안까지 늘인다. 축마다 "광선이 그 축의 벽 평면에 닿는 거리" t = (벽 위치 − 시작점) ÷ 방향을 구하고, 가장 작은 t의 면이 보이는 면이다.
  3. 그 면의 그림을 읽어 칠한다. 방 하나를 큐브맵(방 가운데서 여섯 방향을 찍은 그림)으로 두면, 방 가운데에서 닿은 점을 바라보는 방향으로 읽으면 된다.

비용은 방의 수가 아니라 셰이더가 칠하는 픽셀 수에 달려 있다. 방이 몇 개 안 되면 같은 방이 되풀이되는 티가 나므로, 칸의 가로 번호·층·건물 면으로 정수 해시를 만들어 방을 고르고 좌우 뒤집기, 불 켜짐, 커튼까지 정한다. 해시에 넣는 값을 부동소수점 좌표로 두면 같은 창의 픽셀끼리도 아주 조금 달라 픽셀마다 다른 방이 뽑혀 반짝인다. 칸 번호를 정수로 끊은 뒤 해시해야 한다.

한계도 분명하다. 그림이 모두 벽·바닥·천장에 붙어 있다고 보니 방 가운데 놓인 가구는 바닥에 그린 그림처럼 납작해져 보는 각도에 따라 미끄러진다. 그래서 큰 가구를 벽에 붙이고, 깊이 정보를 더하거나 여러 겹으로 나눠 입체감을 키운다. 건물 면마다 칸을 따로 세면 모서리 방이 앞 창과 옆 창에서 다른 방으로 보이고, 멀리 있는 창은 평면 그림으로 바꿔도 티가 나지 않는다. 포르자 호라이즌 4는 창마다 고른 실내 그림을 시차(parallax)로 밀었고, 스파이더맨 시리즈의 뉴욕 창문도 이 계열이며, 언리얼 엔진에는 InteriorCubemap 머티리얼 함수가 있다.

삼각형 없이 그리는 모양: 거리 함수와 스피어 트레이싱

모양을 삼각형 목록 대신 "이 점에서 표면까지 얼마나 머냐"를 돌려주는 함수로 나타낼 수도 있다. 밖이면 양수, 안이면 음수, 표면에서 0을 내는 이 함수를 부호 있는 거리 함수(SDF, signed distance function)라 한다. 공은 "점과 중심 사이 거리 − 반지름" 한 줄이고, 상자·원기둥·고리도 몇 줄씩이다. 기본 도형의 식은 모아 둔 거리 함수 목록이 널리 쓰인다.

  • 합치기: 두 물체를 합치면 두 거리의 최솟값, 겹친 부분만 남기면 최댓값, A에서 B를 파내면 max(A, −B)다. 모델링이 수식 몇 개로 끝난다.
  • 녹여 붙이기: min 대신 경계 근처에서 두 값을 부드럽게 섞는 smooth min을 쓰면 두 공이 찰흙처럼 이어진다. 삼각형으로는 손이 많이 가는 모양이다.
  • 반복: 좌표를 칸 크기로 나눈 나머지로 접으면 공 하나의 식으로 끝없는 공 격자가 나온다. 공이 늘어도 계산은 그대로다.

그리는 법은 광선을 따라 걷는 레이 마칭이지만 보폭이 특별하다. 광선 위의 점에서 SDF 값이 d라면 "반지름 d 안에는 아무 면도 없다"는 보증이니, 그만큼 한 번에 건너뛴다. 이것이 스피어 트레이싱(sphere tracing)이다. 면에서 멀면 크게, 가까우면 작게 걷다가 d가 아주 작은 문턱보다 작아지면 닿은 것으로, 걸음 수나 거리 상한을 넘으면 빗나간 것으로 본다. 고정 보폭으로 걷는 것보다 걸음이 훨씬 적고, 보폭이 거리보다 크지 않으니 얇은 면을 건너뛰지도 않는다.

스피어 트레이싱: 눈에서 나간 광선이 점마다 가장 가까운 면까지의 거리를 반지름으로 하는 원 안은 비어 있다는 것을 알고 그만큼 건너뛴다. 상자와 조약돌 옆을 지날 때는 걸음이 짧아지고, 공에 가까워지면 걸음이 계속 줄다가 닿는다상자조약돌공눈닿음점선 원 = 그 점에서 가장 가까운 면까지의 거리원 안에는 아무것도 없으니 원 반지름만큼 한 번에 건너뛴다. 걸음 수: 10면 옆을 스칠 때는 걸음이 짧아져 횟수가 늘어난다
스피어 트레이싱. 광선 위 점마다 가장 가까운 면까지의 거리(점선 원)만큼 건너뛴다. 면 옆을 스치면 걸음이 짧아지고, 면에 닿으면 거리가 0에 가까워진다.

닿은 뒤에 필요한 것도 같은 함수에서 나온다. 노멀은 닿은 점을 x·y·z 방향으로 조금씩 옮겨 SDF 값이 얼마나 바뀌는지(기울기)로 구한다. 그림자는 닿은 점에서 광원 쪽으로 한 번 더 걷는데, 걷는 동안 "가장 가까이 스친 거리 ÷ 그때까지 걸은 거리"의 최솟값을 기록하면 면을 아슬아슬하게 비껴간 광선일수록 어두워져 부드러운 반그림자가 거의 공짜로 나온다. 노멀 방향으로 몇 걸음 나가며 거리를 재면 구석이 어두워지는 AO도 얻는다. 이 과정을 단계별로 정리한 SDF 레이 마칭 글이 있다.

화면 전체를 SDF로 그리는 게임은 드물지만 쓰임은 넓다. 언리얼 엔진은 메시마다 거리장(mesh distance field)을 작은 3D 텍스처로 구워 두고 먼 그림자와 AO를 스피어 트레이싱으로 구한다. 글자를 거리 값 텍스처로 저장하면 크게 확대해도 테두리가 뭉개지지 않고, 외곽선과 그림자를 셰이더에서 문턱 하나로 얹는다. 데모신과 셰이더 놀이터에서 장면 전체를 픽셀 셰이더 한 개로 그리는 작품도 대개 이 방식이다.

계단 현상 지우기: 슈퍼샘플링에서 DLSS까지

픽셀 하나는 장면의 점 하나를 잰 값이라 비스듬한 테두리가 계단이 되고, 움직이면 그 계단이 기어다닌다. 덮인 비율만큼 옅게 칠하는 기본 원리는 그래픽 노트에 있다. 여기서는 게임이 그 계산을 어디로 옮겨 왔는지를 본다. 1977년 논문이 이미 고치는 길을 셋으로 정리했다. 더 촘촘히 재기, 다 그린 그림을 흐리기, 점 하나가 픽셀의 넓이를 대표하게 하기다.

방식하는 일대가
슈퍼샘플링(SSAA)화면을 가로세로 몇 배로 그려 줄인다그린 만큼 비싸다(4배면 픽셀 계산도 4배)
멀티샘플링(MSAA)픽셀 안 여러 점은 삼각형에 덮였는지만 재고, 색은 한 번 계산해 덮인 몫만큼 섞는다삼각형 테두리만 고친다. 점마다 버퍼가 커진다
후처리(MLAA·FXAA·SMAA)다 그린 그림에서 계단 모양을 찾아 둘레 색을 섞는다싸지만 텍스처 결까지 뭉개고 움직일 때 반짝임이 남는다
TAA프레임마다 재는 위치를 조금씩 흔들고 지난 결과와 섞는다움직일 때 흐림과 잔상
업스케일러(DLSS·FSR·TSR·XeSS)낮은 해상도로 그리고 TAA 재료로 높은 해상도를 메운다메우는 판정에 따라 결과가 달라진다

멀티샘플링이 막힌 곳은 디퍼드 렌더링이었다. 디퍼드는 색·노멀·깊이를 화면 크기 버퍼(G-버퍼)에 먼저 적는데, 이 값들은 섞을 수 없다. 깊이 0.3과 0.7을 섞은 0.5는 어느 물체에도 없는 깊이다. 점마다 G-버퍼를 따로 두면 해상도가 오를수록 메모리가 감당이 안 돼서, 게임은 다 그린 그림을 문지르는 후처리로 옮겨 갔다.

TAA(temporal anti-aliasing)는 시간으로 슈퍼샘플링을 나눠 한다. 카메라 투영에 픽셀보다 작은 오프셋(지터)을 더해 매 프레임 픽셀 안의 다른 점을 재고, 픽셀마다 지난 프레임에 어디 있었는지 적은 모션 벡터로 지난 결과를 찾아(재투영) 조금씩 섞는다. 새로 드러난 곳은 지난 색이 틀리니, 지난 색이 지금 이웃 픽셀들의 색 범위를 벗어나면 그 범위로 잘라 잔상을 줄인다. 그래도 빠르게 돌면 거듭 보간하느라 흐려진다.

TAA에 지난 프레임이 쌓이니 지금 프레임은 덜 그려도 된다. 콘솔의 체커보드 렌더링이 절반만 그리고 메웠고, DLSS 2·FSR 2·TSR·XeSS 같은 업스케일러는 TAA와 같은 재료(지터, 모션 벡터, 지난 프레임)로 낮은 해상도 화면을 출력 해상도까지 쌓는다. 지난 색을 쓸지 버릴지 정하는 규칙을 사람이 짜면 FSR 2나 TSR, 신경망이 맡으면 DLSS나 FSR 4다. 성능 모드는 가로세로 절반, 곧 픽셀 4분의 1만 그린다. fps와 지연에 주는 영향은 게임 성능 노트에 있다.

모션 블러: 셔터 각과 속도 버퍼

영화는 1초에 24장인데도 부드럽게 보인다. 셔터가 열려 있는 동안 움직임이 한 장에 번져 찍히기 때문이다. 게임이 그린 한 장은 시간이 멈춘 순간이라 가장자리가 선명하고, fps가 낮으면 장 사이의 뜀이 그대로 보인다. 모션 블러는 이 뜀을 번짐으로 잇는 효과다.

  • 셔터 각(shutter angle): 한 프레임 시간 가운데 셔터가 열린 비율이다. 노출 시간 = 프레임 시간 × 셔터 각 ÷ 360°이고, 24fps에 영화의 관행인 180°면 1/48초다. 번짐 길이는 화면 속 속도 × 노출 시간이라, 게임 옵션의 블러 양이 사실상 이 각이다.
  • 속도 버퍼(velocity buffer): 게임은 이미 선명하게 그린 한 장을 나중에 뭉갠다. 픽셀마다 지난 프레임과 이번 프레임의 화면 위치 차이를 적어 두고, 후처리에서 그 방향으로 몇 번 샘플해 평균한다. 널리 쓰이는 필터(2012년 논문)는 화면을 타일로 나눠 타일별 최대 속도로 방향을 잡아, 움직이는 물체 바깥으로 번지는 것까지 다룬다.

게이머가 블러를 먼저 끄는 데에는 이유가 둘 있다. 첫째, 속도에 카메라 회전까지 들어가면 세상에 가만히 서 있는 적도 내가 시점을 돌리는 순간 함께 뭉개진다. 영화와 달리 게임에서는 눈이 화면 속 표적을 따라가기 때문이다. 카메라가 만든 움직임을 빼고 물체 자신의 움직임만 번지게 하면 표적은 선명하게 남는다. 둘째, LCD·OLED는 한 장을 다음 장까지 붙들고 있는 샘플앤홀드(sample-and-hold) 방식이라, 눈이 움직이는 것을 따라가면 이미 한 프레임만큼 잔상이 생긴다. 고주사율에서는 그 위에 블러가 한 번 더 얹힐 뿐이라 줄이거나 끄는 편이 낫다(모니터 노트). 프레임 간격이 들쭉날쭉한 끊김은 블러로 가려지지 않는다 (게임 성능 노트).

풀 수백만 가닥: GPU 인스턴싱과 버텍스 셰이더

들판의 풀이 바람에 한 물결로 넘어가지만 가닥끼리는 서로를 모른다. 게임은 공기를 계산하지 않고 "자리와 시간을 넣으면 바람 세기가 나오는 값"을 만들어 가닥마다 읽힌다. 노이즈(부드럽게 이어지는 무작위 무늬) 한 장을 바람 방향으로 흘려보내면, 바람 아래쪽 가닥은 위쪽 가닥이 조금 전에 읽은 값을 지금 읽으니 물결이 진다. 모든 가닥에 같은 사인파를 주면 들판이 통째로 들썩이고, 가닥마다 박자를 무작위로 주면 자글자글 떨기만 한다.

가닥 하나는 마디 몇 개짜리 띠이고, 높이 비율 t인 점은 t에 비례해(또는 t²에 비례해) 더 휘게 해 뿌리는 곧고 끝은 눕는다. 키·폭·방향은 자리로 만든 해시로 가닥마다 정한다. 이 계산은 가닥끼리 물어볼 것이 없으니 GPU에 맞는다.

  • 버텍스 셰이더(vertex shader): 휨을 CPU에서 계산해 매 프레임 정점을 올리면 감당이 안 된다. GPU가 정점마다 돌리는 프로그램에서 바람 텍스처를 읽어 직접 휜다.
  • 인스턴싱(instancing): 가닥 모양 하나를 가닥 수만큼 한 번의 그리기 호출로 그린다. 물체를 따로따로 그리면 호출마다 드는 CPU 비용이 쌓여 CPU 병목이 된다(게임 성능 노트의 병목 구분).
  • GPU가 심고 거르기: 더 나아가면 컴퓨트 셰이더가 가닥을 심으면서 거리·시야·가림으로 거르고, 남은 목록과 개수를 GPU가 직접 읽어 그린다(간접 그리기, indirect draw). 먼 풀은 마디를 줄인다(LOD).

고스트 오브 쓰시마가 GDC 2021 발표 Procedural Grass in 'Ghost of Tsushima'에서 이런 구조의 풀과 바람을 소개했다. 바람 값을 낙엽·파티클이 읽는 쪽 이야기는 자연 효과 노트에 있다.

폰은 화면을 타일로 쪼개 그린다: 타일 기반 렌더링

데스크톱 그래픽 카드는 보통 즉시 모드(immediate mode)로 그린다. 그리기 명령이 오는 순서대로 삼각형을 하나씩 끝까지 칠하고, 픽셀마다 색과 깊이를 화면 크기 버퍼에 담는다. 이 버퍼는 커서 GPU 칩 밖의 메모리(DRAM)에 있고, 삼각형이 픽셀을 덮을 때마다 깊이를 읽어 비교하고 색을 쓴다. 삼각형이 겹친 만큼 칩 밖을 오간다.

폰에게는 이 왕복이 비싸다. 칩 밖 메모리를 읽고 쓰는 데 드는 전력이 칩 안 메모리보다 훨씬 커서, 몇 와트로 버텨야 하는 폰에서는 대역폭이 곧 배터리와 발열이다. 그래서 대부분의 모바일 GPU는 타일 기반 렌더링(tile-based rendering)으로 순서를 바꾼다.

즉시 모드와 타일 기반 렌더링 비교: 즉시 모드는 삼각형이 픽셀을 덮을 때마다 외부 메모리의 화면 크기 버퍼를 읽고 쓰고, 타일 기반은 삼각형을 타일별 목록으로 나눈 뒤 타일 하나를 칩 안 메모리에서 끝까지 칠하고 완성된 색만 한 번 내보낸다즉시 모드(PC 그래픽 카드)123외부 메모리화면 전체의색·깊이삼각형 순서대로 칠하며덮을 때마다 깊이를 읽고 색을 쓴다겹친 만큼 칩 밖을 오간다(빨간 선)타일 기반(대부분의 폰)123칩 안 메모리타일 한 장외부 메모리목록·완성 색① 삼각형을 타일별 목록으로 나눈다② 타일마다 칩 안에서 끝까지 칠한다③ 완성된 색만 한 번 내보낸다강조한 타일의 목록: 삼각형 1, 3
같은 삼각형 셋을 그리는 두 방식. 즉시 모드는 칩 밖 버퍼를 계속 오가고, 타일 기반은 타일 하나를 칩 안에서 끝까지 칠한 뒤 완성된 색만 내보낸다.
  1. 나누기(binning): 먼저 모든 삼각형의 화면 위치를 계산하고, 화면을 작은 타일(16×16, 32×32픽셀 등 GPU마다 다르다)로 나눠 타일마다 걸친 삼각형 목록을 만든다.
  2. 타일마다 칠하기: 타일 하나의 색과 깊이는 작아서 칩 안 메모리에 들어간다. 목록의 삼각형을 그 안에서 끝까지 칠한다. 깊이 비교는 칩 밖으로 나가지 않는다.
  3. 한 번 내보내기: 다 칠한 타일의 색만 칩 밖으로 쓴다. 깊이는 다음에 쓸 일이 없으면 버린다.

대가는 목록이다. 나누기 단계의 결과(정점마다의 값과 타일 목록)는 칩 밖에 썼다가 칠하기 단계에서 다시 읽으므로, 삼각형이 아주 많고 작으면 이 왕복이 커져 즉시 모드보다 못할 수 있다. 모바일 가이드가 삼각형 하나가 픽셀을 여럿 덮게 하라고 권하는 까닭이다. PowerVR·Apple GPU처럼 칠하기 전에 가려질 픽셀을 걸러 셰이딩을 건너뛰는 방식(TBDR, tile-based deferred rendering)도 있다. 모든 폰이 이 방식인 것은 아니고, 반대로 Apple 실리콘 맥의 GPU도 타일 방식이다.

그래서 폰에서는 최적화의 기준이 PC와 다르다. 렌더 패스를 시작할 때 이전 내용을 불러오지 말고 지우라고(clear) 하고, 끝날 때 다시 쓸 일 없는 깊이는 저장하지 말라고 하며, 프레임 중간에 렌더 타깃을 바꿨다 돌아오거나 읽어 오는 일을 피한다. 모두 타일을 칩 밖으로 내보내고 다시 불러오는 왕복을 늘리기 때문이다. 반대로 MSAA는 타일 안에서 섞고 결과만 내보내면 되어 비교적 싸다.

3D로 그리는 도트: 픽셀 스냅과 정사영

손으로 찍은 도트처럼 보이는데 실은 전부 3D 모델인 게임이 있다. 2D 스프라이트는 방향·동작·장비 조합마다 따로 그려야 하고 빛과 그림자도 그림에 박히지만, 3D면 엔진이 그 일을 맡는다. 대신 3D를 작게 그리기만 해서는 도트로 읽히지 않아서, 그 위에 2D의 규칙을 다시 씌운다.

  • 작게 그려 크게 늘리기: 낮은 해상도로 그린 뒤 최근접(nearest neighbor) 방식으로 정수배 확대한다. 텍스처의 한 칸(텍셀)이 기본 줌에서 화면 한 픽셀이 되게 맞추고, 원근 없이 평행하게 비추는 정사영(orthographic) 카메라를 쓴다.
  • 픽셀 스냅(pixel snap): 카메라가 픽셀의 몇 분의 1씩 움직이면 테두리에 걸친 칸이 켜졌다 꺼졌다 해서 가장자리가 부글부글 기어다닌다. 카메라 위치를 화면 방향 좌표로 바꿔 픽셀 크기로 반올림한 뒤 되돌리면, 그림이 제자리에 있다가 한 칸씩 통째로 옮겨 간다. 물체의 뿌리 위치도 픽셀에 맞춰 같은 모델이 어디서나 같은 실루엣이 되게 한다. 부드러운 스크롤이 필요하면 버린 나머지만큼 확대 단계에서 화면을 밀어 주는 보완이 흔하다.
  • 깊이 대신 정렬: 깊이 버퍼는 픽셀마다 더 가까운 면만 남겨서, 병사가 멘 창끝이 벽에 닿으면 벽 속에 묻힌다. 손으로 그린 도트라면 병사 그림이 벽 위에 통째로 겹칠 뿐이다. 그래서 물체마다 앞뒤 순서를 정하고, 깊이 값에 진짜 거리 대신 그 순서를 넣어 물체 단위로 겹치게 한다.

순서는 화면에서 겹치는 물체 쌍마다 3D 상자로 앞뒤를 정하고, 그 관계 그래프를 위상 정렬해 매긴다. 문제는 A가 B 앞, B가 C 앞, C가 A 앞인 순환이다. 한 줄로 세울 수 없으니 어느 한 쌍은 틀리게 그릴 수밖에 없다. 그래프에서 서로 물고 물린 묶음(강한 연결 요소, 타잔 알고리즘)을 찾아, 화면에서 겹친 넓이가 가장 작은 관계를 끊고 다시 정렬하면 틀리는 넓이를 줄일 수 있다. 고전적인 페인터 알고리즘(1972년 논문)은 순환이 생기면 다각형을 잘라서 풀었다.

함정도 있다. 디퍼드 조명은 깊이 버퍼로 픽셀의 위치를 되살려 빛을 계산하므로, 깊이를 순서로 바꿔 두면 엉뚱한 자리에서 빛을 읽는다. 진짜 위치를 따로 남기거나 정점의 진짜 위치로 조명하는 포워드 방식이 편하다. 3D로 2D 애니메이션의 느낌을 낸 선례로는 길티기어 Xrd(2014)가 자주 꼽힌다.

더 읽을거리