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

9. 金属マテリアル

Ray Tracing in One Weekend (v3.2.3): 9 Metal / 1.9 金属マテリアル

この章では金属マテリアル(鏡面反射)を実装します。そのためにまず,マテリアルを表す抽象インタフェースを導入し,散乱処理を各マテリアルクラスにカプセル化します。これにより 1.8 章のランバーティアン拡散もマテリアルクラスとして整理し直します。

マテリアルクラスの抽象化

異なるマテリアルを分岐なく扱えるよう,マテリアルの抽象インタフェースを導入します。このインタフェースは次の責務を持ちます。

  1. 散乱するかどうかを決定する
  2. 散乱する場合,散乱レイと減衰色(albedo)を計算して返す

C++ の実装です(material.h)。

cpp
class material {
public:
  virtual ~material() {}
  virtual bool scatter(
    const ray& r_in, const hit_record& rec,
    color& attenuation, ray& scattered
  ) const = 0;
};

C++ では純粋仮想関数= 0)でインタフェースを定義します。Rust ではトレイトが同じ役割を果たします。

rust
/// Material trait: determines whether scattering occurs and the scattered ray and attenuation color.
pub trait Material: Send + Sync {
    /// Returns `Some((attenuation, scattered_ray))` if the ray scatters, or `None` if it is absorbed.
    fn scatter(&self, r_in: &Ray, rec: &HitRecord) -> Option<(Color, Ray)>;
}

C++ 版の scattercolor& attenuationray& scattered という出力パラメータ(mutable な参照)で結果を返し,戻り値が bool(散乱したかどうか)です。Rust では Option<(Color, Ray)> というタプルを返す方が自然です。

  • Some((attenuation, scattered)) → 散乱あり
  • None → 吸収(散乱なし)

HitRecord へのマテリアル追加

衝突した球のマテリアルを後続の処理に渡すため,HitRecord にマテリアルへの参照を追加します。

C++ の実装です(hit_recordshared_ptr<material> を追加)。

cpp
struct hit_record {
  point3 p;
  vec3 normal;
  shared_ptr<material> mat_ptr;
  double t;
  bool front_face;
};

Rust の実装です。

rust
use std::sync::Arc;
use crate::material::Material;

pub struct HitRecord {
    pub p: Point3,
    pub normal: Vec3,
    pub t: f64,
    pub front_face: bool,
    pub mat: Option<Arc<dyn Material>>,   // ← added
}

C++ の shared_ptr<material> に対応する Rust の型は Arc<dyn Material> です。

C++Rust意味
shared_ptr<T>(シングルスレッド)Rc<T>参照カウント,単一スレッド専用
shared_ptr<T>(マルチスレッド)Arc<T>原子的な参照カウント,スレッドセーフ

ArcAtomically Reference Counted の略です。WASM の実行環境はシングルスレッドですが,dyn MaterialArc 経由で扱うにはトレイトが Send + Sync を実装している必要があります。今回はトレイト定義に : Send + Sync を要求しているため Arc を使います。

MaterialHitRecord の循環依存

material.rsHitRecord を参照し,hittable.rsMaterial を参照します。互いに依存し合う循環依存が生じます。

C++ では前方宣言class material;)でこれを解決します。

cpp
// hittable.h
class material;  // Forward declaration

struct hit_record {
    shared_ptr<material> mat_ptr;
};

Rust では,同じクレート内のモジュール間は相互参照できます。Rust コンパイラはクレート全体を一度に処理するため,前方宣言は不要です。

rust
// material.rs references hittable.rs
use crate::hittable::HitRecord;

// hittable.rs references material.rs
use crate::material::Material;

ランバーティアンの再実装

1.8 章では ray_color() 内で直接散乱方向を計算していました。1.9 章では Lambertian 構造体の scatter() メソッドにカプセル化します。

rust
pub struct Lambertian {
    pub albedo: Color,
}

impl Material for Lambertian {
    fn scatter(&self, _r_in: &Ray, rec: &HitRecord) -> Option<(Color, Ray)> {
        let mut scatter_direction = rec.normal + random_unit_vector();
        // Fall back to the normal direction if the scatter direction degenerates to a zero vector.
        if scatter_direction.near_zero() {
            scatter_direction = rec.normal;
        }
        let scattered = Ray::new(rec.p, scatter_direction);
        Some((self.albedo, scattered))
    }
}

near_zero() チェックは,法線とランダムベクトルがほぼ逆向きのとき散乱方向がゼロベクトルに縮退するのを防ぐためです。_r_in の先頭 _ は「使用しない引数」を意味し,コンパイラの未使用警告を抑制します。

鏡面反射の数学

金属はレイを鏡面反射させます。入射レイ v と表面の単位法線 n^ が与えられたとき,反射レイ r を求めます。

