Skip to content
Copied!
published on 2026-06-08

7. 矩形と光源

Ray Tracing: The Next Week (v3.2.3): 7 Rectangles and Lights / 2.7 矩形と光源

ここまでのレイトレーサは「上下の青-白グラデーションを空として返す」ことで暗黙の環境光を作ってきました。本章では 発光マテリアル(emissive material)と 軸並行矩形(axis-aligned rectangle)を導入し,シーン中の物体そのものを光源にできるようにします。最終的には有名な Cornell box を組み立てます。

原典 7 節は次の 6 つの小節に分かれています。

  1. 7.29 発光マテリアル
  2. 7.30 ray_color への背景色の追加
  3. 7.31 矩形オブジェクト(XY 平面)
  4. 7.32 矩形を光源にする
  5. 7.33 残りの軸並行矩形(XZ・YZ)
  6. 7.34 空の Cornell box

本稿でも同じ流れで Rust 実装を組み立てます。

発光マテリアルと Material::emitted

これまでの Material トレイトは scatter だけを持ち,「ヒットした位置からどう光を散乱させるか」を表現していました。発光マテリアルはこれに加えて「自分自身が放出する光」を返さなければなりません。原典は基底クラスに emitted(u, v, p) -> color を生やし,デフォルトで黒を返すことで,既存の非発光マテリアルに変更を加えずに新しいインターフェイスを後付けしています。

Rust ではトレイトのデフォルトメソッドで同じことができます。

common/src/material.rs
rust
pub trait Material: Send + Sync {
    fn scatter(&self, r_in: &Ray, rec: &HitRecord) -> Option<(Color, Ray)>;

    /// Light emitted by this material at surface coordinates `(u, v)` and
    /// world-space point `p`. Defaults to black so non-emissive materials
    /// (chapter 1) need not override it.
    fn emitted(&self, _u: f64, _v: f64, _p: Point3) -> Color {
        Color::new(0.0, 0.0, 0.0)
    }
}

Lambertian / Metal / Dielectric はいずれも emitted を何も書かなくてよく,自動的に黒を返すようになります。これが Rust のデフォルトメソッドの便利なところで,既存の impl Material for ... ブロックを一行も書き換えずに機能拡張ができます。

C++ と Rust の違い

C++ 原典は virtual color emitted(...) const { return color(0,0,0); } を基底クラスに置くことで同じ目的を達しています。Rust のデフォルトメソッドはこの C++ パターンの直接の対応物で,override を書かない限り基底実装が継承される点まで含めて挙動が一致します。違いは「Rust ではデフォルトメソッドを呼ぶ側に何の追加コストもない(vtable 経由でも仮想呼び出しだけ)」点で,C++ の virtual 同様にゼロコスト抽象が崩れない点に注目しておきます。

DiffuseLight 自身は次のように,散乱しないマテリアル(scatterNone)として実装します。

common/src/material.rs
rust
pub struct DiffuseLight {
    pub emit: Arc<dyn Texture>,
}

impl DiffuseLight {
    pub fn new(color: Color) -> Self {
        DiffuseLight {
            emit: Arc::new(SolidColor::new(color)),
        }
    }

    pub fn with_texture(emit: Arc<dyn Texture>) -> Self {
        DiffuseLight { emit }
    }
}

impl Material for DiffuseLight {
    fn scatter(&self, _r_in: &Ray, _rec: &HitRecord) -> Option<(Color, Ray)> {
        None
    }

    fn emitted(&self, u: f64, v: f64, p: Point3) -> Color {
        self.emit.value(u, v, p)
    }
}

Lambertian と同様に「色(SolidColor に包む)」と「任意のテクスチャ」の 2 通りのコンストラクタを用意しました。

ray_color への背景色の追加

これまでデモ側で書いてきた ray_color は,何にもヒットしなかったら空のグラデーションを返していました。これを「発光からくる光と,何にもヒットしなかったときに返す background 色」を区別する形に書き換えます。

r207-simple-light/src/lib.rs
rust
fn ray_color(r: &Ray, background: Color, world: &dyn Hittable, depth: i32) -> Color {
    if depth <= 0 {
        return Color::new(0.0, 0.0, 0.0);
    }
    let rec = match world.hit(r, 0.001, f64::INFINITY) {
        Some(rec) => rec,
        None => return background,
    };
    let mat = match &rec.mat {
        Some(mat) => mat.clone(),
        None => return Color::new(0.0, 0.0, 0.0),
    };
    let emitted = mat.emitted(rec.u, rec.v, rec.p);
    match mat.scatter(r, &rec) {
        Some((attenuation, scattered)) => {
            emitted + attenuation * ray_color(&scattered, background, world, depth - 1)
        }
        None => emitted,
    }
}

