"RTX ON: 신세계인가, 렉의 주범인가?"
큰맘 먹고 산 RTX 3070 그래픽카드로 사이버펑크 2077을 켰습니다. 설정에서 "레이 트레이싱: 켜기"를 누르는 순간, 화면에 비친 물웅덩이가 진짜 현실처럼 변했습니다. 네온사인의 반사광이 젖은 도로에 퍼지는 모습을 보며 "이게 게임이 맞나?" 싶었죠. 하지만 동시에 게임 속도는 거북이가 됐습니다 (60프레임 → 30프레임).
RTX 카드를 샀는데도 프레임이 이렇게 떨어지다니, 도대체 무슨 계산을 하길래 컴퓨터가 이렇게 힘겨워하는 걸까요? 궁금해서 찾아보니, 레이 트레이싱은 단순히 "그래픽 품질 올리는 옵션"이 아니었습니다. 빛의 물리 법칙을 게임 안에서 실시간으로 시뮬레이션하는 것이었습니다.
1. 그림자 놀이 vs 진짜 빛의 시뮬레이션
처음엔 레이 트레이싱과 기존 렌더링 방식의 차이가 와닿지 않았습니다. "둘 다 그래픽 그리는 거 아니야?" 싶었죠.
하지만 이렇게 이해하니까 머릿속에 확 들어왔습니다.
래스터화(Rasterization) - 그림자 놀이
기존 방식인 래스터화(Rasterization)는 '그림자 놀이'와 비슷합니다. 손으로 그림자를 만들 때, 실제로 빛이 손가락 세포를 투과하고 산란되는 물리 과정을 계산하진 않잖아요. 그냥 "여기 빛이 있으니까 이쯤에 그림자가 생기겠지?" 하고 '흉내'를 내는 겁니다.
개발자가 미리 짜놓은 셰이더(Shader) 프로그램으로 "빛이 여기 있으면, 이 각도에서 이 정도 밝기로 보이겠지"라고 근사값을 계산합니다. 빠르지만, 거울에 비친 모습이나 유리에 반사된 빛 같은 복잡한 표현은 가짜 티가 납니다.
예를 들어 거울을 구현하려면:
- 거울을 렌더링할 때 카메라를 거울 반대편에 하나 더 만들고
- 그 카메라 시점에서 본 화면을 텍스처로 구워서
- 거울 표면에 붙입니다
결과물은 괜찮아 보이지만, 정적인 환경에서만 작동합니다. 플레이어가 움직이거나, 다른 물체가 반사되어야 할 경우엔 추가 트릭이 필요하죠.
레이 트레이싱(Ray Tracing) - 탐정이 단서를 쫓듯
반면 레이 트레이싱(Ray Tracing)은 '광선(Ray)을 추적(Tracing)'합니다. 마치 탐정이 범인의 발자국을 하나하나 따라가듯, 빛의 경로를 역추적하는 거죠.
현실에선 빛이 광원에서 출발해 여기저기 튕기다가 우리 눈에 들어옵니다. 하지만 대부분의 빛은 눈에 안 들어오니까, 컴퓨터로 그걸 다 계산하면 비효율적이에요.
그래서 레이 트레이싱은 반대로 합니다:
- 가상의 눈(카메라)에서 광선을 쏩니다 (Primary Ray)
- 광선이 물체에 부딪히면, 그 지점에서 빛이 어디서 왔는지 추적합니다 (Shadow Ray, Reflection Ray, Refraction Ray)
- 광선이 광원에 닿거나, 에너지가 소진될 때까지 튕기고 굴절되는 경로를 계산합니다
마치 당구공을 쳤을 때 공이 어떻게 튕겨 나갈지 물리 법칙으로 계산하는 것과 같습니다. 현실 세계의 물리를 그대로 시뮬레이션하는 거죠.
2. 광선 추적의 실제 작동 방식 - 4가지 Ray
레이 트레이싱이 단순히 "빛을 쏜다"라고만 알고 있었는데, 실제론 4가지 종류의 광선을 사용한다는 걸 알았을 때 무릎을 쳤습니다.
Primary Ray (1차 광선)
카메라에서 화면의 각 픽셀을 향해 쏘는 광선입니다. 예를 들어 1920x1080 해상도면, 2,073,600개의 광선을 쏩니다.
for each pixel (x, y) in screen:
ray = create_ray(camera_position, pixel_direction)
color = trace_ray(ray, max_depth=5)
set_pixel_color(x, y, color)
이 광선이 물체에 부딪히면, "이 픽셀은 무슨 색이어야 하나?"를 판단하기 위해 추가 광선을 쏩니다.
Shadow Ray (그림자 광선)
Primary Ray가 물체에 부딪힌 지점에서, 광원을 향해 쏘는 광선입니다. 중간에 다른 물체가 있으면 그림자가 생기고, 없으면 빛을 받는 거죠.
def trace_shadow_ray(hit_point, light_position):
direction = normalize(light_position - hit_point)
ray = Ray(hit_point + epsilon * direction, direction) # epsilon: 자기 자신과의 충돌 방지
if intersects_any_object(ray, distance_to_light):
return 0.0 # 그림자 안
else:
return calculate_lighting(hit_point, light_position) # 빛 받음
래스터화에선 그림자를 "그려 넣는" 반면, 레이 트레이싱은 "빛이 실제로 닿는지 확인"하는 겁니다.
Reflection Ray (반사 광선)
거울이나 금속 표면에 부딪히면, 반사각으로 튕겨 나가는 광선을 추적합니다.
물리 법칙: 입사각 = 반사각
def calculate_reflection(incident_ray, normal):
# 입사 벡터를 법선 벡터 기준으로 반사
return incident_ray - 2 * dot(incident_ray, normal) * normal
def trace_reflection_ray(hit_point, incident_direction, normal):
reflected_direction = calculate_reflection(incident_direction, normal)
reflected_ray = Ray(hit_point + epsilon * normal, reflected_direction)
return trace_ray(reflected_ray, depth + 1) # 재귀 호출
여기서 핵심은 재귀(Recursion)입니다. 반사광이 또 다른 물체에 부딪히면, 거기서 또 반사광을 계산하고... 무한 반복될 수 있어요. 그래서 보통 최대 깊이(max depth)를 5~10으로 제한합니다.
Refraction Ray (굴절 광선)
유리나 물처럼 투명한 물체를 통과할 때, 굴절각으로 꺾이는 광선을 추적합니다.
물리 법칙: 스넬의 법칙(Snell's Law)
n₁ × sin(θ₁) = n₂ × sin(θ₂)
(n: 굴절률, θ: 입사각/굴절각)
def calculate_refraction(incident_ray, normal, n1, n2):
# 스넬의 법칙을 벡터로 계산
eta = n1 / n2
cos_i = -dot(normal, incident_ray)
sin_t2 = eta * eta * (1.0 - cos_i * cos_i)
if sin_t2 > 1.0:
return None # 전반사(Total Internal Reflection)
cos_t = sqrt(1.0 - sin_t2)
return eta * incident_ray + (eta * cos_i - cos_t) * normal
유리잔에 빛이 들어가면서 꺾이고, 나오면서 또 꺾이는 걸 물리 법칙으로 계산하는 겁니다.
Ray-Sphere Intersection (광선-구체 충돌 계산)
광선이 물체에 부딪히는지 어떻게 알까요? 가장 간단한 예시가 구체(Sphere)와의 충돌 테스트입니다.
def ray_sphere_intersection(ray_origin, ray_direction, sphere_center, sphere_radius):
# 광선: P(t) = O + t*D (O: 시작점, D: 방향, t: 거리)
# 구체: |P - C|² = r² (C: 중심, r: 반지름)
oc = ray_origin - sphere_center
a = dot(ray_direction, ray_direction)
b = 2.0 * dot(oc, ray_direction)
c = dot(oc, oc) - sphere_radius * sphere_radius
discriminant = b*b - 4*a*c
if discriminant < 0:
return None # 충돌 없음
else:
t = (-b - sqrt(discriminant)) / (2.0 * a)
if t < 0:
return None # 광선 뒤쪽에 구체가 있음
return ray_origin + t * ray_direction # 충돌 지점
이 계산을 화면의 모든 픽셀, 모든 물체, 모든 광선에 대해 해야 합니다. 그래서 느린 겁니다.
3. 렌더링 방정식 - 빛의 물리학을 한 줄로
레이 트레이싱의 이론적 기반은 렌더링 방정식(Rendering Equation)입니다. 처음 봤을 땐 수식이 복잡해서 도망치고 싶었는데, 핵심만 이해하니까 와닿았습니다.
렌더링 방정식 (간소화 버전)
L_out = L_emit + ∫ L_in × BRDF × cos(θ) dω
이걸 한국어로 풀면:
"어느 방향으로 나가는 빛(L_out) = 자체 발광(L_emit) + 들어오는 빛들의 합"
- L_out: 카메라 방향으로 나가는 빛
- L_emit: 물체가 스스로 내는 빛 (전구, 네온사인 등)
- L_in: 다른 곳에서 들어오는 빛
- BRDF: Bidirectional Reflectance Distribution Function (빛이 어떻게 반사되는가)
- cos(θ): 입사각에 따른 감쇠
- ∫ dω: 모든 방향의 빛을 합산
핵심 깨달음
"한 점에서 나가는 빛은, 그 점으로 들어온 모든 빛의 합이다."
이게 재귀적(Recursive)이라는 게 포인트입니다. A에서 나가는 빛을 계산하려면 → B에서 들어오는 빛을 계산해야 하고 → B에서 나가는 빛을 계산하려면 → C에서 들어오는 빛을 계산해야 하고...
이 무한 재귀를 5~10번 깊이로 끊어내는 게 레이 트레이싱입니다.
Path Tracing vs Ray Tracing
렌더링 방정식을 "어떻게 푸느냐"에 따라 이름이 달라집니다.
| 방식 | Ray Tracing | Path Tracing |
|---|---|---|
| 광선 수 | 픽셀당 1~수개 (결정론적) | 픽셀당 수백~수천 개 (확률론적) |
| 반사 방향 | 정반사만 (거울처럼) | 난반사 포함 (현실처럼) |
| 간접광(Indirect Lighting) | 근사치 | 정확한 물리 시뮬레이션 |
| 용도 | 실시간 게임 (RTX) | 영화 CG (Pixar, ILM) |
게임에서 쓰는 RTX는 엄밀히 말하면 "하이브리드 방식"입니다. 래스터화로 기본 화면을 그리고, 반사/그림자/간접광만 레이 트레이싱으로 추가하는 거죠.
4. BVH: 수억 개의 삼각형을 빠르게 검색하는 비결
레이 트레이싱의 가장 큰 문제는 "모든 광선이 모든 물체와 충돌 테스트를 해야 한다"는 겁니다.
예를 들어:
- 화면: 1920×1080 = 2,073,600 픽셀
- 픽셀당 광선: 4개 (Primary + Shadow + Reflection + Refraction)
- 씬의 삼각형 수: 1,000,000개 (AAA 게임 기준)
총 계산 횟수 = 2,073,600 × 4 × 1,000,000 = 약 8조 번
이걸 1/60초에 끝내야 하니, 당연히 느립니다.
BVH: 공간을 나무 구조로 나눈다
BVH(Bounding Volume Hierarchy)는 이 문제를 해결하는 핵심 자료구조입니다.
비유하자면 "도서관에서 책 찾기"와 같습니다. 100만 권의 책이 있는데, 하나씩 다 뒤지면 평생 걸리겠죠. 대신 이렇게 합니다:
- 건물 → 층 → 서가 → 선반 → 책 순서로 범위를 좁혀갑니다 (계층 구조)
- 광선이 1층 바운딩 박스와 안 부딪히면, 1층 전체를 통째로 무시합니다
class BVHNode:
def __init__(self, bounding_box, left_child, right_child, objects):
self.bbox = bounding_box # 이 노드가 감싸는 영역
self.left = left_child
self.right = right_child
self.objects = objects # 리프 노드일 경우만 채워짐
def intersect_bvh(ray, node):
if not ray_intersects_box(ray, node.bbox):
return None # 바운딩 박스와 안 부딪히면 이 노드 전체 스킵
if node.is_leaf():
# 리프 노드면 실제 물체들과 충돌 테스트
closest_hit = None
for obj in node.objects:
hit = ray_intersects_object(ray, obj)
if hit and (not closest_hit or hit.distance < closest_hit.distance):
closest_hit = hit
return closest_hit
else:
# 내부 노드면 자식들 재귀 탐색
hit_left = intersect_bvh(ray, node.left)
hit_right = intersect_bvh(ray, node.right)
return closer_of(hit_left, hit_right)
효과
BVH 없이: O(n) - 모든 물체 검사 BVH 사용: O(log n) - 트리 탐색
100만 개 물체 기준, 100만 번 → 20번으로 줄어듭니다.
RT 코어는 바로 이 BVH 탐색을 하드웨어로 가속하는 칩입니다.
5. 왜 이제야 상용화됐을까?
이 기술은 사실 1980년대부터 있었습니다. Turner Whitted가 1980년에 발표한 논문 "An Improved Illumination Model for Shaded Display"가 시작이었죠.
하지만 영화 CG(아바타, 아이언맨)를 만들 때나 썼습니다. 영화는 한 프레임 렌더링하는 데 몇 시간이 걸려도 되니까요.
게임은 1초에 60번 그려야 한다
하지만 게임은 1초에 60프레임을 그려야 합니다. 빛의 경로를 수억 번 계산하는 걸 0.016초만에 끝내야 한다는 소리입니다.
기존 GPU로는 불가능했습니다. GPU는 원래 "삼각형을 화면에 그리는" 래스터화에 최적화된 칩이었거든요. 광선 추적은 전혀 다른 작업입니다.
RT 코어 - 빛 계산 전용 칩
이걸 가능하게 만든 게 바로 엔비디아의 RT 코어(Ray Tracing Core)였습니다.
2018년 RTX 2080 출시
RT 코어가 하는 일:
- BVH 탐색 가속: 광선이 어느 물체와 부딪히는지 빠르게 찾기
- Ray-Triangle Intersection 하드웨어 가속: 광선-삼각형 충돌 계산을 전용 회로로 처리
"빛 계산만 죽어라 하는 전용 계산기"를 칩에 박아버린 덕분에, 실시간 레이 트레이싱이 가능해진 겁니다.
| 시대 | 기술 | 렌더링 시간 |
|---|---|---|
| 1980년대 | 소프트웨어 레이 트레이싱 | 한 프레임 수 시간 |
| 2000년대 | GPU 가속 (CUDA) | 한 프레임 수 분 |
| 2018년~ | RT 코어 (RTX) | 초당 60프레임 (실시간) |
6. DLSS: 꼼수의 미학
RT 코어가 있어도, 4K 해상도에서 레이 트레이싱을 켜면 프레임이 뚝 떨어집니다. 그래서 엔비디아는 천재적인 꼼수를 씁니다. 바로 DLSS(Deep Learning Super Sampling)입니다.
DLSS의 작동 원리
- 화면을 작게 그립니다 (예: 1080p) ← 레이 트레이싱 부하를 줄임
- AI(텐서 코어)가 4K로 그림을 확대(Upscaling)합니다
- 빈 공간의 픽셀을 AI가 "여긴 이 색깔이 맞겠지" 하고 추론해서 채워 넣습니다
DLSS가 특별한 이유
기존 업스케일링(Bilinear, Bicubic)은 단순히 픽셀을 보간(Interpolation)합니다. 하지만 DLSS는 시간축 데이터를 활용합니다.
DLSS 입력:
- 현재 프레임 (저해상도)
- 이전 프레임 (고해상도)
- 모션 벡터 (물체가 어느 방향으로 움직였는지)
- 깊이 정보 (어떤 물체가 앞/뒤에 있는지)
DLSS 출력:
- 현재 프레임 (고해상도) - AI가 "이전 프레임에서 이 물체는 이렇게 생겼으니, 지금은 이럴 것"이라고 추론
결과는 대성공이었습니다. 사양은 절반만 먹으면서, 화질은 4K처럼 보이게 만드는 마법. 심지어 네이티브 4K보다 더 선명해 보이는 경우도 있습니다 (앤티에일리어싱 효과 덕분).
FSR: AMD의 오픈소스 대응
AMD는 하드웨어 의존 없이 작동하는 FSR(FidelityFX Super Resolution)을 내놨습니다.
| 구분 | DLSS (NVIDIA) | FSR (AMD) |
|---|---|---|
| 기술 | AI 기반 (텐서 코어) | 공간 업스케일링 (알고리즘) |
| 하드웨어 요구 | RTX 카드 필수 | 모든 GPU 지원 |
| 품질 | 더 선명 (시간축 활용) | 양호 (단일 프레임만) |
| 호환성 | 게임별 학습 필요 | 즉시 적용 가능 |
제 RTX 3070에선 DLSS가 프레임을 30 → 60으로 두 배 올려줬습니다. 이게 제가 저사양으로도 고화질 게임을 즐길 수 있었던 비결이었습니다.
7. 게임 너머 - 건축, 자동차 디자인, 영화
레이 트레이싱은 게임만의 기술이 아닙니다. "빛이 정확해야 하는 모든 분야"에서 쓰입니다.
건축 시각화 (Architectural Rendering)
건물을 짓기 전에 "이 건물이 오후 3시에 어떻게 보일까?"를 미리 보는 겁니다.
Autodesk Arnold, V-Ray, Unreal Engine 5를 쓰면:
- 태양의 각도에 따른 그림자 변화
- 유리에 반사되는 주변 건물
- 실내로 들어오는 자연광
이런 걸 물리적으로 정확하게 렌더링합니다. 건축주에게 보여줄 때 "이게 진짜 사진이에요?"라는 질문을 받을 정도로 현실적이죠.
자동차 디자인 (Automotive Design)
BMW, Audi, Tesla 같은 회사들은 실제 프로토타입을 만들기 전에 레이 트레이싱으로 렌더링합니다.
NVIDIA Omniverse, Autodesk VRED를 쓰면:
- 차체 도료의 금속 반사 (Metallic BRDF)
- 크롬 부품의 거울 효과
- 헤드라이트 불빛이 주변에 어떻게 퍼지는지
이런 걸 실시간으로 확인하면서 디자인을 조정합니다. 물리 기반 렌더링(PBR)이 정확하니까, "실제로 만들면 이렇게 나온다"는 확신을 갖고 진행할 수 있는 거죠.
영화 VFX
Pixar RenderMan, Disney Hyperion은 Path Tracing을 씁니다. 게임보다 훨씬 깊은 재귀 깊이(수백 번)로 계산하죠.
- 아바타(Avatar)의 판도라 정글 - 나뭇잎 사이로 스며드는 빛
- 아이언맨 슈트의 금속 반사
- 겨울왕국(Frozen)의 얼음 표면
이런 장면들은 한 프레임당 수 시간을 렌더링합니다. 하지만 결과물은 실사와 구분이 안 갈 정도로 완벽합니다.
8. 요약 - 가짜에서 진짜로
| 구분 | Rasterization (기존) | Ray Tracing (RTX) |
|---|---|---|
| 비유 | 그림자 놀이 (흉내 내기) | 탐정이 단서를 쫓듯 (추적하기) |
| 현실감 | 반사, 굴절 표현에 한계 | 실사급 (거울, 유리 완벽 구현) |
| 기술 | 셰이더 트릭 | BVH + RT 코어 + DLSS |
| 비용 | 가벼움 (120fps 가능) | 극도로 무거움 (DLSS 없으면 30fps) |
레이 트레이싱을 공부하면서 가장 와닿았던 건 이겁니다:
"컴퓨터 그래픽스의 역사는 '속임수'에서 '진짜'로 가는 여정이었다."
1990년대엔 그림자를 텍스처로 그려 넣었고, 2000년대엔 법선 맵으로 울퉁불퉁한 표면을 흉내 냈고, 2010년대엔 스크린 스페이스 반사(SSR)로 거울을 비슷하게 만들었습니다.
하지만 2020년대에 와서, 우리는 드디어 "빛의 물리 법칙을 실시간으로 시뮬레이션"할 수 있게 됐습니다.
메타버스니 뭐니 해도, 결국 빛이 진짜처럼 움직이지 않으면 뇌는 가짜라고 느낄 테니까요. 레이 트레이싱은 단순히 "그래픽이 좋아진다"는 차원을 넘어, "가상 세계가 물리 법칙을 갖게 된다"는 의미였습니다.