사전 광선 추적
알고리즘

광선 추적

gabury1고친 사람 github-actions[bot]

광선 추적은 화면의 점마다 무엇이 보일지를 광선을 쏘아 알아내는 렌더링 방법입니다. 광선은 눈에서 출발해 장면 속으로 나아갑니다. 처음 맞은 물체가 그 점의 색을 정합니다. 맞은 곳에서 광선을 한 번 더 쏘면 그림자와 반사도 같은 방법으로 구합니다.

쉽고 빠른 이해

광선 추적은 화면의 점마다 「여기서 무엇이 보이나」를 광선을 쏴서 알아냅니다. 창문을 잘게 나눈 칸마다 그 칸 너머로 무엇이 보이는지 하나씩 적어 나가는 것과 비슷합니다.

이렇게 하는 이유는 그림자 · 거울 반사 · 유리의 굴절을 한 가지 계산으로 얻기 때문입니다. 물체를 화면에 찍어 내는 방식은 이런 효과마다 따로 기법을 덧대야 합니다.

  1. 화면의 점마다 눈에서 광선을 하나 쏩니다
  2. 그 광선이 처음 맞은 물체를 찾습니다
  3. 맞은 곳에서 전등 쪽으로 광선을 쏴서 그림자 속인지 봅니다
  4. 거울이나 유리에 맞았으면 튕기거나 꺾인 광선을 다시 쏩니다

대가는 계산량입니다. 광선마다 장면의 물체와 부딪히는지 따져야 해서 물체와 화면이 커질수록 시간이 빠르게 늘어납니다.

그만큼 한 장에 오래 걸려도 되는 영화나 건축 조감도에 주로 씁니다. 1초에 수십 장을 그려야 하는 게임은 반사나 그림자 같은 일부 효과에만 섞어 씁니다.

상세

이 절은 광선 추적이 무엇을 받아 무엇을 내는지부터 봅니다. 다음으로 광선 하나가 공에 맞는지 숫자로 따라갑니다. 그 뒤 그림자와 반사가 같은 계산에서 어떻게 나오는지 봅니다. 끝으로 계산량과 래스터화와의 차이를 짚습니다.

빛을 거꾸로 따라가는 이유

현실에서 빛은 전등에서 나와 물체에 부딪힌 뒤 눈에 들어옵니다. 우리가 보는 색은 눈에 들어온 그 빛의 색입니다.

이 경로를 전등 쪽에서 따라가면 낭비가 큽니다. 전등이 내보낸 빛 대부분은 벽이나 바닥에서 흩어집니다. 눈에는 끝내 안 들어옵니다. 계산한 빛의 대부분을 버리게 됩니다.

그래서 광선 추적은 방향을 뒤집습니다. 눈에서 출발해 화면의 점을 지나 장면 쪽으로 광선을 쏩니다. 이렇게 하면 눈에 들어오는 빛만 골라 셀 수 있습니다.

입력과 출력

넣는 것은 장면 하나입니다. 장면에는 물체의 모양과 재질(머티리얼), 광원의 위치, 카메라의 위치와 방향이 들어 있습니다. 광원은 빛을 내는 물체입니다. 전등이나 해가 그렇습니다.

나오는 것은 이미지 한 장입니다. 이미지에는 픽셀이라는 작은 칸이 격자로 늘어서 있습니다. 칸마다 색 하나가 붙습니다. 광선 추적은 이 칸마다 광선을 쏘아 색을 정합니다.

물체의 모양은 보통 작은 삼각형을 이어 붙여 나타냅니다. 이렇게 이은 삼각형 묶음을 메시라고 부릅니다. 공처럼 수식 하나로 적히는 도형을 그대로 쓰기도 합니다.

광선 하나의 생김새

광선은 출발점 하나와 방향 하나로 정해집니다. 출발점을 o, 방향을 d 라고 두면 광선 위의 점은 o + t·d 로 적습니다. t 는 출발점에서 방향을 따라 얼마나 나아갔는지를 나타내는 수입니다.

t 에 수를 넣으면 광선 위의 점이 하나씩 나옵니다. 아래는 원점에서 z 축 방향으로 쏜 광선입니다.

