셰이더 드로잉: 레이 트레이싱

인터넷 참고 자료를 이용해 학습한 내용을 정리한 글이다.

레이 트레이싱?

마인크래프트 레이 트레이싱
디즈니가 소개하는 광선 추적

레이 트레이싱이란 용어 자체는 게임 설정 창에서 접한 것이 전부다. 그러나 이는 사실 컴퓨터 그래픽 렌더링 기법 중 하나이며, 빛의 작용을 모사하는 방식으로 작동한다.

빛이 정하는 색상

레이 트레이싱이 무엇인지 알기 전 컴퓨터가 이미지를 그려내는 방식에 대해 알아볼 필요가 있다. 컴퓨터 그래픽 이미지는 2차원으로 투영되어야 하고, 이는 개념적으로 우리 눈을 꼭짓점으로 하며 높이가 시선과 평행한 피라미드의 단면으로 볼 수 있다. 이 개념적인 단면을 이미지 평면(image plane)이라 하며, 3차원 공간이 투영되어 2차원 이미지를 형성하는 무대 역할을 한다는 점에서 마치 사진 필름이나 카메라의 디지털 센서, 혹은 화가의 캔버스와 같다.

3차원 물체를 2차원 이미지로 투영한 후엔 이 물체의 윤곽선 안쪽에 색을 칠해야 한다. 장면(scene) 속 물체의 색은 주로 빛과 물체 재질의 상호작용 속에 결정된다. 빛은 광자(photon)라는 전기와 자기의 성질을 동시에 지닌 전자기 입자로 구성되어 있다. 이 입자들은 에너지를 전달하고 음파처럼 진동하며 전진한다. 광자는 물체에 닿으면 반사, 흡수, 투과될 수 있으며, 이는 물체 재질(material) 속성에 의거한다. 그러나 물체와의 상호작용 이후의 광자 총량이 에너지 보존 법칙에 따라 동일하다는 점은 모든 물체에 보편적으로 적용된다. 예를 들어, 100개의 광자가 물체의 표면을 비추면 흡수 및 반사된 광자의 총합도 100개가 되어야 하는 셈이다.

재질은 크게 두 가지로 분류된다. 금속은 도체(conductors), 나무·유리·플라스틱·물 같은 비금속은 유전체(dielectrics)라 부른다. 유전체는 전기 절연체이며, 순수한 물조차 절연체 역할을 한다. 이러한 물질들은 투명도가 제각각이어서, 완전히 불투명한 것도 있고 X선처럼 특정 파장의 전자기파만 투과시키는 것도 있다. 또한 재질은 여러 특성이 결합된 복합 재질이나 적층 재질로 이루어질 수도 있다. 예를 들어 나무에 투명한 바니시를 코팅하면, 색깔 있는 플라스틱 공과 비슷한 은은하면서도 광택 있는 외관을 얻을 수 있다.

불투명하고 빛을 확산(diffuse, 난반사)시키는 재질을 떠올리면 물체가 어떻게 색을 얻는지 이해하기 쉽다. 빨강·초록·파랑 광자가 섞인 백색광 아래에서 어떤 물체가 붉게 보이려면, 초록과 파랑 광자는 흡수하고 빨강 광자만 반사해야 한다. 우리가 그 물체를 본다는 것은 곧 반사된 빨강 광자가 우리 눈에 도달했다는 뜻이며, 빛이 닿은 표면 지점은 그 광자를 거의 모든 방향으로 흩뿌린다. 이 가운데 우리 눈 쪽으로 향하는 빛만 망막에 들어오고, 광수용체에 의해 신경 신호로 변환된 뒤 뇌에서 처리되어 다양한 색과 명암으로 구분된다.

빛에 대한 이해는 오랜 시간에 걸쳐 점진적으로 발전해 왔다. 고대 그리스 철학자들은 눈에서 빛이 나가 물체에 닿아 작용한다고 보았지만, 11세기 초 아랍 학자 이븐 알하이삼(Ibn al-Haytham)은 저서 『광학의 서(Book of Optics)』에서 빛이 사실 물체로부터 우리 눈에 들어오기 때문에 우리가 물체를 볼 수 있다는, 보다 현대적인 광학 이론을 제시했다.

1647년 묘사된 이븐 알하이삼

이븐 알하이삼이 정리한 빛 연구의 핵심 원리는 두 가지다. 첫째, 빛이 없으면 가시성은 성립하지 않는다. 둘째, 상호작용할 대상(물체)이 없으면 빛 자체도 볼 수 없다. 이는 광자가 거의 방해 없이 지나갈 수 있는데도 어둡게 보이는 우주 공간을 떠올리면 분명해진다. 빛을 반사·산란시킬 물질이 없으면, 광자가 우리 눈으로 들어올 경로 자체가 생기지 않기 때문이다. 즉 광자가 우리에게 보이려면, 광원에서 나와 눈으로 직접 들어오거나, 다른 물체에 부딪혀 반사된 뒤 눈에 도달해야 한다.

