12. 焦点ぼけ
Ray Tracing in One Weekend (v3.2.3): 12 Defocus Blur / 1.12 焦点ぼけ
この章では焦点ぼけ(defocus blur)を実装します。写真で被写体の前後がぼける,いわゆる被写界深度(depth of field)効果です。
なぜ焦点ぼけが起きるか
ピンホールカメラはすべての物体に焦点が合い,ぼけが生じません。実際のカメラは大きな開口部(aperture,絞り)を持つレンズで光を集めるため,特定の平面(焦点平面)以外の物体がぼけて映ります。
重要な用語の区別:
| 用語 | 意味 |
|---|---|
| 焦点距離(focal length) | レンズとセンサー(フィルム)の距離 |
| 集束距離(focus distance) | レンズとフォーカスが合う平面の距離 |
集束距離は焦点距離と等しくなるとは限りません。実際のカメラではレンズ位置を動かすことでピント合わせを行います。
ディスクサンプリングによるぼけの生成
以前はすべてのレイが lookfrom(1点)から出発していました。焦点ぼけを表現するには,レイの出発点を lookfrom を中心としたレンズディスク上の点にランダムに散らします。
random_in_unit_disk
単位球内のサンプリング(random_in_unit_sphere)と同じ棄却法で,
C++:
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;
}
}Rust(common/src/vec3.rs に追加):
/// Returns a random point inside the unit disk.
pub fn random_in_unit_disk() -> Vec3 {
use crate::utils::random_double_range;
loop {
let p = Vec3::new(
random_double_range(-1.0, 1.0),
random_double_range(-1.0, 1.0),
0.0,
);
if p.length_squared() < 1.0 {
return p;
}
}
}random_in_unit_sphere との違いは 0.0 に固定している点だけです。
Camera の更新
新しいパラメータ
Camera::new() に aperture(開口径)と focus_dist(集束距離)を追加します。
ビューポートの拡縮
以前の実装では,ビューポートは原点から距離 1 のところに固定されていました(- w を引くだけ)。焦点ぼけでは,ビューポートを focus_dist の距離に置く必要があります。焦点平面上の点に向けてレイを発するためです。
以前との違いをまとめると:
| 変更前 | 変更後 | |
|---|---|---|
| horizontal | viewport_width * u | focus_dist * viewport_width * u |
| vertical | viewport_height * v | focus_dist * viewport_height * v |
| lower_left_corner | ... - w | ... - focus_dist * w |
u, v フィールド | 不要(計算のみ) | 保存(get_ray で使用) |
lens_radius | なし | aperture / 2.0 |
新しい get_ray
C++:
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
);
}Rust:
pub fn get_ray(&self, s: f64, t: f64) -> Ray {
let rd = self.lens_radius * random_in_unit_disk();
let offset = self.u * rd.x() + self.v * rd.y();
Ray::new(
self.origin + offset,
self.lower_left_corner + s * self.horizontal + t * self.vertical - self.origin - offset,
)
}rd はディスク上のランダム点(3D ベクトルだが
aperture = 0.0 なら lens_radius = 0.0 → rd = (0,0,0) → offset = (0,0,0) となり,ぼけなしのこれまでの動作と完全に一致します。
C++ と Rust の比較
| 機能 | C++ | Rust |
|---|---|---|
| ベクトル長 | (v - w).length() | (v - w).length()(同じ) |
| ディスクサンプリング | 自由関数 | 自由関数(同じ構造) |
| スカラー × Vec | t * v | t * v(f64 * Vec3 の Mul 実装済み) |
| フィールドアクセス | u, v | self.u, self.v |
common クレートへの変更
common/src/camera.rs
use crate::vec3::{Point3, Vec3, cross, unit_vector, random_in_unit_disk};
use crate::ray::Ray;
use crate::utils::degrees_to_radians;
/// A positionable camera with thin-lens depth of field.
pub struct Camera {
origin: Point3,
lower_left_corner: Point3,
horizontal: Vec3,
vertical: Vec3,
u: Vec3,
v: Vec3,
lens_radius: f64,
}
impl Camera {
/// `lookfrom`: camera origin, `lookat`: point the camera looks at, `vup`: up-direction vector.
/// `vfov`: vertical field of view (degrees), `aspect_ratio`: width/height ratio.
/// `aperture`: lens aperture diameter (0.0 = pinhole), `focus_dist`: focus distance.
pub fn new(
lookfrom: Point3,
lookat: Point3,
vup: Vec3,
vfov: f64,
aspect_ratio: f64,
aperture: f64,
focus_dist: f64,
) -> Self {
let theta = degrees_to_radians(vfov);
let h = (theta / 2.0).tan();
let viewport_height = 2.0 * h;
let viewport_width = aspect_ratio * viewport_height;
let w = unit_vector(lookfrom - lookat);
let u = unit_vector(cross(vup, w));
let v = cross(w, u);
let origin = lookfrom;
let horizontal = focus_dist * viewport_width * u;
let vertical = focus_dist * viewport_height * v;
let lower_left_corner = origin - horizontal / 2.0 - vertical / 2.0 - focus_dist * w;
let lens_radius = aperture / 2.0;
Camera { origin, lower_left_corner, horizontal, vertical, u, v, lens_radius }
}
/// Returns a ray for the given `(s, t)` viewport coordinates in [0, 1].
pub fn get_ray(&self, s: f64, t: f64) -> Ray {
let rd = self.lens_radius * random_in_unit_disk();
let offset = self.u * rd.x() + self.v * rd.y();
Ray::new(
self.origin + offset,
self.lower_left_corner + s * self.horizontal + t * self.vertical - self.origin - offset,
)
}
}既存クレートの更新
Camera::new() のシグネチャが変わったため,r107〜r111 の既存クレートすべてで aperture と focus_dist を追加します。ぼけなしの従来動作を再現するには aperture = 0.0,focus_dist = 1.0 を渡します(focus_dist = 1.0 は旧来の「ビューポートを距離 1 に置く」挙動と一致)。
// After (no blur; behavior identical to before)
let camera = Camera::new(
lookfrom,
lookat,
vup,
vfov,
aspect_ratio,
0.0, // aperture: no blur
1.0, // focus_dist: same viewport distance as before
);r112-defocus-blur クレートの実装
r112-defocus-blur/Cargo.toml:
[package]
name = "r112-defocus-blur"
version = "0.1.0"
edition = "2024"
[dependencies]
common = { workspace = true }r112-defocus-blur/src/lib.rs:
r112-defocus-blur/src/lib.rs
use std::sync::Arc;
use common::{
Camera, Color, Dielectric, Hittable, HittableList, Lambertian, Metal, Point3,
Sphere, unit_vector, write_color_gamma, Ray, random_double,
};
fn ray_color(r: &Ray, world: &dyn Hittable, depth: i32) -> Color {
if depth <= 0 {
return Color::new(0.0, 0.0, 0.0);
}
if let Some(rec) = world.hit(r, 0.001, f64::INFINITY) {
if let Some(mat) = &rec.mat {
if let Some((attenuation, scattered)) = mat.scatter(r, &rec) {
return attenuation * ray_color(&scattered, world, depth - 1);
}
}
return Color::new(0.0, 0.0, 0.0);
}
let unit_direction = unit_vector(r.direction());
let t = 0.5 * (unit_direction.y() + 1.0);
(1.0 - t) * Color::new(1.0, 1.0, 1.0) + t * Color::new(0.5, 0.7, 1.0)
}
pub fn render_image() -> String {
let aspect_ratio = 16.0_f64 / 9.0;
let image_width = 384_i32;
let image_height = (image_width as f64 / aspect_ratio) as i32;
let samples_per_pixel = 50_i32;
let max_depth = 50_i32;
let mut world = HittableList::new();
// Center: Lambertian (bluish diffuse sphere)
world.add(Box::new(Sphere::with_material(
Point3::new(0.0, 0.0, -1.0), 0.5,
Arc::new(Lambertian::new(Color::new(0.1, 0.2, 0.5))),
)));
// Ground: Lambertian (yellow-green large sphere)
world.add(Box::new(Sphere::with_material(
Point3::new(0.0, -100.5, -1.0), 100.0,
Arc::new(Lambertian::new(Color::new(0.8, 0.8, 0.0))),
)));
// Right: Metal (gold, fuzz=0)
world.add(Box::new(Sphere::with_material(
Point3::new(1.0, 0.0, -1.0), 0.5,
Arc::new(Metal::new(Color::new(0.8, 0.6, 0.2), 0.0)),
)));
// Left: dielectric (glass sphere, ref_idx=1.5)
world.add(Box::new(Sphere::with_material(
Point3::new(-1.0, 0.0, -1.0), 0.5,
Arc::new(Dielectric::new(1.5)),
)));
// Defocus-blur camera: use the distance from lookfrom to lookat as the focus distance.
let lookfrom = Point3::new(3.0, 3.0, 2.0);
let lookat = Point3::new(0.0, 0.0, -1.0);
let aperture = 2.0_f64;
let dist_to_focus = (lookfrom - lookat).length();
let camera = Camera::new(
lookfrom,
lookat,
Point3::new(0.0, 1.0, 0.0),
20.0,
aspect_ratio,
aperture,
dist_to_focus,
);
let mut output = String::new();
output.push_str("P3\n");
output.push_str(&format!("{} {}\n", image_width, image_height));
output.push_str("255\n");
for j in (0..image_height).rev() {
for i in 0..image_width {
let mut pixel_color = Color::new(0.0, 0.0, 0.0);
for _ in 0..samples_per_pixel {
let u = (i as f64 + random_double()) / (image_width - 1) as f64;
let v = (j as f64 + random_double()) / (image_height - 1) as f64;
let r = camera.get_ray(u, v);
pixel_color += ray_color(&r, &world, max_depth);
}
output.push_str(&format!("{}\n", write_color_gamma(pixel_color, samples_per_pixel)));
}
}
output
}dist_to_focus = (lookfrom - lookat).length() は Rust では .length() メソッドを直接呼び出します。C++ の (lookfrom-lookat).length() と完全に対応します。
WASM エクスポート
raytracing-demos/src/lib.rs に追記します。
// Chapter 1.12: Defocus Blur
#[wasm_bindgen]
pub fn render_defocus_blur() -> String {
r112_defocus_blur::render_image()
}