C
// o = (0, 0, 0) · d = (0, 0, 1)
p(t) = o + t * d;

p(0)     // (0, 0, 0) 출발점
p(4)     // (0, 0, 4)
p(-1)    // (0, 0, -1) 눈 뒤쪽

t 가 음수인 점은 출발점 뒤쪽입니다. 눈 뒤에 있는 물체는 보이지 않으므로 광선 추적은 t 가 양수인 점만 봅니다.

광선이 물체에 맞는지 따지기

광선을 쏘고 나서 가장 먼저 할 일은 장면의 물체와 만나는지 따지는 것입니다. 이 계산이 교차 판정입니다. 광선 추적에 드는 시간 대부분이 이 판정에 쓰입니다.

구 하나로 따라가 봅니다. 중심이 (0, 0, 5) 이고 반지름이 1 인 구가 있습니다. 앞의 광선 위의 점 (0, 0, t) 가 구의 표면에 있으려면 구 중심까지 거리가 1 이어야 합니다. 그 거리는 |t - 5| 이므로 t 는 4 또는 6 입니다.

C
// 구  중심 (0, 0, 5) · 반지름 1
hit(ray, sphere)    // t = 4, 6
nearest             // t = 4
p(4)                // (0, 0, 4) 맞은 점

값이 둘 나온 것은 광선이 구에 들어갈 때와 나올 때 한 번씩 표면을 지나기 때문입니다. 눈에 보이는 것은 먼저 만나는 쪽이므로 작은 값 4 를 씁니다.

광선이 비껴가면 답이 없습니다. 출발점을 (0, 3, 0) 으로 옮기면 광선 위의 어느 점도 구 중심에서 3 보다 가까워지지 않습니다. 반지름 1 에 닿는 t 가 없으므로 이 광선은 구를 놓칩니다.

앞의 예도 사실은 이차방정식이었습니다. 거리가 1 이라는 조건을 양쪽 제곱하면 (t - 5)² = 1 이 되기 때문입니다. 근이 4 와 6 둘인 것도 그래서입니다.

방향이 비스듬해도 꼴은 같습니다. 광선 위의 점에서 구 중심까지 거리의 제곱은 언제나 t 에 대한 이차식입니다. 그 값이 반지름의 제곱과 같다는 조건을 풀면 되므로, 근의 개수가 곧 판정입니다.

근 뜻
둘 구를 뚫고 지나간다. 작은 쪽이 들어가는 점이다
하나 표면을 스친다
없음 구를 놓친다

삼각형은 순서가 조금 다릅니다. 먼저 광선이 삼각형이 놓인 평면과 만나는 점을 구합니다. 그다음 그 점이 삼각형 안쪽에 있는지 봅니다.

가장 가까운 물체가 보인다

장면에는 물체가 여럿입니다. 광선 하나가 여러 물체를 뚫고 지나갈 수도 있습니다. 그중 눈에 보이는 것은 t 가 가장 작은 물체, 곧 눈에 가장 가까운 물체입니다.

광선 추적은 물체마다 교차 판정을 합니다. 그중 가장 작은 t 만 남깁니다. 앞에 선 물체가 뒤의 물체를 가리는 일이 이 규칙 하나로 풀립니다.

어느 물체에도 맞지 않은 광선은 장면 밖으로 빠져나간 것입니다. 이런 광선은 하늘 같은 배경색을 받습니다.

맞은 점의 밝기

맞은 점을 찾았으면 그 점의 색을 정합니다. 이 계산이 셰이딩입니다. 가장 단순한 셰이딩은 표면이 광원을 얼마나 정면으로 보고 있는지로 밝기를 정합니다.

표면이 바라보는 방향은 표면에 수직인 화살표 하나로 적습니다. 이 화살표가 법선입니다. 법선이 광원 쪽을 곧게 향할수록 밝습니다. 비스듬하면 어두워집니다. 광원을 등지면 빛을 받지 못합니다.

그림자 광선

맞은 점이 그림자 속인지도 광선으로 알아봅니다. 맞은 점에서 광원 쪽으로 광선을 하나 더 쏩니다. 이 광선을 그림자 광선이라고 부릅니다.

