13. 最後のシーン
Ray Tracing in One Weekend (v3.2.3): 13 Where Next? / 1.13 次は?
この章では「週末レイトレーシング」の最後のシーンを実装します。本のカバー画像として使われている,第 1 週のすべての技術を結集した集大成のシーンです。
- グレーの大きな地面球
- ランダムなマテリアルで敷き詰められた多数の小球
- 中央に配置されたランバーティアン・ガラス・金属の 3 つの大球
新しい仕組みは何も追加しません。これまで実装してきた要素をすべて組み合わせるだけです。
random_scene() 関数
シーン全体を生成する関数です。
地面
半径 1000 の大きなランバーティアン球を
world.add(Box::new(Sphere::with_material(
Point3::new(0.0, -1000.0, 0.0),
1000.0,
Arc::new(Lambertian::new(Color::new(0.5, 0.5, 0.5))),
)));グリッド配置
a in -11..11 (X 方向:22 セル)
b in -5..5 (Z 方向:10 セル)原典(C++)は
各セルの中心座標に微小ランダム値を加えて球の位置を決めます。
let center = Point3::new(
a as f64 + 0.9 * random_double(),
0.2,
b as f64 + 0.9 * random_double(),
);0.9 * random_double() の乗算でセル内でのランダムなずれを生じさせます。
衝突回避
中央 3 球(半径 1.0)に小球が重ならないよう,各小球の中心から 3 球の中心(高さ y=0.2 に下ろした点)までの距離を計算し,0.9 より近い場合はスキップします。
let sphere_center_1 = Point3::new(4.0, 0.2, 0.0);
let sphere_center_2 = Point3::new(0.0, 1.0, 0.0);
let sphere_center_3 = Point3::new(-4.0, 0.2, 0.0);
if (center - sphere_center_1).length() > 0.9
&& (center - sphere_center_2).length() > 0.9
&& (center - sphere_center_3).length() > 0.9
{
// Place sphere
}マテリアル確率
[0, 1) の乱数 choose_mat で 3 種のマテリアルに振り分けます。
| 条件 | 確率 | マテリアル |
|---|---|---|
choose_mat < 0.8 | 80% | ランバーティアン(拡散球) |
choose_mat < 0.95 | 15% | 金属球 |
| それ以外 | 5% | 誘電体(ガラス球) |
ランバーティアンの albedo は random_double() * random_double() の積で生成します。
if choose_mat < 0.8 {
let albedo = Color::new(
random_double() * random_double(),
random_double() * random_double(),
random_double() * random_double(),
);
world.add(Box::new(Sphere::with_material(
center, 0.2, Arc::new(Lambertian::new(albedo)),
)));
} else if choose_mat < 0.95 {
let albedo = Color::new(
random_double_range(0.5, 1.0),
random_double_range(0.5, 1.0),
random_double_range(0.5, 1.0),
);
let fuzz = random_double_range(0.0, 0.5);
world.add(Box::new(Sphere::with_material(
center, 0.2, Arc::new(Metal::new(albedo, fuzz)),
)));
} else {
world.add(Box::new(Sphere::with_material(
center, 0.2, Arc::new(Dielectric::new(1.5)),
)));
}金属球の albedo は random_double_range(0.5, 1.0) を使い,fuzz は
3 つの大球(焦点球)
シーン中央付近に,それぞれ異なるマテリアルの半径 1.0 の球を 3 つ配置します。
| 位置 | マテリアル | 色 |
|---|---|---|
| ランバーティアン | 茶色 (0.4, 0.2, 0.1) | |
| 誘電体(ガラス) | 透明・屈折率 1.5 | |
| 金属(fuzz=0) | 銀色 (0.7, 0.6, 0.5) |
カメラ設定
カメラを高く・遠くに置いてシーン全体を見渡す俯瞰構図にします。
| パラメータ | 値 | 説明 |
|---|---|---|
lookfrom | 右後方上から見おろす視点 | |
lookat | シーン中心を注視 | |
vfov | 20° | 狭い視野角(望遠効果) |
aperture | 0.1 | わずかな焦点ぼけ |
focus_dist | 10.0 | 集束距離 |
aperture = 0.1 は章 12 の 2.0 より大幅に小さく,焦点ぼけはごく控えめです。主要な 3 球付近にフォーカスが合った状態で,背景の小球群がわずかにぼけてリアリティを増します。
レンダリング設定
| パラメータ | WASM デモ | 原典(C++) |
|---|---|---|
| 解像度 | 300 × 169 | 1200 × 800 |
| spp | 30 | 500 |
| 最大深度 | 50 | 50 |
WASM はシングルスレッドの同期実行であるため,解像度とサンプル数を大幅に削減しています。ネイティブ最適化ビルドで約 2.4 秒,WASM では概ね 5〜10 秒程度かかります。
C++ との比較
グリッドの反復
C++ の for (int a = -11; a < 11; a++) は Rust では for a in -11..11 と書きます。
範囲 a..b は半開区間
確率的 if–else の対比
C++:
double choose_mat = random_double();
if (choose_mat < 0.8) {
// Diffuse
} else if (choose_mat < 0.95) {
// Metal
} else {
// Glass
}Rust:完全に同じ構造で書けます。Rust の if は式なので,各ブランチで直接 world.add(...) を呼べます。
Arc<dyn Material> vs shared_ptr<material>
| C++ | Rust | |
|---|---|---|
| スマートポインタ | shared_ptr<material> | Arc<dyn Material> |
| 参照カウント | あり | あり |
| スレッド安全 | shared_ptr は非スレッド安全 | Arc はスレッド安全 |
WASM ではスレッドを使いませんが,Rust の型システムが Arc を要求します(Rc はシングルスレッド専用で,dyn Material を Box で持つ場合は関係しませんが,他クレートとの共有を考えると Arc が安全です)。
ランダムカラーの生成
C++ の原典では:
auto albedo = color::random() * color::random();color::random() は (random_double(), random_double(), random_double()) の短縮記法です。Rust では Color にこのショートカットを定義していないため,各成分を明示的に書きます:
let albedo = Color::new(
random_double() * random_double(),
random_double() * random_double(),
random_double() * random_double(),
);完全な実装
r113-final-scene/Cargo.toml
[package]
name = "r113-final-scene"
version = "0.1.0"
edition = "2024"
[dependencies]
common = { workspace = true }r113-final-scene/src/lib.rs
r113-final-scene/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, random_double_range,
};
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)
}
fn random_scene() -> HittableList {
let mut world = HittableList::new();
// Ground: large Lambertian sphere (gray)
world.add(Box::new(Sphere::with_material(
Point3::new(0.0, -1000.0, 0.0),
1000.0,
Arc::new(Lambertian::new(Color::new(0.5, 0.5, 0.5))),
)));
// Place spheres randomly on a 22×10 grid (a: -11..11, b: -5..5, up to 220 cells).
for a in -11..11 {
for b in -5..5 {
let choose_mat = random_double();
let center = Point3::new(
a as f64 + 0.9 * random_double(),
0.2,
b as f64 + 0.9 * random_double(),
);
// Avoid overlapping the three large focal spheres.
let sphere_center_1 = Point3::new(4.0, 0.2, 0.0);
let sphere_center_2 = Point3::new(0.0, 1.0, 0.0);
let sphere_center_3 = Point3::new(-4.0, 0.2, 0.0);
if (center - sphere_center_1).length() > 0.9
&& (center - sphere_center_2).length() > 0.9
&& (center - sphere_center_3).length() > 0.9
{
if choose_mat < 0.8 {
// Lambertian (diffuse): 80%
let albedo = Color::new(
random_double() * random_double(),
random_double() * random_double(),
random_double() * random_double(),
);
world.add(Box::new(Sphere::with_material(
center,
0.2,
Arc::new(Lambertian::new(albedo)),
)));
} else if choose_mat < 0.95 {
// Metal: 15%
let albedo = Color::new(
random_double_range(0.5, 1.0),
random_double_range(0.5, 1.0),
random_double_range(0.5, 1.0),
);
let fuzz = random_double_range(0.0, 0.5);
world.add(Box::new(Sphere::with_material(
center,
0.2,
Arc::new(Metal::new(albedo, fuzz)),
)));
} else {
// Dielectric (glass): 5%
world.add(Box::new(Sphere::with_material(
center,
0.2,
Arc::new(Dielectric::new(1.5)),
)));
}
}
}
}
// Three prominent focal spheres
// Left: brown Lambertian
world.add(Box::new(Sphere::with_material(
Point3::new(-4.0, 1.0, 0.0),
1.0,
Arc::new(Lambertian::new(Color::new(0.4, 0.2, 0.1))),
)));
// Center: Dielectric (glass sphere)
world.add(Box::new(Sphere::with_material(
Point3::new(0.0, 1.0, 0.0),
1.0,
Arc::new(Dielectric::new(1.5)),
)));
// Right: silver Metal sphere (fuzz=0)
world.add(Box::new(Sphere::with_material(
Point3::new(4.0, 1.0, 0.0),
1.0,
Arc::new(Metal::new(Color::new(0.7, 0.6, 0.5), 0.0)),
)));
world
}
pub fn render_image() -> String {
let aspect_ratio = 16.0_f64 / 9.0;
let image_width = 300_i32;
let image_height = (image_width as f64 / aspect_ratio) as i32;
let samples_per_pixel = 30_i32;
let max_depth = 50_i32;
let world = random_scene();
// Camera: positioned high and far to overlook the entire scene.
let lookfrom = Point3::new(13.0, 2.0, 3.0);
let lookat = Point3::new(0.0, 0.0, 0.0);
let vup = Point3::new(0.0, 1.0, 0.0);
let dist_to_focus = 10.0_f64;
let aperture = 0.1_f64;
let camera = Camera::new(
lookfrom,
lookat,
vup,
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
}WASM エクスポート
raytracing-demos/src/lib.rs への追記は既に完了しています。
// Chapter 1.13: Final Scene
#[wasm_bindgen]
pub fn render_final_scene() -> String {
r113_final_scene::render_image()
}
#[wasm_bindgen]
pub fn render_final_scene_hq() -> String {
r113_final_scene_hq::render_image()
}Web 環境での制約
このシーンは WASM 版では 300×169・30 spp に設定しています。ネイティブ最適化ビルドで約 2.4 秒(実測値),WASM では概ね 5〜10 秒程度です。
進捗表示は実装できません(第 2 章で説明したイベントループのブロック制約)。「レンダリングボタンを押したらしばらく待つ」という使い方になります。
次は?
原著は以下の発展テーマを提案しています。
- ライト — 発光オブジェクトを光源として扱う
- 三角形 — メッシュモデルの基本要素
- サーフェステクスチャ — 画像のマッピング
- 手続き型テクスチャ — Perlin ノイズ
- ボリュームレンダリング — 霧・煙などの participating media
- 並列化 — 複数スレッドや GPU による高速化
これらは「週末レイトレーシング」第 2 週・第 3 週の題材です。
第 1 週の実装はこれで完了です。
時間に余裕があれば,より高品質な版のレンダリングもお試しください:高品質版(600×400,100 spp,WASM 5〜10 分)→