bv の法線方向への射影(逆符号)とすると

b=(vn^)n^

反射レイは

r=v+2b=v2(vn^)n^

C++ の実装です(vec3.h に自由関数として追加)。

cpp
vec3 reflect(const vec3& v, const vec3& n) {
    return v - 2 * dot(v, n) * n;
}

Rust の実装です(vec3.rs に追加)。

rust
/// Computes the reflection vector (v: incident direction, n: unit surface normal).
///
/// **r** = **v** - 2(**v**·**n**)**n**
pub fn reflect(v: Vec3, n: Vec3) -> Vec3 {
    v - 2.0 * dot(v, n) * n
}

Metal マテリアル

反射ベクトルを使って金属マテリアルを実装します。入射レイを単位ベクトルに正規化してから反射を計算し,反射レイが表面の下に抜ける場合は吸収(None)として扱います。

C++ の実装です。

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

  virtual bool scatter(
    const ray& r_in, const hit_record& rec,
    color& attenuation, ray& scattered
  ) const {
    vec3 reflected = reflect(unit_vector(r_in.direction()), rec.normal);
    scattered = ray(rec.p, reflected);
    attenuation = albedo;
    return (dot(scattered.direction(), rec.normal) > 0);
  }

  color albedo;
};

Rust の実装です(ファジー反射を含む最終版)。

rust
impl Material for Metal {
    fn scatter(&self, r_in: &Ray, rec: &HitRecord) -> Option<(Color, Ray)> {
        let reflected = reflect(unit_vector(r_in.direction()), rec.normal);
        let scattered = Ray::new(
            rec.p,
            reflected + self.fuzz * random_in_unit_sphere(),
        );
        // Absorb the ray if the scattered direction goes below the surface.
        if dot(scattered.direction(), rec.normal) > 0.0 {
            Some((self.albedo, scattered))
        } else {
            None
        }
    }
}

ray_color の更新

マテリアルに散乱処理を委譲するよう ray_color を更新します。

C++ の実装です。

cpp
color ray_color(const ray& r, const hittable& world, int depth) {
    if (depth <= 0) return color(0,0,0);
    hit_record rec;
    if (world.hit(r, 0.001, infinity, rec)) {
        ray scattered;
        color attenuation;
        if (rec.mat_ptr->scatter(r, rec, attenuation, scattered))
            return attenuation * ray_color(scattered, world, depth-1);
        return color(0,0,0);
    }
    // ... background color ...
}

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

if let Some(mat) = &rec.matmatNone(マテリアルなし)の場合を安全にスキップします。mat.scatter(r, &rec)Option<(Color, Ray)> を返すので,if let Some((attenuation, scattered)) = ... でタプルを一気にアンラップします。C++ の出力パラメータ 2 つと bool 戻り値を,Rust では 1 つの Option 型にまとめています。

ファジー反射

完全な鏡面反射では写り込みが非現実的に鮮明になります。反射レイの終端を半径 fuzz の小球内のランダム点で摂動させると,反射がぼけます。

scattered=reflected+fuzz×random_in_unit_sphere()
  • fuzz = 0.0 → 完全鏡面反射
  • fuzz = 1.0 → 最大にぼかした反射
rust
pub struct Metal {
    pub albedo: Color,
    /// Fuzz factor (0.0 = perfect mirror, 1.0 = maximum blur).
    pub fuzz: f64,
}

impl Metal {
    pub fn new(albedo: Color, fuzz: f64) -> Self {
        Metal {
            albedo,
            fuzz: fuzz.min(1.0),
        }
    }
}

fuzz.min(1.0) は C++ の三項演算子 f < 1 ? f : 1 の Rust 版です。f64 のメソッドとして呼び出します。

C++ と Rust の比較