그림자 광선이 광원에 닿기 전에 다른 물체에 맞으면 그 점은 그림자 속입니다. 광원의 빛이 그 물체에 막힌 것입니다. 아무것에도 안 맞으면 그 점은 광원의 빛을 받습니다.

광원이 여럿이면 그림자 광선도 광원마다 하나씩 쏩니다. 이때는 가장 가까운 물체를 찾을 필요가 없습니다. 막는 물체가 하나라도 있는지만 알면 되므로 처음 맞는 순간 판정을 멈춥니다.

반사와 굴절

거울 같은 표면에 맞으면 광선이 튕겨 나갑니다. 맞은 점에서 튕긴 방향으로 새 광선을 쏘면 거울에 비친 모습을 얻습니다. 이 광선이 반사 광선입니다.

유리나 물 같은 투명한 표면에서는 광선이 꺾여 안으로 들어갑니다. 꺾인 방향으로 쏘는 광선이 굴절 광선입니다. 꺾이는 정도는 재질마다 다릅니다.

반사 광선과 굴절 광선이 맞은 점에서도 같은 일이 되풀이됩니다. 다시 셰이딩을 합니다. 그림자 광선도 쏩니다. 거울이나 유리에 맞으면 또 새 광선을 쏩니다. 함수가 자기 자신을 다시 부르는 재귀의 꼴입니다.

flowchart TD
    E["눈에서 쏜 광선"] --> G["유리 공에 맞음"]
    G --> S1["그림자 광선 · 막힘 없음"]
    G --> R1["반사 광선"]
    G --> T1["굴절 광선"]
    R1 --> W["벽에 맞음"]
    W --> S2["그림자 광선 · 다른 물체에 막힘"]
    T1 --> B["공을 지나 바닥에 맞음"]
    B --> S3["그림자 광선 · 막힘 없음"]

그림은 픽셀 하나에서 출발한 광선이 갈라지는 모양입니다. 유리 공에 한 번 맞자 광선이 셋으로 늘었습니다. 벽에 맞은 점은 그림자 광선이 막혔으므로 그림자 속으로 칠해집니다.

이렇게 갈라진 모양을 나무에 견주면, 눈에서 쏜 광선이 맨 위의 뿌리입니다. 맨 아래 끝은 잎입니다. 이 픽셀의 색은 잎 쪽에서 구한 색을 뿌리 쪽으로 거슬러 올려 섞어 정합니다.

재귀를 멈추는 조건

재귀에는 끝나는 조건이 있어야 합니다. 마주 보는 거울 두 장 사이에서는 광선이 끝없이 튕길 수 있기 때문입니다.

막는 방법은 튕긴 횟수에 상한을 두는 것입니다. 상한에 이르면 더 쏘지 않고 그 점의 밝기로 끝냅니다. 빛은 튕길 때마다 조금씩 약해지므로, 여러 번 튕긴 뒤의 광선은 색에 보태는 몫이 작습니다.

지금까지의 절차를 광선 하나 기준으로 모으면 아래와 같습니다.

flowchart TD
    A["광선 하나를 쏜다"] --> B{"물체에 맞았나"}
    B -->|아니오| C["배경색을 돌려준다"]
    B -->|예| D["가장 가까운 맞은 점의 밝기를 그림자 광선으로 정한다"]
    D --> E{"거울이나 유리이고 튕긴 횟수가 상한 아래인가"}
    E -->|아니오| F["이 밝기를 돌려준다"]
    E -->|예| G["반사 광선과 굴절 광선을 쏜다"]
    G -->|새 광선마다 처음부터| A
    G -->|두 색이 돌아온 뒤| H["돌아온 색을 이 밝기와 섞어 돌려준다"]

「반사 광선과 굴절 광선을 쏜다」에서 맨 위로 돌아가는 화살표가 재귀입니다. 새로 쏜 광선도 이 흐름을 처음부터 끝까지 따라가 색 하나를 돌려줍니다. 그 색이 돌아온 뒤에야 마지막 칸에서 섞습니다.

계산량

