混合密度

\(\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 を使ったコーネルボックス


Amazon.co.jp アソシエイト (広告)
Audible の無料体験を始めよう
amazon music unlimited で音楽聞き放題
amazon 広告amazon 広告 フォトンマッピング ―実写に迫るコンピュータグラフィックス
amazon 広告amazon 広告 色彩工学入門 -定量的な色の理解と活用-
amazon 広告amazon 広告 イラストレイテッド 光の科学
amazon 広告amazon 広告 Foundations of Game Engine Development, Volume 2: Rendering