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

8. 拡散マテリアル

Ray Tracing in One Weekend (v3.2.3): 8 Diffuse Materials / 1.8 拡散マテリアル

この章では拡散マテリアル(非光沢マテリアル)を実装します。拡散マテリアルは光を吸収しながらランダムな方向に散乱させます。今章では3通りの実装を順に紹介しています。それぞれ散乱方向の確率分布が異なり,仕上がりの明るさや影の雰囲気に違いが生じます。

拡散マテリアルの仕組み

拡散マテリアルに当たった光は次の2つを同時に行います。

  1. 吸収:光の一部が表面に吸収されて失われる
  2. 散乱:残りがランダムな方向に跳ね返る

これをシミュレートするには,交点 P散乱レイを新たに生成し,再帰的に ray_color を呼んで色を求め,係数 0.5 を乗じます(50% 吸収)。

再帰の打ち切り

散乱レイが生成されると再びどこかに当たり,さらに散乱レイを生成……と再帰が続きます。永遠に続かないよう depth パラメータで最大反射回数を制限し,depth <= 0 のときは黒 (0, 0, 0) を返します。

C++ の実装です。

cpp
color ray_color(const ray& r, const hittable& world, int depth) {
    if (depth <= 0)
        return color(0,0,0);
    // ...
    return 0.5 * ray_color(scattered, world, depth-1);
}

Rust の実装です。

rust
fn ray_color(r: &Ray, world: &dyn Hittable, depth: i32) -> Color {
    if depth <= 0 {
        return Color::new(0.0, 0.0, 0.0);
    }
    // ...
    return 0.5 * ray_color(&scattered, world, depth - 1);
}

シャドウアクネの対策

浮動小数点演算の丸め誤差により,交点から生成した散乱レイが t0 で自己交差することがあります。これをシャドウアクネ(shadow acne)と呼びます。

対策は単純です。hitt_min0 から 0.001 に変えることで,交点の直近での自己交差を除外します。

rust
// causes shadow acne
if let Some(rec) = world.hit(r, 0.0, f64::INFINITY) { ... }

// fixed
if let Some(rec) = world.hit(r, 0.001, f64::INFINITY) { ... }

1. 単純な拡散マテリアル(棄却法)

最初の実装では,交点 P の法線外側にある接触単位球(中心 P+n^)の内部ランダム点 S を求め,SP を散乱方向とします。

ランダム点は棄却法(rejection method)で求めます。

棄却法は,確率分布からの乱数生成において,目的とする複雑な確率分布を直接サンプリングする代わりに,より単純な提案分布からサンプルを生成し,一定の基準に基づいて受理または棄却を繰り返すことで目的の分布に従う乱数を得る手法です。具体的には,目的とする確率密度関数を別の扱いやすい確率密度関数と定数の積で上から覆うように設定し,まず提案分布から候補点を生成した後,一様乱数を用いた確率的な判定によってその候補を採用するか破棄するかを決定します。この判定により,最終的に受理されたサンプルは目的の分布に従うことが保証されます。

C++ の実装です。

cpp
vec3 random_in_unit_sphere() {
    while (true) {
        auto p = vec3::random(-1, 1);
        if (p.length_squared() >= 1) continue;
        return p;
    }
}

Rust の実装です。

rust
/// Returns a random point **inside** the unit sphere (rejection method).
pub fn random_in_unit_sphere() -> 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),
            random_double_range(-1.0, 1.0),
        );
        if p.length_squared() < 1.0 {
            return p;
        }
    }
}

loop はRustのループ構文で,while true に相当します。ランダム点が単位球内に収まるまで繰り返します。平均では約1.91回の試行で成功するため,実用上の問題はありません。

散乱レイの生成コードです。

rust
if let Some(rec) = world.hit(r, 0.001, f64::INFINITY) {
    let target = rec.p + rec.normal + random_in_unit_sphere();
    let scattered = Ray::new(rec.p, target - rec.p);
    return 0.5 * ray_color(&scattered, world, depth - 1);
}

ガンマ補正

レンダリング結果がかなり暗く見えます。これは画像ビューアがガンマ補正済みの値を前提としているためです。ここでは γ=2,すなわち各成分の平方根を取る処理です。

colorout=colorlinear

C++ では write_color() 内で行います。

cpp
auto scale = 1.0 / samples_per_pixel;
r = sqrt(scale * r);  // Gamma=2 correction
g = sqrt(scale * g);
b = sqrt(scale * b);

Rust でも同様に write_color_gamma() 関数を追加します。write_color(線形)との違いはスケール後に .sqrt() を挟む点だけです。