광선 추적에 시간이 많이 드는 까닭을 숫자로 봅니다. 가로 1920, 세로 1080 픽셀인 이미지는 픽셀이 약 207만 개입니다. 픽셀마다 광선을 하나만 쏴도 광선이 207만 개입니다.

장면이 삼각형 100만 개로 이루어졌다고 둡니다. 광선마다 삼각형을 전부 교차 판정하면 이미지 한 장에 판정이 약 2조 번 듭니다. 그림자 광선과 반사 광선이 더해지면 그만큼 더 늘어납니다.

픽셀당 광선 수도 튕긴 횟수 상한에 따라 불어납니다. 유리처럼 반사와 굴절이 함께 일어나면 맞을 때마다 광선이 두 갈래로 늘기 때문입니다. 상한이 D 면 픽셀 하나에서 광선이 많게는 2 의 D 제곱 개 가까이 생깁니다.

이 판정을 줄이려고 광선이 닿을 리 없는 물체를 한꺼번에 건너뛰게 해 주는 구조를 둡니다. 이 구조가 가속 구조입니다. 생김새는 바로 아래 소절에서 봅니다. 계산량이 얼마나 줄어드는지는 그 소절 끝에 표로 모읍니다.

가속 구조

물체를 한꺼번에 건너뛰는 열쇠는 묶음입니다. 가까운 물체끼리 묶어 큰 상자 하나로 감싸 둡니다. 광선이 그 상자를 비껴가면 안의 물체는 하나도 볼 필요가 없습니다.

이 묶음을 다시 묶어 나무 모양으로 쌓은 구조가 BVH(Bounding Volume Hierarchy, 바운딩 볼륨 계층)입니다. BVH 는 가속 구조의 여러 갈래 가운데 하나입니다. 광선은 뿌리 상자부터 내려갑니다. 자기가 지나는 상자 쪽 가지만 따라갑니다.

flowchart TD
    R["장면 전체 상자 · 광선이 지남"] --> A["왼쪽 묶음 상자 · 광선이 비껴감"]
    R --> B["오른쪽 묶음 상자 · 광선이 지남"]
    A --> A1["안의 삼각형 · 판정 안 함"]
    B --> B1["작은 상자 · 광선이 비껴감"]
    B --> B2["작은 상자 · 광선이 지남"]
    B1 --> B1T["안의 삼각형 · 판정 안 함"]
    B2 --> B2T["안의 삼각형만 판정"]

그림에서 삼각형까지 내려가 판정하는 가지는 하나뿐입니다. 상자를 반씩 나눠 쌓으면 한 층 내려갈 때마다 남는 후보가 절반쯤으로 줄어듭니다. 그러면 광선 하나가 거치는 층 수는 물체 수 N 이 아니라 log N 정도가 됩니다.

다만 묶음 상자끼리 많이 겹치는 장면에서는 이야기가 달라집니다. 광선이 지나는 곳에 상자 여럿이 포개져 있으면 가지 하나만 고를 수 없습니다. 겹친 가지를 모두 내려가야 하므로 판정이 다시 O(N) 까지 늘 수 있습니다.

픽셀 수를 P, 픽셀당 광선 수를 R, 물체 수를 N 이라 두면 계산량은 아래와 같습니다.

방식 광선 하나의 판정 이미지 한 장
물체를 전부 본다 O(N) O(P·R·N)
가속 구조를 쓴다 · 보통 대략 O(log N) 대략 O(P·R·log N)
가속 구조를 쓴다 · 상자가 많이 겹칠 때 O(N) O(P·R·N)

첫 줄과 둘째 줄의 차이가 가속 구조가 아끼는 몫입니다. 삼각형이 100만 개면 log N 은 약 20 입니다. 광선 하나의 판정이 100만 번에서 수십 번으로 줄어드는 셈입니다.

가속 구조도 공짜는 아닙니다. 판정을 줄여 주는 대신 구조를 만드는 시간이 듭니다. 물체가 움직이면 그 물체를 품은 상자도 고쳐야 합니다.

래스터화와 무엇이 다른가

화면을 그리는 또 하나의 큰 방법이 래스터화입니다. 두 방법은 같은 그림을 반대 순서로 셉니다. 반복문 두 겹의 안팎이 뒤바뀌어 있습니다.

Python
# 래스터화
for 삼각형 in 장면:
    for 픽셀 in 삼각형이_덮는_픽셀:
        칠한다()

# 광선 추적
for 픽셀 in 화면:
    for 삼각형 in 장면:
        맞는지_본다()

래스터화는 삼각형을 하나씩 집어 화면에 눌러 찍습니다. 그 삼각형이 덮는 픽셀을 찾아 색을 칠합니다. 광선 추적은 픽셀을 하나씩 집어 광선을 쏩니다. 그 광선이 맞는 삼각형을 찾아 색을 정합니다.

이 순서 차이가 가림을 다루는 법을 가릅니다. 래스터화는 삼각형을 하나씩 따로 보므로 앞뒤를 바로 알 수 없습니다. 그래서 픽셀마다 지금까지 칠한 물체까지의 거리를 적어 두는 깊이 버퍼를 둡니다. 더 가까운 물체가 올 때만 덮어씁니다.

그림자와 반사도 갈립니다. 래스터화는 화면에 찍히는 삼각형만 알기 때문에 그림자를 얻으려면 광원 쪽에서 장면을 한 번 더 그리는 그림자 매핑 같은 기법을 덧댑니다. 광선 추적은 광선을 한 번 더 쏘면 됩니다.

화면 밖 물체를 다루는 법도 다릅니다. 래스터화는 화면에 찍히는 삼각형만 다룹니다. 카메라 뒤처럼 화면 밖에 있는 물체는 그리는 동안 알 수 없어서, 다른 기법 없이는 거울에 비추지 못합니다. 광선 추적은 반사 광선이 어느 쪽으로 가든 장면 속 물체를 찾으므로 화면 밖 물체도 거울에 비칩니다.

메모리를 쓰는 법도 갈립니다. 래스터화는 삼각형 하나를 칠하고 나면 그 삼각형을 다시 볼 일이 없습니다. 장면을 삼각형 하나씩 차례로 흘려보내듯 그릴 수 있습니다. 광선 추적은 반사 광선이 어느 물체로 향할지 미리 모릅니다. 장면 전체를 메모리에 둬야 어느 물체든 언제라도 찾을 수 있습니다.

래스터화 광선 추적
바깥 반복 삼각형 픽셀
가림 깊이 버퍼로 견준다 가장 작은 t 를 고른다
그림자 · 반사 기법을 따로 더한다 광선을 더 쏜다
화면 밖 물체 그리는 동안 모른다 반사 광선이 닿으면 비친다
장면을 메모리에 삼각형을 하나씩 흘려보내면 된다 광선이 어디로 갈지 몰라 장면 전체를 둔다

시간을 쓸 수 있는 곳과 없는 곳

영화나 건축 조감도처럼 한 장에 오래 걸려도 되는 곳에서는 광선 추적 계열을 씁니다. 그림자와 반사가 따로 손대지 않아도 맞게 나오기 때문입니다. 이렇게 미리 시간을 들여 그리는 방식을 오프라인 렌더링이라고 부릅니다.

게임처럼 1초에 수십 장을 그려야 하는 곳은 오랫동안 래스터화를 썼습니다. 근래에는 화면을 그리는 칩인 GPU(Graphics Processing Unit, 그래픽 처리 장치)에 교차 판정만 맡는 회로가 들어갔습니다. 그 덕에 게임에서도 광선 추적을 씁니다.

프로그램이 GPU 에 광선 추적을 시킬 때는 정해진 단계를 차례로 밟습니다. 광선을 만드는 단계, 맞은 물체를 찾는 단계, 맞았을 때와 놓쳤을 때 색을 정하는 단계입니다. 이 단계들을 이어 놓은 순서가 레이 트레이싱 파이프라인입니다. 레이 트레이싱은 광선 추적의 영어 이름을 소리대로 적은 말입니다.

다만 모든 것을 광선으로 그리면 한 장에 드는 판정이 정해진 시간 안에 다 끝나지 않습니다. 그래서 눈에 보이는 면은 래스터화로 구합니다. 반사와 그림자만 광선 추적으로 구합니다. 이렇게 섞는 방식이 흔합니다.

