Rustでコンパイル時レイトレーシング6
2022/03/31 00:22 | 公開 |
前回記事
リポジトリ
記事の内容はコミットIDbcc80697fd56821fc0f2ef09a2f092284c9bf3ae
のものです。
今回は複数オブジェクトでワールドを作りレイが交差するかを判定するところまでです。
レンダリング結果
今回の変更点
- 球を表す
Sphere
型を追加 - レイとオブジェクトが交差した情報を保持する
HitRecord
型を追加 - レイと交差判定を行える型の列挙
Hittable
を追加 Hittable
を複数保持するHittableList
を追加- レイとの交差判定を単一の球とではなくワールドと行うようにする
です。
また参考元のRay Tracing in One Weekendと大きく変わっている箇所もいくつかあります。
Sphere型を追加
今までは関数に中心座標と半径を渡して計算していましたが今回から球が複数登場するためSphere
型を定義しています。
中心座標と半径を持っているだけのstruct
です。
#[derive(Clone, Copy)]
pub struct Sphere {
pub center: Point3,
pub radius: f64,
}
impl Sphere {
pub const fn new(center: Point3, radius: f64) -> Self { /* 省略 */ }
pub const fn hit(&self, r: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> { /* 省略 */ }
}
レイとオブジェクトの交差判定を保持するHitRecord
型を追加
レイと球の交差した情報を保持するための型であるHitRecord
型を追加しました。
交差した座標(p
)、交差した箇所の法線(normal
)、レイの始点との距離(t
)、交差した箇所が球の表(外側)かどうか(front_face
)を持っています。
ここはRay Tracing in One Weekendと大きな違いはありません。
pub struct HitRecord {
pub p: Point3,
pub normal: Vec3,
pub t: f64,
pub front_face: bool,
}
impl HitRecord {
pub const 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
};
}
}
レイと交差判定を行える型の列挙Hittable
を追加
Hittable
の定義は以下のようになっており、現状ではSphere
のみが要素として存在します。
#[derive(Clone, Copy)]
pub enum Hittable {
Sphere(Sphere),
}
Ray Tracing in One Weekendを参考にするのであればHittable
はTrait
として実装するべきだと思うのですが、それでは実装できない理由があるため最後に書きます。
Hittable
を複数保持するHittableList
を追加
HittableList
はHittable
を複数保持できるstruct
です。
Ray Tracing in One Weekendではvec
を使用していますがconst fn
の中では動的なメモリ確保ができないため配列を使用しています。
HittableList
はusize
型のコンパイル時定数CAPACITY
を受け取ります。これは配列のサイズで、この数までであればHittable
を保持できます。add
関数呼び出し時にCAPACITY
を超える場合にはコンパイルエラーが発生します。
配列の要素として持っているのがHittable
そのものではなくOption<Hittable>
なのは空の要素を表現する必要があるからです。
hit
関数では保持しているSome(Hittable)
全てに対して交差判定を行っています。
pub struct HittableList<const CAPACITY: usize> {
size: usize,
objects: [Option<Hittable>; CAPACITY],
}
impl<const CAPACITY: usize> HittableList<CAPACITY> {
pub const fn new() -> Self {
Self {
size: 0,
objects: [None; CAPACITY],
}
}
}
impl<const CAPACITY: usize> HittableList<CAPACITY> {
pub const fn clear(&mut self) {
self.objects = [None; CAPACITY];
self.size = 0;
}
pub const fn add(&mut self, hittable: Hittable) {
assert!(self.size < CAPACITY);
self.objects[self.size] = Some(hittable);
self.size += 1;
}
pub const fn hit(&self, r: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
let mut result: Option<HitRecord> = None;
let mut closest_so_far = t_max;
let mut index = 0;
while index < self.size {
let hit_record = match &self.objects[index] {
Some(Hittable::Sphere(ref s)) => s.hit(&r, t_min, closest_so_far),
_ => None,
};
if let Some(hit_record) = hit_record {
closest_so_far = hit_record.t;
result = Some(hit_record);
}
index += 1;
}
result
}
}
レイとの交差判定を単一の球とではなくワールドと行うようにする
main.rs
の変更点だけ抜き出して説明します。
まずray_color
関数です。
球ではなくworld: &HittableList<N>
を受け取りworld
との交差判定を行います。交差した場合は交差した箇所の法線を返し、交差しなかった場合は背景色を返すという点は変わりません。
const fn ray_color<const N: usize>(ray: &Ray, world: &HittableList<N>) -> Color {
if let Some(rec) = world.hit(&ray, 0.0, std::f64::INFINITY) {
return 0.5 * (rec.normal + Color::new(1.0, 1.0, 1.0));
}
let unit_direction = unit_vector(&ray.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)
}
次にworld
の作成です。
HittableList<2>
型のworldを作成しそこに球を2つ追加しています。
このコードだけであれば定数ジェネリクスの引数を使用している部分以外はHittableList
の中身がvec
であった場合と変わらないように見えると思います。
let mut world: HittableList<2> = HittableList::<2>::new();
world.add(Hittable::Sphere(Sphere::new(
Vec3::new(0.0, 0.0, -1.0),
0.5,
)));
world.add(Hittable::Sphere(Sphere::new(
Vec3::new(0.0, -100.5, -1.0),
100.0,
)));
最後にray_color
の呼び出しの変更です。
第2引数に&world
を渡すようになりました。
pixel_colors[pixel_index] = ray_color(&r, &world);
Ray Tracing in One Weekendとの変更点
参考元のRay Tracing in One Weekend大きく違う部分がいくつかあります。
HittableList
が保持している変数が(スマート)ポインタの動的配列ではない
参考元ではhittable_list
が保持するobjects
はvec<shared_ptr<hittable>>
として実装されています。
これをRustで実装するのであればVec<Box<Hittable>>
とかVec<Rc<Cell<Hittable>>>
になるかと思います。しかしそのどちらもconst fn
の中で使用することができないので[Option<Hittable>; CAPACITY]
として実装しています。
Hittable
がTrait
ではなくEnum
参考元ではhittable
の定義は以下のようになっており純粋仮想関数のhit
を持つクラス(interface)として実装されています。
class hittable {
public:
virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const = 0;
};
これをRustで実装するのであればHittable
はTrait
として以下のように実装するのが良いように思えます。
trait Hittable {
fn hit(&self, r: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord>;
}
しかしそうするとHittableList
がHittable
の配列を持つには[Box<Hittable>; N]
のように必要があります。
前述したようにconst fn
の中で動的メモリ確保が行えずBox
を使用することはできないのでこのような実装はできませんでした。そのためHittable
はEnum
として実装しています。
また参考元ではhittable_list
もhittable
を継承していますが、Hittable
にHittableList
を追加するとHittable
, HittableList
のサイズが未定となってしまいBox
を使用する必要が出てくるためHittableList
はHittable
に含んでいません。
あとがき
今回はconst fn
の縛りによって動的メモリ確保が行えない影響で普通に書くコードや参考元との差が大きくなったと思います。
次回はレンダリング後の画像にアンチエイリアシングをかけるところまでやります。
ついに乱数が必要になります。