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

13. 最後のシーン

Ray Tracing in One Weekend (v3.2.3): 13 Where Next? / 1.13 次は?

この章では「週末レイトレーシング」の最後のシーンを実装します。本のカバー画像として使われている,第 1 週のすべての技術を結集した集大成のシーンです。

  • グレーの大きな地面球
  • ランダムなマテリアルで敷き詰められた多数の小球
  • 中央に配置されたランバーティアン・ガラス・金属の 3 つの大球

新しい仕組みは何も追加しません。これまで実装してきた要素をすべて組み合わせるだけです。

random_scene() 関数

シーン全体を生成する関数です。

地面

半径 1000 の大きなランバーティアン球を y=1000 に置きます。この球に乗ることで平らな「地面」のように見えます。

rust
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))),
)));

グリッド配置

xz 平面上に 22×10 のグリッドを敷き,各セルに小球を 1 個配置します。

a in -11..11   (X 方向:22 セル)
b in  -5..5    (Z 方向:10 セル)

原典(C++)は b11..11 の 22×22 グリッドですが,WASM 向けにシーンを縮小しています。

各セルの中心座標に微小ランダム値を加えて球の位置を決めます。

rust
let center = Point3::new(
    a as f64 + 0.9 * random_double(),
    0.2,
    b as f64 + 0.9 * random_double(),
);

0.9 * random_double() の乗算でセル内でのランダムなずれを生じさせます。y=0.2 は球の半径と同じで,球が地面に接する高さです。

衝突回避

中央 3 球(半径 1.0)に小球が重ならないよう,各小球の中心から 3 球の中心(高さ y=0.2 に下ろした点)までの距離を計算し,0.9 より近い場合はスキップします。

rust
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.880%ランバーティアン(拡散球)
choose_mat < 0.9515%金属球
それ以外5%誘電体(ガラス球)

ランバーティアンの albedo は random_double() * random_double() の積で生成します。[0,1) の乱数を 2 乗すると分布が 0 側(暗い色)に偏るため,白い中間色より暗くくすんだ配色になります。

rust
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) を使い,[0.5,1.0) の範囲に制限します。こうすることで金属特有の明るく輝く質感になります。ファジー係数 fuzz[0,0.5) でランダムです。

3 つの大球(焦点球)

シーン中央付近に,それぞれ異なるマテリアルの半径 1.0 の球を 3 つ配置します。

位置マテリアル
(4,1,0)(左)ランバーティアン茶色 (0.4, 0.2, 0.1)
(0,1,0)(中央)誘電体(ガラス)透明・屈折率 1.5
(4,1,0)(右)金属(fuzz=0)銀色 (0.7, 0.6, 0.5)

カメラ設定

カメラを高く・遠くに置いてシーン全体を見渡す俯瞰構図にします。

パラメータ説明
lookfrom(13,2,3)右後方上から見おろす視点
lookat(0,0,0)シーン中心を注視
vfov20°狭い視野角(望遠効果)
aperture0.1わずかな焦点ぼけ
focus_dist10.0集束距離

aperture = 0.1 は章 12 の 2.0 より大幅に小さく,焦点ぼけはごく控えめです。主要な 3 球付近にフォーカスが合った状態で,背景の小球群がわずかにぼけてリアリティを増します。

レンダリング設定

パラメータWASM デモ原典(C++)
解像度300 × 1691200 × 800
spp30500
最大深度5050

WASM はシングルスレッドの同期実行であるため,解像度とサンプル数を大幅に削減しています。ネイティブ最適化ビルドで約 2.4 秒,WASM では概ね 5〜10 秒程度かかります。

C++ との比較

グリッドの反復

C++ の for (int a = -11; a < 11; a++) は Rust では for a in -11..11 と書きます。
範囲 a..b は半開区間 [a,b) です。

確率的 if–else の対比

C++:

cpp
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 MaterialBox で持つ場合は関係しませんが,他クレートとの共有を考えると Arc が安全です)。

ランダムカラーの生成

C++ の原典では:

cpp
auto albedo = color::random() * color::random();

color::random()(random_double(), random_double(), random_double()) の短縮記法です。Rust では Color にこのショートカットを定義していないため,各成分を明示的に書きます:

rust
let albedo = Color::new(
    random_double() * random_double(),
    random_double() * random_double(),
    random_double() * random_double(),
);

完全な実装

r113-final-scene/Cargo.toml

r113-final-scene/Cargo.toml
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
rust
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 への追記は既に完了しています。

rust
// 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 章で説明したイベントループのブロック制約)。「レンダリングボタンを押したらしばらく待つ」という使い方になります。

次は?

原著は以下の発展テーマを提案しています。

  1. ライト — 発光オブジェクトを光源として扱う
  2. 三角形 — メッシュモデルの基本要素
  3. サーフェステクスチャ — 画像のマッピング
  4. 手続き型テクスチャ — Perlin ノイズ
  5. ボリュームレンダリング — 霧・煙などの participating media
  6. 並列化 — 複数スレッドや GPU による高速化

これらは「週末レイトレーシング」第 2 週・第 3 週の題材です。


第 1 週の実装はこれで完了です。

時間に余裕があれば,より高品質な版のレンダリングもお試しください:高品質版(600×400,100 spp,WASM 5〜10 分)→