순방향 추적(forward tracing)

물체와 빛의 상호작용을 컴퓨터 그래픽에서 다룰 때 또 하나 짚어야 할 물리적 특성이 있다. 물체에서 반사되는 수많은 광선 가운데 우리 눈에 도달하는 것은 극히 일부에 불과하다는 점이다. 단 하나의 광자만을 방출하는 광원을 가정해 보자. 방출된 광자는 물체에 부딪히고, 물체가 이를 흡수하지 않는다면 임의의 방향으로 반사된다. 그 광자가 우리 눈까지 도달했을 때 비로소 우리는 그 물체를 본다.

여기서 광자가 반사되는 ‘임의의 방향’은 단순한 무작위가 아니라, 사실상 모든 방향에 걸쳐 있다고 보는 편이 정확하다. 빛을 받은 물체의 표면은 육안으로는 매끈해 보일지라도, 종이를 현미경으로 들여다보면 거친 섬유 구조가 드러나듯 미시적으로는 매우 울퉁불퉁하다. 광자는 이 미세 구조와 입사각에 따라 반사 방향이 달라지는데, 그 결과 너무도 많은 방향으로 흩어지기 때문에 사실상 가능한 모든 방향으로 반사된다고 보아도 무방하다. 광자-표면 상호작용 시뮬레이션에서 광선을 무작위 방향으로 발사하는 것도 이러한 통계적 분포를 모사하기 위함이다.

컴퓨터 그래픽에서는 이 ‘우리의 눈’을 픽셀로 이루어진 이미지 평면으로 대체한다. 광자는 물체 표면에서 반사되어 각 픽셀에 닿고, 이 과정이 누적되며 물체의 형상이 점차 선명해진다. 이를 순방향 추적(forward tracing)이라고 한다. 광원에서 출발한 광자가 물체를 거쳐 픽셀에 도달하기까지의 경로를 순차적으로 따라가는 방식이다.

다만 이 방식에는 명백한 문제가 있다. 앞서는 반사된 광자가 모두 우리 눈에 들어온다고 가정했지만, 실제로는 광자가 가능한 모든 방향으로 흩어지기 때문에 그중 눈(=픽셀)에 도달하는 것은 극히 일부에 지나지 않는다. 따라서 눈에 닿는 광자를 충분히 모으려면 광원에서 어마어마한 수의 광자를 쏘아야 하며, 이는 자연 현상을 충실히 모방하긴 해도 시뮬레이션의 부담을 크게 가중시킨다.

수천만 개에 달하는 광자를 시뮬레이션해도 물체가 완전히 그려지지 않을 수 있고, 물체를 충분히 정확하게 묘사할 만큼 광자가 쌓였는지를 사람이 주관적으로 판단할 때까지 프로그램을 계속 돌려야 한다는 점이 이 방식의 근본적인 한계다. 렌더링 과정을 줄곧 지켜봐야 한다는 뜻이기도 하다. 게다가 광자를 만들어내는 연산 자체는 가벼울지라도, 각 광자가 어느 물체와 교차하는지를 계산하는 단계에서 엄청난 컴퓨팅 자원이 소모되어 결국 그 부분이 병목이 된다.

컴퓨터 그래픽 연구자 터너 휘티드(Turner Whitted)는 1980년 발표한 논문 「An Improved Illumination Model for Shaded Display」에서 이러한 방식을 다음과 같이 평가한 바 있다.

“In an evident approach to ray tracing, light rays emanating from a source are traced through their paths until they strike the viewer. Since only a few will reach the viewer, this approach could be better. In a second approach suggested by Appel, rays are traced in the opposite direction, from the viewer to the objects in the scene.”
“광선 추적의 직관적인 방식은 광원에서 방출된 광선이 관찰자에게 도달할 때까지 그 경로를 추적하는 것이다. 그러나 관찰자에게 닿는 광선은 극히 일부에 지나지 않기에 이 방법에는 개선의 여지가 있다. 아펠(Appel)이 제안한 두 번째 방식에서는 광선이 반대 방향, 즉 관찰자로부터 장면 속 물체를 향해 추적된다.”

여기서 언급된 아서 아펠(Arthur Appel)은 1968년 논문 「Some techniques for shading machine renderings of solids」에서 각 픽셀(관찰점, 눈)로부터 장면 속으로 광선을 쏘는 방식을 처음 제시한 선행 연구자다. 이제 휘티드가 제안한 레이 트레이싱 방식(Whitted-style ray tracing)을 살펴보자.

역방향 추적(backward tracing)

