混合密度

\(\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 にはサポートすべき関数が二つあることが分かる:

  1. 与えられた方向に対する PDF の値を求める。
  2. 分布に従うランダムな方向を生成する。

具体的な関数の処理は \(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;
};
リスト 3.22 [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;
};
リスト 3.23 [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;
}
リスト 3.24 [main.cc] コサイン密度を使った ray_color 関数

このプログラムからは今までと同じ結果が得られる。何が起こったかというと、リファクタリングによって pdf が計算される場所が移動したのである。

図 3.13:
コサイン密度を使ったコーネルボックス

物体に向けてレイをサンプルする

続いて 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;
};
リスト 3.25 [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);
  }
};
リスト 3.26 [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;
  }
  ...
}
リスト 3.27 [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;
}
リスト 3.28 [main.cc] hittable の PDF に対応した ray_color 関数

\(10\) サンプル/ピクセルでレンダリングすると次の画像が得られる:

図 3.14:
hittable のライトをサンプルしたコーネルボックス

混合 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];
};
リスト 3.29 [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;
}
リスト 3.30 [main.cc] 混合 PDF を使った ray_color 関数

\(1000\) サンプル/ピクセルとしてレンダリングすれば次の画像が得られる:

図 3.15:
混合 PDF を使ったコーネルボックス
広告