6. 法線と複数オブジェクト
Ray Tracing in One Weekend (v3.2.3): 6 Surface Normals and Multiple Objects / 1.6 法線と複数オブジェクト
前章では球のシルエットを赤一色で塗りました。この章では法線ベクトルを使って球の表面をカラフルに可視化します。さらに,複数の物体を扱うための抽象化(Hittable トレイト,Sphere,HittableList)を導入し,地面の球を追加した2オブジェクトのシーンを完成させます。
法線を使ったシェーディング
球の法線ベクトル
球の表面上の点
単位球(半径 1)では長さが 1 になりますが,一般の球では半径
法線のカラーマップ
法線の各成分は
この式は確認用の可視化手法として広く使われます。法線が正面を向いた領域は青みがかり,上を向いた領域は緑がかり,側面は赤みがかります。
hit_sphere を f64 を返すよう改修
前章の hit_sphere は bool を返していました。交点の座標を計算するには,交差パラメータ
C++ の実装です。
double hit_sphere(const point3& center, double radius, const ray& r) {
vec3 oc = r.origin() - center;
auto a = dot(r.direction(), r.direction());
auto b = 2.0 * dot(oc, r.direction());
auto c = dot(oc, oc) - radius*radius;
auto discriminant = b*b - 4*a*c;
if (discriminant < 0) {
return -1.0;
} else {
return (-b - sqrt(discriminant)) / (2.0*a);
}
}Rust の実装です。
fn hit_sphere(center: &Point3, radius: f64, r: &Ray) -> f64 {
let oc = r.origin() - *center;
let a = dot(r.direction(), r.direction());
let b = 2.0 * dot(oc, r.direction());
let c = dot(oc, oc) - radius * radius;
let discriminant = b * b - 4.0 * a * c;
if discriminant < 0.0 {
-1.0
} else {
(-b - discriminant.sqrt()) / (2.0 * a)
}
}t > 0.0 で前方交差のみを受け付けます。前章の「カメラ後方の球が見える」問題もこれで解消されます。
法線をカラーに変換する ray_color
C++ の実装です。
color ray_color(const ray& r) {
auto t = hit_sphere(point3(0,0,-1), 0.5, r);
if (t > 0.0) {
vec3 N = unit_vector(r.at(t) - vec3(0,0,-1));
return 0.5*color(N.x()+1, N.y()+1, N.z()+1);
}
vec3 unit_direction = unit_vector(r.direction());
t = 0.5*(unit_direction.y() + 1.0);
return (1.0-t)*color(1.0, 1.0, 1.0) + t*color(0.5, 0.7, 1.0);
}Rust の実装です。
fn ray_color(r: &Ray) -> Color {
let center = Point3::new(0.0, 0.0, -1.0);
let t = hit_sphere(¢er, 0.5, r);
if t > 0.0 {
let n = unit_vector(r.at(t) - center);
return 0.5 * Color::new(n.x() + 1.0, n.y() + 1.0, n.z() + 1.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)
}r.at(t) で交点を求め,そこから中心を引いて正規化することで法線が得られます。
係数の簡略化
前章の二次方程式
係数 2 が消えて計算が整理されます。half_b(
fn hit_sphere(center: &Point3, radius: f64, r: &Ray) -> f64 {
let oc = r.origin() - *center;
let a = r.direction().length_squared();
let half_b = dot(oc, r.direction());
let c = oc.length_squared() - radius * radius;
let discriminant = half_b * half_b - a * c;
if discriminant < 0.0 {
-1.0
} else {
(-half_b - discriminant.sqrt()) / a
}
}length_squared() の利用と half_b による簡略化で,乗算の回数が減ります。
Hittable トレイトによる抽象化
球が1個だからこそ hit_sphere 関数で済みましたが,物体の種類・数が増えれば統一したインターフェイスが必要です。原典は hittable 抽象クラスを導入します。
HitRecord
交差の結果を保持する構造体です。
C++ の実装です。
struct hit_record {
point3 p;
vec3 normal;
double t;
bool front_face;
void set_face_normal(const ray& r, const vec3& outward_normal) {
front_face = dot(r.direction(), outward_normal) < 0;
normal = front_face ? outward_normal : -outward_normal;
}
};Rust の実装です。
pub struct HitRecord {
pub p: Point3,
pub normal: Vec3,
pub t: f64,
pub front_face: bool,
}
impl HitRecord {
pub fn set_face_normal(&mut self, r: &Ray, outward_normal: Vec3) {
self.front_face = dot(r.direction(), outward_normal) < 0.0;
self.normal = if self.front_face { outward_normal } else { -outward_normal };
}
}front_face フィールドは「レイが表面に当たったか,裏面に当たったか」を記録します。法線は常にレイと逆向きに設定され,後のマテリアル計算で使われます。
Hittable トレイト
C++ では純粋仮想関数をもつ抽象クラスとして定義されます。
class hittable {
public:
virtual ~hittable() = default;
virtual bool hit(const ray& r, double ray_tmin, double ray_tmax, hit_record& rec) const = 0;
};Rust では トレイト(trait) で同じことを表現します。
pub trait Hittable {
fn hit(&self, r: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord>;
}C++ と Rust の違い
出力パラメータ vs Option<T>
C++ の hit メソッドは bool を返し,交差結果を hit_record& rec に書き込みます。返り値をパラメータに含める「出力パラメータ」パターンを使っています。
virtual bool hit(const ray& r, double ray_tmin, double ray_tmax, hit_record& rec) const = 0;Rust では可変参照で引数を「書き込み」として渡すことも可能ですが,「成功か失敗か+値」をまとめた Option<T> の返り値がより慣用的です。
fn hit(&self, r: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord>;Some(rec) で交差あり,None で交差なしを表します。呼び出し側は if let Some(rec) = ... で結果を処理します。
shared_ptr vs Box<dyn Trait>
C++ では多態(ポリモーフィズム)を実現するために shared_ptr<hittable> が使われます。
std::vector<shared_ptr<hittable>> objects;
objects.push_back(make_shared<sphere>(...));Rust ではトレイトオブジェクトの Box<dyn Hittable> が対応します。
Vec<Box<dyn Hittable>>
world.add(Box::new(Sphere::new(...)));Box<T> はヒープに確保した値の所有権を持つスマートポインタです。dyn Hittable は「Hittable トレイトを実装する何らかの型」を指す動的ディスパッチの記法で,実行時に実際の型のメソッドが呼ばれます。
virtual vs デフォルト実装なし
C++ の純粋仮想関数(= 0)に対応する Rust の概念は,デフォルト実装のないトレイトメソッドです。トレイトを impl する型は必ずそのメソッドを実装する義務があります。
// A type that impls Hittable is guaranteed to have a hit() method
impl Hittable for Sphere {
fn hit(&self, r: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
// Must be implemented
}
}Sphere 構造体
C++ の実装です。
class sphere : public hittable {
public:
sphere(const point3& center, double radius) : center(center), radius(fmax(0,radius)) {}
bool hit(const ray& r, double ray_tmin, double ray_tmax, hit_record& rec) const override {
vec3 oc = center - r.origin();
auto a = r.direction().length_squared();
auto h = dot(r.direction(), oc);
auto c = oc.length_squared() - radius*radius;
auto discriminant = h*h - a*c;
if (discriminant < 0) return false;
auto sqrtd = sqrt(discriminant);
auto root = (h - sqrtd) / a;
if (root <= ray_tmin || ray_tmax <= root) {
root = (h + sqrtd) / a;
if (root <= ray_tmin || ray_tmax <= root)
return false;
}
rec.t = root;
rec.p = r.at(rec.t);
vec3 outward_normal = (rec.p - center) / radius;
rec.set_face_normal(r, outward_normal);
return true;
}
private:
point3 center;
double radius;
};Rust の実装です。
pub struct Sphere {
pub center: Point3,
pub radius: f64,
}
impl Sphere {
pub fn new(center: Point3, radius: f64) -> Self {
Sphere { center, radius }
}
}
impl Hittable for Sphere {
fn hit(&self, r: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
let oc = r.origin() - self.center;
let a = r.direction().length_squared();
let half_b = dot(oc, r.direction());
let c = oc.length_squared() - self.radius * self.radius;
let discriminant = half_b * half_b - a * c;
if discriminant < 0.0 {
return None;
}
let sqrtd = discriminant.sqrt();
let mut root = (-half_b - sqrtd) / a;
if root < t_min || root > t_max {
root = (-half_b + sqrtd) / a;
if root < t_min || root > t_max {
return None;
}
}
let p = r.at(root);
let outward_normal = (p - self.center) / self.radius;
let mut rec = HitRecord {
t: root,
p,
normal: outward_normal,
front_face: false,
};
rec.set_face_normal(r, outward_normal);
Some(rec)
}
}原典のC++実装では oc = center - r.origin() と逆向きに取っているため h の符号が反転していますが,本実装では oc = r.origin() - center の向きに統一しています。half_b の符号が変わるだけで結果は同等です。
HittableList
C++ の実装です。
class hittable_list : public hittable {
public:
std::vector<shared_ptr<hittable>> objects;
void add(shared_ptr<hittable> object) { objects.push_back(object); }
bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
hit_record temp_rec;
bool hit_anything = false;
auto closest_so_far = ray_t.max;
for (const auto& object : objects) {
if (object->hit(r, interval(ray_t.min, closest_so_far), temp_rec)) {
hit_anything = true;
closest_so_far = temp_rec.t;
rec = temp_rec;
}
}
return hit_anything;
}
};Rust の実装です。ループ中に closest_so_far を更新することで,最も近い交点だけを最終的に返します。
pub struct HittableList {
pub objects: Vec<Box<dyn Hittable>>,
}
impl HittableList {
pub fn new() -> Self {
HittableList { objects: Vec::new() }
}
pub fn add(&mut self, object: Box<dyn Hittable>) {
self.objects.push(object);
}
}
impl Hittable for HittableList {
fn hit(&self, r: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
let mut closest_so_far = t_max;
let mut result = None;
for object in &self.objects {
if let Some(rec) = object.hit(r, t_min, closest_so_far) {
closest_so_far = rec.t;
result = Some(rec);
}
}
result
}
}ユーティリティ定数(rtweekend.h)
原典は rtweekend.h で共通の定数・関数を定義します。
const double infinity = std::numeric_limits<double>::infinity();
const double pi = 3.1415926535897932385;
inline double degrees_to_radians(double degrees) {
return degrees * pi / 180.0;
}Rust の標準ライブラリには同等の機能が最初から揃っています。今後必要になったときは以下の定数を使えます。
f64::INFINITY // Positive infinity
std::f64::consts::PI // Pi
fn degrees_to_radians(degrees: f64) -> f64 {
degrees * std::f64::consts::PI / 180.0
}この章では f64::INFINITY を HittableList::hit の初期 t_max として使います。
common クレートへの追加
HitRecord,Hittable,Sphere,HittableList は以降の章でも使い回すため,common クレートに追加します。
common/src/hittable.rs
use crate::vec3::{Point3, Vec3, dot};
use crate::ray::Ray;
/// Stores the result of a ray–object intersection.
pub struct HitRecord {
/// Intersection point.
pub p: Point3,
/// Surface normal at the intersection (always points against the incident ray).
pub normal: Vec3,
/// Ray parameter at the intersection.
pub t: f64,
/// `true` if the ray hit the front (outside) face.
pub front_face: bool,
}
impl HitRecord {
/// Sets `front_face` and `normal` so the normal always points against the incident ray.
pub fn set_face_normal(&mut self, r: &Ray, outward_normal: Vec3) {
self.front_face = dot(r.direction(), outward_normal) < 0.0;
self.normal = if self.front_face { outward_normal } else { -outward_normal };
}
}
/// Trait for objects that can be intersected by a ray.
pub trait Hittable {
/// Returns `Some(HitRecord)` if the ray intersects in the interval `[t_min, t_max]`, or `None`.
fn hit(&self, r: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord>;
}common/src/sphere.rs
use crate::vec3::{Point3, dot};
use crate::ray::Ray;
use crate::hittable::{HitRecord, Hittable};
/// A sphere defined by a center and a radius.
pub struct Sphere {
/// Center of the sphere.
pub center: Point3,
/// Radius of the sphere.
pub radius: f64,
}
impl Sphere {
/// Creates a new sphere.
pub fn new(center: Point3, radius: f64) -> Self {
Sphere { center, radius }
}
}
impl Hittable for Sphere {
fn hit(&self, r: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
let oc = r.origin() - self.center;
let a = r.direction().length_squared();
let half_b = dot(oc, r.direction());
let c = oc.length_squared() - self.radius * self.radius;
let discriminant = half_b * half_b - a * c;
if discriminant < 0.0 {
return None;
}
let sqrtd = discriminant.sqrt();
let mut root = (-half_b - sqrtd) / a;
if root < t_min || root > t_max {
root = (-half_b + sqrtd) / a;
if root < t_min || root > t_max {
return None;
}
}
let p = r.at(root);
let outward_normal = (p - self.center) / self.radius;
let mut rec = HitRecord {
t: root,
p,
normal: outward_normal,
front_face: false,
};
rec.set_face_normal(r, outward_normal);
Some(rec)
}
}common/src/hittable_list.rs
use crate::ray::Ray;
use crate::hittable::{HitRecord, Hittable};
/// A collection of `Hittable` objects tested together.
pub struct HittableList {
/// The list of hittable objects.
pub objects: Vec<Box<dyn Hittable>>,
}
impl HittableList {
/// Creates an empty `HittableList`.
pub fn new() -> Self {
HittableList { objects: Vec::new() }
}
/// Appends `object` to the list.
pub fn add(&mut self, object: Box<dyn Hittable>) {
self.objects.push(object);
}
}
impl Hittable for HittableList {
fn hit(&self, r: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
let mut closest_so_far = t_max;
let mut result = None;
for object in &self.objects {
if let Some(rec) = object.hit(r, t_min, closest_so_far) {
closest_so_far = rec.t;
result = Some(rec);
}
}
result
}
}common/src/lib.rs(更新)
pub mod vec3;
pub use vec3::{Color, Point3, Vec3, cross, dot, unit_vector, write_color};
pub mod ray;
pub use ray::Ray;
pub mod hittable;
pub use hittable::{HitRecord, Hittable};
pub mod sphere;
pub use sphere::Sphere;
pub mod hittable_list;
pub use hittable_list::HittableList;r106-sphere-normals クレートの実装
r106-sphere-normals/Cargo.toml を用意します。
[package]
name = "r106-sphere-normals"
version = "0.1.0"
edition = "2024"
[dependencies]
common = { workspace = true }r106-sphere-normals/src/lib.rs の完全な実装です。hit_sphere が f64 を返し,法線をカラーマップします。
r106-sphere-normals/src/lib.rs
use common::{Color, Point3, Vec3, dot, unit_vector, write_color, Ray};
fn hit_sphere(center: &Point3, radius: f64, r: &Ray) -> f64 {
let oc = r.origin() - *center;
let a = r.direction().length_squared();
let half_b = dot(oc, r.direction());
let c = oc.length_squared() - radius * radius;
let discriminant = half_b * half_b - a * c;
if discriminant < 0.0 {
-1.0
} else {
(-half_b - discriminant.sqrt()) / a
}
}
fn ray_color(r: &Ray) -> Color {
let center = Point3::new(0.0, 0.0, -1.0);
let t = hit_sphere(¢er, 0.5, r);
if t > 0.0 {
let n = unit_vector(r.at(t) - center);
return 0.5 * Color::new(n.x() + 1.0, n.y() + 1.0, n.z() + 1.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 viewport_height = 2.0_f64;
let viewport_width = aspect_ratio * viewport_height;
let focal_length = 1.0_f64;
let origin = Point3::new(0.0, 0.0, 0.0);
let horizontal = Vec3::new(viewport_width, 0.0, 0.0);
let vertical = Vec3::new(0.0, viewport_height, 0.0);
let lower_left_corner =
origin - horizontal / 2.0 - vertical / 2.0 - Vec3::new(0.0, 0.0, focal_length);
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 u = i as f64 / (image_width - 1) as f64;
let v = j as f64 / (image_height - 1) as f64;
let r = Ray::new(
origin,
lower_left_corner + u * horizontal + v * vertical - origin,
);
let pixel_color = ray_color(&r);
output.push_str(&format!("{}\n", write_color(pixel_color)));
}
}
output
}r106-hittable-list クレートの実装
r106-hittable-list/Cargo.toml を用意します。
[package]
name = "r106-hittable-list"
version = "0.1.0"
edition = "2024"
[dependencies]
common = { workspace = true }r106-hittable-list/src/lib.rs の完全な実装です。Hittable トレイトを使い,2つの球(空中の小球+地面の大球)を配置します。
use common::{Color, Point3, Vec3, unit_vector, write_color, Ray, Hittable, HittableList, Sphere};
fn ray_color(r: &Ray, world: &dyn Hittable) -> Color {
if let Some(rec) = world.hit(r, 0.0, f64::INFINITY) {
return 0.5 * (rec.normal + Color::new(1.0, 1.0, 1.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 viewport_height = 2.0_f64;
let viewport_width = aspect_ratio * viewport_height;
let focal_length = 1.0_f64;
let origin = Point3::new(0.0, 0.0, 0.0);
let horizontal = Vec3::new(viewport_width, 0.0, 0.0);
let vertical = Vec3::new(0.0, viewport_height, 0.0);
let lower_left_corner =
origin - horizontal / 2.0 - vertical / 2.0 - Vec3::new(0.0, 0.0, focal_length);
let mut world = HittableList::new();
world.add(Box::new(Sphere::new(Point3::new(0.0, 0.0, -1.0), 0.5)));
world.add(Box::new(Sphere::new(Point3::new(0.0, -100.5, -1.0), 100.0)));
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 u = i as f64 / (image_width - 1) as f64;
let v = j as f64 / (image_height - 1) as f64;
let r = Ray::new(
origin,
lower_left_corner + u * horizontal + v * vertical - origin,
);
let pixel_color = ray_color(&r, &world);
output.push_str(&format!("{}\n", write_color(pixel_color)));
}
}
output
}小球(中心 (0, 0, -1),半径 0.5)と大球(中心 (0, -100.5, -1),半径 100)を配置しています。大球は「地面」として機能し,下半分がなだらかな球面地形になります。
WASM エクスポート
raytracing-demos/src/lib.rs に追記します。
// Chapter 1.6: Surface Normals and Multiple Objects
#[wasm_bindgen]
pub fn render_sphere_normals() -> String {
r106_sphere_normals::render_image()
}
#[wasm_bindgen]
pub fn render_hittable_list() -> String {
r106_hittable_list::render_image()
}