휘티드가 제시한 이 방식은 역추적(backward tracing) 또는 수용체 추적(eye tracing)이라 불리며, 앞서 살펴본 순방향 추적의 흐름을 그대로 뒤집은 형태다. 눈에서 장면을 향해 광선을 쏘아 어떤 물체에 닿으면, 그 접촉점에서 다시 광원 쪽으로 그림자 광선(shadow ray)이라는 또 다른 광선을 발사해 그 지점에 빛이 닿는지 평가한다. 만약 그림자 광선이 광원에 도달하기 전에 다른 물체에 가로막힌다면, 접촉점은 그늘에 가려져 빛을 받지 못하는 상태라는 뜻이다. 컴퓨터 그래픽 문헌에서는 눈(또는 카메라)에서 장면으로 발사되는 최초의 광선을 기본 광선(primary ray), 가시성 광선(visibility ray), 카메라 광선(camera ray)이라 부른다.

보통 광원에서 눈으로 향하는 추적을 순방향 추적, 그 반대를 역방향 추적이라고 하지만, 자료에 따라 두 용어가 정반대로 쓰이기도 한다. 이런 혼동을 줄이기 위해 광원 추적(light tracing), 수용체 추적(eye tracing)이라는 표현을 쓰기도 한다. 한편 컴퓨터 그래픽에서는 광원이나 눈에서 광선을 쏘아 그 경로를 따라가는 기법을 통틀어 경로 추적(path tracing)이라 부르기도 한다. 경로 추적은 기본적으로 광선 추적(ray tracing)과 같은 범주의 기법이지만, 간접 조명이나 코스틱(caustics) 같은 광학 현상까지 시뮬레이션하는 데 초점이 맞춰진 용어로 보인다.

레이 트레이싱 구현

광선은 시작점(ray origin)으로부터 광선 방향(ray direction)으로 t만큼 뻗어 나간다. 결국 모든 광선 추적은 “이 광선이 도형과 만나는 지점의 t는 무엇인가?”라는 질문에 답하는 일이다.

$$P(t) = O + t \cdot D, \quad t \geq 0$$

변수 의미
O ray origin — 광선의 시작점
D ray direction — 방향 단위벡터
t 광선이 출발지에서 얼마나 갔는가 (거리)
P(t) t만큼 갔을 때의 좌표

평면은 한 점 P0과 법선 벡터 N으로 정의된다. 어떤 점 P가 평명 위에 있다는 것은 P0에서 P로 향하는 벡터가 법선 벡터에 수직이라는 뜻이며 이에 평면 위의 모든 점 P는 다음을 만족한다.

$$(P - P_0) \cdot N = 0$$

광선 P(t) = O + tD 를 대입한 뒤, t에 대해 식을 풀면 아래와 같다.

$$(O + tD - P_0) \cdot N = 0$$

$$t = \frac{(P_0 - O) \cdot N}{D \cdot N}$$

분모 D·N이 0이면 광선이 평면과 평행하다는 뜻이며, 이 경우 광선이 평면을 비껴가거나 평면 안에 완전히 포함된 상태가 된다. t가 음수라면 교차점이 카메라 뒤쪽에 놓인다는 의미이므로 유효한 교차로 보지 않는다. 그 외의 경우는 광선이 평면과 정상적으로 교차한 것이며, 이를 그대로 GLSL 함수로 옮기면 다음과 같다.

// 교차하지 않으면 -1 반환
float intersectPlane(vec3 ro, vec3 rd, vec3 p0, vec3 n) {
    float denom = dot(rd, n);
    if (abs(denom) < 1e-6) return -1.0;   // 광선이 평면과 평행
    float t = dot(p0 - ro, n) / denom;
    return (t > 0.0) ? t : -1.0;          // 카메라 뒤쪽이면 무시
}
놀랍게도 y축으로 1 떨어진 평면을 그린 모습

이제 구와 교차하는 t를 구해보자. 구는 어떤 한 점으로부터 같은 거리만큼 떨어진 점들의 집합이다. 즉 구의 중심 C로부터 구 위의 임의의 점 P까지의 거리는 구의 반지름 r과 같다.

$$|P - C|^2 = r^2$$

평면과 마찬가지로 P에 O+D·t를 대입한 뒤, t에 대해 식을 풀면 된다.

$$|O + tD - C|^2 = r^2$$

표기를 간결하게 하기 위해, 구의 중심 C에서 광선 시작점 O로 향하는 벡터 L = O − C를 정의하자. 그러면 식은 다음과 같이 줄어든다.

$$|L + tD|^2 = r^2$$

벡터의 길이 제곱은 자기 자신과의 내적과 같다. 예를 들어 2차원 벡터 v = (x, y)의 길이 |v|는 sqrt(x*x + y*y)이므로 길이 제곱은 (x*x + y*y) 즉 v끼리의 내적과 같기 때문이다. 따라서 식은 아래와 같다.