rust
/// Averages `pixel_color` over `samples_per_pixel` samples, applies gamma=2 correction (square root), and converts to an "R G B" string.
pub fn write_color_gamma(pixel_color: Color, samples_per_pixel: i32) -> String {
    let scale = 1.0 / samples_per_pixel as f64;
    let r = (pixel_color.x() * scale).sqrt().clamp(0.0, 0.999);
    let g = (pixel_color.y() * scale).sqrt().clamp(0.0, 0.999);
    let b = (pixel_color.z() * scale).sqrt().clamp(0.0, 0.999);
    let ir = (256.0 * r) as i32;
    let ig = (256.0 * g) as i32;
    let ib = (256.0 * b) as i32;
    format!("{} {} {}", ir, ig, ib)
}

1.8 章以降は write_color_gamma を使います。write_color(非ガンマ)は 1.2〜1.7 章の既存クレートとの互換用に残します。

2. 真のランバーティアン反射

棄却法による random_in_unit_sphere() は,確率密度が cos3ϕ に比例します(ϕ は法線との角度)。理想的なランバーティアン反射では cosϕ が正確な分布です。

単位球面上のランダム点を random_unit_vector() で求めることで,正確な cosϕ 分布が得られます。

C++ の実装です。

cpp
vec3 random_unit_vector() {
    return unit_vector(random_in_unit_sphere());
}

Rust の実装です。

rust
/// Returns a random point **on** the unit sphere surface (true Lambertian reflection).
pub fn random_unit_vector() -> Vec3 {
    unit_vector(random_in_unit_sphere())
}

散乱レイを rec.normal + random_unit_vector() に変えるだけです。

rust
let target = rec.p + rec.normal + random_unit_vector();

見た目の違いは,影がより薄く,球がより明るくなることです。これは cosϕ 分布が法線方向に近いレイを多くサンプリングするためです。

3. 半球一様サンプリング

初期の研究論文が採用した方法では,法線方向の半球を一様にサンプリングします。

C++ の実装です。

cpp
vec3 random_in_hemisphere(const vec3& normal) {
    vec3 in_unit_sphere = random_in_unit_sphere();
    if (dot(in_unit_sphere, normal) > 0.0)
        return in_unit_sphere;
    else
        return -in_unit_sphere;
}

Rust の実装です。

rust
/// Returns a random point in the hemisphere around the normal (uniform hemisphere sampling).
pub fn random_in_hemisphere(normal: Vec3) -> Vec3 {
    let in_unit_sphere = random_in_unit_sphere();
    if dot(in_unit_sphere, normal) > 0.0 {
        in_unit_sphere
    } else {
        -in_unit_sphere
    }
}

散乱レイには rec.p + random_in_hemisphere(rec.normal) を使います(rec.normal を足す必要がない点に注意)。

rust
let target = rec.p + random_in_hemisphere(rec.normal);

3手法のビジュアル比較です(いずれもガンマ補正あり・シャドウアクネ対策済み)。

手法散乱関数確率密度見た目の特徴
単純な拡散random_in_unit_sphere()cos3ϕやや暗め・重い影
真のランバーティアンrandom_unit_vector()cosϕ明るめ・薄い影
半球一様random_in_hemisphere()一様中間的な見た目

C++ と Rust の違い

loop vs while(true)

C++ の無限ループは while(true) が一般的ですが,Rust では loop キーワードが推奨されます。

rust
// C++
while (true) { ... }

// Rust
loop { ... }

loop はコンパイラに「必ずどこかで break または return がある」と知らせるため,戻り値の型推論が正確になります。

sqrt の呼び出し方

cpp
// C++
double r = sqrt(scale * pixel_color.x());
rust
// Rust
let r = (pixel_color.x() * scale).sqrt();

Rust では f64::sqrt() はメソッドとして呼び出します。C の sqrt() 関数のような自由関数は使いません。

common クレートへの追加

common/src/vec3.rs(追加分)

common/src/vec3.rs
rust
impl Vec3 {
    // ... add to existing methods ...

    /// Returns `true` if all components are very close to zero (zero-vector check).
    pub fn near_zero(self) -> bool {
        const S: f64 = 1e-8;
        self.e[0].abs() < S && self.e[1].abs() < S && self.e[2].abs() < S
    }
}

/// Returns a random point **inside** the unit sphere (rejection method).
pub fn random_in_unit_sphere() -> 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),
            random_double_range(-1.0, 1.0),
        );
        if p.length_squared() < 1.0 {
            return p;
        }
    }
}

/// Returns a random point **on** the unit sphere surface (true Lambertian reflection).
pub fn random_unit_vector() -> Vec3 {
    unit_vector(random_in_unit_sphere())
}