ポイントは 3 つです。

  • 何もヒットしないと background をそのまま返す(空のグラデーション計算は廃止)。
  • ヒットしたら,まず emitted を取り出し,散乱の有無にかかわらず加算する(光源面そのものに当たった場合は scatterNone なので emitted だけが残る)。
  • 散乱があれば従来通り attenuation * ray_color(...) を反射成分として emitted に足す。

この ray_color は本章以降のすべてのデモで使い回せる汎用版で,第 1 編・第 2 編前半のデモも background = Color::new(0.7, 0.8, 1.0) を渡せば従来と同じ結果を返します。

軸並行矩形

原典は xy_rect / xz_rect / yz_rect の 3 つを同じヘッダ aarect.h に並べる流儀です。Rust でも同じく common/src/aarect.rs に 3 つの構造体 XyRect / XzRect / YzRect を置きます。

XY 平面 z=k 上の,[x0,x1]×[y0,y1] を覆う矩形は,光線 P(t)=A+tb に対して

t=kAzbz,x=Ax+tbx,y=Ay+tby

を計算し,t[tmin,tmax] かつ x0xx1 かつ y0yy1 を満たせばヒットです。

common/src/aarect.rs(XyRect 部分)
rust
const PAD: f64 = 0.0001;

pub struct XyRect {
    pub x0: f64,
    pub x1: f64,
    pub y0: f64,
    pub y1: f64,
    pub k: f64,
    pub material: Arc<dyn Material>,
}

impl XyRect {
    pub fn new(x0: f64, x1: f64, y0: f64, y1: f64, k: f64, material: Arc<dyn Material>) -> Self {
        XyRect { x0, x1, y0, y1, k, material }
    }
}

impl Hittable for XyRect {
    fn hit(&self, r: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
        let dz = r.direction().z();
        if dz == 0.0 {
            return None;
        }
        let t = (self.k - r.origin().z()) / dz;
        if t < t_min || t > t_max {
            return None;
        }
        let x = r.origin().x() + t * r.direction().x();
        let y = r.origin().y() + t * r.direction().y();
        if x < self.x0 || x > self.x1 || y < self.y0 || y > self.y1 {
            return None;
        }
        let outward_normal = Vec3::new(0.0, 0.0, 1.0);
        let mut rec = HitRecord {
            t,
            u: (x - self.x0) / (self.x1 - self.x0),
            v: (y - self.y0) / (self.y1 - self.y0),
            p: r.at(t),
            normal: outward_normal,
            front_face: false,
            mat: Some(self.material.clone()),
        };
        rec.set_face_normal(r, outward_normal);
        Some(rec)
    }

    fn bounding_box(&self, _time0: f64, _time1: f64) -> Option<Aabb> {
        Some(Aabb::new(
            Point3::new(self.x0, self.y0, self.k - PAD),
            Point3::new(self.x1, self.y1, self.k + PAD),
        ))
    }
}

XzRecty=k)と YzRectx=k)も対応する軸を入れ替えるだけで,構造は同じです(実装全体はソース参照)。原典との実質的な違いは 2 点だけです。

  • 法線に「外向き」を渡してから set_face_normal で表裏判定するのは,2.3 で BVH を入れて以来一貫している流儀。XyRect の外向き法線を +z に固定しておけば,どちら側から当てても front_face が正しく入ります。
  • bounding_box は 0 厚の平面では困るので,法線方向に ±0.0001 パディングを入れた AABB を返します。第 2 編で BVH に乗せたときに「箱の幅 0」で割り算が壊れるのを防ぐ,原典と同じ防衛策です。

(u, v) は矩形上の正規化座標で,XzRect の場合 u=(xx0)/(x1x0), v=(zz0)/(z1z0) になります。これにより矩形にも ImageTexture をそのまま貼れます。

C++ と Rust の違い

C++ 原典の xy_rect::hitb_z = 0 の場合を素通りさせていますが,Rust 実装では dz == 0.0 を早期リターンで除外しました(その先で 0 除算による inf を発生させても無害ですが,明示しておく方がデバッガで動きを追いやすいため)。これは原典の意味を損ねない範囲の安全側改良です。

デモ:矩形ライト付きの Perlin 球

原典 7.32 の simple_light() シーンを r207-simple-light クレートに移植します。Perlin マーブル球(前章 2.5 の構成)の手前に,色 (4,4,4)DiffuseLight を貼った 2 m × 2 m の矩形を浮かべるだけです。(4, 4, 4) のように 白を超える明るさを許容しているのが肝心で,これが他の表面を「実際に照らせる」強さになります。