$$(L + tD) \cdot (L + tD) = r^2$$
$$L \cdot L + 2t(L \cdot D) + t^2 (D \cdot D) = r^2$$

여기서 D는 단위벡터기에 D·D의 값은 1이며, L·L을 거리 제곱으로 표현한 뒤 식을 정리하면 아래와 같다.

$$t^2 + 2(L \cdot D)t + (|L|^2 - r^2) = 0$$

이는 중학교 때 배운(것 같기도 한) t에 관한 2차 방정식이므로, 근의 공식으로 해를 구할 수 있다.

계수
a 1 (단위벡터 가정)
b 2 (L · D)
c |L|² - r²

판별식 Δ = b² - 4ac

판별식 의미
Δ < 0 해 없음 — 광선이 구를 빗나감
Δ = 0 해 1개 — 광선이 구에 접함
Δ > 0 해 2개 — 광선이 구를 통과 (앞면 + 뒷면)

판별식 b²−4ac가 음수면 광선이 구를 빗나간 것이고, 0이면 접한 것, 양수면 두 점에서 만난 것이다. 두 교차점 중에서는 카메라에 더 가까운 쪽만 필요하므로 작은 쪽 t를 취한다. 이를 GLSL 함수로 옮기면 다음과 같다.

$$t = \frac{-b \pm \sqrt{\Delta}}{2a}$$

// 교차하지 않으면 -1, 교차하면 가까운 쪽 t를 반환
float intersectSphere(vec3 ro, vec3 rd, vec3 c, float r) {
    vec3 L = ro - c;
    float b = 2.0 * dot(L, rd);
    float cc = dot(L, L) - r*r;         // 매개변수 c와 이름 충돌을 피해 cc
    float disc = b*b - 4.0*cc;
    if (disc < 0.0) return -1.0;        // 빗나감
    float t = (-b - sqrt(disc)) / 2.0;  // 두 해 중 작은 쪽 (가까운 교차점)
    return (t > 0.0) ? t : -1.0;        // 카메라 뒤쪽이면 무시
}
놀랍게도 (0, 0, 0) 위치에 반지름이 1인 원을 그린 모습

참고로 rd(광선 방향)을 단위벡터화하지 않아서 잠시 헤메다 GPT에게 물으니 “입문자가 자주 하는 실수: rd 가 단위벡터가 아니면 a = dot(rd, rd) ≠ 1 이라 식이 깨진다. 광선 방향은 항상 normalize 하자.” 라는 답변을 들었다. 또, 토러스나 캡슐 모양의 교차점을 구하려면 4차 방정식까지 풀어야 한다고 하는데 수학 정규교육을 제곱근까지만 받은 나에겐 무리다. 이니고 퀼레즈(Inigo Quilez)가 자신의 사이트에 광선 교차점 함수들을 정리해 놓았으니 앞으로는 이를 사용하는 것이 좋겠다.

조명과 법선 벡터

광선과 도형의 교차점을 알아내긴 했지만, 음영과 반사광이 없는 지금 상태는 사실상 *연산 비용만 많이 드는 원*을 그리고 있는 셈이다. 구처럼 보이게 하려면 조명과 표면의 법선 벡터가 필요하다. 다행히 화면에는 구와 평면밖에 없으므로 가장 단순한 방법으로 충분하다. 구의 경우 중심 C에서 교차점 P를 향하는 벡터를 반지름으로 나누면 그것이 곧 표면의 법선 벡터이고, 평면은 처음부터 법선 벡터로 정의되어 있으므로 따로 구할 필요가 없다.

$${n} = \frac{{p} - {c}}{r}$$

법선 벡터를 색으로

표면에 닿은 빛이 여러 방향으로 흩어지며 반사되는 현상을 난반사(diffuse reflection)라 한다. 이를 구현하는 방법은 여러 가지가 있지만, 여기서는 요한 하인리히 람베르트(Johann Heinrich Lambert)가 제안한 모델을 사용한다. 람베르트 반사율(Lambertian reflectance)은 표면이 모든 방향으로 균일하게 빛을 반사한다고 가정하는 모델로, 표면의 법선 벡터와 표면에서 광원으로 향하는 단위 벡터를 내적하여 계산한다. 참고로 이처럼 어느 방향에서 관측해도 같은 밝기로 보이는 이상적인 표면을 완벽 확산체(Perfect Diffuser), 혹은 람베르트 표면(Lambertian surface)이라 부른다.