/// Returns a random point in the hemisphere around the normal (uniform hemisphere sampling).
pub fn random_in_hemisphere(normal: Vec3) -> Vec3 {
    let in_unit_sphere = random_in_unit_sphere();
    if dot(in_unit_sphere, normal) > 0.0 {
        in_unit_sphere
    } else {
        -in_unit_sphere
    }
}

/// Averages `pixel_color` over `samples_per_pixel` samples, applies gamma=2 correction (square root), and converts to an "R G B" string.
pub fn write_color_gamma(pixel_color: Color, samples_per_pixel: i32) -> String {
    let scale = 1.0 / samples_per_pixel as f64;
    let r = (pixel_color.x() * scale).sqrt().clamp(0.0, 0.999);
    let g = (pixel_color.y() * scale).sqrt().clamp(0.0, 0.999);
    let b = (pixel_color.z() * scale).sqrt().clamp(0.0, 0.999);
    let ir = (256.0 * r) as i32;
    let ig = (256.0 * g) as i32;
    let ib = (256.0 * b) as i32;
    format!("{} {} {}", ir, ig, ib)
}

r108-diffuse クレートの実装(単純な拡散)

r108-diffuse/Cargo.toml を用意します。

r108-diffuse/Cargo.toml
toml
[package]
name = "r108-diffuse"
version = "0.1.0"
edition = "2024"

[dependencies]
common = { workspace = true }

r108-diffuse/src/lib.rs の完全な実装です。

r108-diffuse/src/lib.rs
rust
use common::{
    Camera, Color, Hittable, HittableList, Point3, Sphere,
    unit_vector, write_color_gamma, Ray, random_double, random_in_unit_sphere,
};

fn ray_color(r: &Ray, world: &dyn Hittable, depth: i32) -> Color {
    if depth <= 0 {
        return Color::new(0.0, 0.0, 0.0);
    }
    // t_min = 0.001 prevents shadow acne.
    if let Some(rec) = world.hit(r, 0.001, f64::INFINITY) {
        let target = rec.p + rec.normal + random_in_unit_sphere();
        let scattered = Ray::new(rec.p, target - rec.p);
        return 0.5 * ray_color(&scattered, world, depth - 1);
    }
    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 = 20_i32;

    let mut world = HittableList::new();
    world.add(Box::new(Sphere::new(Point3::new(0.0, 0.0, -1.0), 0.5)));
    world.add(Box::new(Sphere::new(Point3::new(0.0, -100.5, -1.0), 100.0)));

    let camera = Camera::new();

    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
}

r108-lambertian クレートの実装(真のランバーティアン)

r108-lambertian/src/lib.rs の完全な実装です。random_in_unit_sphere() の代わりに random_unit_vector() を使います。

r108-lambertian/src/lib.rs
rust
use common::{
    Camera, Color, Hittable, HittableList, Point3, Sphere,
    unit_vector, write_color_gamma, Ray, random_double, random_unit_vector,
};

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) {
        let target = rec.p + rec.normal + random_unit_vector();
        let scattered = Ray::new(rec.p, target - rec.p);
        return 0.5 * ray_color(&scattered, world, depth - 1);
    }
    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 {
    // ... same settings as r108-diffuse ...
}

r108-hemisphere クレートの実装(半球一様サンプリング)

r108-hemisphere/src/lib.rs の完全な実装です。random_in_hemisphere(rec.normal) を使い,接線球への法線オフセットは不要です。

r108-hemisphere/src/lib.rs
rust
use common::{
    Camera, Color, Hittable, HittableList, Point3, Sphere,
    unit_vector, write_color_gamma, Ray, random_double, random_in_hemisphere,
};

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) {
        // Uniform sampling from the hemisphere around the normal (no need to add rec.normal)
        let target = rec.p + random_in_hemisphere(rec.normal);
        let scattered = Ray::new(rec.p, target - rec.p);
        return 0.5 * ray_color(&scattered, world, depth - 1);
    }
    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 {
    // ... same settings as r108-diffuse ...
}

WASM エクスポート

raytracing-demos/src/lib.rs に追記します。

raytracing-demos/src/lib.rs
rust
// Chapter 1.8: Diffuse Materials
#[wasm_bindgen]
pub fn render_diffuse() -> String {
    r108_diffuse::render_image()
}

#[wasm_bindgen]
pub fn render_lambertian() -> String {
    r108_lambertian::render_image()
}

#[wasm_bindgen]
pub fn render_hemisphere() -> String {
    r108_hemisphere::render_image()
}