機能C++Rust
抽象インタフェース純粋仮想関数 virtual ... = 0trait Material { ... }
動的ディスパッチvirtual(vtable)dyn Material(vtable)
参照カウント共有shared_ptr<material>Arc<dyn Material>
循環依存の解決前方宣言 class material;不要(モジュール間相互参照)
scatter の戻り値bool(出力パラメータ付き)Option<(Color, Ray)>
上限クランプf < 1 ? f : 1fuzz.min(1.0)
使わない引数引数名を省略できる_ プレフィックス(例:_r_in

common クレートへの追加

common/src/material.rs

common/src/material.rs
rust
use crate::hittable::HitRecord;
use crate::ray::Ray;
use crate::vec3::{Color, dot, random_in_unit_sphere, random_unit_vector, reflect, unit_vector};

/// Material trait: determines whether scattering occurs and the scattered ray and attenuation color.
pub trait Material: Send + Sync {
    /// Returns `Some((attenuation, scattered_ray))` if the ray scatters, or `None` if it is absorbed.
    fn scatter(&self, r_in: &Ray, rec: &HitRecord) -> Option<(Color, Ray)>;
}

/// Lambertian (diffuse) material.
pub struct Lambertian {
    /// Albedo (diffuse color).
    pub albedo: Color,
}

impl Lambertian {
    /// Creates a `Lambertian` material with the given `albedo`.
    pub fn new(albedo: Color) -> Self {
        Lambertian { albedo }
    }
}

impl Material for Lambertian {
    fn scatter(&self, _r_in: &Ray, rec: &HitRecord) -> Option<(Color, Ray)> {
        let mut scatter_direction = rec.normal + random_unit_vector();
        if scatter_direction.near_zero() {
            scatter_direction = rec.normal;
        }
        let scattered = Ray::new(rec.p, scatter_direction);
        Some((self.albedo, scattered))
    }
}

/// Metal (specular reflection) material.
pub struct Metal {
    /// Albedo (specular color).
    pub albedo: Color,
    /// Fuzz factor (0.0 = perfect mirror, 1.0 = maximum blur).
    pub fuzz: f64,
}

impl Metal {
    /// Creates a `Metal` material. `fuzz` is clamped to [0, 1].
    pub fn new(albedo: Color, fuzz: f64) -> Self {
        Metal { albedo, fuzz: fuzz.min(1.0) }
    }
}

impl Material for Metal {
    fn scatter(&self, r_in: &Ray, rec: &HitRecord) -> Option<(Color, Ray)> {
        let reflected = reflect(unit_vector(r_in.direction()), rec.normal);
        let scattered = Ray::new(
            rec.p,
            reflected + self.fuzz * random_in_unit_sphere(),
        );
        if dot(scattered.direction(), rec.normal) > 0.0 {
            Some((self.albedo, scattered))
        } else {
            None
        }
    }
}

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

common/src/vec3.rs
rust
/// Computes the reflection vector (v: incident direction, n: unit surface normal).
///
/// **r** = **v** - 2(**v**·**n**)**n**
pub fn reflect(v: Vec3, n: Vec3) -> Vec3 {
    v - 2.0 * dot(v, n) * n
}

common/src/sphere.rs(変更点)

common/src/sphere.rs
rust
use std::sync::Arc;
use crate::material::Material;

/// A sphere defined by a center, a radius, and an optional material.
pub struct Sphere {
    /// Center of the sphere.
    pub center: Point3,
    /// Radius of the sphere (negative radius creates a hollow sphere).
    pub radius: f64,
    /// Material of the sphere (`None` means no material is assigned).
    pub material: Option<Arc<dyn Material>>,
}

impl Sphere {
    /// Creates a sphere without a material (backward-compatible with earlier chapters).
    pub fn new(center: Point3, radius: f64) -> Self {
        Sphere { center, radius, material: None }
    }

    /// Creates a sphere with a material (used from chapter 1.9 onward).
    pub fn with_material(center: Point3, radius: f64, material: Arc<dyn Material>) -> Self {
        Sphere { center, radius, material: Some(material) }
    }
}

hit() 内で HitRecord を生成する際,mat: self.material.clone() を追加します。Arc::clone は参照カウントをインクリメントするだけで,データはコピーされません。

r109-metal クレートの実装

r109-metal/Cargo.toml を用意します。

r109-metal/Cargo.toml
toml
[package]
name = "r109-metal"
version = "0.1.0"
edition = "2024"

[dependencies]
common = { workspace = true }

r109-metal/src/lib.rs の完全な実装です。

r109-metal/src/lib.rs
rust
use std::sync::Arc;
use common::{
    Camera, Color, 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 = 20_i32;

    let mut world = HittableList::new();
    world.add(Box::new(Sphere::with_material(
        Point3::new(0.0, 0.0, -1.0), 0.5,
        Arc::new(Lambertian::new(Color::new(0.7, 0.3, 0.3))),
    )));
    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))),
    )));
    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)),
    )));
    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.8, 0.8), 0.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
}

r109-metal-fuzz クレートの実装(ファジー反射)

r109-metal-fuzzr109-metal と同一の構成で,右球を fuzz=0.3,左球を fuzz=1.0 に変えます。

rust
// Right: Metal (gold, fuzz=0.3)
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.3)),
)));
// Left: Metal (silver, fuzz=1.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.8, 0.8), 1.0)),
)));

WASM エクスポート

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

raytracing-demos/src/lib.rs
rust
// Chapter 1.9: Metal Materials
#[wasm_bindgen]
pub fn render_metal() -> String {
    r109_metal::render_image()
}

#[wasm_bindgen]
pub fn render_metal_fuzz() -> String {
    r109_metal_fuzz::render_image()
}