Skip to content
Copied!
published on 2026-04-05

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 を中心としたレンズディスク上の点にランダムに散らします。

出発点=lookfrom+offsetoffset=rlenspdisk

pdisk は単位ディスク(z=0 の単位円板)内のランダム点です。ディスクの半径 rlens=aperture/2 が大きいほど,ぼけが強くなります。rlens=0(ピンホール)ならぼけはありません。

random_in_unit_disk

単位球内のサンプリング(random_in_unit_sphere)と同じ棄却法で,z=0 に制限します。

C++:

cpp
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 に追加):

rust
/// 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 との違いは z 成分を 0.0 に固定している点だけです。

Camera の更新

新しいパラメータ

Camera::new()aperture(開口径)と focus_dist(集束距離)を追加します。

ビューポートの拡縮

以前の実装では,ビューポートは原点から距離 1 のところに固定されていました(w を単位ベクトルとして - w を引くだけ)。焦点ぼけでは,ビューポートを focus_dist の距離に置く必要があります。焦点平面上の点に向けてレイを発するためです。

horizontal=focus_dist×viewport_width×uvertical=focus_dist×viewport_height×vlower_left_corner=originhorizontal2vertical2focus_distw

以前との違いをまとめると:

変更前変更後
horizontalviewport_width * ufocus_dist * viewport_width * u
verticalviewport_height * vfocus_dist * viewport_height * v
lower_left_corner... - w... - focus_dist * w
u, v フィールド不要(計算のみ)保存get_ray で使用)
lens_radiusなしaperture / 2.0

新しい get_ray

C++:

cpp
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:

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 ベクトルだが z=0)で,これをカメラローカル座標系の uv に射影して世界座標系のオフセットに変換します。

aperture = 0.0 なら lens_radius = 0.0rd = (0,0,0)offset = (0,0,0) となり,ぼけなしのこれまでの動作と完全に一致します。

C++ と Rust の比較

機能C++Rust
ベクトル長(v - w).length()(v - w).length()(同じ)
ディスクサンプリング自由関数自由関数(同じ構造)
スカラー × Vect * vt * vf64 * Vec3Mul 実装済み)
フィールドアクセスu, vself.u, self.v

common クレートへの変更

common/src/camera.rs

common/src/camera.rs
rust
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 の既存クレートすべてで aperturefocus_dist を追加します。ぼけなしの従来動作を再現するには aperture = 0.0focus_dist = 1.0 を渡します(focus_dist = 1.0 は旧来の「ビューポートを距離 1 に置く」挙動と一致)。

rust
// 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

r112-defocus-blur/Cargo.toml
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
rust
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 に追記します。

raytracing-demos/src/lib.rs
rust
// Chapter 1.12: Defocus Blur
#[wasm_bindgen]
pub fn render_defocus_blur() -> String {
    r112_defocus_blur::render_image()
}