고전 광선 추적이 못 그리는 것

지금까지 본 방식을 흔히 고전 광선 추적이라고 부릅니다. 이 방식은 맞은 점에서 광선을 정해진 방향으로만 쏩니다. 거울 반사는 한 방향입니다. 그림자 광선도 광원의 한 점을 향합니다.

그래서 그림자 경계가 칼로 자른 듯 날카롭게 나옵니다. 현실의 전등은 크기가 있어서 그림자 가장자리가 흐릿하게 번집니다. 광원을 점 하나로 보면 이 번짐이 안 생깁니다.

벽에 튕긴 빛이 옆 물체를 비추는 간접광도 빠집니다. 빨간 벽 옆에 놓인 흰 공이 불그스름해지는 것이 그 예입니다. 이런 빛까지 셈하는 것을 전역 조명이라고 부릅니다.

이를 메우려고 맞은 점에서 광선을 여러 방향으로 흩어 쏘는 방법들이 나왔습니다. 분산 광선 추적과 경로 추적(패스 트레이싱)이 그 갈래입니다. 둘 다 광선 하나 대신 여러 광선의 평균으로 색을 정합니다.

광선 투사와의 차이

눈에서 광선을 쏴서 처음 맞은 물체만 찾고 멈추는 방법은 흔히 광선 투사라고 부릅니다. 그림자 광선이나 반사 광선을 다시 쏘지 않습니다. 광선 추적은 이 광선 투사에 재귀를 더한 것으로 볼 수 있습니다.

같은 이름의 다른 쓰임

렌즈를 설계하는 광학에서도 빛의 경로를 하나씩 따라가는 계산을 광선 추적이라고 부릅니다. 전파나 소리가 건물 사이에서 어떻게 퍼지는지 계산할 때도 같은 이름을 씁니다. 이 문서는 그림을 그리는 쪽을 다룹니다.

관련 항목

광선 추적이 속하는 상위 분류

렌더링 · 그래픽스 · 오프라인 렌더링 · 실시간 렌더링 · 물리 기반 셰이딩

광선 추적과 같은 일을 두고 겨루는 렌더링 방식

래스터화 · 광선 투사 · 광선 행진 · 라디오시티 · 스캔라인 렌더링

광선 추적을 넓힌 렌더링 알고리즘

분산 광선 추적 · 패스 트레이싱 · 양방향 패스 트레이싱 · 포톤 매핑 · 메트로폴리스 광 수송 · 렌더링 방정식 · 몬테카를로

광선 추적이 쏘는 광선의 종류

1차 광선 · 그림자 광선 · 반사 광선 · 굴절 광선

광선이 장면에서 겪는 빛의 현상

반사 · 굴절 · 그림자 · 소프트 섀도 · 전역 조명 · 간접광 · 커스틱 · 스넬의 법칙 · 프레넬 방정식

교차 판정과 셰이딩에 쓰는 기하 계산

교차 판정 · 법선 · 내적 · 벡터 · 이차방정식 · 무게중심 좌표 · 묄러-트럼보어 알고리즘 · 셰이딩 · 램버트 반사 · 재귀

교차 판정 횟수를 줄이는 가속 구조

가속 구조 · BVH · 바운딩 볼륨 · AABB · k-d 트리 · 옥트리 · 균일 격자 · 표면적 휴리스틱

광선 추적이 읽는 장면 데이터

메시 · 프리미티브 · 머티리얼 · 카메라 · 광원 · 픽셀

래스터화가 같은 효과를 내려고 쓰는 기법

깊이 버퍼 · 그림자 매핑 · 환경 매핑 · 스크린 스페이스 반사

광선 추적을 실시간으로 돌리는 하드웨어와 인터페이스

GPU · 레이 트레이싱 파이프라인 · RT 코어 · DXR · Vulkan · OptiX

광선 추적의 화질을 다듬는 기법

안티에일리어싱 · 슈퍼샘플링 · 디노이징 · 중요도 샘플링

다른 이름: ray tracing · raytracing · 레이 트레이싱 · 레이트레이싱 · 광선 추적법