렌더러가 한 점 P를 칠한다고 말하지만, 물리적으로는 면적이 0인 점에 빛이 닿는다고 생각하기 어렵다. 컴퓨터 그래픽에서는 그 점 주변의 아주 작은 표면 조각을 생각한다. 그러한 미소 면적이 그림의 dA이며, 해당 표면 조각에 빛이 미치는 영향을 생각해야 한다. 빛이 해당 영역과 수직한다면 해당 영역이 받는 빛 에너지는 가장 강할 것이며, 빛의 각도가 수평에 가까워질 수록 빛이 비추는 표면적은 늘어나며 단위 면적당 받는 빛 에너지는 점차 줄어들다 수평하는 순간 표면은 어떤 빛도 받지 않는 상태가 될 것이다. 해당 표면이 받는 빛의 정도가 곧 해당 표면이 난반사하는 빛의 정도라고 근사하는 셈이다.

광원이 표면의 정면(법선 방향) 위에 있을 때 표면은 가장 밝고, 광원의 위치가 옆으로 비껴갈수록 밝기는 줄어든다. 컴퓨터 그래픽에서는 이러한 물리적 특성을 *벡터의 내적*으로 구현한다. 표면의 법선 N과 표면에서 광원으로 향하는 방향 L 사이의 각을 theta θ라 하자. 두 벡터가 모두 단위 벡터일 때, 둘의 내적은 정의상 N⋅L = cos(⁡θ)다. 따라서 L이 N과 같은 방향을 가리키면 cos(0°)=1 (가장 밝음), 서로 수직이면 cos(90°)=0 (빛을 받지 못하는 상태), 반대 방향이면 cos(180°)=-1이다. 음의 밝기는 현실에 존재하지 않으므로 이 값을 0에서 1사이의 구간으로 잘라내면(clamp), 결과적으로 그 표면 조각이 난반사로 내보내는 빛의 양을 얻게 된다.

이제 표면색(albedo)과 빛의 세기(lightIntensity)를 곱하면 입문용 확산 셰이딩 식이 된다.

$$C = \rho \cdot L_i \cdot \max(0, N \cdot L)$$

vec3 diffuse = albedo * lightColor * lightIntensity * max(dot(N, L), 0.0);

여기서 알베도(albedo)란 라틴어로 ‘희다’에서 유래한 용어로 ‘반사율’을 의미한다고 한다. 빛이 표면에 닿으면 일부는 흡수되고 일부는 반사된다. 예를 들어 빨간 플라스틱 표면의 알베도가 (1.0, 0.0, 0.0)이라면 들어온 흰빛 중에서 빨간 성분은 많이 반사하고, 초록/파랑 성분은 거의 반사하지 않는다는 뜻이다. 보통 그리스 문자 rho(ρ)로 쓰는데, rho = 0이면 완전히 흡수하는 표면이고, rho = 1이면 받은 빛을 전부 반사하는 이상적인 표면이다. 실제 물체의 albedo는 보통 0과 1 사이에 있다.

거울 같은 표면은 빛을 입사각에 따라 특정 방향으로 강하게 반사한다. 반면 완전 확산 표면(perfect diffuse surface, Lambertian surface)은 빛을 표면 위 반구의 모든 방향으로 고르게 퍼뜨리는 이상화된 표면을 의미한다. 이 말은 시점 방향이 식에 직접 들어가지 않는다는 뜻이기도 하다. 람베르트 확산은 관찰자의 관점에 독립적이다. 어떤 방향에서 보든 같은 위치의 확산 밝기는 동일하다고 본다.

완전 확산 표면은 빛을 받은 점에서 표면 아래쪽으로는 빛이 나가지 않고, 위쪽 반구 형태로 빛을 동일하게 반사한다고 본다. 이 때 ρ(알베도, albedo)는 총 반사율 즉 표면이 빛을 반사하는 정도이다. 예를 들어 받은 빛이 100이고 ρ가 0.6이라면 총 60의 빛을 반사하는 셈이다. 여기서 중요한 건, ρ는 반구의 한 방향으로 반사되는 빛의 양이 아니라, 전체 방향을 다 합쳤을 때의 총 반사 비율이라는 점이다.

양방향반사도분포함수(Bidirectional Reflectance Distribution Function, BRDF)는 빛이 들어오는 방향과 표면에서 우리 눈(혹은 카메라)를 향하는 방향을 고려하여 빛을 어떻게 반사하는지 설명하는 함수를 뜻하는데, 빛을 모든 방향으로 균일하게 반사하는 완전 확산 표면에서는 방향에 따라 변하지 않는 상수로 생각한다. 여기서 ρ는 총량인데, BRDF는 방향별 분배값이기 때문에 상수 k = ρ가 될 수 없다.

여기서 더 나아가면 복잡해지는 듯하니 더 자세한 내용은 패스 트레이싱 글에서 물리 기반 렌더링(Physically Based Rendering, PBR)과 함께 다루도록 한다. 또한 지금은 표면 난반사를 훨씬 단순하게 근사하는 방식을 적용할 것이다.

$$C = \max(0, N \cdot L)$$

