**레이 트레이싱: 다음 주**
[Peter Shirley][], [Trevor David Black][], [Steve Hollasch][]
버전 4.0.2, 2025-04-25
Copyright 2018-2024 Peter Shirley. 모든 권리를 보유합니다.
개요
====================================================================================================
주말 동안 배우는 레이 트레이싱에서 여러분은 단순한 무차별 대입 방식의 경로 추적기를 만들었습니다. 이번 편에서는 텍스처, 볼륨(안개 등), 사각형, 인스턴스, 광원을 추가하고, BVH를 사용하여 많은 수의 객체를 지원하도록 하겠습니다. 이 과정을 마치면 "진짜" 레이 트레이서를 갖게 될 것입니다.
저를 포함한 많은 사람들이 믿는 레이 트레이싱의 경험적 법칙이 하나 있는데, 대부분의 최적화는 코드를 복잡하게 만들 뿐 속도는 그다지 향상시키지 못한다는 것입니다. 이 작은 책에서 저는 설계상의 결정을 내릴 때마다 가장 단순한 접근 방식을 택할 것입니다. 프로젝트와 관련된 추가 자료는 [추가 읽을거리 위키 페이지][wiki-further]를 참고하십시오. 그러나 섣부른 최적화는 하지 않기를 강력히 권합니다. 실행 시간 프로파일에서 상위에 나타나지 않는다면, 모든 기능이 지원될 때까지는 최적화할 필요가 없습니다!
이 책에서 가장 어려운 두 부분은 BVH와 펄린 텍스처입니다. 이 때문에 제목에서 이 작업에 주말이 아니라 일주일을 들이라고 제안하는 것입니다. 하지만 주말 프로젝트를 원한다면 이 두 부분을 마지막으로 미뤄도 됩니다. 이 책에서 소개하는 개념들은 순서가 그다지 중요하지 않으며, BVH와 펄린 텍스처가 없어도 여전히 코넬 박스를 얻을 수 있습니다!
이 프로젝트, GitHub 저장소, 디렉터리 구조, 빌드 및 실행, 수정 사항과 기여를 작성하거나 참조하는 방법에 대한 정보는 [프로젝트 README][readme] 파일을 참고하십시오.
이 책들은 브라우저에서 바로 인쇄해도 보기 좋도록 서식이 지정되어 있습니다. 또한 [각 릴리스마다][releases] "Assets" 섹션에 각 책의 PDF를 포함하고 있습니다.
이 프로젝트에 도움을 주신 모든 분께 감사드립니다. 그분들은 이 책 마지막의 감사의 말 절에서 확인하실 수 있습니다.
모션 블러
====================================================================================================
레이 트레이싱을 하기로 결정했을 때, 여러분은 시각적 품질이 실행 시간보다 더 가치 있다고 결정한 것입니다. 퍼지 반사와 디포커스 블러를 렌더링할 때 우리는 픽셀당 여러 개의 샘플을 사용했습니다. 일단 그 길에 한 걸음 내디뎠다면, 좋은 소식은 거의 _모든_ 효과를 비슷하게 무차별 대입 방식으로 처리할 수 있다는 것입니다. 모션 블러도 분명 그중 하나입니다.
실제 카메라에서는 셔터가 짧은 시간 동안 열려 있으며, 그동안 카메라와 세계 속 물체들이 움직일 수 있습니다. 이러한 카메라 촬영을 정확하게 재현하기 위해, 우리는 셔터가 세계를 향해 열려 있는 동안 카메라가 감지하는 것의 평균을 구하고자 합니다.
시공간 레이 트레이싱 소개
--------------------------------------
셔터가 열려 있는 동안 임의의 한 순간에 광선 하나를 보내면 (단순화한) 광자 하나에 대한 무작위 추정값을 얻을 수 있습니다. 그 순간에 물체가 어디에 있어야 하는지만 알 수 있다면, 같은 순간에 그 광선이 전달하는 빛을 정확하게 측정할 수 있습니다. 이것은 무작위(몬테카를로) 레이 트레이싱이 결국 꽤 단순해진다는 것을 보여 주는 또 하나의 예입니다. 이번에도 무차별 대입 방식이 이깁니다!
레이 트레이서의 “엔진”이 각 광선마다 물체가 있어야 할 위치에 있도록 보장해 주기만 하면 되므로, 교차 계산의 핵심 부분은 크게 바뀌지 않습니다. 이를 위해 각 광선의 정확한 시간을 저장해야 합니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class ray {
public:
ray() {}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
ray(const point3& origin, const vec3& direction, double time)
: orig(origin), dir(direction), tm(time) {}
ray(const point3& origin, const vec3& direction)
: ray(origin, direction, 0) {}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
const point3& origin() const { return orig; }
const vec3& direction() const { return dir; }
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
double time() const { return tm; }
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
point3 at(double t) const {
return orig + t*dir;
}
private:
point3 orig;
vec3 dir;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
double tm;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [time-ray]: [ray.h] 시간 정보를 담은 광선]
시간 관리하기
--------------
계속하기 전에 시간에 대해, 그리고 하나 이상의 연속된 렌더에 걸쳐 시간을 어떻게 관리할 수 있을지 생각해 보겠습니다. 셔터 타이밍에는 생각해 볼 두 가지 측면이 있습니다: 셔터가 한 번 열린 때부터 다음에 열릴 때까지의 시간, 그리고 각 프레임마다 셔터가 열려 있는 시간입니다. 예전에 표준 영화 필름은 초당 24프레임으로 촬영되었습니다. 현대 디지털 영화는 초당 24, 30, 48, 60, 120프레임일 수도 있고, 감독이 원하는 만큼의 프레임일 수도 있습니다.
각 프레임은 고유한 셔터 속도를 가질 수 있습니다. 이 셔터 속도는 전체 프레임의 최대 지속 시간일 필요가 없으며 -- 일반적으로도 그렇지 않습니다 -- . 매 프레임마다 셔터를 1/1000초 동안 열 수도 있고, 1/60초 동안 열 수도 있습니다.
이미지 시퀀스를 렌더링하려면 적절한 셔터 타이밍, 즉 프레임 간 주기, 셔터/렌더 지속 시간, 전체 프레임 수(전체 촬영 시간)로 카메라를 설정해야 합니다. 카메라가 움직이고 월드가 정적이라면 그대로 진행하면 됩니다. 그러나 월드 안에서 무언가가 움직인다면, 모든 물체가 현재 프레임의 시간 구간을 알 수 있도록 `hittable`에 메서드를 추가해야 합니다. 이 메서드는 모든 애니메이션 물체가 해당 프레임 동안의 움직임을 설정할 수 있는 방법을 제공합니다.
이는 꽤 간단하며, 원하신다면 직접 실험해 볼 만한 분명히 재미있는 방향입니다. 그러나 지금 우리의 목적을 위해서는 훨씬 더 단순한 모델로 진행하겠습니다. 시간 = 0에서 시작하여 시간 = 1에서 끝난다고 암묵적으로 가정하고, 단일 프레임만 렌더링하겠습니다. 첫 번째 작업은 $[0,1]$ 범위의 무작위 시간으로 광선을 발사하도록 카메라를 수정하는 것이고, 두 번째 작업은 애니메이션되는 구 클래스를 만드는 것입니다.
모션 블러를 시뮬레이션하도록 카메라 업데이트하기
--------------------------------------------
시작 시간과 종료 시간 사이의 무작위 순간에 광선을 생성하도록 카메라를 수정해야 합니다. 카메라가 시간 구간을 추적해야 할까요, 아니면 광선을 생성할 때 카메라 사용자에게 맡겨야 할까요? 확신이 서지 않을 때 저는 호출이 단순해진다면 생성자를 복잡하게 만드는 편을 선호하므로 카메라가 추적하도록 하겠지만, 이는 개인적인 취향입니다. 지금은 카메라가 움직일 수 없고 일정 시간 동안 광선을 내보내기만 하므로, 카메라에는 많은 변경이 필요하지 않습니다.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class camera {
...
private:
...
ray get_ray(int i, int j) const {
// Construct a camera ray originating from the defocus disk and directed at a randomly
// sampled point around the pixel location i, j.
auto offset = sample_square();
auto pixel_sample = pixel00_loc
+ ((i + offset.x()) * pixel_delta_u)
+ ((j + offset.y()) * pixel_delta_v);
auto ray_origin = (defocus_angle <= 0) ? center : defocus_disk_sample();
auto ray_direction = pixel_sample - ray_origin;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
auto ray_time = random_double();
return ray(ray_origin, ray_direction, ray_time);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [time-camera]: [camera.h] 시간 정보를 담은 카메라]
움직이는 구 추가하기
----------------------
이제 움직이는 물체를 만들어 보겠습니다. 구의 중심이 시간=0일 때의 `center1`에서 시간=1일 때의 `center2`까지 선형으로 이동하도록 구 클래스를 업데이트하겠습니다. (그 시간 구간 밖에서도 무한히 계속 이동하므로 실제로 어느 시간에서든 샘플링할 수 있습니다.) 이를 위해 3D 중심점을 시간=0일 때의 원래 위치와 시간=1일 때의 끝 위치까지의 변위를 나타내는 3D 광선으로 대체하겠습니다.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class sphere : public hittable {
public:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
// Stationary Sphere
sphere(const point3& static_center, double radius, shared_ptr mat)
: center(static_center, vec3(0,0,0)), radius(std::fmax(0,radius)), mat(mat) {}
// Moving Sphere
sphere(const point3& center1, const point3& center2, double radius,
shared_ptr mat)
: center(center1, center2 - center1), radius(std::fmax(0,radius)), mat(mat) {}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
private:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
ray center;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
double radius;
shared_ptr mat;
};
#endif
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [moving-sphere]: [sphere.h] 움직이는 구]
업데이트된 `sphere::hit()` 함수는 이전 `sphere::hit()` 함수와 거의 동일합니다. 이제 애니메이션되는 중심의 현재 위치만 결정하면 됩니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class sphere : public hittable {
public:
...
bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
point3 current_center = center.at(r.time());
vec3 oc = current_center - r.origin();
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
auto a = r.direction().length_squared();
auto h = dot(r.direction(), oc);
auto c = oc.length_squared() - radius*radius;
...
rec.t = root;
rec.p = r.at(rec.t);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
vec3 outward_normal = (rec.p - current_center) / radius;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
rec.set_face_normal(r, outward_normal);
get_sphere_uv(outward_normal, rec.u, rec.v);
rec.mat = mat;
return true;
}
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [moving-sphere-hit]: [sphere.h] 움직이는 구의 충돌 판정 함수]
광선 교차 시간 추적하기
--------------------------------------
이제 광선에 시간 속성이 생겼으므로, 교차 시간을 반영하도록 `material::scatter()` 메서드를 업데이트해야 합니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class lambertian : public material {
...
bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered)
const override {
auto scatter_direction = rec.normal + random_unit_vector();
// Catch degenerate scatter direction
if (scatter_direction.near_zero())
scatter_direction = rec.normal;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
scattered = ray(rec.p, scatter_direction, r_in.time());
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
attenuation = albedo;
return true;
}
...
};
class metal : public material {
...
bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered)
const override {
vec3 reflected = reflect(r_in.direction(), rec.normal);
reflected = unit_vector(reflected) + (fuzz * random_unit_vector());
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
scattered = ray(rec.p, reflected, r_in.time());
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
attenuation = albedo;
return (dot(scattered.direction(), rec.normal) > 0);
}
...
};
class dielectric : public material {
...
bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered)
const override {
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
scattered = ray(rec.p, direction, r_in.time());
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
return true;
}
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [material-time]: [material.h] material::scatter() 메서드에서 광선 시간 처리하기]
모든 것을 종합하기
----------------------------
아래 코드는 이전 책 마지막 장면의 예제 확산 구들을 가져와서, 이미지를 렌더링하는 동안 움직이게 합니다. 각 구는 시간 $t=0$에서의 중심 $\mathbf{C}$로부터 시간 $t=1$에서의 $\mathbf{C} + (0, r/2, 0)$로 이동합니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
int main() {
hittable_list world;
auto ground_material = make_shared(color(0.5, 0.5, 0.5));
world.add(make_shared(point3(0,-1000,0), 1000, ground_material));
for (int a = -11; a < 11; a++) {
for (int b = -11; b < 11; b++) {
auto choose_mat = random_double();
point3 center(a + 0.9*random_double(), 0.2, b + 0.9*random_double());
if ((center - point3(4, 0.2, 0)).length() > 0.9) {
shared_ptr sphere_material;
if (choose_mat < 0.8) {
// diffuse
auto albedo = color::random() * color::random();
sphere_material = make_shared(albedo);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
auto center2 = center + vec3(0, random_double(0,.5), 0);
world.add(make_shared(center, center2, 0.2, sphere_material));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
} else if (choose_mat < 0.95) {
...
}
...
camera cam;
cam.aspect_ratio = 16.0 / 9.0;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
cam.image_width = 400;
cam.samples_per_pixel = 100;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
cam.max_depth = 50;
cam.vfov = 20;
cam.lookfrom = point3(13,2,3);
cam.lookat = point3(0,0,0);
cam.vup = vec3(0,1,0);
cam.defocus_angle = 0.6;
cam.focus_dist = 10.0;
cam.render(world);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [scene-spheres-moving]: [main.cc] 이전 책의 최종 장면에 움직이는 구를 적용한 것]
그러면 다음과 같은 결과를 얻습니다:

경계 볼륨 계층 구조
====================================================================================================
이 부분은 우리가 작업 중인 레이 트레이서에서 단연코 가장 어렵고 복잡한 부분입니다. 코드가 더 빠르게 실행될 수 있도록, 그리고 이 부분이 `hittable`을 약간 리팩터링하기 때문에 이 장에 넣었습니다. 이렇게 하면 나중에 직사각형과 상자를 추가할 때 다시 돌아가서 리팩터링할 필요가 없습니다.
광선-물체 교차는 레이 트레이서에서 시간을 가장 많이 잡아먹는 주요 병목이며, 실행 시간은 물체의 수에 선형적으로 비례합니다. 하지만 이는 같은 장면에 대해 반복되는 탐색이므로, 이진 탐색과 같은 방식으로 로그 시간 탐색으로 만들 수 있어야 합니다. 같은 장면에 수백만에서 수십억 개의 광선을 보내므로, 장면의 물체들을 정렬해 두면 각 광선 교차는 선형 미만의 탐색이 될 수 있습니다. 가장 일반적인 두 가지 정렬 방법은 1) 공간을 분할하는 것과 2) 물체를 분할하는 것입니다. 후자는 보통 코드로 작성하기가 훨씬 쉽고, 대부분의 모델에서 실행 속도도 그에 못지않게 빠릅니다.
핵심 아이디어
-------------
프리미티브 집합에 대한 경계 볼륨을 만드는 핵심 아이디어는 모든 물체를 완전히 둘러싸는(경계 짓는) 볼륨을 찾는 것입니다. 예를 들어, 10개의 물체를 둘러싸는 구를 계산했다고 가정해 보겠습니다. 경계 구를 빗나가는 광선은 그 안에 있는 10개의 물체도 모두 확실히 빗나갑니다. 광선이 경계 구에 맞으면, 10개의 물체 중 하나에 맞을 수도 있습니다. 따라서 경계 코드는 항상 다음과 같은 형태입니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
if (ray hits bounding object)
return whether ray hits bounded objects
else
return false
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
이 경계 볼륨들은 장면의 물체들을 하위 그룹으로 묶는 데 사용한다는 점에 유의하십시오. 화면이나 장면 공간을 나누는 것이 *아닙니다*. 경계 볼륨끼리는 겹칠 수 있지만, 각 물체는 오직 하나의 경계 볼륨에만 속하도록 합니다.
경계 볼륨의 계층 구조
--------------------------------
선형 미만으로 만들려면 경계 볼륨을 계층적으로 구성해야 합니다. 예를 들어, 물체 집합을 빨간색과 파란색의 두 그룹으로 나누고 직사각형 경계 볼륨을 사용한다면 다음과 같습니다:
![Figure [bvol-hierarchy]: 경계 볼륨 계층 구조](../images/fig-2.01-bvol-hierarchy.jpg)
파란색과 빨간색 경계 볼륨은 보라색 경계 볼륨 안에 포함되지만 서로 겹칠 수 있으며, 순서가 정해져 있지 않다는 점에 유의하십시오 -- 둘 다 단지 안에 들어 있을 뿐입니다. 따라서 오른쪽에 표시된 트리에서는 왼쪽 자식과 오른쪽 자식 사이에 순서라는 개념이 없습니다. 자식들은 단순히 안에 들어 있을 뿐입니다. 코드는 다음과 같습니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
if (hits purple)
hit0 = hits blue enclosed objects
hit1 = hits red enclosed objects
if (hit0 or hit1)
return true and info of closer hit
return false
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
축 정렬 경계 상자(AABB)
------------------------------------
이 모든 것이 작동하도록 하려면 나쁜 분할이 아닌 좋은 분할을 만드는 방법과 광선을 경계 볼륨과 교차시키는 방법이 필요합니다. 광선과 경계 볼륨의 교차 계산은 빨라야 하며, 경계 볼륨은 상당히 촘촘해야 합니다. 실제로 대부분의 모델에서는 축 정렬 상자가 다른 대안(앞에서 언급한 구형 경계 등)보다 더 잘 작동하지만, 다른 유형의 경계 모델을 접하게 된다면 이 설계 선택을 항상 염두에 두어야 합니다.
지금부터는 축 정렬 경계 직육면체(정확하게 말하자면 실제로 이렇게 불러야 합니다)를 _축 정렬 경계 상자_, 즉 AABB라고 부르겠습니다. (코드에서는 "bounding box"를 줄인 "bbox"라는 이름도 보게 될 것입니다.) 광선과 AABB를 교차시키는 데에는 어떤 방법을 사용해도 괜찮습니다. 그리고 우리가 알아야 할 것은 광선이 그것에 맞았는지 여부뿐이며, 교차점이나 법선, 또는 물체를 표시하는 데 필요한 그 어떤 것도 필요하지 않습니다.
대부분의 사람들은 “슬랩(slab)” 방법을 사용합니다. 이는 n차원 AABB가 흔히 “슬랩”이라고 불리는 $n$개의 축 정렬 구간의 교차일 뿐이라는 관찰에 기반합니다. 구간이란 두 끝점 사이에 있는 점들일 뿐이라는 것을 떠올려 보십시오. 예를 들어 $3 \leq x \leq 5$를 만족하는 $x$, 더 간결하게는 $[3,5]$ 안의 $x$입니다. 2D에서 AABB(직사각형)는 두 구간의 겹침으로 정의됩니다:
![Figure [2d-aabb]: 2D 축 정렬 경계 상자](../images/fig-2.02-2d-aabb.jpg)
광선이 하나의 구간에 부딪히는지 판단하려면, 먼저 광선이 경계에 부딪히는지 알아내야 합니다. 예를 들어 1D에서 광선과 두 평면의 교차는 광선 매개변수 $t_0$와 $t_1$을 산출합니다. (광선이 평면과 평행하다면 어떤 평면과의 교차도 정의되지 않습니다.)
![Figure [ray-slab]: 광선-슬랩 교차](../images/fig-2.03-ray-slab.jpg)
광선과 평면의 교차점은 어떻게 찾을까요? 광선은 단지 매개변수 $t$가 주어지면 위치 $\mathbf{P}(t)$를 반환하는 함수로 정의된다는 것을 떠올려 보십시오:
$$ \mathbf{P}(t) = \mathbf{Q} + t \mathbf{d} $$
이 방정식은 x/y/z 세 좌표 모두에 적용됩니다. 예를 들어 $x(t) = Q_x + t d_x$입니다. 이 광선은 다음 방정식을 만족하는 매개변수 $t$에서 평면 $x = x_0$에 부딪힙니다:
$$ x_0 = Q_x + t_0 d_x $$
따라서 교차점에서의 $t$는 다음과 같이 주어집니다
$$ t_0 = \frac{x_0 - Q_x}{d_x} $$
$x_1$에 대해서도 비슷한 식을 얻습니다:
$$ t_1 = \frac{x_1 - Q_x}{d_x} $$
이 1D 수학을 2D 또는 3D 충돌 테스트로 바꾸는 핵심 관찰은 다음과 같습니다. 광선이 모든 평면 쌍으로 둘러싸인 상자와 교차한다면, 모든 $t$-구간이 겹칩니다. 예를 들어 2D에서 초록색과 파란색의 겹침은 광선이 경계 상자와 교차할 때에만 발생합니다:
![Figure [ray-slab-interval]: 광선-슬랩 $t$-구간 겹침
](../images/fig-2.04-ray-slab-interval.jpg)
이 그림에서 위쪽 광선의 구간들은 겹치지 않으므로, 광선이 초록색과 파란색 평면으로 둘러싸인 2D 상자에 부딪히지 _않는다_ 는 것을 알 수 있습니다. 아래쪽 광선의 구간들은 _실제로_ 겹치므로, 아래쪽 광선이 경계 상자에 _실제로_ 부딪힌다는 것을 알 수 있습니다.
AABB와 광선의 교차
------------------------------
다음 의사 코드는 슬랩에서 $t$ 구간들이 겹치는지 판단합니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
interval_x ← compute_intersection_x (ray, x0, x1)
interval_y ← compute_intersection_y (ray, y0, y1)
return overlaps(interval_x, interval_y)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
정말 놀라울 정도로 간단하며, 3D 버전이 위 내용을 간단히 확장한 것이라는 점이 사람들이 슬랩 방법을 좋아하는 이유입니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
interval_x ← compute_intersection_x (ray, x0, x1)
interval_y ← compute_intersection_y (ray, y0, y1)
interval_z ← compute_intersection_z (ray, z0, z1)
return overlaps(interval_x, interval_y, interval_z)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
하지만 처음 보기보다 덜 깔끔하게 만드는 몇 가지 주의 사항이 있습니다. $t_0$와 $t_1$에 대한 1D 방정식을 다시 살펴보겠습니다:
$$ t_0 = \frac{x_0 - Q_x}{d_x} $$
$$ t_1 = \frac{x_1 - Q_x}{d_x} $$
첫째, 광선이 음의 $\mathbf{x}$ 방향으로 진행한다고 가정해 보겠습니다. 위에서 계산한 구간 $(t_{x0}, t_{x1})$은 예를 들어 $(7, 3)$처럼 뒤집혀 있을 수 있습니다. 둘째, 분모 $d_x$가 0이 되어 무한대 값이 나올 수 있습니다. 그리고 광선의 원점이 슬랩 경계 중 하나 위에 있으면 분자와 분모가 모두 0이 될 수 있으므로 `NaN`이 나올 수 있습니다. 또한 IEEE 부동 소수점을 사용할 때 0에는 ± 부호가 붙습니다.
$d_x = 0$인 경우의 좋은 소식은 $t_{x0}$와 $t_{x1}$이 같아진다는 것입니다: $x_0$와 $x_1$ 사이에 있지 않다면 둘 다 +∞이거나 -∞입니다. 따라서 min과 max를 사용하면 올바른 답을 얻을 수 있습니다:
$$ t_{x0} = \min(
\frac{x_0 - Q_x}{d_x},
\frac{x_1 - Q_x}{d_x})
$$
$$ t_{x1} = \max(
\frac{x_0 - Q_x}{d_x},
\frac{x_1 - Q_x}{d_x})
$$
그렇게 했을 때 남는 까다로운 경우는 $d_x = 0$이면서 $x_0 - Q_x = 0$ 또는 $x_1 - Q_x = 0$이어서 `NaN`이 나오는 경우입니다. 이 경우에는 적중 또는 비적중 중 어느 쪽으로든 임의로 해석할 수 있지만, 이 부분은 나중에 다시 살펴보겠습니다.
이제 의사 함수 `overlaps`를 살펴보겠습니다. 구간이 뒤집혀 있지 않다고 가정할 수 있고, 구간들이 겹칠 때 true를 반환하고 싶다고 가정해 보겠습니다. 불리언 함수 `overlaps()`는 $t$ 구간 `t_interval1`과 `t_interval2`의 겹침을 계산하고, 이를 사용하여 그 겹침이 비어 있지 않은지 판단합니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
bool overlaps(t_interval1, t_interval2)
t_min ← max(t_interval1.min, t_interval2.min)
t_max ← min(t_interval1.max, t_interval2.max)
return t_min < t_max
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
그 과정에 `NaN`이 섞여 있으면 비교 연산은 false를 반환하므로, 스치는 경우까지 신경 쓴다면 경계 상자에 약간의 여백을 두어야 합니다(그리고 레이 트레이서에서는 결국 모든 경우가 발생하므로 아마 _그렇게 해야 할_ 것입니다).
이를 위해 먼저 구간을 주어진 양만큼 확장하는 새로운 `interval` 함수 `expand`를 추가하겠습니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class interval {
public:
...
double clamp(double x) const {
if (x < min) return min;
if (x > max) return max;
return x;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
interval expand(double delta) const {
auto padding = delta/2;
return interval(min - padding, max + padding);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
static const interval empty, universe;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [interval-expand]: [interval.h] interval::expand() 메서드]
이제 새로운 AABB 클래스를 구현하는 데 필요한 모든 것이 갖춰졌습니다.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#ifndef AABB_H
#define AABB_H
class aabb {
public:
interval x, y, z;
aabb() {} // The default AABB is empty, since intervals are empty by default.
aabb(const interval& x, const interval& y, const interval& z)
: x(x), y(y), z(z) {}
aabb(const point3& a, const point3& b) {
// Treat the two points a and b as extrema for the bounding box, so we don't require a
// particular minimum/maximum coordinate order.
x = (a[0] <= b[0]) ? interval(a[0], b[0]) : interval(b[0], a[0]);
y = (a[1] <= b[1]) ? interval(a[1], b[1]) : interval(b[1], a[1]);
z = (a[2] <= b[2]) ? interval(a[2], b[2]) : interval(b[2], a[2]);
}
const interval& axis_interval(int n) const {
if (n == 1) return y;
if (n == 2) return z;
return x;
}
bool hit(const ray& r, interval ray_t) const {
const point3& ray_orig = r.origin();
const vec3& ray_dir = r.direction();
for (int axis = 0; axis < 3; axis++) {
const interval& ax = axis_interval(axis);
const double adinv = 1.0 / ray_dir[axis];
auto t0 = (ax.min - ray_orig[axis]) * adinv;
auto t1 = (ax.max - ray_orig[axis]) * adinv;
if (t0 < t1) {
if (t0 > ray_t.min) ray_t.min = t0;
if (t1 < ray_t.max) ray_t.max = t1;
} else {
if (t1 > ray_t.min) ray_t.min = t1;
if (t0 < ray_t.max) ray_t.max = t0;
}
if (ray_t.max <= ray_t.min)
return false;
}
return true;
}
};
#endif
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [aabb]: [aabb.h] 축 정렬 경계 상자 클래스]
Hittable 객체의 경계 상자 구성하기
------------------------------------------
이제 모든 hittable 객체의 경계 상자를 계산하는 함수를 추가해야 합니다. 그런 다음 모든 프리미티브에 대해 상자의 계층 구조를 만들고, 구와 같은 개별 프리미티브는 리프에 위치하게 됩니다.
인수 없이 생성된 `interval` 값은 기본적으로 비어 있다는 점을 떠올려 보십시오. `aabb` 객체는 세 개의 차원 각각에 대해 구간을 가지므로, 각 구간은 기본적으로 비어 있게 되고, 따라서 `aabb` 객체도 기본적으로 비어 있게 됩니다. 따라서 일부 객체는 빈 경계 볼륨을 가질 수 있습니다. 예를 들어, 자식이 없는 `hittable_list` 객체를 생각해 보십시오. 다행히도 우리가 interval 클래스를 설계한 방식 덕분에 수학적으로 모두 잘 맞아떨어집니다.
마지막으로, 일부 객체는 애니메이션될 수 있다는 점을 떠올려 보십시오. 이러한 객체는 time=0부터 time=1까지 전체 움직임 범위에 걸친 경계를 반환해야 합니다.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
#include "aabb.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class material;
...
class hittable {
public:
virtual ~hittable() = default;
virtual bool hit(const ray& r, interval ray_t, hit_record& rec) const = 0;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
virtual aabb bounding_box() const = 0;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [hittable-bbox]: [hittable.h] 경계 상자를 갖춘 Hittable 클래스]
정지해 있는 구의 경우 `bounding_box` 함수는 간단합니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class sphere : public hittable {
public:
// Stationary Sphere
sphere(const point3& static_center, double radius, shared_ptr mat)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
: center(static_center, vec3(0,0,0)), radius(std::fmax(0,radius)), mat(mat)
{
auto rvec = vec3(radius, radius, radius);
bbox = aabb(static_center - rvec, static_center + rvec);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
aabb bounding_box() const override { return bbox; }
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
private:
ray center;
double radius;
shared_ptr mat;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
aabb bbox;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [sphere-bbox]: [sphere.h] 경계 상자가 있는 구]
움직이는 구의 경우, 전체 이동 범위를 감싸는 경계가 필요합니다. 이를 위해 time=0일 때의 구의 상자와 time=1일 때의 구의 상자를 구한 다음, 이 두 상자를 감싸는 상자를 계산할 수 있습니다.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class sphere : public hittable {
public:
...
// Moving Sphere
sphere(const point3& center1, const point3& center2, double radius,
shared_ptr mat)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
: center(center1, center2 - center1), radius(std::fmax(0,radius)), mat(mat)
{
auto rvec = vec3(radius, radius, radius);
aabb box1(center.at(0) - rvec, center.at(0) + rvec);
aabb box2(center.at(1) - rvec, center.at(1) + rvec);
bbox = aabb(box1, box2);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [moving-sphere-bbox]: [sphere.h] 경계 상자가 있는 움직이는 구]
이제 두 개의 상자를 입력으로 받는 새로운 `aabb` 생성자가 필요합니다. 먼저, 이를 위해 새로운 interval 생성자를 추가하겠습니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class interval {
public:
double min, max;
interval() : min(+infinity), max(-infinity) {} // Default interval is empty
interval(double _min, double _max) : min(_min), max(_max) {}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
interval(const interval& a, const interval& b) {
// Create the interval tightly enclosing the two input intervals.
min = a.min <= b.min ? a.min : b.min;
max = a.max >= b.max ? a.max : b.max;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
double size() const {
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [interval-from-intervals]: [interval.h] 두 구간으로부터 만드는 interval 생성자]
이제 이것을 사용하여 두 입력 상자로부터 축 정렬 경계 상자를 만들 수 있습니다.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class aabb {
public:
...
aabb(const point3& a, const point3& b) {
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
aabb(const aabb& box0, const aabb& box1) {
x = interval(box0.x, box1.x);
y = interval(box0.y, box1.y);
z = interval(box0.z, box1.z);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [aabb-from-two-aabb]: [aabb.h] 두 AABB 입력으로부터 만드는 AABB 생성자]
객체 목록의 경계 상자 만들기
--------------------------------------------
이제 `hittable_list` 객체를 갱신하여 자식들의 경계를 계산하겠습니다. 새로운 자식이 추가될 때마다 경계 상자를 점진적으로 갱신하겠습니다.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
#include "aabb.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "hittable.h"
#include
class hittable_list : public hittable {
public:
std::vector> objects;
...
void add(shared_ptr object) {
objects.push_back(object);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
bbox = aabb(bbox, object->bounding_box());
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
aabb bounding_box() const override { return bbox; }
private:
aabb bbox;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [hit-list-bbox]: [hittable_list.h] 경계 상자가 있는 hittable 목록]
BVH 노드 클래스
-------------------
BVH 역시 `hittable`이 될 것입니다 -- `hittable`의 목록과 마찬가지입니다. 실제로는 컨테이너이지만, “이 광선이 당신과 충돌합니까?”라는 질의에 응답할 수 있습니다. 설계상의 한 가지 질문은 트리를 위한 클래스와 트리의 노드를 위한 클래스, 이렇게 두 개의 클래스를 둘 것인지, 아니면 클래스를 하나만 두고 루트를 우리가 가리키는 하나의 노드로 삼을 것인지입니다. `hit` 함수는 꽤 간단합니다: 노드의 상자와 충돌하는지 확인하고, 충돌한다면 자식들을 확인하여 세부 사항을 처리합니다.
저는 가능하다면 단일 클래스 설계를 선호합니다. 다음은 그러한 클래스입니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#ifndef BVH_H
#define BVH_H
#include "aabb.h"
#include "hittable.h"
#include "hittable_list.h"
class bvh_node : public hittable {
public:
bvh_node(hittable_list list) : bvh_node(list.objects, 0, list.objects.size()) {
// There's a C++ subtlety here. This constructor (without span indices) creates an
// implicit copy of the hittable list, which we will modify. The lifetime of the copied
// list only extends until this constructor exits. That's OK, because we only need to
// persist the resulting bounding volume hierarchy.
}
bvh_node(std::vector>& objects, size_t start, size_t end) {
// To be implemented later.
}
bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
if (!bbox.hit(r, ray_t))
return false;
bool hit_left = left->hit(r, ray_t, rec);
bool hit_right = right->hit(r, interval(ray_t.min, hit_left ? rec.t : ray_t.max), rec);
return hit_left || hit_right;
}
aabb bounding_box() const override { return bbox; }
private:
shared_ptr left;
shared_ptr right;
aabb bbox;
};
#endif
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [bvh]: [bvh.h] 경계 볼륨 계층 구조]
BVH 볼륨 분할하기
----------------------
BVH를 포함한 모든 효율화 구조에서 가장 복잡한 부분은 그것을 구축하는 것입니다. 이 작업은 생성자에서 수행합니다. BVH의 멋진 점은 `bvh_node`에 있는 객체 목록이 두 개의 하위 목록으로 나뉘기만 하면 hit 함수가 작동한다는 것입니다. 두 자식이 부모의 경계 상자보다 더 작은 경계 상자를 갖도록 분할이 잘 이루어지면 가장 잘 작동하지만, 이는 속도를 위한 것이지 정확성을 위한 것은 아닙니다. 저는 중간 지점을 택하여, 각 노드에서 하나의 축을 따라 목록을 분할하겠습니다. 단순함을 추구하겠습니다:
1. 무작위로 축을 하나 선택합니다
2. 프리미티브를 정렬합니다 (`using std::sort`)
3. 각 하위 트리에 절반씩 넣습니다
들어오는 리스트의 요소가 두 개일 때는 각 하위 트리에 하나씩 넣고 재귀를 종료합니다. 순회 알고리즘은 매끄러워야 하며 널 포인터를 확인할 필요가 없어야 하므로, 요소가 하나뿐이라면 그것을 복제하여 각 하위 트리에 넣습니다. 요소가 세 개인 경우를 명시적으로 확인하고 재귀를 한쪽만 따라가면 약간 도움이 되겠지만, 어차피 나중에 전체 방법이 최적화될 것이라고 생각합니다. 다음 코드는 아직 정의하지 않은 세 가지 메서드--`box_x_compare`, `box_y_compare`, `box_z_compare`--를 사용합니다.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "aabb.h"
#include "hittable.h"
#include "hittable_list.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
#include
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class bvh_node : public hittable {
public:
...
bvh_node(std::vector>& objects, size_t start, size_t end) {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
int axis = random_int(0,2);
auto comparator = (axis == 0) ? box_x_compare
: (axis == 1) ? box_y_compare
: box_z_compare;
size_t object_span = end - start;
if (object_span == 1) {
left = right = objects[start];
} else if (object_span == 2) {
left = objects[start];
right = objects[start+1];
} else {
std::sort(std::begin(objects) + start, std::begin(objects) + end, comparator);
auto mid = start + object_span/2;
left = make_shared(objects, start, mid);
right = make_shared(objects, mid, end);
}
bbox = aabb(left->bounding_box(), right->bounding_box());
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [bvh-node]: [bvh.h] 경계 볼륨 계층 구조 노드]
여기서는 새로운 함수 `random_int()`를 사용합니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
inline double random_double(double min, double max) {
// Returns a random real in [min,max).
return min + (max-min)*random_double();
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
inline int random_int(int min, int max) {
// Returns a random integer in [min,max].
return int(random_double(min, max+1));
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [random-int]: [rtweekend.h] 범위 내의 임의의 정수를 반환하는 함수]
경계 상자가 존재하는지 확인하는 것은 경계 상자가 없는 무한 평면 같은 것을 전달하는 경우에 대비하기 위함입니다. 우리에게는 그런 프리미티브가 없으므로, 여러분이 그런 것을 추가하기 전까지는 이런 일이 발생하지 않을 것입니다.
상자 비교 함수
-----------------------------
이제 `std::sort()`가 사용하는 상자 비교 함수를 구현해야 합니다. 이를 위해 추가적인 축 인덱스 인수가 주어졌을 때 첫 번째 인수가 두 번째 인수보다 작으면 true를 반환하는 범용 비교자를 만듭니다. 그런 다음 범용 비교 함수를 사용하는 축별 비교 함수를 정의합니다.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class bvh_node : public hittable {
...
private:
shared_ptr left;
shared_ptr right;
aabb bbox;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
static bool box_compare(
const shared_ptr a, const shared_ptr b, int axis_index
) {
auto a_axis_interval = a->bounding_box().axis_interval(axis_index);
auto b_axis_interval = b->bounding_box().axis_interval(axis_index);
return a_axis_interval.min < b_axis_interval.min;
}
static bool box_x_compare (const shared_ptr a, const shared_ptr b) {
return box_compare(a, b, 0);
}
static bool box_y_compare (const shared_ptr a, const shared_ptr b) {
return box_compare(a, b, 1);
}
static bool box_z_compare (const shared_ptr a, const shared_ptr b) {
return box_compare(a, b, 2);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [bvh-x-comp]: [bvh.h] BVH 비교 함수, X축]
이제 새로운 BVH 코드를 사용할 준비가 되었습니다. 무작위 구 장면에 사용해 보겠습니다.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "rtweekend.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
#include "bvh.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "camera.h"
#include "hittable.h"
#include "hittable_list.h"
#include "material.h"
#include "sphere.h"
int main() {
...
auto material2 = make_shared(color(0.4, 0.2, 0.1));
world.add(make_shared(point3(-4, 1, 0), 1.0, material2));
auto material3 = make_shared(color(0.7, 0.6, 0.5), 0.0);
world.add(make_shared(point3(4, 1, 0), 1.0, material3));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
world = hittable_list(make_shared(world));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
camera cam;
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [random-spheres-bvh]: [main.cc] BVH를 사용한 무작위 구]
렌더링된 이미지는 [이미지 1](#image-bouncing-spheres)에 표시된 BVH를 사용하지 않은 버전과 동일해야 합니다. 그러나 두 버전의 실행 시간을 측정해 보면 BVH 버전이 더 빠를 것입니다. 제 경우에는 이전 버전보다 거의 _여섯 배 반_ 빨라졌습니다.
또 다른 BVH 최적화
-------------------------
BVH 최적화의 속도를 조금 더 높일 수 있습니다. 분할 축을 무작위로 선택하는 대신, 가장 많이 세분화되도록 둘러싸는 경계 상자의 가장 긴 축을 분할하겠습니다. 변경 사항은 간단하지만, 이 과정에서 `aabb` 클래스에 몇 가지를 추가하겠습니다.
첫 번째 작업은 BVH 생성자에서 객체 범위의 축 정렬 경계 상자를 구성하는 것입니다. 기본적으로 경계 상자를 비어 있는 상태로 초기화한 다음(`aabb::empty`는 곧 정의하겠습니다) 객체 범위의 각 경계 상자로 확장하여, 이 범위로부터 `bvh_node`의 경계 상자를 구성합니다.
경계 상자를 얻고 나면 분할 축을 가장 긴 변을 가진 축으로 설정합니다. 이를 수행해 주는 함수 `aabb::longest_axis()`가 있다고 가정하겠습니다. 마지막으로, 객체 범위의 경계 상자를 미리 계산하므로 왼쪽과 오른쪽의 합집합으로 경계 상자를 계산하던 원래 줄은 삭제할 수 있습니다.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class bvh_node : public hittable {
public:
...
bvh_node(std::vector>& objects, size_t start, size_t end) {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
// Build the bounding box of the span of source objects.
bbox = aabb::empty;
for (size_t object_index=start; object_index < end; object_index++)
bbox = aabb(bbox, objects[object_index]->bounding_box());
int axis = bbox.longest_axis();
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
auto comparator = (axis == 0) ? box_x_compare
: (axis == 1) ? box_y_compare
: box_z_compare;
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete
bbox = aabb(left->bounding_box(), right->bounding_box());
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [object-span-bbox]: [bvh.h] BVH 객체 범위에 대한 bbox 구성]
이제 비어 있는 `aabb` 코드와 새로운 `aabb::longest_axis()` 함수를 구현하겠습니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class aabb {
public:
...
bool hit(const ray& r, interval ray_t) const {
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
int longest_axis() const {
// Returns the index of the longest axis of the bounding box.
if (x.size() > y.size())
return x.size() > z.size() ? 0 : 2;
else
return y.size() > z.size() ? 1 : 2;
}
static const aabb empty, universe;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
const aabb aabb::empty = aabb(interval::empty, interval::empty, interval::empty);
const aabb aabb::universe = aabb(interval::universe, interval::universe, interval::universe);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [aabb-empty-and-axis]: [aabb.h] 새로운 aabb 상수와 longest_axis() 함수]
이전과 마찬가지로 [이미지 1](#image-bouncing-spheres)과 동일한 결과가 나오지만, 렌더링은 조금 더 빨라집니다. 제 시스템에서는 렌더링 속도가 대략 18% 추가로 향상됩니다. 약간의 추가 작업치고는 나쁘지 않습니다.
텍스처 매핑
====================================================================================================
컴퓨터 그래픽스에서 _텍스처 매핑_ 은 장면 안의 물체에 재질 효과를 적용하는 과정입니다. "텍스처" 부분은 효과를 뜻하고, "매핑" 부분은 한 공간을 다른 공간으로 대응시킨다는 수학적 의미입니다. 이 효과는 색상, 광택, 범프 기하 구조(_범프 매핑_ 이라고 합니다), 심지어 재질의 존재 여부(표면에 잘려 나간 영역을 만들기 위한 것)까지, 어떤 재질 속성이든 될 수 있습니다.
가장 일반적인 유형의 텍스처 매핑은 이미지를 물체 표면에 매핑하여 물체 표면의 각 점에서의 색상을 정의합니다. 실제로는 이 과정을 역으로 구현합니다. 즉, 물체 위의 어떤 점이 주어지면 텍스처 맵이 정의하는 색상을 조회합니다.
먼저 텍스처 색상을 절차적으로 만들고, 상수 색상의 텍스처 맵을 만들겠습니다. 대부분의 프로그램은 상수 RGB 색상과 텍스처를 서로 다른 클래스로 관리하므로 다르게 구현하셔도 좋지만, 저는 어떤 색상이든 텍스처로 만들 수 있다는 점이 매우 좋기 때문에 이 구조를 적극 지지합니다.
텍스처 조회를 수행하려면 _텍스처 좌표_ 가 필요합니다. 이 좌표는 여러 방식으로 정의할 수 있으며, 진행하면서 이 개념을 발전시켜 나가겠습니다. 지금은 2차원 텍스처 좌표를 전달하겠습니다. 관례적으로 텍스처 좌표는 $u$와 $v$로 명명합니다. 상수 텍스처의 경우 모든 $(u,v)$ 쌍이 상수 색상을 반환하므로, 실제로는 좌표를 완전히 무시해도 됩니다. 그러나 다른 텍스처 유형에서는 이 좌표가 필요하므로 메서드 인터페이스에는 그대로 유지합니다.
텍스처 클래스의 주요 메서드는 `color value(...)` 메서드이며, 입력 좌표가 주어지면 텍스처 색상을 반환합니다. 점의 텍스처 좌표 $u$와 $v$를 받는 것 외에도, 나중에 밝혀질 이유로 해당 점의 위치도 함께 제공합니다.
상수 색상 텍스처
-----------------------
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#ifndef TEXTURE_H
#define TEXTURE_H
class texture {
public:
virtual ~texture() = default;
virtual color value(double u, double v, const point3& p) const = 0;
};
class solid_color : public texture {
public:
solid_color(const color& albedo) : albedo(albedo) {}
solid_color(double red, double green, double blue) : solid_color(color(red,green,blue)) {}
color value(double u, double v, const point3& p) const override {
return albedo;
}
private:
color albedo;
};
#endif
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [texture]: [texture.h] 텍스처 클래스]
광선-물체 교차점의 $u,v$ 표면 좌표를 저장하도록 `hit_record` 구조체를 수정해야 합니다.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class hit_record {
public:
vec3 p;
vec3 normal;
shared_ptr mat;
double t;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
double u;
double v;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
bool front_face;
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [hit-record-uv]: [hittable.h] `hit_record`에 $u,v$ 좌표 추가하기]
앞으로 각 유형의 `hittable`마다 주어진 점에 대한 $(u,v)$ 텍스처 좌표를 계산해야 합니다. 이에 대해서는 나중에 자세히 다루겠습니다.
솔리드 텍스처: 체커 텍스처
----------------------------------
솔리드(또는 공간) 텍스처는 3D 공간에서 각 점의 위치에만 의존합니다. 솔리드 텍스처는 공간 안의 특정 물체를 칠하는 것이 아니라, 마치 공간 자체의 모든 점을 칠하는 것처럼 생각할 수 있습니다. 이런 이유로 물체는 위치가 바뀜에 따라 텍스처의 색상 사이를 지나가며 움직일 수 있지만, 보통은 물체와 솔리드 텍스처 사이의 관계를 고정하게 됩니다.
공간 텍스처를 살펴보기 위해 3차원 체커 패턴을 구현하는 공간 `checker_texture` 클래스를 구현하겠습니다. 공간 텍스처 함수는 공간 내의 주어진 위치에 의해 결정되므로, 텍스처 `value()` 함수는 `u`와 `v` 매개변수를 무시하고 `p` 매개변수만 사용합니다.
체크무늬 패턴을 구현하기 위해 먼저 입력 점의 각 성분에 대해 내림(floor)을 계산합니다. 좌표를 절삭할 수도 있지만, 그렇게 하면 값이 0 쪽으로 당겨져 0의 양쪽에서 같은 색이 나오게 됩니다. floor 함수는 항상 값을 왼쪽(음의 무한대 방향)의 정수 값으로 이동시킵니다. 이 세 정수 결과($\lfloor x \rfloor, \lfloor y \rfloor, \lfloor z \rfloor$)가 주어지면 이들의 합을 구하고 그 결과를 2로 나눈 나머지를 계산하며, 이는 0 또는 1이 됩니다. 0은 짝수 색에, 1은 홀수 색에 대응됩니다.
마지막으로, 장면에서 체커 패턴의 크기를 조절할 수 있도록 텍스처에 배율 인자를 추가합니다.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class checker_texture : public texture {
public:
checker_texture(double scale, shared_ptr even, shared_ptr odd)
: inv_scale(1.0 / scale), even(even), odd(odd) {}
checker_texture(double scale, const color& c1, const color& c2)
: checker_texture(scale, make_shared(c1), make_shared(c2)) {}
color value(double u, double v, const point3& p) const override {
auto xInteger = int(std::floor(inv_scale * p.x()));
auto yInteger = int(std::floor(inv_scale * p.y()));
auto zInteger = int(std::floor(inv_scale * p.z()));
bool isEven = (xInteger + yInteger + zInteger) % 2 == 0;
return isEven ? even->value(u, v, p) : odd->value(u, v, p);
}
private:
double inv_scale;
shared_ptr even;
shared_ptr odd;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [checker-texture]: [texture.h] 체크무늬 텍스처]
이 체커의 홀수/짝수 매개변수는 상수 텍스처를 가리킬 수도 있고 다른 절차적 텍스처를 가리킬 수도 있습니다. 이는 1980년대에 Pat Hanrahan이 도입한 셰이더 네트워크의 정신을 따른 것입니다.
절차적 텍스처를 지원하기 위해, `lambertian` 클래스가 색상 대신 텍스처로 동작하도록 확장하겠습니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "hittable.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
#include "texture.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
class lambertian : public material {
public:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
lambertian(const color& albedo) : tex(make_shared(albedo)) {}
lambertian(shared_ptr tex) : tex(tex) {}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered)
const override {
auto scatter_direction = rec.normal + random_unit_vector();
// Catch degenerate scatter direction
if (scatter_direction.near_zero())
scatter_direction = rec.normal;
scattered = ray(rec.p, scatter_direction, r_in.time());
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
attenuation = tex->value(rec.u, rec.v, rec.p);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
return true;
}
private:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
shared_ptr tex;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [lambertian-textured]: [material.h] 텍스처를 사용하는 램버시안 재질]
이것을 메인 장면에 추가하면:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "rtweekend.h"
#include "bvh.h"
#include "camera.h"
#include "hittable.h"
#include "hittable_list.h"
#include "material.h"
#include "sphere.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
#include "texture.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
int main() {
hittable_list world;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
auto checker = make_shared(0.32, color(.2, .3, .1), color(.9, .9, .9));
world.add(make_shared(point3(0,-1000,0), 1000, make_shared(checker)));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
for (int a = -11; a < 11; a++) {
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [checker-example]: [main.cc] 사용 중인 체크무늬 텍스처]
다음과 같은 결과를 얻습니다:

입체 체커 텍스처 렌더링하기
------------------------------------
프로그램에 두 번째 장면을 추가할 것이며, 이 책을 진행하면서 그 이후에도 더 많은 장면을 추가할 것입니다. 이를 돕기 위해, 실행할 때마다 원하는 장면을 선택할 수 있도록 switch 문을 설정하겠습니다. 투박한 방식이지만, 최대한 단순하게 유지하면서 레이 트레이싱에 집중하고자 합니다. 여러분의 레이 트레이서에서는 명령줄 인수를 지원하는 등 다른 방식을 사용하고 싶을 수도 있습니다.
다음은 단일 무작위 구 장면에 맞게 리팩터링한 후의 main.cc 모습입니다. `main()`의 이름을 `bouncing_spheres()`로 바꾸고, 이를 호출하는 새 `main()` 함수를 추가하십시오:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "rtweekend.h"
#include "bvh.h"
#include "camera.h"
#include "hittable.h"
#include "hittable_list.h"
#include "material.h"
#include "sphere.h"
#include "texture.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete
int main() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
void bouncing_spheres() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
hittable_list world;
auto ground_material = make_shared(color(0.5, 0.5, 0.5));
world.add(make_shared(point3(0,-1000,0), 1000, ground_material));
...
cam.render(world);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
int main() {
bouncing_spheres();
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [main-scenes]: [main.cc] 선택된 장면으로 분기하는 main]
이제 체크무늬 구 두 개를 위아래로 겹쳐 놓은 장면을 추가합니다.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "rtweekend.h"
#include "bvh.h"
#include "camera.h"
#include "hittable.h"
#include "hittable_list.h"
#include "material.h"
#include "sphere.h"
#include "texture.h"
void bouncing_spheres() {
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
void checkered_spheres() {
hittable_list world;
auto checker = make_shared(0.32, color(.2, .3, .1), color(.9, .9, .9));
world.add(make_shared(point3(0,-10, 0), 10, make_shared(checker)));
world.add(make_shared(point3(0, 10, 0), 10, make_shared(checker)));
camera cam;
cam.aspect_ratio = 16.0 / 9.0;
cam.image_width = 400;
cam.samples_per_pixel = 100;
cam.max_depth = 50;
cam.vfov = 20;
cam.lookfrom = point3(13,2,3);
cam.lookat = point3(0,0,0);
cam.vup = vec3(0,1,0);
cam.defocus_angle = 0;
cam.render(world);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
int main() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
switch (2) {
case 1: bouncing_spheres(); break;
case 2: checkered_spheres(); break;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [main-two-spheres]: [main.cc] 체크무늬 구 두 개]
다음과 같은 결과를 얻습니다:

결과가 조금 이상해 보인다고 생각할 수도 있습니다. `checker_texture`는 공간 텍스처이므로, 실제로는 3차원 체커 공간을 가로지르는 구의 표면을 보고 있는 것입니다. 이것이 완벽하거나 적어도 충분한 상황은 많습니다. 그러나 다른 많은 상황에서는 물체의 표면에 일관된 효과를 얻고 싶습니다. 그 방법은 다음에 다룹니다.
구의 텍스처 좌표
--------------------------------
단색 텍스처는 좌표를 사용하지 않습니다. 솔리드(또는 공간) 텍스처는 공간상의 한 점의 좌표를 사용합니다. 이제 $u,v$ 텍스처 좌표를 활용할 차례입니다. 이 좌표는 2D 원본 이미지(또는 어떤 2D 매개변수화된 공간)상의 위치를 지정합니다. 이를 얻으려면 3D 물체 표면 위의 임의의 점에 대한 $u,v$ 좌표를 찾는 방법이 필요합니다. 이 매핑은 완전히 임의적이지만, 일반적으로는 표면 전체를 덮으면서 2D 이미지를 어느 정도 합리적인 방식으로 크기 조정하고, 방향을 정하고, 늘일 수 있기를 원할 것입니다. 먼저 구의 $u,v$ 좌표를 구하는 방식을 유도하는 것부터 시작하겠습니다.
구의 경우 텍스처 좌표는 보통 경도와 위도의 어떤 형태, _즉_, 구면 좌표를 기반으로 합니다. 따라서 구면 좌표 $(\theta,\phi)$를 계산하는데, 여기서 $\theta$는 아래쪽 극에서부터 위로 잰 각도(즉, -Y에서부터 위로 잰 각도)이고, $\phi$는 Y축을 중심으로 도는 각도(-X에서 +Z, +X, -Z를 거쳐 다시 -X로)입니다.
$\theta$와 $\phi$를 각각 $[0,1]$ 범위에 있는 텍스처 좌표 $u$와 $v$로 매핑하려 하며, 이때 $(u=0,v=0)$은 텍스처의 왼쪽 아래 모서리에 대응합니다. 따라서 $(\theta,\phi)$에서 $(u,v)$로의 정규화는 다음과 같습니다:
$$ u = \frac{\phi}{2\pi} $$
$$ v = \frac{\theta}{\pi} $$
원점을 중심으로 하는 단위 구 위의 주어진 점에 대해 $\theta$와 $\phi$를 계산하기 위해, 먼저 대응하는 데카르트 좌표에 대한 방정식에서 시작합니다:
$$ \begin{align*}
y &= -\cos(\theta) \\
x &= -\cos(\phi) \sin(\theta) \\
z &= \quad\sin(\phi) \sin(\theta)
\end{align*}
$$
$\theta$와 $\phi$를 구하려면 이 방정식들의 역을 구해야 합니다. 사인과 코사인에 비례하는 임의의 수 쌍을 받아 각도를 반환하는 훌륭한 `` 함수 `std::atan2()` 덕분에, $x$와 $z$를 전달하여($\sin(\theta)$는 상쇄됩니다) $\phi$를 구할 수 있습니다:
$$ \phi = \operatorname{atan2}(z, -x) $$
`std::atan2()`는 $-\pi$에서 $\pi$ 범위의 값을 반환하지만, 그 값은 0에서 $\pi$까지 증가한 뒤 $-\pi$로 뒤집혀 다시 0으로 돌아갑니다. 이는 수학적으로는 올바르지만, 우리는 $u$가 $0$에서 $1/2$까지, 그리고 $-1/2$에서 $0$까지가 아니라 $0$에서 $1$까지의 범위를 갖기를 원합니다. 다행히도,
$$ \operatorname{atan2}(a,b) = \operatorname{atan2}(-a,-b) + \pi, $$
그리고 두 번째 식은 $0$에서 $2\pi$까지 연속적인 값을 냅니다. 따라서 $\phi$를 다음과 같이 계산할 수 있습니다
$$ \phi = \operatorname{atan2}(-z, x) + \pi $$
$\theta$의 유도는 더 간단합니다:
$$ \theta = \arccos(-y) $$
따라서 구의 경우 $(u,v)$ 좌표 계산은 원점을 중심으로 하는 단위 구 위의 점을 받아 $u$와 $v$를 계산하는 유틸리티 함수로 수행됩니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class sphere : public hittable {
...
private:
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
static void get_sphere_uv(const point3& p, double& u, double& v) {
// p: a given point on the sphere of radius one, centered at the origin.
// u: returned value [0,1] of angle around the Y axis from X=-1.
// v: returned value [0,1] of angle from Y=-1 to Y=+1.
// <1 0 0> yields <0.50 0.50> <-1 0 0> yields <0.00 0.50>
// <0 1 0> yields <0.50 1.00> < 0 -1 0> yields <0.50 0.00>
// <0 0 1> yields <0.25 0.50> < 0 0 -1> yields <0.75 0.50>
auto theta = std::acos(-p.y());
auto phi = std::atan2(-p.z(), p.x()) + pi;
u = phi / (2*pi);
v = theta / pi;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [get-sphere-uv]: [sphere.h] get_sphere_uv 함수]
이 함수를 사용하여 히트 레코드의 UV 좌표를 갱신하도록 `sphere::hit()` 함수를 수정하십시오.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class sphere : public hittable {
public:
...
bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
...
rec.t = root;
rec.p = r.at(rec.t);
vec3 outward_normal = (rec.p - current_center) / radius;
rec.set_face_normal(r, outward_normal);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
get_sphere_uv(outward_normal, rec.u, rec.v);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
rec.mat = mat;
return true;
}
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [get-sphere-uv-call]: [sphere.h] 히트 지점으로부터 구한 구의 UV 좌표]
히트 지점 $\mathbf{P}$로부터 표면 좌표 $(u,v)$를 계산합니다. 그런 다음 이 좌표를 사용하여 (대리석 같은) 절차적 솔리드 텍스처를 인덱싱합니다. 또한 이미지를 읽어 들여 2D $(u,v)$ 텍스처 좌표로 그 이미지를 인덱싱할 수도 있습니다.
이미지에서 스케일된 $(u,v)$를 사용하는 직접적인 방법은 $u$와 $v$를 정수로 반올림하여 이를 $(i,j)$ 픽셀로 사용하는 것입니다. 이 방법은 번거롭습니다. 이미지 해상도를 바꿀 때마다 코드를 수정하고 싶지는 않기 때문입니다. 그래서 그 대신, 그래픽스에서 가장 보편적인 비공식 표준 중 하나는 이미지 픽셀 좌표 대신 텍스처 좌표를 사용하는 것입니다. 이는 단지 이미지 내에서의 일종의 비율 위치일 뿐입니다. 예를 들어, 가로 $N_x$, 세로 $N_y$ 크기 이미지의 픽셀 $(i,j)$에 대한 이미지 텍스처 위치는 다음과 같습니다:
$$ u = \frac{i}{N_x-1} $$
$$ v = \frac{j}{N_y-1} $$
이는 단지 비율 위치일 뿐입니다.
텍스처 이미지 데이터에 접근하기
-----------------------------
이제 이미지를 담는 텍스처 클래스를 만들 차례입니다. 저는 제가 가장 좋아하는 이미지 유틸리티인 [stb_image][]를 사용하겠습니다. 이 유틸리티는 이미지 데이터를 32비트 부동소수점 값의 배열로 읽어 들입니다. 이 값들은 각 성분이 [0,1] 범위(검은색에서 완전한 흰색까지)에 있는, 패킹된 RGB일 뿐입니다. 또한 이미지는 선형 색 공간(감마 = 1)으로 로드됩니다 -- 이는 우리가 모든 계산을 수행하는 색 공간입니다.
이미지 파일 로딩을 더욱 쉽게 하기 위해, 이 모든 것을 관리하는 헬퍼 클래스인 `rtw_image`를 제공합니다. 이 클래스는 각 픽셀의 8비트 RGB 바이트 값을 가져오는 헬퍼 함수 -- `pixel_data(int x, int y)` -- 를 제공합니다. 다음 코드는 `stb_image.h` 헤더를 `external`이라는 폴더에 복사해 두었다고 가정합니다. 여러분의 디렉터리 구조에 맞게 조정하십시오.
C나 C++ 이외의 언어로 구현하고 있다면, 비슷한 기능을 제공하는 이미지 로딩 라이브러리를 찾거나 직접 작성해야 합니다.
`image_texture` 클래스는 `rtw_image` 클래스를 사용합니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
#include "rtw_stb_image.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
class checker_texture : public texture {
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
class image_texture : public texture {
public:
image_texture(const char* filename) : image(filename) {}
color value(double u, double v, const point3& p) const override {
// If we have no texture data, then return solid cyan as a debugging aid.
if (image.height() <= 0) return color(0,1,1);
// Clamp input texture coordinates to [0,1] x [1,0]
u = interval(0,1).clamp(u);
v = 1.0 - interval(0,1).clamp(v); // Flip V to image coordinates
auto i = int(u * image.width());
auto j = int(v * image.height());
auto pixel = image.pixel_data(i,j);
auto color_scale = 1.0 / 255.0;
return color(color_scale*pixel[0], color_scale*pixel[1], color_scale*pixel[2]);
}
private:
rtw_image image;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [img-texture]: [texture.h] 이미지 텍스처 클래스]
이미지 텍스처 렌더링하기
----------------------------
저는 웹에서 아무 지구 지도나 하나 가져왔습니다 -- 우리의 목적에는 어떤 표준 투영법이든 괜찮습니다.

다음은 파일에서 이미지를 읽은 다음 이를 확산 재질에 할당하는 코드입니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
void earth() {
auto earth_texture = make_shared("earthmap.jpg");
auto earth_surface = make_shared(earth_texture);
auto globe = make_shared(point3(0,0,0), 2, earth_surface);
camera cam;
cam.aspect_ratio = 16.0 / 9.0;
cam.image_width = 400;
cam.samples_per_pixel = 100;
cam.max_depth = 50;
cam.vfov = 20;
cam.lookfrom = point3(0,0,12);
cam.lookat = point3(0,0,0);
cam.vup = vec3(0,1,0);
cam.defocus_angle = 0;
cam.render(hittable_list(globe));
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
int main() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
switch (3) {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
case 1: bouncing_spheres(); break;
case 2: checkered_spheres(); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
case 3: earth(); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [stbi-load-use]: [main.cc] 지구 텍스처 맵으로 렌더링하기]
모든 색이 텍스처라는 것의 위력이 조금씩 드러나기 시작합니다 -- 램버시안 재질에 어떤 종류의 텍스처든 지정할 수 있으며, 램버시안은 그것을 알 필요가 없습니다.
사진 가운데에 커다란 청록색 구가 나타난다면, `stb_image`가 지구 지도 사진을 찾지 못한 것입니다. 프로그램은 실행 파일과 같은 디렉터리에서 파일을 찾습니다. 반드시 지구 사진을 빌드 디렉터리에 복사하거나, `earth()`가 다른 위치를 가리키도록 다시 작성하십시오.

펄린 노이즈
====================================================================================================
멋져 보이는 솔리드 텍스처를 얻기 위해 대부분의 사람들은 어떤 형태로든 펄린 노이즈를 사용합니다. 이 이름은 발명자인 Ken Perlin(켄 펄린)의 이름을 딴 것입니다. 펄린 텍스처는 다음과 같은 백색 노이즈를 반환하지 않습니다:

대신 흐리게 처리한 백색 노이즈와 비슷한 결과를 반환합니다:

펄린 노이즈의 핵심은 반복 가능하다는 점입니다: 3D 점을 입력으로 받아 항상 동일한 무작위스러운 수를 반환합니다. 가까운 점들은 비슷한 숫자를 반환합니다. 펄린 노이즈의 또 다른 중요한 점은 단순하고 빨라야 한다는 것이며, 그래서 보통 일종의 편법으로 구현됩니다. Andrew Kensler의 설명을 바탕으로 그 편법을 단계적으로 만들어 가겠습니다.
난수 블록 사용하기
-------------------------------
공간 전체를 3D 난수 배열로 타일링하여 블록 단위로 사용할 수도 있습니다. 그러면 반복이 뚜렷하게 보이는 블록 형태의 결과가 나옵니다:

타일링 대신 일종의 해싱을 사용하여 이를 뒤섞어 보겠습니다. 이를 구현하려면 약간의 보조 코드가 필요합니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#ifndef PERLIN_H
#define PERLIN_H
class perlin {
public:
perlin() {
for (int i = 0; i < point_count; i++) {
randfloat[i] = random_double();
}
perlin_generate_perm(perm_x);
perlin_generate_perm(perm_y);
perlin_generate_perm(perm_z);
}
double noise(const point3& p) const {
auto i = int(4*p.x()) & 255;
auto j = int(4*p.y()) & 255;
auto k = int(4*p.z()) & 255;
return randfloat[perm_x[i] ^ perm_y[j] ^ perm_z[k]];
}
private:
static const int point_count = 256;
double randfloat[point_count];
int perm_x[point_count];
int perm_y[point_count];
int perm_z[point_count];
static void perlin_generate_perm(int* p) {
for (int i = 0; i < point_count; i++)
p[i] = i;
permute(p, point_count);
}
static void permute(int* p, int n) {
for (int i = n-1; i > 0; i--) {
int target = random_int(0, i);
int tmp = p[i];
p[i] = p[target];
p[target] = tmp;
}
}
};
#endif
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [perlin]: [perlin.h] 펄린 텍스처 클래스와 함수]
이제 0과 1 사이의 이 부동소수점 값을 받아 회색을 만드는 실제 텍스처를 만들면 다음과 같습니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
#include "perlin.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "rtw_stb_image.h"
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
class noise_texture : public texture {
public:
noise_texture() {}
color value(double u, double v, const point3& p) const override {
return color(1,1,1) * noise.noise(p);
}
private:
perlin noise;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [noise-texture]: [texture.h] 노이즈 텍스처]
이 텍스처를 몇 개의 구에 사용할 수 있습니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
void perlin_spheres() {
hittable_list world;
auto pertext = make_shared();
world.add(make_shared(point3(0,-1000,0), 1000, make_shared(pertext)));
world.add(make_shared(point3(0,2,0), 2, make_shared(pertext)));
camera cam;
cam.aspect_ratio = 16.0 / 9.0;
cam.image_width = 400;
cam.samples_per_pixel = 100;
cam.max_depth = 50;
cam.vfov = 20;
cam.lookfrom = point3(13,2,3);
cam.lookat = point3(0,0,0);
cam.vup = vec3(0,1,0);
cam.defocus_angle = 0;
cam.render(world);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
int main() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
switch (4) {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
case 1: bouncing_spheres(); break;
case 2: checkered_spheres(); break;
case 3: earth(); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
case 4: perlin_spheres(); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [scene-perlin]: [main.cc] 펄린 텍스처를 입힌 두 구가 있는 장면]
그리고 해싱은 기대한 대로 값을 뒤섞습니다:

결과를 부드럽게 만들기
-------------------------
부드럽게 만들기 위해 선형 보간을 할 수 있습니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class perlin {
public:
...
double noise(const point3& p) const {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
auto u = p.x() - std::floor(p.x());
auto v = p.y() - std::floor(p.y());
auto w = p.z() - std::floor(p.z());
auto i = int(std::floor(p.x()));
auto j = int(std::floor(p.y()));
auto k = int(std::floor(p.z()));
double c[2][2][2];
for (int di=0; di < 2; di++)
for (int dj=0; dj < 2; dj++)
for (int dk=0; dk < 2; dk++)
c[di][dj][dk] = randfloat[
perm_x[(i+di) & 255] ^
perm_y[(j+dj) & 255] ^
perm_z[(k+dk) & 255]
];
return trilinear_interp(c, u, v, w);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
...
private:
...
static void permute(int* p, int n) {
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
static double trilinear_interp(double c[2][2][2], double u, double v, double w) {
auto accum = 0.0;
for (int i=0; i < 2; i++)
for (int j=0; j < 2; j++)
for (int k=0; k < 2; k++)
accum += (i*u + (1-i)*(1-u))
* (j*v + (1-j)*(1-v))
* (k*w + (1-k)*(1-w))
* c[i][j][k];
return accum;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [perlin-trilinear]: [perlin.h] 삼선형 보간을 적용한 펄린]
그러면 다음과 같은 결과를 얻습니다:

에르미트 스무딩을 통한 개선
-------------------------------------
스무딩으로 결과가 개선되지만, 여전히 격자 형태가 뚜렷하게 보입니다. 그중 일부는 마하 밴드로, 색상의 선형 보간에서 생기는 것으로 알려진 지각적 아티팩트입니다. 표준적인 기법은 에르미트 3차 함수를 사용하여 보간을 부드럽게 다듬는 것입니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class perlin (
public:
...
double noise(const point3& p) const {
auto u = p.x() - std::floor(p.x());
auto v = p.y() - std::floor(p.y());
auto w = p.z() - std::floor(p.z());
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
u = u*u*(3-2*u);
v = v*v*(3-2*v);
w = w*w*(3-2*w);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
auto i = int(std::floor(p.x()));
auto j = int(std::floor(p.y()));
auto k = int(std::floor(p.z()));
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [perlin-smoothed]: [perlin.h] 에르미트 평활화를 적용한 펄린]
이렇게 하면 더 부드러운 이미지를 얻을 수 있습니다:

주파수 조정하기
-----------------------
또한 주파수가 다소 낮습니다. 입력 점의 크기를 조절하면 더 빠르게 변하도록 만들 수 있습니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class noise_texture : public texture {
public:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
noise_texture(double scale) : scale(scale) {}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
color value(double u, double v, const point3& p) const override {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
return color(1,1,1) * noise.noise(scale * p);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
private:
perlin noise;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
double scale;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [perlin-smoothed-2]: [texture.h] 평활화된 펄린, 더 높은 주파수]
그런 다음 `perlin_spheres()` 장면 설정에 그 배율을 추가합니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
void perlin_spheres() {
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
auto pertext = make_shared(4);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
world.add(make_shared(point3(0,-1000,0), 1000, make_shared(pertext)));
world.add(make_shared(point3(0, 2, 0), 2, make_shared(pertext)));
camera cam;
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [scale-perlin]: [main.cc] 노이즈에 스케일을 적용한 펄린 텍스처 구]
그러면 다음과 같은 결과가 나옵니다:

격자점에 무작위 벡터 사용하기
-------------------------------------------
아직도 약간 블록처럼 보이는데, 아마도 패턴의 최솟값과 최댓값이 항상 정수 x/y/z 위에 정확히 놓이기 때문일 것입니다. Ken Perlin(켄 펄린)의 매우 영리한 기법은 격자점에 (단순한 실수 대신) 무작위 단위 벡터를 배치하고, 내적을 사용하여 최솟값과 최댓값을 격자에서 벗어나게 하는 것이었습니다. 따라서 먼저 무작위 부동소수점 수를 무작위 벡터로 바꿔야 합니다. 이 벡터들은 적당히 불규칙한 방향의 집합이면 무엇이든 되며, 굳이 정확하게 균일하도록 만들지는 않겠습니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class perlin {
public:
perlin() {
for (int i = 0; i < point_count; i++) {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
randvec[i] = unit_vector(vec3::random(-1,1));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
perlin_generate_perm(perm_x);
perlin_generate_perm(perm_y);
perlin_generate_perm(perm_z);
}
...
private:
static const int point_count = 256;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
vec3 randvec[point_count];
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
int perm_x[point_count];
int perm_y[point_count];
int perm_z[point_count];
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [perlin-randunit]: [perlin.h] 무작위 단위 이동을 적용한 펄린]
이제 펄린 클래스의 `noise()` 메서드는 다음과 같습니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class perlin {
public:
...
double noise(const point3& p) const {
auto u = p.x() - std::floor(p.x());
auto v = p.y() - std::floor(p.y());
auto w = p.z() - std::floor(p.z());
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete
u = u*u*(3-2*u);
v = v*v*(3-2*v);
w = w*w*(3-2*w);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
auto i = int(std::floor(p.x()));
auto j = int(std::floor(p.y()));
auto k = int(std::floor(p.z()));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
vec3 c[2][2][2];
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
for (int di=0; di < 2; di++)
for (int dj=0; dj < 2; dj++)
for (int dk=0; dk < 2; dk++)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
c[di][dj][dk] = randvec[
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
perm_x[(i+di) & 255] ^
perm_y[(j+dj) & 255] ^
perm_z[(k+dk) & 255]
];
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
return perlin_interp(c, u, v, w);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [perlin-2]: [perlin.h] 새로운 noise() 메서드를 가진 펄린 클래스(노이즈)]
그리고 보간은 조금 더 복잡해집니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class perlin {
...
private:
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete
static double trilinear_interp(double c[2][2][2], double u, double v, double w) {
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
static double perlin_interp(const vec3 c[2][2][2], double u, double v, double w) {
auto uu = u*u*(3-2*u);
auto vv = v*v*(3-2*v);
auto ww = w*w*(3-2*w);
auto accum = 0.0;
for (int i=0; i < 2; i++)
for (int j=0; j < 2; j++)
for (int k=0; k < 2; k++) {
vec3 weight_v(u-i, v-j, w-k);
accum += (i*uu + (1-i)*(1-uu))
* (j*vv + (1-j)*(1-vv))
* (k*ww + (1-k)*(1-ww))
* dot(c[i][j][k], weight_v);
}
return accum;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [perlin-interp]: [perlin.h] 지금까지의 펄린 보간 함수]
펄린 보간 함수의 출력은 음수 값을 반환할 수 있습니다. 이러한 음수 값은 양수 입력만을 기대하는 `linear_to_gamma()` 색상 함수로 전달됩니다. 이를 완화하기 위해 $[-1,+1]$ 범위의 값을 $[0,1]$로 매핑하겠습니다.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class noise_texture : public texture {
public:
noise_texture(double scale) : scale(scale) {}
color value(double u, double v, const point3& p) const override {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
return color(1,1,1) * 0.5 * (1.0 + noise.noise(scale * p));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
private:
perlin noise;
double scale;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [perlin-smoothed-2]: [texture.h] 펄린 노이즈 스무딩, 더 높은 주파수]
이제 드디어 좀 더 그럴듯해 보이는 결과가 나옵니다:

난류 도입하기
-----------------------
여러 주파수를 합한 합성 노이즈가 매우 자주 사용됩니다. 이는 보통 난류라고 불리며, 노이즈를 반복 호출한 결과의 합입니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class perlin {
...
public:
...
double noise(const point3& p) const {
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
double turb(const point3& p, int depth) const {
auto accum = 0.0;
auto temp_p = p;
auto weight = 1.0;
for (int i = 0; i < depth; i++) {
accum += weight * noise(temp_p);
weight *= 0.5;
temp_p *= 2;
}
return std::fabs(accum);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [perlin-turb]: [perlin.h] 난류 함수]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class noise_texture : public texture {
public:
noise_texture(double scale) : scale(scale) {}
color value(double u, double v, const point3& p) const override {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
return color(1,1,1) * noise.turb(p, 7);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
private:
perlin noise;
double scale;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [noise-tex-2]: [texture.h] 난류를 적용한 노이즈 텍스처]
난류를 직접 사용하면 일종의 위장망 같은 모습이 나타납니다:

위상 조정하기
--------------------
그러나 난류는 보통 간접적으로 사용됩니다. 예를 들어, 절차적 솔리드 텍스처의 “hello world”는 간단한 대리석 같은 텍스처입니다. 기본 아이디어는 색상을 사인 함수 같은 것에 비례하도록 만들고, 난류를 사용해 위상을 조정하여(즉, $\sin(x)$의 $x$를 이동시켜) 줄무늬가 물결치게 만드는 것입니다. 단순 노이즈와 난류를 주석 처리하고 대리석 같은 효과를 주는 코드는 다음과 같습니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class noise_texture : public texture {
public:
noise_texture(double scale) : scale(scale) {}
color value(double u, double v, const point3& p) const override {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
return color(.5, .5, .5) * (1 + std::sin(scale * p.z() + 10 * noise.turb(p, 7)));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
private:
perlin noise;
double scale;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [noise-tex-3]: [texture.h] 대리석 무늬 텍스처를 적용한 노이즈 텍스처]
그 결과는 다음과 같습니다:

사각형
====================================================================================================
우리는 구만을 유일한 기하 프리미티브로 사용하여 이 3권짜리 시리즈의 절반 이상을 진행해 왔습니다. 이제 두 번째 프리미티브인 사각형을 추가할 차례입니다.
사각형 정의하기
---------------------------
새 프리미티브의 이름을 `quad`라고 짓겠지만, 엄밀히 말하면 일반적인 사각형이 아니라 평행사변형(마주 보는 변이 평행함)입니다. 여기서는 사각형을 정의하는 데 세 가지 기하학적 요소를 사용하겠습니다:
1. $\mathbf{Q}$, 시작 꼭짓점입니다.
2. $\mathbf{u}$, 첫 번째 변을 나타내는 벡터입니다. $\mathbf{Q} + \mathbf{u}$는 $\mathbf{Q}$에 인접한 꼭짓점 중 하나를 나타냅니다.
3. $\mathbf{v}$, 두 번째 변을 나타내는 벡터입니다. $\mathbf{Q} + \mathbf{v}$는 $\mathbf{Q}$에 인접한 다른 꼭짓점을 나타냅니다.
사각형에서 $\mathbf{Q}$의 맞은편 꼭짓점은 $\mathbf{Q} + \mathbf{u} + \mathbf{v}$로 주어집니다. 사각형 자체는 2차원 객체이지만, 이 값들은 3차원입니다. 예를 들어, 꼭짓점이 원점에 있고 Z 방향으로 2단위, Y 방향으로 1단위 뻗어 있는 사각형은 $\mathbf{Q} = (0,0,0), \mathbf{u} = (0,0,2), \text{and } \mathbf{v} = (0,1,0)$ 값을 가집니다.
다음 그림은 사각형의 구성 요소를 보여 줍니다.
![Figure [quad-def]: 사각형의 구성 요소](../images/fig-2.05-quad-def.jpg)
사각형은 평평하므로, 사각형이 XY, YZ 또는 ZX 평면에 놓이면 축 정렬 경계 상자의 한 차원 두께가 0이 됩니다. 이는 광선 교차 계산에서 수치적 문제를 일으킬 수 있지만, 경계 상자에서 크기가 0인 차원에 패딩을 추가하여 해결할 수 있습니다. 패딩을 추가해도 괜찮은 이유는 사각형의 교차 자체를 바꾸는 것이 아니기 때문입니다. 수치적 문제의 가능성을 없애기 위해 경계 상자를 확장할 뿐이며, 어차피 경계는 실제 형태의 대략적인 근사일 뿐입니다. 이 상황을 해결하기 위해, 새로 생성된 AABB가 항상 0이 아닌 부피를 갖도록 작은 패딩을 삽입합니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
class aabb {
public:
...
aabb(const interval& x, const interval& y, const interval& z)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
: x(x), y(y), z(z)
{
pad_to_minimums();
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
aabb(const point3& a, const point3& b) {
// Treat the two points a and b as extrema for the bounding box, so we don't require a
// particular minimum/maximum coordinate order.
x = interval(std::fmin(a[0],b[0]), std::fmax(a[0],b[0]));
y = interval(std::fmin(a[1],b[1]), std::fmax(a[1],b[1]));
z = interval(std::fmin(a[2],b[2]), std::fmax(a[2],b[2]));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
pad_to_minimums();
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
...
static const aabb empty, universe;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
private:
void pad_to_minimums() {
// Adjust the AABB so that no side is narrower than some delta, padding if necessary.
double delta = 0.0001;
if (x.size() < delta) x = x.expand(delta);
if (y.size() < delta) y = y.expand(delta);
if (z.size() < delta) z = z.expand(delta);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [aabb]: [aabb.h] 새로운 aabb::pad_to_minimums() 메서드]
이제 새로운 `quad` 클래스의 첫 번째 초안을 작성할 준비가 되었습니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#ifndef QUAD_H
#define QUAD_H
#include "hittable.h"
class quad : public hittable {
public:
quad(const point3& Q, const vec3& u, const vec3& v, shared_ptr mat)
: Q(Q), u(u), v(v), mat(mat)
{
set_bounding_box();
}
virtual void set_bounding_box() {
// Compute the bounding box of all four vertices.
auto bbox_diagonal1 = aabb(Q, Q + u + v);
auto bbox_diagonal2 = aabb(Q + u, Q + v);
bbox = aabb(bbox_diagonal1, bbox_diagonal2);
}
aabb bounding_box() const override { return bbox; }
bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
return false; // To be implemented
}
private:
point3 Q;
vec3 u, v;
shared_ptr mat;
aabb bbox;
};
#endif
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [quad]: [quad.h] 2차원 사각형(평행사변형) 클래스]
광선-평면 교차
-----------------------
이전 코드에서 볼 수 있듯이, `quad::hit()`은 아직 구현되지 않았습니다. 구와 마찬가지로, 주어진 레이가 해당 프리미티브와 교차하는지 판단해야 하며, 교차한다면 그 교차의 여러 속성(교차점, 법선, 텍스처 좌표 등)을 구해야 합니다.
레이-사각형 교차는 다음 세 단계로 판정합니다.
1. 해당 사각형을 포함하는 평면 찾기,
2. 레이와 사각형을 포함하는 평면의 교차 구하기,
3. 교차점이 사각형 내부에 있는지 판정하기.
먼저 가운데 단계인 일반적인 광선-평면 교차를 푸는 것부터 다루겠습니다.
구는 음함수 공식 덕분에 레이 교차를 매우 쉽게 구할 수 있으므로, 일반적으로 가장 먼저 배우는 레이 트레이싱 프리미티브입니다. 구와 마찬가지로 평면에도 음함수 공식이 있으며, 이 음함수 공식을 사용하여 광선-평면 교차를 푸는 알고리즘을 만들 수 있습니다. 실제로 광선-평면 교차는 광선-구 교차보다 _훨씬 더 쉽게_ 풀 수 있습니다.
평면에 대한 다음 음함수 공식을 이미 알고 계실 수도 있습니다.
$$ Ax + By + Cz + D = 0 $$
여기서 $A,B,C,D$는 단순한 상수이고, $x,y,z$는 평면 위에 있는 임의의 점 $(x,y,z)$의 값입니다. 따라서 평면은 위 공식을 만족하는 모든 점 $(x,y,z)$의 집합입니다. 다음과 같은 대안 공식을 사용하면 조금 더 편리합니다.
$$ Ax + By + Cz = D $$
(D의 부호를 뒤집지 않은 것은 D가 나중에 구할 어떤 상수일 뿐이기 때문입니다.)
이 공식을 직관적으로 이해하는 방법은 다음과 같습니다. 법선 벡터 $\mathbf{n} = (A,B,C)$에 수직인 평면과 위치 벡터 $\mathbf{v} = (x,y,z)$(즉, 원점에서 평면 위의 임의의 점까지의 벡터)가 주어지면, 내적을 사용하여 $D$를 구할 수 있습니다.
$$ \mathbf{n} \cdot \mathbf{v} = D $$
이는 평면 위의 모든 위치에 대해 성립합니다. 이는 위에서 제시한 $Ax + By + Cz = D$ 공식과 동등한 공식이며, 다만 이제 벡터로 표현한 것입니다.
이제 어떤 레이 $\mathbf{R}(t) = \mathbf{P} + t\mathbf{d}$와의 교차를 구해 보겠습니다. 레이 방정식을 대입하면 다음을 얻습니다.
$$ \mathbf{n} \cdot ( \mathbf{P} + t \mathbf{d} ) = D $$
$t$에 대해 풀면 다음과 같습니다.
$$ \mathbf{n} \cdot \mathbf{P} + \mathbf{n} \cdot t \mathbf{d} = D $$
$$ \mathbf{n} \cdot \mathbf{P} + t(\mathbf{n} \cdot \mathbf{d}) = D $$
$$ t = \frac{D - \mathbf{n} \cdot \mathbf{P}}{\mathbf{n} \cdot \mathbf{d}} $$
이렇게 $t$를 얻으며, 이를 레이 방정식에 대입하여 교차점을 구할 수 있습니다. 레이가 평면과 평행하면 분모 $\mathbf{n} \cdot \mathbf{d}$가 0이 된다는 점에 유의하십시오. 이 경우 레이와 평면이 교차하지 않는 것으로 즉시 기록할 수 있습니다. 다른 프리미티브와 마찬가지로, 레이의 $t$ 매개변수가 허용 가능한 최솟값보다 작은 경우에도 교차하지 않는 것으로 기록합니다.
좋습니다. 이제 레이와 주어진 사각형을 포함하는 평면 사이의 교차점을 구할 수 있습니다. 사실 이 방법을 사용하면 삼각형이나 원판처럼 _모든_ 평면 프리미티브를 판정할 수 있습니다(이에 대해서는 나중에 더 다룹니다).
주어진 사각형을 포함하는 평면 찾기
------------------------------------------------------
평면 방정식이 주어졌다고 가정하고, 위의 두 번째 단계인 광선-평면 교차를 풀었습니다. 이를 위해서는 위의 첫 번째 단계인 사각형을 포함하는 평면의 방정식 찾기를 해결해야 합니다. 사각형 매개변수 $\mathbf{Q}$, $\mathbf{u}$, $\mathbf{v}$가 있으며, 이 세 값으로 정의되는 사각형을 포함하는 평면의 방정식을 구하고자 합니다.
다행히 이것은 매우 간단합니다. 방정식 $Ax + By + Cz = D$에서 $(A,B,C)$가 법선 벡터를 나타낸다는 것을 떠올려 보십시오. 이를 구하려면 두 변 벡터 $\mathbf{u}$와 $\mathbf{v}$의 외적을 사용하기만 하면 됩니다:
$$ \mathbf{n} = \operatorname{unit\_vector}(\mathbf{u} \times \mathbf{v}) $$
평면은 방정식 $Ax + By + Cz = D$를 만족하는 모든 점 $(x,y,z)$로 정의됩니다. 그런데 $\mathbf{Q}$가 평면 위에 있다는 것을 알고 있으므로, 이것만으로 $D$를 구하기에 충분합니다:
$$ \begin{align*}
D &= n_x Q_x + n_y Q_y + n_z Q_z \\
&= \mathbf{n} \cdot \mathbf{Q} \\
\end{align*}
$$
`quad` 클래스에 평면 값들을 추가합니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class quad : public hittable {
public:
quad(const point3& Q, const vec3& u, const vec3& v, shared_ptr mat)
: Q(Q), u(u), v(v), mat(mat)
{
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
auto n = cross(u, v);
normal = unit_vector(n);
D = dot(normal, Q);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
set_bounding_box();
}
...
private:
point3 Q;
vec3 u, v;
shared_ptr mat;
aabb bbox;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
vec3 normal;
double D;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [quad-plane1]: [quad.h] 평면 값 캐싱하기]
두 값 `normal`과 `D`를 사용하여 주어진 광선과 사각형을 포함하는 평면 사이의 교차점을 구할 것입니다.
점진적인 단계로서, 사각형을 포함하는 무한 평면을 처리하도록 `hit()` 메서드를 구현해 보겠습니다.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class quad : public hittable {
...
bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
auto denom = dot(normal, r.direction());
// No hit if the ray is parallel to the plane.
if (std::fabs(denom) < 1e-8)
return false;
// Return false if the hit point parameter t is outside the ray interval.
auto t = (D - dot(normal, r.origin())) / denom;
if (!ray_t.contains(t))
return false;
auto intersection = r.at(t);
rec.t = t;
rec.p = intersection;
rec.mat = mat;
rec.set_face_normal(r, normal);
return true;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [quad-plane2]: [quad.h] 무한 평면에 대한 hit() 메서드]
평면 위의 점 위치 정하기
------------------------------
이 단계에서 교차점은 사각형을 포함하는 평면 위에 있지만, 평면 위 _어디에나_ 있을 수 있습니다: 광선-평면 교차점은 사각형의 안쪽이나 바깥쪽에 놓이게 됩니다. 사각형 안쪽에 놓인 교차점(적중)을 검사하고, 바깥쪽에 놓인 점(빗나감)은 제외해야 합니다. 점이 사각형에 대해 어디에 놓이는지 판단하고 교차점에 텍스처 좌표를 할당하려면, 평면 위에서 교차점의 위치를 정해야 합니다.
이를 위해 평면에 대한 _좌표계_ 를 구성할 것입니다 -- 이는 평면 위에 위치한 임의의 점의 위치를 정하는 방법입니다. 우리는 이미 3D 공간에 대한 좌표계를 사용해 왔습니다 -- 이는 원점 $\mathbf{O}$와 세 개의 기저 벡터 $\mathbf{x}$, $\mathbf{y}$, $\mathbf{z}$로 정의됩니다.
평면은 2D 구조이므로, 평면 원점 $\mathbf{Q}$와 _두 개의_ 기저 벡터 $\mathbf{u}$와 $\mathbf{v}$만 있으면 됩니다. 일반적으로 축들은 서로 수직입니다. 하지만 전체 공간을 생성하기 위해 반드시 그럴 필요는 없습니다 -- 서로 평행하지 않은 두 축만 있으면 됩니다.
![Figure [ray-plane]: 광선-평면 교차](../images/fig-2.06-ray-plane.jpg)
그림 [ray-plane]의 광선-평면 교차를 예로 살펴보겠습니다. 광선 $\mathbf{R}$이 평면과 교차하여 교차점 $\mathbf{P}$가 생깁니다(위에서 언급한 광선의 원점 $\mathbf{P}$와 혼동하지 마십시오). 평면 벡터 $\mathbf{u}$와 $\mathbf{v}$를 기준으로 측정하면, 위 예에서 교차점 $\mathbf{P}$는 $\mathbf{Q} + (1)\mathbf{u} + (\frac{1}{2})\mathbf{v}$에 있습니다. 다시 말해, 교차점 $\mathbf{P}$의 $\mathbf{UV}$(평면) 좌표는 $(1,\frac{1}{2})$입니다.
일반적으로, 임의의 점 $\mathbf{P}$가 주어졌을 때 다음을 만족하는 두 스칼라 값 $\alpha$와 $\beta$를 구합니다
$$ \mathbf{P} = \mathbf{Q} + \alpha \mathbf{u} + \beta \mathbf{v} $$
모자에서 토끼를 꺼내듯 결과부터 제시하자면, 평면 좌표 $\alpha$와 $\beta$는 다음 방정식으로 주어집니다:
$$ \alpha = \mathbf{w} \cdot (\mathbf{p} \times \mathbf{v}) $$
$$ \beta = \mathbf{w} \cdot (\mathbf{u} \times \mathbf{p}) $$
여기서
$$ \mathbf{p} = \mathbf{P} - \mathbf{Q} $$
$$ \mathbf{w} = \frac{\mathbf{n}}{\mathbf{n} \cdot (\mathbf{u} \times \mathbf{v})}
= \frac{\mathbf{n}}{\mathbf{n} \cdot \mathbf{n}}$$
벡터 $\mathbf{w}$는 주어진 사각형에 대해 상수이므로, 그 값을 캐싱하겠습니다.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class quad : public hittable {
public:
quad(const point3& Q, const vec3& u, const vec3& v, shared_ptr mat)
: Q(Q), u(u), v(v), mat(mat)
{
auto n = cross(u, v);
normal = unit_vector(n);
D = dot(normal, Q);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
w = n / dot(n,n);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
set_bounding_box();
}
...
private:
point3 Q;
vec3 u, v;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
vec3 w;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
shared_ptr mat;
aabb bbox;
vec3 normal;
double D;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [quad-w]: [quad.h] 사각형의 w 값 캐싱하기]
평면 좌표 유도하기
--------------------------------
(이 절에서는 위 방정식의 유도 과정을 다룹니다. 관심이 없다면 다음 절로 건너뛰어도 좋습니다.)
광선-평면 교차를 나타낸 그림 [ray-plane]을 다시 참조하십시오. 평면 기저 벡터 $\mathbf{u}$와 $\mathbf{v}$가 서로 직교한다는 것(둘 사이에 90° 각도를 이룬다는 것)이 보장된다면, $\alpha$와 $\beta$를 구하는 일은 내적을 사용하여 $\mathbf{P}$를 각 기저 벡터 $\mathbf{u}$와 $\mathbf{v}$에 투영하기만 하면 되는 간단한 문제일 것입니다. 그러나 우리는 $\mathbf{u}$와 $\mathbf{v}$가 직교하도록 제한하지 않으므로, 수학이 조금 더 까다롭습니다.
준비 단계로, 다음을 생각해 보십시오.
$$ \mathbf{P} = \mathbf{Q} + \alpha \mathbf{u} + \beta \mathbf{v}$$
$$ \mathbf{p} = \mathbf{P} - \mathbf{Q} = \alpha \mathbf{u} + \beta \mathbf{v} $$
여기서 $\mathbf{P}$는 교차 _점_ 이고, $\mathbf{p}$는 $\mathbf{Q}$에서 $\mathbf{P}$로 향하는 _벡터_ 입니다.
$\mathbf{p}$에 대한 방정식을 각각 $\mathbf{u}$ 및 $\mathbf{v}$와 외적하십시오.
$$ \begin{align*}
\mathbf{u} \times \mathbf{p} &= \mathbf{u} \times (\alpha \mathbf{u} + \beta \mathbf{v}) \\
&= \mathbf{u} \times \alpha \mathbf{u} + \mathbf{u} \times \beta \mathbf{v} \\
&= \alpha(\mathbf{u} \times \mathbf{u}) + \beta(\mathbf{u} \times \mathbf{v})
\end{align*} $$
$$ \begin{align*}
\mathbf{v} \times \mathbf{p} &= \mathbf{v} \times (\alpha \mathbf{u} + \beta \mathbf{v}) \\
&= \mathbf{v} \times \alpha \mathbf{u} + \mathbf{v} \times \beta \mathbf{v} \\
&= \alpha(\mathbf{v} \times \mathbf{u}) + \beta(\mathbf{v} \times \mathbf{v})
\end{align*} $$
어떤 벡터든 자기 자신과 외적하면 0이 되므로, 이 방정식들은 다음과 같이 단순화됩니다.
$$ \mathbf{v} \times \mathbf{p} = \alpha(\mathbf{v} \times \mathbf{u}) $$
$$ \mathbf{u} \times \mathbf{p} = \beta(\mathbf{u} \times \mathbf{v}) $$
이제 계수 $\alpha$와 $\beta$를 구해 보겠습니다. 벡터 수학이 처음이시라면 $\mathbf{u} \times \mathbf{v}$와 $\mathbf{v} \times \mathbf{u}$로 나누려고 하실 수도 있지만, 벡터로는 나눌 수 없습니다. 대신, 위 방정식들의 양변에 평면 법선 $\mathbf{n} = \mathbf{u} \times \mathbf{v}$와의 내적을 취하여 양변을 스칼라로 만들 수 있으며, 스칼라로는 나눌 수 _있습니다_.
$$ \mathbf{n} \cdot (\mathbf{v} \times \mathbf{p})
= \mathbf{n} \cdot \alpha(\mathbf{v} \times \mathbf{u}) $$
$$ \mathbf{n} \cdot (\mathbf{u} \times \mathbf{p})
= \mathbf{n} \cdot \beta(\mathbf{u} \times \mathbf{v}) $$
이제 계수를 분리하는 것은 간단한 나눗셈 문제입니다.
$$ \alpha = \frac{\mathbf{n} \cdot (\mathbf{v} \times \mathbf{p})}
{\mathbf{n} \cdot (\mathbf{v} \times \mathbf{u})} $$
$$ \beta = \frac{\mathbf{n} \cdot (\mathbf{u} \times \mathbf{p})}
{\mathbf{n} \cdot (\mathbf{u} \times \mathbf{v})} $$
$\alpha$의 분자와 분모 모두에서 외적의 순서를 뒤집으면($\mathbf{a} \times \mathbf{b} = - \mathbf{b} \times \mathbf{a}$임을 떠올리십시오) 두 계수에 대한 공통 분모를 얻습니다.
$$ \alpha = \frac{\mathbf{n} \cdot (\mathbf{p} \times \mathbf{v})}
{\mathbf{n} \cdot (\mathbf{u} \times \mathbf{v})} $$
$$ \beta = \frac{\mathbf{n} \cdot (\mathbf{u} \times \mathbf{p})}
{\mathbf{n} \cdot (\mathbf{u} \times \mathbf{v})} $$
이제 마지막 단순화를 한 번 수행하여, 임의의 평면 위 점 $\mathbf{P}$에 대해 평면의 기저 좌표계에 대해 상수가 되는 벡터 $\mathbf{w}$를 계산할 수 있습니다.
$$ \mathbf{w} = \frac{\mathbf{n}}{\mathbf{n} \cdot (\mathbf{u} \times \mathbf{v})}
= \frac{\mathbf{n}}{\mathbf{n} \cdot \mathbf{n}}$$
$$ \alpha = \mathbf{w} \cdot (\mathbf{p} \times \mathbf{v}) $$
$$ \beta = \mathbf{w} \cdot (\mathbf{u} \times \mathbf{p}) $$
UV 좌표를 사용한 교차점의 내부 판정
----------------------------------------------------------
이제 교차점의 평면 좌표 $\alpha$와 $\beta$를 얻었으므로, 이를 사용하여 교차점이 사각형 내부에 있는지 -- 즉, 광선이 실제로 사각형에 맞았는지를 쉽게 판단할 수 있습니다.
평면은 다음과 같이 좌표 영역으로 나뉩니다.
![Figure [quad-coords]: 사각형 좌표](../images/fig-2.07-quad-coords.jpg)
따라서 평면 좌표가 $(\alpha,\beta)$인 점이 사각형 내부에 있는지 확인하려면, 다음 조건만 만족하면 됩니다.
1. $ 0 \leq \alpha \leq 1 $
2. $ 0 \leq \beta \leq 1 $
이것이 사각형 프리미티브를 구현하는 데 필요한 마지막 요소입니다.
이러한 실험을 조금 더 쉽게 하기 위해, $(\alpha,\beta)$ 내부 판정 메서드를 hit 메서드에서 분리하겠습니다.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class quad : public hittable {
public:
...
bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
auto denom = dot(normal, r.direction());
// No hit if the ray is parallel to the plane.
if (std::fabs(denom) < 1e-8)
return false;
// Return false if the hit point parameter t is outside the ray interval.
auto t = (D - dot(normal, r.origin())) / denom;
if (!ray_t.contains(t))
return false;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
// Determine if the hit point lies within the planar shape using its plane coordinates.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
auto intersection = r.at(t);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
vec3 planar_hitpt_vector = intersection - Q;
auto alpha = dot(w, cross(planar_hitpt_vector, v));
auto beta = dot(w, cross(u, planar_hitpt_vector));
if (!is_interior(alpha, beta, rec))
return false;
// Ray hits the 2D shape; set the rest of the hit record and return true.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
rec.t = t;
rec.p = intersection;
rec.mat = mat;
rec.set_face_normal(r, normal);
return true;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
virtual bool is_interior(double a, double b, hit_record& rec) const {
interval unit_interval = interval(0, 1);
// Given the hit point in plane coordinates, return false if it is outside the
// primitive, otherwise set the hit record UV coordinates and return true.
if (!unit_interval.contains(a) || !unit_interval.contains(b))
return false;
rec.u = a;
rec.v = b;
return true;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
private:
point3 Q;
vec3 u, v;
vec3 w;
shared_ptr mat;
aabb bbox;
vec3 normal;
double D;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [quad-final]: [quad.h] 최종 사각형 클래스]
이제 새로운 `quad` 프리미티브를 보여 주기 위해 새 장면을 추가합니다.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "rtweekend.h"
#include "bvh.h"
#include "camera.h"
#include "hittable.h"
#include "hittable_list.h"
#include "material.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
#include "quad.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "sphere.h"
#include "texture.h"
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
void quads() {
hittable_list world;
// Materials
auto left_red = make_shared(color(1.0, 0.2, 0.2));
auto back_green = make_shared(color(0.2, 1.0, 0.2));
auto right_blue = make_shared(color(0.2, 0.2, 1.0));
auto upper_orange = make_shared(color(1.0, 0.5, 0.0));
auto lower_teal = make_shared(color(0.2, 0.8, 0.8));
// Quads
world.add(make_shared(point3(-3,-2, 5), vec3(0, 0,-4), vec3(0, 4, 0), left_red));
world.add(make_shared(point3(-2,-2, 0), vec3(4, 0, 0), vec3(0, 4, 0), back_green));
world.add(make_shared(point3( 3,-2, 1), vec3(0, 0, 4), vec3(0, 4, 0), right_blue));
world.add(make_shared(point3(-2, 3, 1), vec3(4, 0, 0), vec3(0, 0, 4), upper_orange));
world.add(make_shared(point3(-2,-3, 5), vec3(4, 0, 0), vec3(0, 0,-4), lower_teal));
camera cam;
cam.aspect_ratio = 1.0;
cam.image_width = 400;
cam.samples_per_pixel = 100;
cam.max_depth = 50;
cam.vfov = 80;
cam.lookfrom = point3(0,0,9);
cam.lookat = point3(0,0,0);
cam.vup = vec3(0,1,0);
cam.defocus_angle = 0;
cam.render(world);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
int main() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
switch (5) {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
case 1: bouncing_spheres(); break;
case 2: checkered_spheres(); break;
case 3: earth(); break;
case 4: perlin_spheres(); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
case 5: quads(); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [quad-scene]: [main.cc] 사각형이 있는 새 장면]

추가 2D 프리미티브
-------------------------
여기서 잠시 멈추고, $(\alpha,\beta)$ 좌표를 사용하여 점이 사각형(평행사변형) 내부에 있는지 판단한다면, 이와 같은 2D 좌표를 사용하여 교차점이 _어떤_ 다른 2D(평면) 프리미티브 내부에 있는지도 판단할 수 있으리라는 것을 어렵지 않게 떠올릴 수 있다는 점을 생각해 보십시오!
예를 들어, `is_interior()` 함수가 `sqrt(a*a + b*b) < r`일 때 true를 반환하도록 바꾼다고 가정해 보십시오. 그러면 반지름이 `r`인 원판 프리미티브가 구현됩니다. 삼각형의 경우에는 `a > 0 && b > 0 && a + b < 1`을 시도해 보십시오.
추가적인 2D 도형의 가능성은 탐구하고자 하는 의향에 따라 독자 여러분의 연습 과제로 남겨 두겠습니다. 텍스처 맵의 픽셀을 기반으로 한 오려 내기 스텐실이나 만델브로트 도형을 만들 수도 있습니다! 작은 이스터 에그로, 소스 저장소의 `alternate-2D-primitives` 태그를 확인해 보십시오. 이 태그에는 `src/TheNextWeek/quad.h`에 삼각형, 타원, 환형(고리)에 대한 해법이 들어 있습니다.
광원
====================================================================================================
조명은 레이 트레이싱의 핵심 구성 요소입니다. 초기의 단순한 레이 트레이서는 공간상의 점이나 방향과 같은 추상적인 광원을 사용했습니다. 현대적인 접근 방식에서는 위치와 크기를 가진, 보다 물리 기반의 광원을 사용합니다. 이러한 광원을 만들려면 임의의 일반 물체를 가져와 장면에 빛을 방출하는 무언가로 바꿀 수 있어야 합니다.
발광 재질
-------------------
먼저, 빛을 방출하는 재질을 만들어 보겠습니다. 방출 함수를 추가해야 합니다(대신 `hit_record`에 추가할 수도 있습니다 -- 이는 설계 취향의 문제입니다). 배경과 마찬가지로, 이 함수는 광선에 그것이 어떤 색인지만 알려 줄 뿐 반사는 수행하지 않습니다. 매우 간단합니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class dielectric : public material {
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
class diffuse_light : public material {
public:
diffuse_light(shared_ptr tex) : tex(tex) {}
diffuse_light(const color& emit) : tex(make_shared(emit)) {}
color emitted(double u, double v, const point3& p) const override {
return tex->value(u, v, p);
}
private:
shared_ptr tex;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [diffuse-light]: [material.h] 확산 광원 클래스]
빛을 방출하지 않는 모든 재질이 `emitted()`를 구현하지 않아도 되도록, 기본 클래스가 검은색을 반환하게 했습니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class material {
public:
virtual ~material() = default;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
virtual color emitted(double u, double v, const point3& p) const {
return color(0,0,0);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const {
return false;
}
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [matl-emit]: [material.h] material 클래스의 새로운 emitted 함수]
광선 색상 함수에 배경색 추가하기
--------------------------------------------------
다음으로, 장면의 유일한 빛이 발광체에서만 나오도록 완전히 검은 배경을 사용하고자 합니다. 이를 위해 `ray_color` 함수에 배경색 매개변수를 추가하고, 새로운 `color_from_emission` 값에 주목하겠습니다.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class camera {
public:
double aspect_ratio = 1.0; // Ratio of image width over height
int image_width = 100; // Rendered image width in pixel count
int samples_per_pixel = 10; // Count of random samples for each pixel
int max_depth = 10; // Maximum number of ray bounces into scene
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
color background; // Scene background color
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
private:
...
color ray_color(const ray& r, int depth, const hittable& world) const {
// If we've exceeded the ray bounce limit, no more light is gathered.
if (depth <= 0)
return color(0,0,0);
hit_record rec;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
// If the ray hits nothing, return the background color.
if (!world.hit(r, interval(0.001, infinity), rec))
return background;
ray scattered;
color attenuation;
color color_from_emission = rec.mat->emitted(rec.u, rec.v, rec.p);
if (!rec.mat->scatter(r, rec, attenuation, scattered))
return color_from_emission;
color color_from_scatter = attenuation * ray_color(scattered, depth-1, world);
return color_from_emission + color_from_scatter;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [ray-color-emitted]: [camera.h] 배경과 발광 재질을 사용하는 ray_color 함수]
이전 장면들의 배경색을 설정하도록 `main()`을 갱신합니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
void bouncing_spheres() {
...
camera cam;
cam.aspect_ratio = 16.0 / 9.0;
cam.image_width = 400;
cam.samples_per_pixel = 100;
cam.max_depth = 20;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
cam.background = color(0.70, 0.80, 1.00);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
}
void checkered_spheres() {
...
camera cam;
cam.aspect_ratio = 16.0 / 9.0;
cam.image_width = 400;
cam.samples_per_pixel = 100;
cam.max_depth = 50;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
cam.background = color(0.70, 0.80, 1.00);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
}
void earth() {
...
camera cam;
cam.aspect_ratio = 16.0 / 9.0;
cam.image_width = 400;
cam.samples_per_pixel = 100;
cam.max_depth = 50;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
cam.background = color(0.70, 0.80, 1.00);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
}
void perlin_spheres() {
...
camera cam;
cam.aspect_ratio = 16.0 / 9.0;
cam.image_width = 400;
cam.samples_per_pixel = 100;
cam.max_depth = 50;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
cam.background = color(0.70, 0.80, 1.00);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
}
void quads() {
...
camera cam;
cam.aspect_ratio = 1.0;
cam.image_width = 400;
cam.samples_per_pixel = 100;
cam.max_depth = 50;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
cam.background = color(0.70, 0.80, 1.00);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [use-bg-color]: [main.cc] 새로운 배경색 지정하기]
광선이 하늘에 닿았을 때 하늘의 색을 결정하는 데 사용하던 코드를 제거하므로, 이전 장면을 렌더링하려면 새로운 색상 값을 전달해야 합니다. 하늘 전체에 균일한 푸르스름한 흰색을 사용하기로 했습니다. 이전 스카이박스 코드와 새로운 단색 배경 사이를 전환하도록 불리언 값을 전달할 수도 있습니다. 여기서는 간단하게 유지하겠습니다.
물체를 광원으로 바꾸기
----------------------------
직사각형을 광원으로 설정하면:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
void simple_light() {
hittable_list world;
auto pertext = make_shared(4);
world.add(make_shared(point3(0,-1000,0), 1000, make_shared(pertext)));
world.add(make_shared(point3(0,2,0), 2, make_shared(pertext)));
auto difflight = make_shared(color(4,4,4));
world.add(make_shared(point3(3,1,-2), vec3(2,0,0), vec3(0,2,0), difflight));
camera cam;
cam.aspect_ratio = 16.0 / 9.0;
cam.image_width = 400;
cam.samples_per_pixel = 100;
cam.max_depth = 50;
cam.background = color(0,0,0);
cam.vfov = 20;
cam.lookfrom = point3(26,3,6);
cam.lookat = point3(0,2,0);
cam.vup = vec3(0,1,0);
cam.defocus_angle = 0;
cam.render(world);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
int main() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
switch (6) {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
case 1: bouncing_spheres(); break;
case 2: checkered_spheres(); break;
case 3: earth(); break;
case 4: perlin_spheres(); break;
case 5: quads(); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
case 6: simple_light(); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [rect-light]: [main.cc] 간단한 직사각형 광원]
다음과 같은 결과를 얻습니다:

광원이 $(1,1,1)$보다 밝다는 점에 유의하십시오. 이렇게 하면 사물을 비출 수 있을 만큼 충분히 밝아집니다.
구 몇 개를 광원으로 만드는 것도 자유롭게 시도해 보십시오.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
void simple_light() {
...
auto difflight = make_shared(color(4,4,4));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
world.add(make_shared(point3(0,7,0), 2, difflight));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
world.add(make_shared(point3(3,1,-2), vec3(2,0,0), vec3(0,2,0), difflight));
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [rect-light]: [main.cc] 간단한 사각형 광원과 빛을 받는 공]

빈 “코넬 박스” 만들기
--------------------------------
“코넬 박스”는 확산 표면 사이의 빛 상호작용을 모델링하기 위해 1984년에 소개되었습니다. 상자의 벽 5개와 광원을 만들어 보겠습니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
void cornell_box() {
hittable_list world;
auto red = make_shared(color(.65, .05, .05));
auto white = make_shared(color(.73, .73, .73));
auto green = make_shared(color(.12, .45, .15));
auto light = make_shared(color(15, 15, 15));
world.add(make_shared(point3(555,0,0), vec3(0,555,0), vec3(0,0,555), green));
world.add(make_shared(point3(0,0,0), vec3(0,555,0), vec3(0,0,555), red));
world.add(make_shared(point3(343, 554, 332), vec3(-130,0,0), vec3(0,0,-105), light));
world.add(make_shared(point3(0,0,0), vec3(555,0,0), vec3(0,0,555), white));
world.add(make_shared(point3(555,555,555), vec3(-555,0,0), vec3(0,0,-555), white));
world.add(make_shared(point3(0,0,555), vec3(555,0,0), vec3(0,555,0), white));
camera cam;
cam.aspect_ratio = 1.0;
cam.image_width = 600;
cam.samples_per_pixel = 200;
cam.max_depth = 50;
cam.background = color(0,0,0);
cam.vfov = 40;
cam.lookfrom = point3(278, 278, -800);
cam.lookat = point3(278, 278, 0);
cam.vup = vec3(0,1,0);
cam.defocus_angle = 0;
cam.render(world);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
int main() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
switch (7) {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
case 1: bouncing_spheres(); break;
case 2: checkered_spheres(); break;
case 3: earth(); break;
case 4: perlin_spheres(); break;
case 5: quads(); break;
case 6: simple_light(); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
case 7: cornell_box(); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [cornell-box-empty]: [main.cc] 빈 코넬 박스 장면]
다음과 같은 결과를 얻습니다

광원이 작아서 대부분의 무작위 광선이 광원에 닿지 않기 때문에 이 이미지는 노이즈가 매우 많습니다.
인스턴스
====================================================================================================
코넬 박스에는 보통 두 개의 블록이 들어 있습니다. 이 블록들은 벽을 기준으로 회전되어 있습니다. 먼저, 사각형 여섯 개로 이루어진 `hittable_list`를 생성하여 상자를 반환하는 함수를 만들어 보겠습니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "hittable.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
#include "hittable_list.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class quad : public hittable {
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
inline shared_ptr box(const point3& a, const point3& b, shared_ptr mat)
{
// Returns the 3D box (six sides) that contains the two opposite vertices a & b.
auto sides = make_shared();
// Construct the two opposite vertices with the minimum and maximum coordinates.
auto min = point3(std::fmin(a.x(),b.x()), std::fmin(a.y(),b.y()), std::fmin(a.z(),b.z()));
auto max = point3(std::fmax(a.x(),b.x()), std::fmax(a.y(),b.y()), std::fmax(a.z(),b.z()));
auto dx = vec3(max.x() - min.x(), 0, 0);
auto dy = vec3(0, max.y() - min.y(), 0);
auto dz = vec3(0, 0, max.z() - min.z());
sides->add(make_shared(point3(min.x(), min.y(), max.z()), dx, dy, mat)); // front
sides->add(make_shared(point3(max.x(), min.y(), max.z()), -dz, dy, mat)); // right
sides->add(make_shared(point3(max.x(), min.y(), min.z()), -dx, dy, mat)); // back
sides->add(make_shared(point3(min.x(), min.y(), min.z()), dz, dy, mat)); // left
sides->add(make_shared(point3(min.x(), max.y(), max.z()), dx, -dz, mat)); // top
sides->add(make_shared(point3(min.x(), min.y(), min.z()), dx, dz, mat)); // bottom
return sides;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [box-class]: [quad.h] 상자 객체]
이제 두 개의 블록을 추가할 수 있습니다(단, 회전은 하지 않습니다).
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
void cornell_box() {
...
world.add(make_shared(point3(555,0,0), vec3(0,555,0), vec3(0,0,555), green));
world.add(make_shared(point3(0,0,0), vec3(0,555,0), vec3(0,0,555), red));
world.add(make_shared(point3(343, 554, 332), vec3(-130,0,0), vec3(0,0,-105), light));
world.add(make_shared(point3(0,0,0), vec3(555,0,0), vec3(0,0,555), white));
world.add(make_shared(point3(555,555,555), vec3(-555,0,0), vec3(0,0,-555), white));
world.add(make_shared(point3(0,0,555), vec3(555,0,0), vec3(0,555,0), white));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
world.add(box(point3(130, 0, 65), point3(295, 165, 230), white));
world.add(box(point3(265, 0, 295), point3(430, 330, 460), white));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
camera cam;
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [add-boxes]: [main.cc] 상자 객체 추가하기]
결과는 다음과 같습니다:

이제 상자가 생겼으니, _실제_ 코넬 박스와 일치하도록 상자를 약간 회전시켜야 합니다. 레이 트레이싱에서는 보통 _인스턴스_ 를 사용하여 이를 수행합니다. 인스턴스는 장면에 배치된 기하 프리미티브의 복사본입니다. 이 인스턴스는 해당 프리미티브의 다른 복사본과 완전히 독립적이며, 이동하거나 회전할 수 있습니다. 이 경우 우리의 기하 프리미티브는 hittable `box` 객체이며, 이를 회전시키려고 합니다. 레이 트레이싱에서는 장면의 객체를 실제로 이동시킬 필요 없이 광선을 반대 방향으로 이동시키면 되므로 이 작업이 특히 쉽습니다. 예를 들어, _평행 이동_ (흔히 _이동_ 이라고 부릅니다)을 생각해 보십시오. 원점에 있는 분홍색 상자를 가져와 모든 x 성분에 2를 더할 수도 있고, (레이 트레이싱에서 거의 항상 그렇게 하듯이) 상자는 그 자리에 그대로 두고 교차 루틴에서 광선의 원점의 x 성분에서 2를 뺄 수도 있습니다.
![Figure [ray-box]: 이동된 광선과 상자 간의 광선-상자 교차](../images/fig-2.08-ray-box.jpg)
인스턴스 평행 이동
---------------------
이를 이동으로 볼지 좌표 변환으로 볼지는 여러분의 선택입니다. 이를 추론하는 방법은 입사 광선을 오프셋만큼 뒤로 이동시키고, 교차가 발생하는지 판단한 다음, 그 교차점을 오프셋만큼 앞으로 이동시킨다고 생각하는 것입니다.
교차가 실제로 입사 광선의 경로상에 있도록 교차점을 오프셋만큼 앞으로 이동시켜야 합니다. 교차점을 앞으로 이동시키는 것을 잊으면 교차가 오프셋된 광선의 경로상에 놓이게 되며, 이는 올바르지 않습니다. 이를 구현하는 코드를 추가하겠습니다.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class hittable {
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
class translate : public hittable {
public:
bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
// Move the ray backwards by the offset
ray offset_r(r.origin() - offset, r.direction(), r.time());
// Determine whether an intersection exists along the offset ray (and if so, where)
if (!object->hit(offset_r, ray_t, rec))
return false;
// Move the intersection point forwards by the offset
rec.p += offset;
return true;
}
private:
shared_ptr object;
vec3 offset;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [translate-hit]: [hittable.h] Hittable 이동의 hit 함수]
... 그런 다음 `translate` 클래스의 나머지 부분을 완성합니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class translate : public hittable {
public:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
translate(shared_ptr object, const vec3& offset)
: object(object), offset(offset)
{
bbox = object->bounding_box() + offset;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
aabb bounding_box() const override { return bbox; }
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
private:
shared_ptr object;
vec3 offset;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
aabb bbox;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [translate-class]: [hittable.h] Hittable 이동 클래스]
경계 상자도 오프셋해야 한다는 점을 잊지 말아야 합니다. 그렇지 않으면 입사 광선이 잘못된 위치를 보고 교차를 간단히 기각해 버릴 수 있습니다. 위의 `object->bounding_box() + offset` 표현식에는 몇 가지 추가 지원이 필요합니다.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class aabb {
...
};
const aabb aabb::empty = aabb(interval::empty, interval::empty, interval::empty);
const aabb aabb::universe = aabb(interval::universe, interval::universe, interval::universe);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
aabb operator+(const aabb& bbox, const vec3& offset) {
return aabb(bbox.x + offset.x(), bbox.y + offset.y(), bbox.z + offset.z());
}
aabb operator+(const vec3& offset, const aabb& bbox) {
return bbox + offset;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [aabb-plus-offset]: [aabb.h] aabb + offset 연산자]
`aabb`의 각 차원은 interval로 표현되므로, `interval`에도 덧셈 연산자를 추가해야 합니다.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class interval {
...
};
const interval interval::empty = interval(+infinity, -infinity);
const interval interval::universe = interval(-infinity, +infinity);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
interval operator+(const interval& ival, double displacement) {
return interval(ival.min + displacement, ival.max + displacement);
}
interval operator+(double displacement, const interval& ival) {
return ival + displacement;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [interval-plus-displacement]: [interval.h] interval + displacement 연산자]
인스턴스 회전
------------------
회전은 이해하거나 공식을 유도하기가 그리 쉽지 않습니다. 흔히 쓰이는 그래픽스 기법은 모든 회전을 x, y, z축을 기준으로 적용하는 것입니다. 이러한 회전은 어떤 의미에서 축 정렬되어 있습니다. 먼저 z축을 기준으로 theta만큼 회전해 보겠습니다. 이 회전은 x와 y만 변경하며, 그 방식은 z에 의존하지 않습니다.
![Figure [rot-z]: Z축을 기준으로 한 회전](../images/fig-2.09-rot-z.jpg)
여기에는 기본적인 삼각법이 사용되며, 해당 공식은 여기서 다루지 않겠습니다. 다소 복잡하지만 어렵지 않으며, 어떤 그래픽스 교재나 많은 강의 노트에서도 찾아볼 수 있습니다. z축을 기준으로 반시계 방향으로 회전한 결과는 다음과 같습니다:
$$ x' = \cos(\theta) \cdot x - \sin(\theta) \cdot y $$
$$ y' = \sin(\theta) \cdot x + \cos(\theta) \cdot y $$
좋은 점은 이 공식이 모든 $\theta$에 대해 성립하며, 사분면 등에 따른 경우 구분이 전혀 필요하지 않다는 것입니다. 역변환은 반대의 기하학적 연산, 즉 $-\theta$만큼 회전하는 것입니다. 여기서 $\cos(\theta) = \cos(-\theta)$이고 $\sin(-\theta) = -\sin(\theta)$임을 떠올리면, 공식은 매우 간단합니다.
마찬가지로 (상자 안의 블록에 대해 하려는 것처럼) y축을 기준으로 회전하는 공식은 다음과 같습니다:
$$ x' = \cos(\theta) \cdot x + \sin(\theta) \cdot z $$
$$ z' = -\sin(\theta) \cdot x + \cos(\theta) \cdot z $$
그리고 x축을 기준으로 회전하려면 다음과 같습니다:
$$ y' = \cos(\theta) \cdot y - \sin(\theta) \cdot z $$
$$ z' = \sin(\theta) \cdot y + \cos(\theta) \cdot z $$
이동을 초기 광선의 단순한 움직임으로 생각하는 것은 무슨 일이 일어나는지 추론하는 좋은 방법입니다. 하지만 회전처럼 더 복잡한 연산에서는 실수로 항을 뒤바꾸거나 (음의 부호를 빠뜨리기) 쉬우므로, 회전은 좌표 변환으로 생각하는 편이 낫습니다.
위의 `translate::hit` 함수에 대한 의사 코드는 이 함수를 _이동_ 의 관점에서 설명합니다:
1. 광선을 오프셋만큼 뒤로 이동합니다
2. 오프셋이 적용된 광선을 따라 교차점이 존재하는지(존재한다면 어디인지) 판단합니다
3. 교차점을 오프셋만큼 앞으로 이동합니다
하지만 이는 _좌표 변환_ 의 관점에서도 생각할 수 있습니다:
1. 광선을 월드 공간에서 객체 공간으로 변환합니다
2. 객체 공간에서 교차점이 존재하는지(존재한다면 어디인지) 판단합니다
3. 교차점을 객체 공간에서 월드 공간으로 변환합니다
객체를 회전하면 교차점만 바뀌는 것이 아니라 표면 법선 벡터도 바뀌며, 이에 따라 반사와 굴절의 방향도 바뀝니다. 따라서 법선도 함께 변경해야 합니다. 다행히도 법선은 벡터와 비슷하게 회전하므로, 위와 같은 공식을 사용할 수 있습니다. 회전과 평행 이동을 거치는 객체에서는 법선과 벡터가 동일하게 보일 수 있지만, 크기 변환을 거치는 객체에서는 법선이 표면에 직교하도록 유지하기 위해 특별한 주의가 필요합니다. 여기서는 이를 다루지 않겠지만, 크기 변환을 구현한다면 표면 법선 변환에 대해 조사해 보십시오.
먼저 광선을 월드 공간에서 객체 공간으로 변환해야 하며, 회전의 경우 이는 $-\theta$만큼 회전하는 것을 의미합니다.
$$ x' = \cos(\theta) \cdot x - \sin(\theta) \cdot z $$
$$ z' = \sin(\theta) \cdot x + \cos(\theta) \cdot z $$
이제 y축 회전을 위한 클래스를 만들 수 있습니다. 먼저 hit 함수부터 다루어 보겠습니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class translate : public hittable {
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
class rotate_y : public hittable {
public:
bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
// Transform the ray from world space to object space.
auto origin = point3(
(cos_theta * r.origin().x()) - (sin_theta * r.origin().z()),
r.origin().y(),
(sin_theta * r.origin().x()) + (cos_theta * r.origin().z())
);
auto direction = vec3(
(cos_theta * r.direction().x()) - (sin_theta * r.direction().z()),
r.direction().y(),
(sin_theta * r.direction().x()) + (cos_theta * r.direction().z())
);
ray rotated_r(origin, direction, r.time());
// Determine whether an intersection exists in object space (and if so, where).
if (!object->hit(rotated_r, ray_t, rec))
return false;
// Transform the intersection from object space back to world space.
rec.p = point3(
(cos_theta * rec.p.x()) + (sin_theta * rec.p.z()),
rec.p.y(),
(-sin_theta * rec.p.x()) + (cos_theta * rec.p.z())
);
rec.normal = vec3(
(cos_theta * rec.normal.x()) + (sin_theta * rec.normal.z()),
rec.normal.y(),
(-sin_theta * rec.normal.x()) + (cos_theta * rec.normal.z())
);
return true;
}
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [rot-y-hit]: [hittable.h] Hittable Y축 회전 hit 함수]
... 그리고 이제 클래스의 나머지 부분입니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class rotate_y : public hittable {
public:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
rotate_y(shared_ptr object, double angle) : object(object) {
auto radians = degrees_to_radians(angle);
sin_theta = std::sin(radians);
cos_theta = std::cos(radians);
bbox = object->bounding_box();
point3 min( infinity, infinity, infinity);
point3 max(-infinity, -infinity, -infinity);
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
for (int k = 0; k < 2; k++) {
auto x = i*bbox.x.max + (1-i)*bbox.x.min;
auto y = j*bbox.y.max + (1-j)*bbox.y.min;
auto z = k*bbox.z.max + (1-k)*bbox.z.min;
auto newx = cos_theta*x + sin_theta*z;
auto newz = -sin_theta*x + cos_theta*z;
vec3 tester(newx, y, newz);
for (int c = 0; c < 3; c++) {
min[c] = std::fmin(min[c], tester[c]);
max[c] = std::fmax(max[c], tester[c]);
}
}
}
}
bbox = aabb(min, max);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
aabb bounding_box() const override { return bbox; }
private:
shared_ptr object;
double sin_theta;
double cos_theta;
aabb bbox;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [rot-y]: [hittable.h] Hittable Y축 회전 클래스]
그리고 Cornell 장면의 변경 사항은 다음과 같습니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
void cornell_box() {
...
world.add(make_shared(point3(0,0,555), vec3(555,0,0), vec3(0,555,0), white));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
shared_ptr box1 = box(point3(0,0,0), point3(165,330,165), white);
box1 = make_shared(box1, 15);
box1 = make_shared(box1, vec3(265,0,295));
world.add(box1);
shared_ptr box2 = box(point3(0,0,0), point3(165,165,165), white);
box2 = make_shared(box2, -18);
box2 = make_shared(box2, vec3(130,0,65));
world.add(box2);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
camera cam;
...
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [scene-rot-y]: [main.cc] Y축으로 회전한 상자가 있는 Cornell 장면]
그 결과는 다음과 같습니다:

볼륨
====================================================================================================
레이 트레이서에 추가하면 좋은 것 중 하나는 연기/안개/박무입니다. 이것들은 때때로 _볼륨_ 또는 _참여 매질_ 이라고 불립니다. 추가하면 좋은 또 다른 기능은 하위 표면 산란으로, 이는 객체 내부의 짙은 안개와 비슷한 것입니다. 볼륨은 표면과는 전혀 다른 존재이기 때문에 이는 보통 소프트웨어 아키텍처에 큰 혼란을 가져오지만, 볼륨을 무작위 표면으로 만드는 재치 있는 기법이 있습니다. 연기 덩어리는 볼륨 내의 모든 지점에서 확률적으로 존재할 수도 있고 존재하지 않을 수도 있는 표면으로 대체할 수 있습니다. 코드를 보면 이것이 더 잘 이해될 것입니다.
균일 밀도 매질
-------------------------
먼저 밀도가 일정한 볼륨부터 시작하겠습니다. 볼륨을 지나가는 광선은 볼륨 내부에서 산란할 수도 있고, 그림의 가운데 광선처럼 볼륨을 끝까지 통과할 수도 있습니다. 옅은 안개처럼 더 얇고 투명한 볼륨일수록 가운데 광선과 같은 광선이 나타날 가능성이 더 높습니다. 광선이 볼륨 속을 얼마나 멀리 지나가야 하는지도 광선이 볼륨을 끝까지 통과할 가능성을 결정합니다.
![Figure [ray-vol]: 광선-볼륨 상호작용](../images/fig-2.10-ray-vol.jpg)
광선은 볼륨을 통과하는 동안 어느 지점에서든 산란할 수 있습니다. 볼륨의 밀도가 높을수록 산란할 가능성도 커집니다. 광선이 임의의 작은 거리 $\Delta L$ 안에서 산란할 확률은 다음과 같습니다:
$$ \mathit{probability} = C \cdot \Delta L $$
여기서 $C$는 볼륨의 광학 밀도에 비례합니다. 미분 방정식을 모두 풀어 보면, 난수 하나에 대해 산란이 일어나는 거리를 얻게 됩니다. 그 거리가 볼륨 밖에 있으면 “충돌”은 없습니다. 밀도가 일정한 볼륨에는 밀도 $C$와 경계만 있으면 됩니다. 경계로는 또 다른 hittable을 사용하겠습니다.
그 결과로 만들어지는 클래스는 다음과 같습니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#ifndef CONSTANT_MEDIUM_H
#define CONSTANT_MEDIUM_H
#include "hittable.h"
#include "material.h"
#include "texture.h"
class constant_medium : public hittable {
public:
constant_medium(shared_ptr boundary, double density, shared_ptr tex)
: boundary(boundary), neg_inv_density(-1/density),
phase_function(make_shared(tex))
{}
constant_medium(shared_ptr boundary, double density, const color& albedo)
: boundary(boundary), neg_inv_density(-1/density),
phase_function(make_shared(albedo))
{}
bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
hit_record rec1, rec2;
if (!boundary->hit(r, interval::universe, rec1))
return false;
if (!boundary->hit(r, interval(rec1.t+0.0001, infinity), rec2))
return false;
if (rec1.t < ray_t.min) rec1.t = ray_t.min;
if (rec2.t > ray_t.max) rec2.t = ray_t.max;
if (rec1.t >= rec2.t)
return false;
if (rec1.t < 0)
rec1.t = 0;
auto ray_length = r.direction().length();
auto distance_inside_boundary = (rec2.t - rec1.t) * ray_length;
auto hit_distance = neg_inv_density * std::log(random_double());
if (hit_distance > distance_inside_boundary)
return false;
rec.t = rec1.t + hit_distance / ray_length;
rec.p = r.at(rec.t);
rec.normal = vec3(1,0,0); // arbitrary
rec.front_face = true; // also arbitrary
rec.mat = phase_function;
return true;
}
aabb bounding_box() const override { return boundary->bounding_box(); }
private:
shared_ptr boundary;
double neg_inv_density;
shared_ptr phase_function;
};
#endif
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [const-med-class]: [constant_medium.h] 균일 매질 클래스]
isotropic의 산란 함수는 균일한 무작위 방향을 선택합니다:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class diffuse_light : public material {
...
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
class isotropic : public material {
public:
isotropic(const color& albedo) : tex(make_shared(albedo)) {}
isotropic(shared_ptr tex) : tex(tex) {}
bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered)
const override {
scattered = ray(rec.p, random_unit_vector(), r_in.time());
attenuation = tex->value(rec.u, rec.v, rec.p);
return true;
}
private:
shared_ptr tex;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [isotropic-class]: [material.h] isotropic 클래스]
경계 주변의 로직을 이렇게 신중하게 다뤄야 하는 이유는 광선의 원점이 볼륨 내부에 있을 때에도 이 코드가 올바르게 동작하도록 보장해야 하기 때문입니다. 구름 속에서는 빛이 이리저리 많이 튀기 때문에 이런 경우가 흔합니다.
또한 위의 코드는 광선이 균일 매질의 경계를 한 번 빠져나가면 경계 밖에서 영원히 계속 진행한다고 가정합니다. 다시 말해, 경계의 형태가 볼록하다고 가정합니다. 따라서 이 구현은 상자나 구 같은 경계에서는 동작하지만, 토러스나 빈 공간을 포함한 형태에서는 동작하지 않습니다. 임의의 형태를 처리하는 구현을 작성할 수도 있지만, 이는 독자를 위한 연습 문제로 남겨 두겠습니다.
연기 상자와 안개 상자가 있는 코넬 박스 렌더링하기
-------------------------------------------------
두 블록을 연기와 안개(어두운 입자와 밝은 입자)로 바꾸고, 빠른 수렴을 위해 조명을 더 크게(그리고 장면이 하얗게 날아가지 않도록 더 어둡게) 만들면:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "bvh.h"
#include "camera.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
#include "constant_medium.h"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "hittable.h"
#include "hittable_list.h"
#include "material.h"
#include "quad.h"
#include "sphere.h"
#include "texture.h"
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
void cornell_smoke() {
hittable_list world;
auto red = make_shared(color(.65, .05, .05));
auto white = make_shared(color(.73, .73, .73));
auto green = make_shared(color(.12, .45, .15));
auto light = make_shared(color(7, 7, 7));
world.add(make_shared(point3(555,0,0), vec3(0,555,0), vec3(0,0,555), green));
world.add(make_shared(point3(0,0,0), vec3(0,555,0), vec3(0,0,555), red));
world.add(make_shared(point3(113,554,127), vec3(330,0,0), vec3(0,0,305), light));
world.add(make_shared(point3(0,555,0), vec3(555,0,0), vec3(0,0,555), white));
world.add(make_shared(point3(0,0,0), vec3(555,0,0), vec3(0,0,555), white));
world.add(make_shared(point3(0,0,555), vec3(555,0,0), vec3(0,555,0), white));
shared_ptr box1 = box(point3(0,0,0), point3(165,330,165), white);
box1 = make_shared(box1, 15);
box1 = make_shared(box1, vec3(265,0,295));
shared_ptr box2 = box(point3(0,0,0), point3(165,165,165), white);
box2 = make_shared(box2, -18);
box2 = make_shared(box2, vec3(130,0,65));
world.add(make_shared(box1, 0.01, color(0,0,0)));
world.add(make_shared(box2, 0.01, color(1,1,1)));
camera cam;
cam.aspect_ratio = 1.0;
cam.image_width = 600;
cam.samples_per_pixel = 200;
cam.max_depth = 50;
cam.background = color(0,0,0);
cam.vfov = 40;
cam.lookfrom = point3(278, 278, -800);
cam.lookat = point3(278, 278, 0);
cam.vup = vec3(0,1,0);
cam.defocus_angle = 0;
cam.render(world);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
int main() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
switch (8) {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
case 1: bouncing_spheres(); break;
case 2: checkered_spheres(); break;
case 3: earth(); break;
case 4: perlin_spheres(); break;
case 5: quads(); break;
case 6: simple_light(); break;
case 7: cornell_box(); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
case 8: cornell_smoke(); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [cornell-smoke]: [main.cc] 연기가 있는 코넬 박스]
다음과 같은 결과를 얻습니다:

모든 새 기능을 테스트하는 장면
====================================================================================================
이제 장면 전체를 덮는 크고 옅은 안개와 파란색 표면 하 반사 구를 넣어 모든 것을 하나로 합쳐 보겠습니다(표면 하 산란을 명시적으로 구현하지는 않았지만, 유전체 내부에 볼륨을 넣은 것이 바로 표면 하 재질입니다). 렌더러에 남아 있는 가장 큰 한계는 그림자 광선이 없다는 점이지만, 바로 그 덕분에 커스틱과 표면 하 산란을 공짜로 얻을 수 있습니다. 이는 양날의 검과 같은 설계 결정입니다.
또한 빠른 테스트를 위해 더 낮은 품질로도 렌더링할 수 있도록 이 최종 장면을 매개변수화한다는 점에 유의하십시오.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
void final_scene(int image_width, int samples_per_pixel, int max_depth) {
hittable_list boxes1;
auto ground = make_shared(color(0.48, 0.83, 0.53));
int boxes_per_side = 20;
for (int i = 0; i < boxes_per_side; i++) {
for (int j = 0; j < boxes_per_side; j++) {
auto w = 100.0;
auto x0 = -1000.0 + i*w;
auto z0 = -1000.0 + j*w;
auto y0 = 0.0;
auto x1 = x0 + w;
auto y1 = random_double(1,101);
auto z1 = z0 + w;
boxes1.add(box(point3(x0,y0,z0), point3(x1,y1,z1), ground));
}
}
hittable_list world;
world.add(make_shared(boxes1));
auto light = make_shared(color(7, 7, 7));
world.add(make_shared(point3(123,554,147), vec3(300,0,0), vec3(0,0,265), light));
auto center1 = point3(400, 400, 200);
auto center2 = center1 + vec3(30,0,0);
auto sphere_material = make_shared(color(0.7, 0.3, 0.1));
world.add(make_shared(center1, center2, 50, sphere_material));
world.add(make_shared(point3(260, 150, 45), 50, make_shared(1.5)));
world.add(make_shared(
point3(0, 150, 145), 50, make_shared(color(0.8, 0.8, 0.9), 1.0)
));
auto boundary = make_shared(point3(360,150,145), 70, make_shared(1.5));
world.add(boundary);
world.add(make_shared(boundary, 0.2, color(0.2, 0.4, 0.9)));
boundary = make_shared(point3(0,0,0), 5000, make_shared(1.5));
world.add(make_shared(boundary, .0001, color(1,1,1)));
auto emat = make_shared(make_shared("earthmap.jpg"));
world.add(make_shared(point3(400,200,400), 100, emat));
auto pertext = make_shared(0.2);
world.add(make_shared(point3(220,280,300), 80, make_shared(pertext)));
hittable_list boxes2;
auto white = make_shared(color(.73, .73, .73));
int ns = 1000;
for (int j = 0; j < ns; j++) {
boxes2.add(make_shared(point3::random(0,165), 10, white));
}
world.add(make_shared(
make_shared(
make_shared(boxes2), 15),
vec3(-100,270,395)
)
);
camera cam;
cam.aspect_ratio = 1.0;
cam.image_width = image_width;
cam.samples_per_pixel = samples_per_pixel;
cam.max_depth = max_depth;
cam.background = color(0,0,0);
cam.vfov = 40;
cam.lookfrom = point3(478, 278, -600);
cam.lookat = point3(278, 278, 0);
cam.vup = vec3(0,1,0);
cam.defocus_angle = 0;
cam.render(world);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
int main() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
switch (9) {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
case 1: bouncing_spheres(); break;
case 2: checkered_spheres(); break;
case 3: earth(); break;
case 4: perlin_spheres(); break;
case 5: quads(); break;
case 6: simple_light(); break;
case 7: cornell_box(); break;
case 8: cornell_smoke(); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
case 9: final_scene(800, 10000, 40); break;
default: final_scene(400, 250, 4); break;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Listing [scene-final]: [main.cc] 최종 장면]
픽셀당 광선 10,000개로 실행하면(푹 주무십시오) 다음과 같은 결과를 얻습니다:

이제 여러분만의 정말 멋진 이미지를 직접 만들어 보십시오! 프로젝트와 관련된 추가 자료는 [저희의 더 읽을거리 위키 페이지][wiki-further]를 참조하십시오. 질문, 의견, 멋진 이미지가 있으면 언제든지 ptrshrl@gmail.com 주소로 이메일을 보내 주십시오.
(insert acknowledgments.md.html here)
이 책 인용하기
====================================================================================================
일관된 인용은 이 저작물의 출처, 위치, 버전을 더 쉽게 식별할 수 있게 해 줍니다. 이 책을 인용하실 경우, 가능하면 다음 형식 중 하나를 사용해 주시기 바랍니다.
기본 정보
-----------
- **제목 (시리즈)**: “주말 동안 배우는 레이 트레이싱 시리즈”
- **제목 (책)**: “레이 트레이싱: 다음 주”
- **저자**: Peter Shirley, Trevor David Black, Steve Hollasch
- **버전/판**: v4.0.2
- **날짜**: 2025-04-25
- **URL (시리즈)**:
- **URL (책)**:
스니펫
---------
### Markdown
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[_Ray Tracing: The Next Week_](https://raytracing.github.io/books/RayTracingTheNextWeek.html)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
### HTML
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Ray Tracing: The Next Week
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
### LaTeX 및 BibTex
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~\cite{Shirley2025RTW2}
@misc{Shirley2025RTW2,
title = {Ray Tracing: The Next Week},
author = {Peter Shirley, Trevor David Black, Steve Hollasch},
year = {2025},
month = {April},
note = {\small \texttt{https://raytracing.github.io/books/RayTracingTheNextWeek.html}},
url = {https://raytracing.github.io/books/RayTracingTheNextWeek.html}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
### BibLaTeX
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
\usepackage{biblatex}
~\cite{Shirley2025RTW2}
@online{Shirley2025RTW2,
title = {Ray Tracing: The Next Week},
author = {Peter Shirley, Trevor David Black, Steve Hollasch},
year = {2025},
month = {April},
url = {https://raytracing.github.io/books/RayTracingTheNextWeek.html}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
### IEEE
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
“Ray Tracing: The Next Week.” raytracing.github.io/books/RayTracingTheNextWeek.html
(accessed MMM. DD, YYYY)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
### MLA
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Ray Tracing: The Next Week. raytracing.github.io/books/RayTracingTheNextWeek.html
Accessed DD MMM. YYYY.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[Peter Shirley]: https://github.com/petershirley
[Steve Hollasch]: https://github.com/hollasch
[Trevor David Black]: https://github.com/trevordblack
[stb_image]: https://github.com/nothings/stb
[readme]: ../README.md
[releases]: https://github.com/RayTracing/raytracing.github.io/releases/
[wiki-further]: https://github.com/RayTracing/raytracing.github.io/wiki/Further-Readings