モーションブラー

レンダリングにレイトレーシングを選択したということは、実行時間よりも画像の質を優先したということである。これまでのぼやけた反射焦点ぼけではピクセルごとに複数のサンプルが必要だった。レイトレーシングで嬉しいのが、ほとんど全てのエフェクトが総当たりで解決できる点である。モーションブラー (motion blur) はそんなエフェクトの分かりやすい例と言える。現実のカメラは有限の時間に渡ってシャッターを開いたままにするので、その間にカメラや被写体が移動する可能性がある。カメラが撮影する画像はシャッターが開いている間にカメラのセンサーに届く像の平均値である。

時空間レイトレーシング入門

カメラからレイを放つときにシャッターが開いている瞬間をランダムに選び、その瞬間におけるレイを放つことでモーションブラーを近似できる。シーン内のオブジェクトがレイの考えている瞬間における位置にあれば、レイをたくさん放つことで現実のカメラが撮影するであろう平均値を計算可能になる。ランダムなレイトレーシングが単純になることが多いのはこれが基本的な理由である。

基本的な考え方はシャッターが開いているランダムな瞬間におけるレイを生成し、その瞬間におけるオブジェクトとの衝突を判定するというものだ。実装ではカメラとオブジェクトを両方動かせるようにして、その上で特定の瞬間にだけ存在するレイを考えるという方法が取られることが多い。こうするとレイトレーサーの「エンジン」でオブジェクトをレイの時刻における位置に動かすだけで、衝突判定部分はあまり変更せずに実装できる。

まずレイが存在する時刻を収める変数を ray クラスに追加する:

class ray {
public:
  ray() {}
  ray(const point3& origin, const vec3& direction, double time = 0.0)
    : orig(origin), dir(direction), tm(time) {}

  point3 origin() const  { return orig; }
  vec3 direction() const { return dir; }
  double time() const    { return tm; }

  point3 at(double t) const {
    return orig + t*dir;
  }

public:
  point3 orig;
  vec3 dir;
  double tm;
};
リスト 2.1 [ray.h] 時間情報を持ったレイ

モーションブラーをシミュレートするカメラ

続いて time0time1 の間のランダムに選んだ時刻のレイを生成するようカメラを変更する。time0time1 はカメラが保持するべきだろうか、それともカメラのユーザーがレイを生成するときに渡すべきだろうか? 私は関数の呼び出しが単純になるならコンストラクタを複雑したほうがよいと考えているので、ここではカメラに時間を保持させる (個人的な好みであり、どちらでもよい)。カメラはまだ移動しないので、変更は多くない。レイを生成するときに時間も生成するだけである:

class camera {
public:
  camera(
    point3 lookfrom,
    point3 lookat,
    vec3   vup,
    double vfov, // 垂直方向の視野角 (弧度法)
    double aspect_ratio,
    double aperture,
    double focus_dist,
    double t0 = 0,
    double t1 = 0
  ) {
    auto theta = degrees_to_radians(vfov);
    auto h = tan(theta/2);
    auto viewport_height = 2.0 * h;
    auto viewport_width = aspect_ratio * viewport_height;

    w = unit_vector(lookfrom - lookat);
    u = unit_vector(cross(vup, w));
    v = cross(w, u);

    origin = lookfrom;
    horizontal = focus_dist * viewport_width * u;
    vertical = focus_dist * viewport_height * v;
    lower_left_corner = origin - horizontal/2 - vertical/2 - focus_dist*w;

    lens_radius = aperture / 2;
    time0 = t0;
    time1 = t1;
  }

  ray get_ray(double s, double t) const {
    vec3 rd = lens_radius * random_in_unit_disk();
    vec3 offset = u * rd.x() + v * rd.y();
    return ray(
      origin + offset,
      lower_left_corner + s*horizontal + t*vertical - origin - offset,
      random_double(time0, time1)
    );
  }

private:
  point3 origin;
  point3 lower_left_corner;
  vec3 horizontal;
  vec3 vertical;
  vec3 u, v, w;
  double lens_radius;
  double time0, time1;  // シャッターの開閉時間
};
リスト 2.2 [camera.h] 時間情報を持ったカメラ

運動する球の追加

運動するオブジェクトも必要になので、等速直線運動する球を表すクラスを作る。この球の中心は時刻 time1center0 にあり、時刻 time1center1 にある。この区間外でも球は同じ運動を続けると考えるので、カメラがシャッターを開けてから閉じるまでの間に time0time1 が含まれる必要はない。

class moving_sphere : public hittable {
public:
  moving_sphere() {}
  moving_sphere(
    point3 cen0,
    point3 cen1,
    double t0,
    double t1,
    double r,
    shared_ptr<material> m
  ): center0(cen0),
     center1(cen1),
     time0(t0),
     time1(t1),
     radius(r),
     mat_ptr(m) {}

  virtual bool hit(
    const ray& r, double tmin, double tmax, hit_record& rec
  ) const;

  point3 center(double time) const;

public:
  point3 center0, center1;
  double time0, time1;
  double radius;
  shared_ptr<material> mat_ptr;
};