빛으로 결정되는 픽셀 색상

그림자 광선(shadow ray)

기본 광선이 형태에 교차하는지 구했다면, 해당 교차점으로부터 광원을 향하는 광선을 방출할 필요가 있다. 이를 그림자 광선(shadow ray)라 칭한다.

그림자 광선은 기본 광선과 물체의 교차 지점 p로부터 법선 벡터 방향으로 약간 떨어져 발사되어야 한다. 표면 p에서 바로 ray를 쏘면 수치 오차 때문에 자기 자신과 다시 교차하는 문제가 생길 수 있기 때문이다. 참고로 이처럼 약간 띄울 때 사용되는 아주 작은 양수를 엡실론(epsilon)이라 부른다고 한다.

이렇게 광원을 향해 발사된 그림자 광선은 광원과 p 사이에 다른 교차가 발생하는지, 즉 다른 물체가 존재하는지, 그리고 그 물체와의 거리가 광원과의 거리보다 가까운지 검사한다. 만약 광원보다 가까운 지점에서 교차가 발생한다면 해당 p는 그림자 진 것으로 처리하는 것이다.

그림자가 적용된 이미지

퐁 반사 모델(Phong reflection model)

람베르트는 표면에 부드러운 음영을 만들었다면 퐁(Bui Tuong Phong)은 거기에 하이라이트를 더한다. 1975년에 그가 제시한 빛의 반사를 근사하는 방식을 퐁 반사 모델(Phong reflection model, Phong lighting, Phong illumonation)이라고 하며, 1977년엔 블린(Jim Blinn)에 의해 개선된 블린–퐁 반사 모델(Blinn–Phong reflection model)이 등장하였다.

퐁 모델은 빛이 반사되는 현상을 환경광(ambient), 난반사(diffuse), 정반사(specular) 세 가지 성분으로 각각 계산한 다음 그걸 다 더하여 빛의 작용을 근사한다. 우선 환경광은 현실에서 빛이 이리저리 튕기며 그늘진 곳도 어느 정도 밝은 현상을 모사하는 값이며, 퐁 모델에선 그냥 모든 곳에 기본적으로 깔려 있는 아주 약한 빛이 있다고 치는 환경광 계수로 표현된다. 난반사 값은 반구 전체의 면적을 적분하고 π로 나누는 복잡한 과정 없이 빛 방향과 법선 벡터의 내적만을 사용하여 해당 표면이 빛을 얼마나 받고 있는지만 계산에 포함한다.

그림을 그릴 때 아주 하얀 물감을 사용해야 하는 경우가 있다. 대개 표면이 매끄러운 물체의 하이라이트를 그리기 위해서이며, 이는 컴퓨터 그래픽에서도 적용된다. 광택점(specular)은 표면에서 빛이 거울처럼 반사되어 시선에 들어올 때 보인다. 퐁 모델에서는 이를 입사각에 따른 반사 벡터와 표면에서 우리 눈(카메라)를 향하는 벡터의 내적을 발광의 날카로움 계수(shineness)만큼 거듭제곱하여 계산한다. 이는 반사된 빛이 우리의 눈 방향으로 얼마나 정렬되어 들어오는지 구하는 것이며, 발광 계수로 거듭제곱하는 이유는 0부터 1 사이의 내적값이 1에서 조금만 멀어져도 빠르게 감소하게 하여 발광 영역을 날카롭게 하기 위함이다.

$$\mathrm{Specular} = (\vec R \cdot \vec V)^n$$

이를 위해 빛의 반사 방향을 알아야 하는데, GLSL에 해당 기능을 구현한 reflect라는 내장 함수가 존재한다. 해당 함수는 입사 벡터와 법선 벡터(단위 벡터)를 입력받아 반사 벡터를 반환한다. 더 구체적으로는 입사 벡터와 법선 벡터를 내적한 뒤 법선 벡터를 곱해 법선 벡터 방향으로 프로젝션된 벡터를 입사 벡터에 두 번 빼 주어 반사 벡터를 구한다.

$$\vec R = \vec I - 2(\vec I \cdot \vec N)\vec N$$

$$= reflect(\vec I, \vec N)$$

블린–퐁 모델은 퐁 모델에서 스페큘러의 계산 방식을 조금 바꾼 버전이다. 퐁 모델은 반사 방향 R과 시선 방향 V를 비교한다. 블린–퐁 모델은 여기서 반사 방향 R 대신 중간 방향 벡터(half vector) H를 사용한다. 길이가 서로 다를 수 있는 일반 벡터의 덧셈은 두 벡터를 인접한 변으로 하는 평행사변형의 대각선 방향을 만든다. 하지만 일반적인 평행사변형의 대각선은 두 벡터 사이의 각도를 이등분하지 않는다. 반면 두 벡터의 길이가 같으면 그 평행사변형은 마름모가 되고, 마름모의 대각선은 꼭짓점의 각도를 이등분한다. 따라서 단위벡터끼리 더하면 두 방향 사이의 각도를 이등분하는 방향이 나오며, 그 결과를 다시 단위화하면 중간 방향 벡터를 얻을 수 있다.

