混合密度
\(\cos \theta\) となる PDF とライトをサンプルする PDF の両方が手に入った。次はこの二つを組み合わせた PDF が作りたい。
ライティングと反射の平均
確率論では複数の密度関数を混ぜて混合密度 (mixture density) を作るテクニックがよく使われる。例えば二つの密度関数の平均を取れば混合密度となる: \[ p_{\text{mixture}}(\text{direction}) = \frac{1}{2} p_{\text{reflection}}(\text{direction}) + \frac{1}{2} p_{\text{light}}(\text{direction}) \]
これを実装する方法を考える。非常に重要な細かい問題があるので、実装は意外なほど難しくなる。まずランダムな方向の選択は簡単に行える:
if (random_double() < 0.5)
pdf_reflection に従って方向を選択する
else
pdf_light に従って方向を選択する
\(p_{\text{mixture}}\) の計算にはさらに慎重な議論が必要になる。上のコードが選択する方向が両方の PDF から得られる可能性もあるので、\(p_{\text{reflection}}\) と \(p_{\text{light}}\) の両方を計算しなければならない。例えば \(p_{\text{reflection}}\) を使って生成した方向がライトを向いている可能性がある。
ここまでを振り返ると、PDF にはサポートすべき関数が二つあることが分かる:
- 与えられた方向に対する PDF の値を求める。
- 分布に従うランダムな方向を生成する。
具体的な関数の処理は \(p_{\text{reflection}}\) や \(p_{\text{light}}\) および二つの混合密度の間で異なる。これはクラスの継承が発明された理由そのものだ! 抽象クラスで正確に何が必要になるかは分からないので、最小限のインターフェースを作って上手く行くことを願うという貪欲なアプローチを私は取る。このアプローチで作った PDF クラスを示す:
class pdf {
public:
virtual ~pdf() {}
virtual double value(const vec3& direction) const = 0;
virtual vec3 generate() const = 0;
};
pdf.h
] pdf
クラス
この設計で大丈夫かどうかは \(p_{\text{reflection}}\) や \(p_{\text{light}}\) を子クラスとして実装すれば明らかになる。ライトのサンプルでは hittable
が今までにないクエリに答える必要があるので、インターフェースを新しく追加する必要があるだろう。そのときは AABB と同じように、親の hittable
クラスにメンバー関数を追加して子クラスの実装を省けないかを最初に考える。
まずコサイン密度を実装しよう:
inline vec3 random_cosine_direction() {
auto r1 = random_double();
auto r2 = random_double();
auto z = sqrt(1-r2);
auto phi = 2*pi*r1;
auto x = cos(phi)*sqrt(r2);
auto y = sin(phi)*sqrt(r2);
return vec3(x, y, z);
}
class cosine_pdf : public pdf {
public:
cosine_pdf(const vec3& w) { uvw.build_from_w(w); }
virtual double value(const vec3& direction) const {
auto cosine = dot(unit_vector(direction), uvw.w());
return (cosine <= 0) ? 0 : cosine/pi;
}
virtual vec3 generate() const {
return uvw.local(random_cosine_direction());
}
public:
onb uvw;
};
pdf.h
] cosine_pdf
クラス
ray_color()
関数を次のように変更すればこのクラスを試せる。pdf
クラスをコードに組み込むのに加えて、名前の衝突を避けるためにローカル変数 pdf
の名前を変える必要がある。
color ray_color(
const ray& r, const color& background, const hittable& world, int depth
) {
hit_record rec;
// 反射回数が一定よりも多くなったら、その時点で追跡をやめる
if (depth <= 0)
return color(0,0,0);
// レイがどのオブジェクトとも交わらないなら、背景色を返す
if (!world.hit(r, 0.001, infinity, rec))
return background;
ray scattered;
color attenuation;
color emitted = rec.mat_ptr->emitted(r, rec, rec.u, rec.v, rec.p);
double pdf_val;
color albedo;
if (!rec.mat_ptr->scatter(r, rec, albedo, scattered, pdf_val))
return emitted;
cosine_pdf p(rec.normal);
scattered = ray(rec.p, p.generate(), r.time());
pdf_val = p.value(scattered.direction());
return emitted
+ albedo * rec.mat_ptr->scattering_pdf(r, rec, scattered)
* ray_color(scattered, background, world, depth-1)
/ pdf_val;
}
main.cc
] コサイン密度を使った ray_color
関数
このプログラムからは今までと同じ結果が得られる。何が起こったかというと、リファクタリングによって pdf
が計算される場所が移動したのである。
物体に向けてレイをサンプルする
続いて hittable
に向かうレイのサンプルを実装しよう。例えばライトが hittable
となる。
class hittable_pdf : public pdf {
public:
hittable_pdf(shared_ptr<hittable> p, const point3& origin)
: ptr(p), o(origin) {}
virtual double value(const vec3& direction) const {
return ptr->pdf_value(o, direction);
}
virtual vec3 generate() const {
return ptr->random(o);
}
public:
shared_ptr<hittable> ptr;
point3 o;
};
pdf.h
] hittable_pdf
クラス
このクラスはまだ実装していない hittable
クラスの関数を二つ使っている。hittable
の子クラス全てで実装するのを避けるために、hittable
クラスにダミー関数を二つ追加する:
class hittable {
public:
virtual bool hit(
const ray& r, double t_min, double t_max, hit_record& rec
) const = 0;
virtual bool bounding_box(double t0, double t1, aabb& output_box) const = 0;
virtual double pdf_value(const point3& o, const vec3& v) const {
return 0.0;
}
virtual vec3 random(const vec3& o) const {
return vec3(1, 0, 0);
}
};
hittable.h
] 新しく二つのメソッドを追加した hittable
クラス
この関数を xz_rect
で実装する:
class xz_rect: public hittable {
public:
...
virtual double pdf_value(const point3& origin, const vec3& v) const {
hit_record rec;
if (!this->hit(ray(origin, v), 0.001, infinity, rec))
return 0;
auto area = (x1-x0)*(z1-z0);
auto distance_squared = rec.t * rec.t * v.length_squared();
auto cosine = fabs(dot(v, rec.normal) / v.length());
return distance_squared / (cosine * area);
}
virtual vec3 random(const point3& origin) const {
auto random_point = point3(random_double(x0,x1), k, random_double(z0,z1));
return random_point - origin;
}
...
}
aarect.h
] pdf
を追加した xz_rect
そして ray_color()
も変更する:
color ray_color(
const ray& r, const color& background, const hittable& world, int depth
) {
hit_record rec;
// 反射回数が一定よりも多くなったら、その時点で追跡をやめる
if (depth <= 0)
return color(0,0,0);
// レイがどのオブジェクトとも交わらないなら、背景色を返す
if (!world.hit(r, 0.001, infinity, rec))
return background;
ray scattered;
color attenuation;
color emitted = rec.mat_ptr->emitted(r, rec, rec.u, rec.v, rec.p);
double pdf_val;
color albedo;
if (!rec.mat_ptr->scatter(r, rec, albedo, scattered, pdf_val))
return emitted;
shared_ptr<hittable> light_shape =
make_shared<xz_rect>(213, 343, 227, 332, 554, nullptr);
hittable_pdf p(light_shape, rec.p);
scattered = ray(rec.p, p.generate(), r.time());
pdf_val = p.value(scattered.direction());
return emitted
+ albedo * rec.mat_ptr->scattering_pdf(r, rec, scattered)
* ray_color(scattered, background, world, depth-1)
/ pdf_val;
}
main.cc
] hittable
の PDF に対応した ray_color
関数
\(10\) サンプル/ピクセルでレンダリングすると次の画像が得られる:
混合 PDF クラス
次はコサインとライトの混合密度を使ったサンプリングを行う。混合密度を表すクラスは簡単に書ける:
class mixture_pdf : public pdf {
public:
mixture_pdf(shared_ptr<pdf> p0, shared_ptr<pdf> p1) {
p[0] = p0;
p[1] = p1;
}
virtual double value(const vec3& direction) const {
return 0.5 * p[0]->value(direction) + 0.5 *p[1]->value(direction);
}
virtual vec3 generate() const {
if (random_double() < 0.5)
return p[0]->generate();
else
return p[1]->generate();
}
public:
shared_ptr<pdf> p[2];
};
pdf.h
] mixture_pdf
クラス
これを ray_color()
に組み込む:
color ray_color(
const ray& r, const color& background, const hittable& world, int depth
) {
hit_record rec;
// 反射回数が一定よりも多くなったら、その時点で追跡をやめる
if (depth <= 0)
return color(0,0,0);
// レイがどのオブジェクトとも交わらないなら、背景色を返す
if (!world.hit(r, 0.001, infinity, rec))
return background;
ray scattered;
color attenuation;
color emitted = rec.mat_ptr->emitted(r, rec, rec.u, rec.v, rec.p);
double pdf_val;
color albedo;
if (!rec.mat_ptr->scatter(r, rec, albedo, scattered, pdf_val))
return emitted;
shared_ptr<hittable> light_ptr =
make_shared<xz_rect>(213, 343, 227, 332, 554, nullptr);
auto p0 = make_shared<hittable_pdf>(light_shape, rec.p);
auto p1 = make_shared<cosine_pdf>(rec.normal);
mixture_pdf p(p0, p1);
scattered = ray(rec.p, p.generate(), r.time());
pdf_val = p.value(scattered.direction());
return emitted
+ albedo * rec.mat_ptr->scattering_pdf(r, rec, scattered)
* ray_color(scattered, background, world, depth-1)
/ pdf_val;
}
main.cc
] 混合 PDF を使った ray_color
関数
\(1000\) サンプル/ピクセルとしてレンダリングすれば次の画像が得られる: