9. 金属マテリアル
Ray Tracing in One Weekend (v3.2.3): 9 Metal / 1.9 金属マテリアル
この章では金属マテリアル(鏡面反射)を実装します。そのためにまず,マテリアルを表す抽象インタフェースを導入し,散乱処理を各マテリアルクラスにカプセル化します。これにより 1.8 章のランバーティアン拡散もマテリアルクラスとして整理し直します。
マテリアルクラスの抽象化
異なるマテリアルを分岐なく扱えるよう,マテリアルの抽象インタフェースを導入します。このインタフェースは次の責務を持ちます。
- 散乱するかどうかを決定する
- 散乱する場合,散乱レイと減衰色(albedo)を計算して返す
C++ の実装です(material.h)。
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 ではトレイトが同じ役割を果たします。
/// 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++ 版の scatter は color& attenuation と ray& scattered という出力パラメータ(mutable な参照)で結果を返し,戻り値が bool(散乱したかどうか)です。Rust では Option<(Color, Ray)> というタプルを返す方が自然です。
Some((attenuation, scattered))→ 散乱ありNone→ 吸収(散乱なし)
HitRecord へのマテリアル追加
衝突した球のマテリアルを後続の処理に渡すため,HitRecord にマテリアルへの参照を追加します。
C++ の実装です(hit_record に shared_ptr<material> を追加)。
struct hit_record {
point3 p;
vec3 normal;
shared_ptr<material> mat_ptr;
double t;
bool front_face;
};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> | 原子的な参照カウント,スレッドセーフ |
Arc は Atomically Reference Counted の略です。WASM の実行環境はシングルスレッドですが,dyn Material を Arc 経由で扱うにはトレイトが Send + Sync を実装している必要があります。今回はトレイト定義に : Send + Sync を要求しているため Arc を使います。
Material と HitRecord の循環依存
material.rs は HitRecord を参照し,hittable.rs は Material を参照します。互いに依存し合う循環依存が生じます。
C++ では前方宣言(class material;)でこれを解決します。
// hittable.h
class material; // Forward declaration
struct hit_record {
shared_ptr<material> mat_ptr;
};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() メソッドにカプセル化します。
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 の先頭 _ は「使用しない引数」を意味し,コンパイラの未使用警告を抑制します。
鏡面反射の数学
金属はレイを鏡面反射させます。入射レイ
反射レイは
C++ の実装です(vec3.h に自由関数として追加)。
vec3 reflect(const vec3& v, const vec3& n) {
return v - 2 * dot(v, n) * n;
}Rust の実装です(vec3.rs に追加)。
/// 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++ の実装です。
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 の実装です(ファジー反射を含む最終版)。
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++ の実装です。
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 の実装です。
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.mat は mat が None(マテリアルなし)の場合を安全にスキップします。mat.scatter(r, &rec) が Option<(Color, Ray)> を返すので,if let Some((attenuation, scattered)) = ... でタプルを一気にアンラップします。C++ の出力パラメータ 2 つと bool 戻り値を,Rust では 1 つの Option 型にまとめています。
ファジー反射
完全な鏡面反射では写り込みが非現実的に鮮明になります。反射レイの終端を半径 fuzz の小球内のランダム点で摂動させると,反射がぼけます。
fuzz = 0.0→ 完全鏡面反射fuzz = 1.0→ 最大にぼかした反射
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 ... = 0 | trait Material { ... } |
| 動的ディスパッチ | virtual(vtable) | dyn Material(vtable) |
| 参照カウント共有 | shared_ptr<material> | Arc<dyn Material> |
| 循環依存の解決 | 前方宣言 class material; | 不要(モジュール間相互参照) |
scatter の戻り値 | bool(出力パラメータ付き) | Option<(Color, Ray)> |
| 上限クランプ | f < 1 ? f : 1 | fuzz.min(1.0) |
| 使わない引数 | 引数名を省略できる | _ プレフィックス(例:_r_in) |
common クレートへの追加
common/src/material.rs
common/src/material.rs
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(追加分)
/// 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(変更点)
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 を用意します。
[package]
name = "r109-metal"
version = "0.1.0"
edition = "2024"
[dependencies]
common = { workspace = true }r109-metal/src/lib.rs の完全な実装です。
r109-metal/src/lib.rs
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-fuzz は r109-metal と同一の構成で,右球を fuzz=0.3,左球を fuzz=1.0 に変えます。
// 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 に追記します。
// 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()
}