$$\vec H = \mathrm{normalize}(\vec L + \vec V)$$

그렇게 얻은 중간 방향 벡터 H와 표면 법선 벡터 N을 내적하여 정반사 강도를 구할 수 있다. 이상적인 거울 반사 상황에서는 법선 N이 광원 입사 벡터 L과 표면에서 시선을 향하는 벡터 V 사이를 반으로 가른다. 이 표면의 법선 N이, 빛과 카메라 사이의 이상적인 중간 방향 H와 얼마나 가까운지 구하는 것이다. 빛과 시선의 거울선상이 가장 반사가 강하게 되는 구간이며, 반사 벡터를 직접 구하지 않고 빛과 시선의 이상적인 거울선을 이용해 현재 표면 방향이 빛을 카메라로 반사하기 좋은 방향인지 구한다는 것 외엔 퐁 모델과 동일하다.

환경 값, 난반사 값, 그리고 정반사 값을 더한 결과
알베도 값과 레이가 교차하지 않을 때 배경 값을 적용

림 라이트(rim light)와 프레넬(fresnel)

림 라이트(rim light, 역광)는 물체의 가장자리, 즉 시선이 표면을 스치듯 보는 부분을 밝히는 기법이다. 사진과 영화에서는 실제 백라이트로 연출하지만, 셰이더에서는 보통 dot(N, V)를 이용한다. N은 물체 표면의 법선 벡터, V는 표면에서 눈(카메라)를 향하는 방향 벡터다. 즉 법선이 카메라를 똑바로 향할 때 1, 수직할 때 0, 반대라면 -1을 반환한다. 이를 이용해 물체의 가장자리를 구하는 것이 림 라이트 구현의 기본 방식이다. 보통 아래와 같은 방식으로 구현한다.

float rim = pow(1.0 - max(dot(N, V), 0.0), power);
vec3 rimCol = rim * color * strength;
알베도 값 vec3(1.0), 림 라이트를 적용한 모습

순수 림 라이트는 빛의 방향과 무관하게 카메라 방향의 영향만을 받기에 빛이 앞에서 오든 뒤에서 오든 항상 가장자리가 빛난다. 물리적인 백라이트 느낌을 원하면 빛 방향도 섞는다. 뒤쪽에서 오는 빛의 영향만을 허용하려면 표면에서 광원을 향하는 벡터 L과 -V의 내적을 이용하면 된다. 이와 더불어 림 라이트가 붕 떠 보이는 것을 방지하기 위해 어두운 면에만 적용하기도 한다. 이 둘을 함께 사용하면 실제로 뒤에서 빛이 와서 어두운 가장자리가 살짝 빛나는 느낌을 낼 수 있다. 그리고 림 라이트 값 자체를 이용해 형태 아웃라인 또한 그려볼 수 있다.

float rim = pow(1.0 - max(dot(norm, V), 0.0), 3.0);
float backLight = max(dot(lDir, -V), 0.0);
float darkSide = 1.0 - max(dot(norm, lDir), 0.0);

finalCol += rim * backLight * darkSide * vec3(0.35, 0.65, 1.0);
float rim = pow(1.0 - max(dot(norm, V), 0.0), 3.0);
float edge = smoothstep(0.55, 0.75, rim);
finalCol += edge * vec3(0.4, 0.7, 1.0);

오른쪽 사진의 평면 형태의 경계선은 제대로 그려지지 않는 것을 확인할 수 있는데, 이는 수평선을 따라 무한한 평면이기에 화면의 아주 넓은 영역에 걸쳐 N과 V 사이의 각도 변화가 극도로 완만하게 일어나기 때문이다.

프레넬(fresnel)은 표면을 보는 각도에 따라 반사율이 달라지는 현상을, 정확히는 공기 ↔ 물 처럼 두 매질의 경계에서 생기는 반사 법칙을 의미한다. 물, 유리, 플라스틱, 금속 모두 이 성질을 가지고 있으며, 정면으로 보면 덜 반사되고, 표면을 스치듯 보면 더 반사된다. 예를 들어, 호수 바로 아래를 내려다보면 물속이 보이지만 멀리 있는 수면을 낮은 각도로 보면 하늘이 거울처럼 비친다. 정면에 가까운 각도에선 빛이 매질 안으로 투과되거나 산란되는 비중이 크고, 표면을 스치는 각도에선 더 많이 반사되는 것이다. 렌더링에서 이 각도는 보통 dot(N, V)로 잡는다.