point3 moving_sphere::center(double time) const{
  return center0 + ((time - time0) / (time1 - time0))*(center1 - center0);
}
リスト 2.3 [moving_sphere.h] 運動する球

運動する球を新しく追加する代わりに、今までの球を動くように変更することもできる (静止した球では center0center1 が同じになる)。ここにはクラスの数を減らすか静止した球の効率を上げるかというトレードオフが存在するが、自分の好きなように選択してほしい。さて衝突判定のコードは centercenter(time) という関数呼び出しに代わるだけで、大きな変化はない。

bool moving_sphere::hit(
  const ray& r, double t_min, double t_max, hit_record& rec) const {
  vec3 oc = r.origin() - center(r.time());
  auto a = r.direction().length_squared();
  auto half_b = dot(oc, r.direction());
  auto c = oc.length_squared() - radius*radius;

  auto discriminant = half_b*half_b - a*c;

  if (discriminant > 0) {
    auto root = sqrt(discriminant);

    auto temp = (-half_b - root)/a;
    if (temp < t_max && temp > t_min) {
      rec.t = temp;
      rec.p = r.at(rec.t);
      auto outward_normal = (rec.p - center(r.time())) / radius;
      rec.set_face_normal(r, outward_normal);
      rec.mat_ptr = mat_ptr;
      return true;
    }

    temp = (-half_b + root) / a;
    if (temp < t_max && temp > t_min) {
      rec.t = temp;
      rec.p = r.at(rec.t);
      auto outward_normal = (rec.p - center(r.time())) / radius;
      rec.set_face_normal(r, outward_normal);
      rec.mat_ptr = mat_ptr;
      return true;
    }
  }
  return false;
}
リスト 2.4 [moving-sphere.h] moving_sphere::hit 関数

レイが衝突したときの時間の伝播

material では散乱レイに入射レイと同じ時間を設定する必要がある。

class lambertian : public material {
public:
  lambertian(const color& a) : albedo(a) {}

  virtual bool scatter(
    const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
  ) const {
    vec3 scatter_direction = rec.normal + random_unit_vector();
    scattered = ray(rec.p, scatter_direction, r_in.time());
    attenuation = albedo;
    return true;
  }

  color albedo;
};
リスト 2.5 [material.h] 運動する物体に対する lambertian マテリアル

まとめ

前巻の最後でレンダリングしたシーンに含まれる拡散球を運動するように変更したコードを次に示す。カメラのシャッターが \(t = 0\) から \(t = 1\) まで開かれ、\(t = 0\) で \(C\) にある球の中心 \(C\) は \(t = 1\) で \(C + (0, r/2, 0)\) に移動する (\(r\) は \(0\) 以上 \(1\) 未満のランダムな実数)。

hittable_list random_scene() {
  hittable_list world;

  auto ground_material = make_shared<lambertian>(color(0.5, 0.5, 0.5));
  world.add(make_shared<sphere>(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 - vec3(4, 0.2, 0)).length() > 0.9) {
        shared_ptr<material> sphere_material;
        if (choose_mat < 0.8) {
          // diffuse
          auto albedo = color::random() * color::random();
          sphere_material = make_shared<lambertian>(albedo);
          auto center2 = center + vec3(0, random_double(0,.5), 0);
          world.add(make_shared<moving_sphere>(
            center, center2, 0.0, 1.0, 0.2, sphere_material));
        } else if (choose_mat < 0.95) {
          // metal
          auto albedo = color::random(0.5, 1);
          auto fuzz = random_double(0, 0.5);
          sphere_material = make_shared<metal>(albedo, fuzz);
          world.add(make_shared<sphere>(center, 0.2, sphere_material));
        } else {
          // glass
          sphere_material = make_shared<dielectric>(1.5);
          world.add(make_shared<sphere>(center, 0.2, sphere_material));
        }
      }
    }
  }

  auto material1 = make_shared<dielectric>(1.5);
  world.add(make_shared<sphere>(point3(0, 1, 0), 1.0, material1));
  auto material2 = make_shared<lambertian>(color(0.4, 0.2, 0.1));
  world.add(make_shared<sphere>(point3(-4, 1, 0), 1.0, material2));
  auto material3 = make_shared<metal>(color(0.7, 0.6, 0.5), 0.0);
  world.add(make_shared<sphere>(point3(4, 1, 0), 1.0, material3));

  return world;
}
リスト 2.6 [main.cc] 拡散球を運動させた前巻最後のシーン

視点のパラメータも設定する:

point3 lookfrom(13,2,3);
point3 lookat(0,0,0);
vec3 vup(0,1,0);
auto dist_to_focus = 10.0;
auto aperture = 0.0;

camera cam(
  lookfrom, lookat, vup, 20, aspect_ratio, aperture, dist_to_focus, 0.0, 1.0
);
リスト 2.7 [main.cc] 視点のパラメータ

次の画像が得られる:

図 2.1: 跳ねる球

この画像では反射・屈折して写る球が運動していない。これは metaldielectricscatter 関数でレイに時間を渡していないことで起こるバグである。簡単に直せるので直してもいいし、これから時間を扱うことはないので直さなくてもよい。



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