重点サンプリング付きマテリアル

ここからいくつかの節を使って、画像のノイズを減らすためにレイを光源に向かって多く放つようプログラムを改変する。光源へ多くレイを放つときの PDF を \(p_{\text{light}}(\text{direction})\) として、\(s(\text{direction})\) が大きい部分へ多くレイを放つときの PDF を \(p_{\text{surface}}(\text{direction})\) とする。PDF に関して便利なのが、複数の PDF の線形和を取って確率密度を混ぜることで別の PDF を作れることだ。例えば一番簡単な線形和 \[ p(\text{direction}) = \frac{1}{2}\cdotp p_{\text{light}}(\text{direction}) + \frac{1}{2}\cdot p_{\text{surface}}(\text{direction}) \] は PDF である。重みが正で積分が \(1\) である限り、複数 PDF を組み合わせてできる関数は PDF となる。PDF はどんなものでも構わないことを思い出そう: 任意の PDF を使ったモンテカルロ積分は真の値に収束する。よって問題は \(s(\text{direction}) \cdot \text{color}(\text{direction})\) が大きい部分の PDF を大きくする方法である。拡散表面では、これは \(\text{color}(\text{direction})\) が高い部分を推定する問題となる。

鏡面では \(s(\text{direction})\) が特定の方向の周りでのみ非常に大きくなるので、拡散表面より \(s(\text{direction})\) がずっと重要になる。実は多くのレンダラは鏡面を特殊ケースとしており、\(s/p\) を陰に計算して済ませている ──現在の私たちのコードもこうしている。

コーネルボックス

少しリファクタリングをして、ランバーティアンでないマテリアルを取り除こう。コーネルボックスのシーンを使って、モデルを生成する関数の中でカメラも生成する:

hittable_list cornell_box(camera& cam, double aspect) {
  hittable_list world;

  auto red   = make_shared<lambertian>(make_shared<solid_color>(.65, .05, .05));
  auto white = make_shared<lambertian>(make_shared<solid_color>(.73, .73, .73));
  auto green = make_shared<lambertian>(make_shared<solid_color>(.12, .45, .15));
  auto light = make_shared<diffuse_light>(make_shared<solid_color>(15, 15, 15));

  world.add(make_shared<yz_rect>(0, 555, 0, 555, 555, green));
  world.add(make_shared<yz_rect>(0, 555, 0, 555, 0, red));
  world.add(make_shared<xz_rect>(213, 343, 227, 332, 554, light));
  world.add(make_shared<xz_rect>(0, 555, 0, 555, 555, white));
  world.add(make_shared<xz_rect>(0, 555, 0, 555, 0, white));
  world.add(make_shared<xy_rect>(0, 555, 0, 555, 555, white));

  shared_ptr<hittable> box1 =
    make_shared<box>(point3(0,0,0), point3(165,330,165), white);
  box1 = make_shared<rotate_y>(box1, 15);
  box1 = make_shared<translate>(box1, vec3(265,0,295));
  world.add(box1);

  shared_ptr<hittable> box2 =
    make_shared<box>(point3(0,0,0), point3(165,165,165), white);
  box2 = make_shared<rotate_y>(box2, -18);
  box2 = make_shared<translate>(box2, vec3(130,0,65));
  world.add(box2);

  point3 lookfrom(278, 278, -800);
  point3 lookat(278, 278, 0);
  vec3 vup(0, 1, 0);
  auto dist_to_focus = 10.0;
  auto aperture = 0.0;
  auto vfov = 40.0;
  auto t0 = 0.0;
  auto t1 = 1.0;

  cam = camera(
    lookfrom, lookat, vup, vfov, aspect, aperture, dist_to_focus, t0, t1
  );

  return world;
}
リスト 3.10 [main.cc] リファクタリングしたコーネルボックス

解像度を 500 × 500 として 1 ピクセルに 500 本のレイを放つと、私の Macbook (1 コア使用) では次の画像が 10 分で得られた:

図 3.6: リファクタリングしたコーネルボックス

このノイズを減らすのが目標である。これからライトに向けてレイを多く放つ PDF を構築することでこれを達成する。

まず、明示的に PDF のサンプルと正規化を行うようコードを変更する。モンテカルロ積分の基本を思い出そう: \(\displaystyle \int f(x) \approx f(r)/p(r)\) である。lambertian マテリアルでは今までと同じように \(p(\text{direction}) = \cos \theta / \pi\) としてサンプルを行うことにする。

基底クラス material に重点サプリングを追加する:

class material {
public:

  virtual bool scatter(
    const ray& r_in,
    const hit_record& rec,
    color& albedo,
    ray& scattered,
    double& pdf
  ) const {
    return false;
  }

  virtual double scattering_pdf(
    const ray& r_in, const hit_record& rec, const ray& scattered
  ) const {
    return 0;
  }

  virtual color emitted(double u, double v, const point3& p) const {
    return color(0,0,0);
  }
};
リスト 3.11 [material.h] 重点サプリングを追加した material クラス

lambertian クラスは次のようになる:

class lambertian : public material {
public:
  lambertian(shared_ptr<texture> a) : albedo(a) {}

  virtual bool scatter(
    const ray& r_in,
    const hit_record& rec,
    color& alb,
    ray& scattered,
    double& pdf
  ) const {
    point3 target = rec.p + rec.normal + random_unit_vector();
    scattered = ray(rec.p, unit_vector(target-rec.p), r_in.time());
    alb = albedo->value(rec.u, rec.v, rec.p);
    pdf = dot(rec.normal, scattered.direction()) / pi;
    return true;
  }

  double scattering_pdf(
    const ray& r_in, const hit_record& rec, const ray& scattered
  ) const {
    auto cosine = dot(rec.normal, unit_vector(scattered.direction()));
    return cosine < 0 ? 0 : cosine/pi;
  }

public:
  shared_ptr<texture> albedo;
};
リスト 3.12 [material.h] 重点サプリングを追加した lambertian クラス

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(rec.u, rec.v, rec.p);
  double pdf;
  color albedo;

  if (!rec.mat_ptr->scatter(r, rec, albedo, scattered, pdf))
    return emitted;

  return emitted
     + albedo * rec.mat_ptr->scattering_pdf(r, rec, scattered)
          * ray_color(scattered, background, world, depth-1) / pdf;
}
リスト 3.13 [main.cc] 重点サプリングを追加した ray_color 関数

レンダリングされる画像は変わらないはずである。

ランダムな半球サンプリング

ここで実験として、異なるサンプリング戦略を試してみよう。第一巻で示した、物体表面の半球上を一様ランダムにサンプルする方法を使う。このとき \(p(\text{direction}) = \frac{1}{2}\pi\) であり、scatter 関数は次のようになる:

bool scatter(
  const ray& r_in,
  const hit_record& rec,
  color& alb,
  ray& scattered,
  double& pdf
) const {
  vec3 direction = random_in_hemisphere(rec.normal);
  scattered = ray(rec.p, unit_vector(direction), r_in.time());
  alb = albedo->value(rec.u, rec.v, rec.p);
  pdf = 0.5 / pi;
  return true;
}
リスト 3.14 [material.h] 変更した scatter 関数

これまでと同じ画像が得られるが、ノイズの付き方が変わる。

ノイズを減らすために、もう少し理論を説明する。