빛이 표면에 닿으면 일부는 반사되고 일부는 표면 안쪽으로 들어간다. 프레넬 F는 이 중 빛이 반사되는 비율을 의미한다. 예를 들어 F = 0.04라면 4%는 표면에서 반사되고, 96%는 재질 내부로 들어간 셈이다. 단, 이 96%가 반드시 “투명하게 통과한다”는 뜻은 아니다. 굴절 되어 통과되거나 난반사 될 수 있는 것이다. 여기서 난반사는 일상적으로 표면에서 이리저리 반사되는 것으로 설명하지만, 엄격하게는 물질 안으로 들어갔다가 산란되어 다시 나온 빛으로 보는 것이 정확하다. 즉, F는 정말 표면에서 바로 거울처럼 튕긴 빛이며, 프레넬은 그 F를 정하는 함수다.

F0은 정면에서 봤을 때의 반사율이다. 유리나 플라스틱 같은 비금속은 보통 정면 반사율이 낮다. 일반적으로 두 매질 n1, n2 사이의 정면 반사율 F0은 아래와 같이 구할 수 있다.

매질 IOR 대략적인 F0
공기 1.00 0.00
1.33 0.02
피부 1.4 부근 0.028 부근
유리/플라스틱 1.50 0.04
다이아몬드 2.42 0.17

$$F_0 = \left(\frac{n_1 - n_2}{n_1 + n_2}\right)^2$$

여기서 n(Index of Refraction, IOR)은 굴절률이다. 예를 들어 공기에서 유리로 들어갈 때 공기의 n은 1.0, 유리의 n은 1.5이다.

$$F_0 = \left(\frac{1.0 - 1.5}{1.0 + 1.5}\right)^2 = 0.04$$

유리의 F0가 0.04라는 말은 정면에서는 대략 4%만 반사하고 나머지는 안으로 들어간다는 뜻이다. 하지만 가장자리로 갈수록 반사율은 1에 가까워진다. 그래서 유리를 정면으로 보면 투명하지만, 비스듬히 보면 반사가 강해지는 것이다.

표면에서 반사되는 빛의 비율이 F이고, 표면에서 안으로 들어가는 빛의 비율은 1 - F이다. 그 다음 작용은 재질이 결정한다. 예를 들어 유리는 안으로 들어간 빛이 굴절되어 밖으로 나가고, 플라스틱은 난반사되며, 피부는 얕게 산란(귀에 빛을 비추면 붉게 보이는 것이 이와 관련되어있는 듯하다.)된다. 또 비금속은 보통 정면 반사색이 흰색 계열이지만, 금속에서는 빛이 매우 빠르게 흡수되어 난반사가 거의 일어나지 않으며 반사 자체가 색을 가진다. 따라서 금속의 경우 거의 프레넬 만으로 그려내야 한다.

F 값을 구하는 방법 중 가장 정확한 방법은 프레넬 방정식(Fresnel equations)을 푸는 것이다. 하지만 이는 부담스럽기에 실시간 렌더링에서는 주로 슐릭 근사(Schlick’s approximation)를 이용하며 공식은 다음과 같다.

$$F(\theta) = F_0 + (1 - F_0)(1 - \cos\theta)^5$$

float fresnelSchlick(float cosTheta, float F0) {
    float x = clamp(1.0 - cosTheta, 0.0, 1.0);
    return F0 + (1.0 - F0) * pow(x, 5.0);
}

시선과 표면이 이루는 각도, 즉 위 코드의 cosTheta를 구하는 방식 또한 여러 가지이다. 물체의 표면을 완전히 매끈하게 볼 경우 위처럼 표면의 법선 방향과 시선의 방향을 이용하지만, 표면이 각각의 법선 방향에 따라 반사되는 다수의 극소면(여러 방향으로 기울어진 아주 작은 유리 조각)으로 이루어져 있다고 본다면 계산 방식을 달리 해야 한다. 그래서 빛이 정확히 카메라 방향으로 반사되려면 그 빛을 V 방향으로 튕겨 보낼 수 있는 특정 미세면이 필요하며, 그 미세면의 법선 방향이 H이다. 거울 반사에서는 법선이 입사 방향과 반사 방향의 가운데에 있어야 하기에 LV의 중간 방향을 구해야 하며, 이것이 곧 L을 V로 반사시킬 수 있는 미세면의 법선이기 때문이다. 이에 따라 F 또한 N 기준이 아니라 H 기준으로 구하면 된다. 이는 주로 조금 더 사실적인 렌더링에 사용되는 듯하다.

//L 대신 H를 사용
vec3 H = normalize(L + V);
float cosTheta = max(dot(V, H), 0.0);
vec3 F = fresnelSchlick(cosTheta, F0);

반사(reflection)와 굴절(refraction)

작성 중…