7. 矩形と光源
Ray Tracing: The Next Week (v3.2.3): 7 Rectangles and Lights / 2.7 矩形と光源
ここまでのレイトレーサは「上下の青-白グラデーションを空として返す」ことで暗黙の環境光を作ってきました。本章では 発光マテリアル(emissive material)と 軸並行矩形(axis-aligned rectangle)を導入し,シーン中の物体そのものを光源にできるようにします。最終的には有名な Cornell box を組み立てます。
原典 7 節は次の 6 つの小節に分かれています。
- 7.29 発光マテリアル
- 7.30
ray_colorへの背景色の追加 - 7.31 矩形オブジェクト(XY 平面)
- 7.32 矩形を光源にする
- 7.33 残りの軸並行矩形(XZ・YZ)
- 7.34 空の Cornell box
本稿でも同じ流れで Rust 実装を組み立てます。
発光マテリアルと Material::emitted
これまでの Material トレイトは scatter だけを持ち,「ヒットした位置からどう光を散乱させるか」を表現していました。発光マテリアルはこれに加えて「自分自身が放出する光」を返さなければなりません。原典は基底クラスに emitted(u, v, p) -> color を生やし,デフォルトで黒を返すことで,既存の非発光マテリアルに変更を加えずに新しいインターフェイスを後付けしています。
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 自身は次のように,散乱しないマテリアル(scatter が None)として実装します。
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 色」を区別する形に書き換えます。
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を取り出し,散乱の有無にかかわらず加算する(光源面そのものに当たった場合はscatterがNoneなので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 平面
を計算し,
common/src/aarect.rs(XyRect 部分)
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),
))
}
}XzRect(YzRect(
- 法線に「外向き」を渡してから
set_face_normalで表裏判定するのは,2.3 で BVH を入れて以来一貫している流儀。XyRectの外向き法線をに固定しておけば,どちら側から当てても front_faceが正しく入ります。 bounding_boxは 0 厚の平面では困るので,法線方向にパディングを入れた AABB を返します。第 2 編で BVH に乗せたときに「箱の幅 0」で割り算が壊れるのを防ぐ,原典と同じ防衛策です。
(u, v) は矩形上の正規化座標で,XzRect の場合 ImageTexture をそのまま貼れます。
C++ と Rust の違い
C++ 原典の xy_rect::hit は b_z = 0 の場合を素通りさせていますが,Rust 実装では dz == 0.0 を早期リターンで除外しました(その先で 0 除算による inf を発生させても無害ですが,明示しておく方がデバッガで動きを追いやすいため)。これは原典の意味を損ねない範囲の安全側改良です。
デモ:矩形ライト付きの Perlin 球
原典 7.32 の simple_light() シーンを r207-simple-light クレートに移植します。Perlin マーブル球(前章 2.5 の構成)の手前に,色 DiffuseLight を貼った 2 m × 2 m の矩形を浮かべるだけです。(4, 4, 4) のように 白を超える明るさを許容しているのが肝心で,これが他の表面を「実際に照らせる」強さになります。
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 を組み立てます。シーンは
- 緑の右側壁(
YzRectat) - 赤の左側壁(
YzRectat) - 天井の小さなライト(
XzRect, , at ) - 白い床(
XzRectat)と天井( XzRectat) - 白い奥壁(
XyRectat)
の 6 つのプリミティブだけで構成されます。手前面(カメラ側)には壁を置かないのが Cornell box の流儀で,カメラから箱の中をのぞき込む形になります。
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。光源面の輝度は
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への変更ゼロで「発光マテリアル」を導入できる。DiffuseLightをcommon/src/material.rsに追加。scatterはNone,emittedがテクスチャ値を返すだけのシンプルな実装。- 軸並行矩形
XyRect/XzRect/YzRectをcommon/src/aarect.rsに追加。0 厚を避けるためのパディング付き AABB,set_face_normalによる表裏判定で BVH と互換に保った。 - デモ
r207-simple-lightで「環境光のないシーンで矩形ライトだけが照らす」レンダリングを,r207-cornell-boxで 5 面 Cornell box を確認した。ray_colorはbackground引数を取る一般形に書き換え,第 1 編のシーンには白っぽい空色を渡せばよい設計に整理した。
次章 2.8 では,矩形 6 枚で直方体(box)を組み立て,それを 回転・平行移動して動かす「インスタンス」を導入します。Cornell box の中に 2 つの箱を傾けて置く,あの定番の絵を作るのが目標です。