焦点ぼけ

最後に焦点ぼけ (defocus blur) を実装する。普通はこれを被写体深度差 (depth of field) と呼ぶので、「焦点ぼけ」という言葉は友達とだけ使うようにしよう。

現実のカメラで焦点ぼけが生じるのは、光を集めるために (ピンホールではなく) 大きな穴を使っているためである。この穴をそのまま使うと全ての像がぼやけるが、この穴にレンズを設置すれば特定の距離にある平面上の物体にフォーカスを合わせることができる。言い換えると、この距離にある点から放たれレンズを通る任意のレイは画像センサー上の一点に集まる。

フォーカスの合う平面と射影点 (レンズ) の距離を集束距離 (focus distance) と呼ぶ。集束距離は焦点距離 (focal length) とは異なる。焦点距離は射影点から画像平面までの距離を表す。

現実のカメラはレンズとフィルム (センサー) の距離を変化させることで集束距離を調節する。カメラのピントを変えたときにレンズが動くのはこのためである (スマートフォンのカメラでは逆にセンサーが動く)。写真を撮るとき実際に光を集めるレンズの大きさを口径 (apature) と呼ぶ。物理的なカメラでは光が足りないときに口径を大きくするが、そうすると焦点ぼけが大きくなる。私たちが作っている仮想的なカメラには完璧なセンサーがあるので、光が足りなくなることはない。つまり口径を考えるのは焦点ぼけが欲しいときだけである。

薄レンズ近似

現実のカメラではいくつものレンズが複雑に組み合わさっている。センサー・レンズ・口径を正確にシミュレートすることも当然できる。そうすればレイをどこに放つべきかが分かり、計算結果を反転させれば画像が得られる (像はフィルム上に反転して現れる)。しかしグラフィックスプログラマーはたいていの場合次に示す薄いレンズを使った単純な近似 (薄レンズ近似, thin lens approximation) を使う:

図 1.36:
薄いレンズを使ったカメラレンズのモデル

さらにカメラの内部は何もシミュレートする必要がない。カメラ外部の画像をレンダリングしているのだから、カメラの内部を考えてもコードが不必要に複雑になるだけだ。その代わりレイをレンズから放ち、レイの目標を完全にフォーカスが合う焦点平面 (focus_dist だけ離れた平面) 上の点とする。

図 1.37:
集束距離に置かれた仮想的なフィルム

サンプルレイの生成

口径を考えない場合には全てのレイが lookfrom から放たれる。焦点ぼけを再現するには、レイを lookfrom を中心とした円盤から放てばよい。この円盤の半径を大きくすると、それだけ焦点ぼけも大きくなる。これまでに実装したカメラでは全てのレイが円盤の中心を始点とするので、この焦点円盤の半径が \(0\) だとみなせる (つまり焦点ぼけは存在しない)。

vec3 random_in_unit_disk() {
  while (true) {
    auto p = vec3(random_double(-1,1), random_double(-1,1), 0);
    if (p.length_squared() >= 1) continue;
    return p;
  }
}
リスト 1.64 [vec3.h] 単位円盤上にランダムな点を生成する関数
class camera {
public:
  camera(
    point3 lookfrom,
    point3 lookat,
    vec3   vup,
    double vfov,  // 垂直方向の視野角 (弧度法)
    double aspect_ratio,
    double aperture,
    double focus_dist
  ) {
    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;
  }

  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
    );
  }

private:
  point3 origin;
  point3 lower_left_corner;
  vec3 horizontal;
  vec3 vertical;
  vec3 u, v, w;
  double lens_radius;
};
リスト 1.65 [camera.h] 被写体深度差 (pov) を設定可能なカメラ

大口径の画像を生成しよう:

point3 lookfrom(3,3,2);
point3 lookat(0,0,-1);
vec3 vup(0,1,0);
auto dist_to_focus = (lookfrom-lookat).length();
auto aperture = 2.0;

camera cam(lookfrom, lookat, vup, 20, aspect_ratio, aperture, dist_to_focus);
リスト 1.66 [camera.h] カメラのパラメータ

次の画像が得られる:

図 1.38:
被写体深度差を使ってレンダリングした球
広告