r207-simple-light/src/lib.rs
rust
fn simple_light() -> HittableList {
    let mut objects = HittableList::new();

    let pertext: Arc<dyn Texture> = Arc::new(NoiseTexture::new(NoiseMode::Marble, 4.0));
    objects.add(Box::new(Sphere::with_material(
        Point3::new(0.0, -1000.0, 0.0),
        1000.0,
        Arc::new(Lambertian::with_texture(pertext.clone())),
    )));
    objects.add(Box::new(Sphere::with_material(
        Point3::new(0.0, 2.0, 0.0),
        2.0,
        Arc::new(Lambertian::with_texture(pertext)),
    )));

    let difflight = Arc::new(DiffuseLight::new(Color::new(4.0, 4.0, 4.0)));
    objects.add(Box::new(XyRect::new(3.0, 5.0, 1.0, 3.0, -2.0, difflight)));

    objects
}

カメラは lookfrom = (26, 3, 6)lookat = (0, 2, 0)vfov = 20°background = (0, 0, 0) を渡してシーン中の唯一の光源を矩形ライトに限定しています。WASM 上で実用速度に収めるためサンプル数は 200/px と原典より控えめですが,球の照らされ方(地面側より上面が暗く,手前の光源側だけ明るい)は読み取れます。

Cornell box の 5 面

r207-cornell-box クレートでは,原典 7.34 の空の Cornell box を組み立てます。シーンは

  • 緑の右側壁(YzRect at x=555
  • 赤の左側壁(YzRect at x=0
  • 天井の小さなライト(XzRect 213x343, 227z332, at y=554
  • 白い床(XzRect at y=0)と天井(XzRect at y=555
  • 白い奥壁(XyRect at z=555

の 6 つのプリミティブだけで構成されます。手前面(カメラ側)には壁を置かないのが Cornell box の流儀で,カメラから箱の中をのぞき込む形になります。

r207-cornell-box/src/lib.rs
rust
fn cornell_box() -> HittableList {
    let mut objects = HittableList::new();

    let red = Arc::new(Lambertian::new(Color::new(0.65, 0.05, 0.05)));
    let white = Arc::new(Lambertian::new(Color::new(0.73, 0.73, 0.73)));
    let green = Arc::new(Lambertian::new(Color::new(0.12, 0.45, 0.15)));
    let light = Arc::new(DiffuseLight::new(Color::new(15.0, 15.0, 15.0)));

    objects.add(Box::new(YzRect::new(0.0, 555.0, 0.0, 555.0, 555.0, green)));
    objects.add(Box::new(YzRect::new(0.0, 555.0, 0.0, 555.0, 0.0, red)));
    objects.add(Box::new(XzRect::new(213.0, 343.0, 227.0, 332.0, 554.0, light)));
    objects.add(Box::new(XzRect::new(0.0, 555.0, 0.0, 555.0, 0.0, white.clone())));
    objects.add(Box::new(XzRect::new(0.0, 555.0, 0.0, 555.0, 555.0, white.clone())));
    objects.add(Box::new(XyRect::new(0.0, 555.0, 0.0, 555.0, 555.0, white)));

    objects
}

カメラは lookfrom = (278, 278, -800), lookat = (278, 278, 0), vfov = 40°,アスペクト比 1:1。光源面の輝度は (15,15,15) と非常に強いですが,ライト面積が小さい(130 × 105)ため到達するレイの大半は光源を見つけられず黒として戻ります。これが Cornell box の絵に見られる 強いノイズの正体で,第 3 編で 重要度サンプリング(importance sampling)を導入するときに改善されます。

C++ と Rust の違い

C++ では make_shared<lambertian>(color(.73, .73, .73)) のような短い書き方ができますが,Rust では Arc::new(Lambertian::new(Color::new(0.73, 0.73, 0.73))) と入れ子が深くなりがちです。マテリアルを 4 種類変数に束ねておいて使い回す書き方は,ネストを抑えるテクニックとして本章のような場面で特に有効です(原典の C++ コードもまったく同じ構造になっています)。

まとめ

  • Material トレイトにデフォルトメソッド emitted を追加し,デフォルトで黒を返すようにした。これで Lambertian / Metal / Dielectric への変更ゼロで「発光マテリアル」を導入できる。
  • DiffuseLightcommon/src/material.rs に追加。scatterNoneemitted がテクスチャ値を返すだけのシンプルな実装。
  • 軸並行矩形 XyRect / XzRect / YzRectcommon/src/aarect.rs に追加。0 厚を避けるためのパディング付き AABB,set_face_normal による表裏判定で BVH と互換に保った。
  • デモ r207-simple-light で「環境光のないシーンで矩形ライトだけが照らす」レンダリングを,r207-cornell-box で 5 面 Cornell box を確認した。ray_colorbackground 引数を取る一般形に書き換え,第 1 編のシーンには白っぽい空色を渡せばよい設計に整理した。

次章 2.8 では,矩形 6 枚で直方体(box)を組み立て,それを 回転・平行移動して動かす「インスタンス」を導入します。Cornell box の中に 2 つの箱を傾けて置く,あの定番の絵を作るのが目標です。