aboutsummaryrefslogtreecommitdiff
path: root/src/hittable
diff options
context:
space:
mode:
authorlamp2023-03-05 21:45:56 +0000
committerlamp2023-03-05 21:45:56 +0000
commit78ddaff5855bf8446adef9e18eb0d7b7ddcee52a (patch)
tree0d0e93cfa28751a2f96518eeb231cf715958e1fa /src/hittable
init
Diffstat (limited to 'src/hittable')
-rw-r--r--src/hittable/aabb.rs46
-rw-r--r--src/hittable/bvh_node.rs90
-rw-r--r--src/hittable/constant_medium.rs65
-rw-r--r--src/hittable/hittable_box.rs40
-rw-r--r--src/hittable/hittable_list.rs52
-rw-r--r--src/hittable/instance/mod.rs10
-rw-r--r--src/hittable/instance/moving.rs40
-rw-r--r--src/hittable/instance/rotate_x.rs83
-rw-r--r--src/hittable/instance/rotate_y.rs83
-rw-r--r--src/hittable/instance/rotate_z.rs83
-rw-r--r--src/hittable/instance/translate.rs27
-rw-r--r--src/hittable/mod.rs76
-rw-r--r--src/hittable/model.rs200
-rw-r--r--src/hittable/sphere.rs61
-rw-r--r--src/hittable/triangle.rs79
-rw-r--r--src/hittable/xy_rect.rs48
-rw-r--r--src/hittable/xz_rect.rs48
-rw-r--r--src/hittable/yz_rect.rs48
18 files changed, 1179 insertions, 0 deletions
diff --git a/src/hittable/aabb.rs b/src/hittable/aabb.rs
new file mode 100644
index 0000000..5c5c9fa
--- /dev/null
+++ b/src/hittable/aabb.rs
@@ -0,0 +1,46 @@
+use crate::ray::Ray;
+use crate::vec3::Point3;
+
+#[derive(Clone)]
+pub struct AABB {
+ pub minimum: Point3,
+ pub maximum: Point3,
+}
+
+impl AABB {
+ pub fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> bool {
+ let mut t_min = t_min;
+ let mut t_max = t_max;
+ for a in 0..3 {
+ let inv_d = 1.0 / ray.direction.get(a).unwrap();
+ let mut t0 = (self.minimum.get(a).unwrap() - ray.origin.get(a).unwrap()) * inv_d;
+ let mut t1 = (self.maximum.get(a).unwrap() - ray.origin.get(a).unwrap()) * inv_d;
+ if inv_d < 0.0 {
+ // TODO: destructuring assignments are unstable :(
+ //(t0, t1) = (t1, t0);
+ std::mem::swap(&mut t0, &mut t1);
+ }
+ t_min = if t0 > t_min { t0 } else { t_min };
+ t_max = if t1 < t_max { t1 } else { t_max };
+ if t_max <= t_min {
+ return false;
+ }
+ }
+ true
+ }
+
+ pub fn surrounding_box(&self, other: &AABB) -> AABB {
+ AABB {
+ minimum: Point3 {
+ x: self.minimum.x.min(other.minimum.x),
+ y: self.minimum.y.min(other.minimum.y),
+ z: self.minimum.z.min(other.minimum.z),
+ },
+ maximum: Point3 {
+ x: self.maximum.x.max(other.maximum.x),
+ y: self.maximum.y.max(other.maximum.y),
+ z: self.maximum.z.max(other.maximum.z),
+ },
+ }
+ }
+}
diff --git a/src/hittable/bvh_node.rs b/src/hittable/bvh_node.rs
new file mode 100644
index 0000000..d215bc3
--- /dev/null
+++ b/src/hittable/bvh_node.rs
@@ -0,0 +1,90 @@
+use std::{cmp, sync::Arc};
+
+use rand::seq::SliceRandom;
+
+use crate::hittable::{HitRecord, Hittable, AABB, hittable_list::HittableList};
+use crate::ray::Ray;
+
+pub struct BVHNode {
+ left: Arc<dyn Hittable>,
+ right: Arc<dyn Hittable>,
+ aabb: AABB,
+}
+
+#[derive(Clone, Copy)]
+enum Axis {
+ X,
+ Y,
+ Z,
+}
+
+impl BVHNode {
+ pub fn new(hittable_list: &HittableList, time_start: f64, time_end: f64) -> BVHNode {
+ Self::from_objects(&hittable_list.objects, 0, hittable_list.objects.len(), time_start, time_end)
+ }
+
+ fn from_objects(src_objects: &Vec<Arc<dyn Hittable>>, start: usize, end: usize, time_start: f64, time_end: f64) -> BVHNode {
+ let mut objects = src_objects.clone();
+ let comparator = [
+ |a: &Arc<dyn Hittable>, b: &Arc<dyn Hittable>| Self::box_compare(a.clone(), b.clone(), Axis::X),
+ |a: &Arc<dyn Hittable>, b: &Arc<dyn Hittable>| Self::box_compare(a.clone(), b.clone(), Axis::Y),
+ |a: &Arc<dyn Hittable>, b: &Arc<dyn Hittable>| Self::box_compare(a.clone(), b.clone(), Axis::Z),
+ ].choose(&mut rand::thread_rng()).unwrap();
+ let object_span = end - start;
+
+ let (left, right) = match object_span {
+ 1 => (objects.get(start).unwrap().clone(), objects.get(start).unwrap().clone()),
+ 2 => match comparator(objects.get(start).unwrap(), objects.get(start + 1).unwrap()) {
+ cmp::Ordering::Less => (objects.get(start).unwrap().clone(), objects.get(start + 1).unwrap().clone()),
+ _ => (objects.get(start + 1).unwrap().clone(), objects.get(start).unwrap().clone()),
+ }
+ _ => {
+ objects[start..end].sort_by(comparator);
+ let mid = start + object_span / 2;
+ (Arc::new(BVHNode::from_objects(&objects, start, mid, time_start, time_end)) as Arc<dyn Hittable>,
+ Arc::new(BVHNode::from_objects(&objects, mid, end, time_start, time_end)) as Arc<dyn Hittable>)
+
+ }
+ };
+ let box_left = left.bounding_box(time_start, time_end).expect("No bounding box in bvh_node constructor!");
+ let box_right = right.bounding_box(time_start, time_end).expect("No bounding box in bvh_node constructor!");
+
+ BVHNode {
+ left,
+ right,
+ aabb: box_left.surrounding_box(&box_right),
+ }
+ }
+
+ fn box_compare (a: Arc<dyn Hittable>, b: Arc<dyn Hittable>, axis: Axis) -> cmp::Ordering {
+ let box_a = a.bounding_box(0.0, 0.0).expect("No bounding box in bvh_node constructor!");
+ let box_b = b.bounding_box(0.0, 0.0).expect("No bounding box in bvh_node constructor!");
+
+ // TODO: total_cmp is unstable :(
+ box_a.minimum.get(axis as usize).unwrap().partial_cmp(box_b.minimum.get(axis as usize).unwrap()).unwrap()
+ }
+}
+
+impl Hittable for BVHNode {
+ fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+ if !self.aabb.hit(ray, t_min, t_max) {
+ return None
+ }
+ let hit_left = self.left.hit(ray, t_min, t_max);
+ let hit_right_threshold = if let Some(hit_record_left) = &hit_left {
+ hit_record_left.t
+ } else {
+ t_max
+ };
+ let hit_right = self.right.hit(ray, t_min, hit_right_threshold);
+ if let Some(_) = &hit_right {
+ hit_right
+ } else {
+ hit_left
+ }
+ }
+
+ fn bounding_box(&self, _: f64, _: f64) -> Option<AABB> {
+ Some(self.aabb.clone())
+ }
+}
diff --git a/src/hittable/constant_medium.rs b/src/hittable/constant_medium.rs
new file mode 100644
index 0000000..b047c23
--- /dev/null
+++ b/src/hittable/constant_medium.rs
@@ -0,0 +1,65 @@
+use std::sync::Arc;
+
+use crate::{hittable::{HitRecord, Hittable, AABB}, material::{Isotropic, Material}, ray::Ray, texture::Texture, vec3::Vec3};
+
+pub struct ConstantMedium {
+ boundary: Arc<dyn Hittable>,
+ phase_function: Arc<dyn Material>,
+ neg_inv_density: f64,
+}
+
+impl ConstantMedium {
+ pub fn new(boundary: Arc<dyn Hittable>, density: f64, texture: Arc<dyn Texture>) -> Self {
+ Self {
+ boundary,
+ phase_function: Arc::new(Isotropic::from_texture(texture)),
+ neg_inv_density: -1.0/density,
+ }
+ }
+}
+
+impl Hittable for ConstantMedium {
+ // TODO: this only support convex shapes.
+ fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+ let mut record_1 = self.boundary.hit(ray, -f64::INFINITY, f64::INFINITY)?;
+ let mut record_2 = self.boundary.hit(ray, record_1.t + 0.0001, f64::INFINITY)?;
+
+ if record_1.t < t_min {
+ record_1.t = t_min;
+ }
+ if record_2.t > t_max {
+ record_2.t = t_max;
+ }
+
+ if record_1.t >= record_2.t {
+ return None;
+ }
+
+ if record_1.t < 0.0 {
+ record_1.t = 0.0;
+ }
+
+ let ray_length = ray.direction.length();
+ let distance_inside_boundary = (record_2.t - record_1.t) * ray_length;
+ let hit_distance = self.neg_inv_density * rand::random::<f64>().ln();
+
+ if hit_distance > distance_inside_boundary {
+ return None;
+ }
+
+ let t = record_1.t + hit_distance / ray_length;
+ Some(HitRecord {
+ p: ray.at(t),
+ t,
+ material: Some(self.phase_function.clone()),
+ normal: Vec3 { x: 1.0, y: 0.0, z: 0.0 }, // arbitrary
+ front_face: true, // arbitrary
+ u: 0.0, // arbitrary
+ v: 0.0, // arbitrary
+ })
+ }
+
+ fn bounding_box(&self, time_start: f64, time_end: f64) -> Option<AABB> {
+ self.boundary.bounding_box(time_start, time_end)
+ }
+}
diff --git a/src/hittable/hittable_box.rs b/src/hittable/hittable_box.rs
new file mode 100644
index 0000000..7b95cc7
--- /dev/null
+++ b/src/hittable/hittable_box.rs
@@ -0,0 +1,40 @@
+use std::sync::Arc;
+
+use crate::{hittable::{HitRecord, Hittable, AABB, hittable_list::HittableList, xy_rect::XYRect, xz_rect::XZRect, yz_rect::YZRect}, material::Material, ray::Ray, vec3::Point3};
+
+pub struct HittableBox {
+ min: Point3,
+ max: Point3,
+ sides: HittableList,
+}
+
+impl HittableBox {
+ pub fn new(min: Point3, max: Point3, material: Arc<dyn Material>) -> Self {
+ let mut sides = HittableList::new();
+
+ sides.add(Arc::new(XYRect { material: material.clone(), x0: min.x, x1: max.x, y0: min.y, y1: max.y, k: max.z }));
+ sides.add(Arc::new(XYRect { material: material.clone(), x0: min.x, x1: max.x, y0: min.y, y1: max.y, k: min.z }));
+
+ sides.add(Arc::new(XZRect { material: material.clone(), x0: min.x, x1: max.x, z0: min.z, z1: max.z, k: max.y }));
+ sides.add(Arc::new(XZRect { material: material.clone(), x0: min.x, x1: max.x, z0: min.z, z1: max.z, k: min.y }));
+
+ sides.add(Arc::new(YZRect { material: material.clone(), y0: min.y, y1: max.y, z0: min.z, z1: max.z, k: max.x }));
+ sides.add(Arc::new(YZRect { material: material.clone(), y0: min.y, y1: max.y, z0: min.z, z1: max.z, k: min.x }));
+
+ Self {
+ min,
+ max,
+ sides,
+ }
+ }
+}
+
+impl Hittable for HittableBox {
+ fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+ self.sides.hit(ray, t_min, t_max)
+ }
+
+ fn bounding_box(&self, _: f64, _: f64) -> Option<AABB> {
+ Some(AABB { minimum: self.min.clone(), maximum: self.max.clone() })
+ }
+}
diff --git a/src/hittable/hittable_list.rs b/src/hittable/hittable_list.rs
new file mode 100644
index 0000000..735509c
--- /dev/null
+++ b/src/hittable/hittable_list.rs
@@ -0,0 +1,52 @@
+use std::sync::Arc;
+
+use crate::hittable::{HitRecord, Hittable, AABB};
+use crate::ray::Ray;
+
+pub struct HittableList {
+ pub objects: Vec<Arc<dyn Hittable>>,
+}
+
+impl HittableList {
+ pub fn new() -> HittableList {
+ HittableList {
+ objects: Vec::new(),
+ }
+ }
+
+ pub fn clear(&mut self) {
+ self.objects.clear();
+ }
+
+ pub fn add(&mut self, object: Arc<dyn Hittable>) {
+ self.objects.push(object);
+ }
+}
+
+impl Hittable for HittableList {
+ fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+ let mut record = None;
+ let mut closest_so_far = t_max;
+
+ for object in &self.objects {
+ let temp_rec = object.hit(ray, t_min, closest_so_far);
+ if let Some(hit_record) = &temp_rec {
+ closest_so_far = hit_record.t;
+ record = temp_rec;
+ }
+ }
+ record
+ }
+
+ fn bounding_box(&self, time_start: f64, time_end: f64) -> Option<AABB> {
+ let mut output_box: Option<AABB> = None;
+ for object in &self.objects {
+ let temp_box = object.bounding_box(time_start, time_end)?;
+ output_box = match output_box {
+ Some(aabb) => Some(aabb.surrounding_box(&temp_box)),
+ None => Some(temp_box.clone()),
+ };
+ }
+ output_box
+ }
+}
diff --git a/src/hittable/instance/mod.rs b/src/hittable/instance/mod.rs
new file mode 100644
index 0000000..d40dcc5
--- /dev/null
+++ b/src/hittable/instance/mod.rs
@@ -0,0 +1,10 @@
+mod moving;
+pub use moving::Moving;
+mod rotate_y;
+pub use rotate_y::RotateY;
+mod rotate_x;
+pub use rotate_x::RotateX;
+mod rotate_z;
+pub use rotate_z::RotateZ;
+mod translate;
+pub use translate::Translate;
diff --git a/src/hittable/instance/moving.rs b/src/hittable/instance/moving.rs
new file mode 100644
index 0000000..418fdbb
--- /dev/null
+++ b/src/hittable/instance/moving.rs
@@ -0,0 +1,40 @@
+use std::sync::Arc;
+
+use crate::{ray::Ray, vec3::Vec3};
+use crate::hittable::{HitRecord, Hittable, AABB};
+
+pub struct Moving {
+ pub hittable: Arc<dyn Hittable>,
+ pub offset_start: Vec3,
+ pub offset_end: Vec3,
+ pub time_start: f64,
+ pub time_end: f64,
+}
+
+impl Moving {
+ fn offset_at(&self, time: f64) -> Vec3 {
+ &self.offset_start + ((time - self.time_start) / (self.time_end - self.time_start)) * (&self.offset_end - &self.offset_start)
+ }
+}
+
+impl Hittable for Moving {
+ fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+ let moved_ray = Ray { origin: &ray.origin - &self.offset_at(ray.time), direction: ray.direction.clone(), time: ray.time };
+ let mut hit_record = self.hittable.hit(&moved_ray, t_min, t_max)?;
+ hit_record.p += self.offset_at(ray.time).clone();
+ let normal = hit_record.normal.clone();
+ hit_record.set_face_normal(&moved_ray, &normal);
+ Some(hit_record)
+ }
+
+ fn bounding_box(&self, time_start: f64, time_end: f64) -> Option<AABB> {
+ let output_box = self.hittable.bounding_box(time_start, time_end)?;
+ Some(AABB {
+ minimum: &output_box.minimum + &self.offset_at(time_start),
+ maximum: &output_box.maximum + &self.offset_at(time_start),
+ }.surrounding_box(&AABB {
+ minimum: &output_box.minimum + &self.offset_at(time_end),
+ maximum: &output_box.maximum + &self.offset_at(time_end),
+ }))
+ }
+}
diff --git a/src/hittable/instance/rotate_x.rs b/src/hittable/instance/rotate_x.rs
new file mode 100644
index 0000000..4ebc04d
--- /dev/null
+++ b/src/hittable/instance/rotate_x.rs
@@ -0,0 +1,83 @@
+use std::sync::Arc;
+
+use crate::{hittable::{HitRecord, Hittable, AABB}, ray::Ray, util::degrees_to_radians, vec3::{Point3, Vec3}};
+
+pub struct RotateX {
+ hittable: Arc<dyn Hittable>,
+ sin_theta: f64,
+ cos_theta: f64,
+ aabb: Option<AABB>,
+}
+
+impl RotateX {
+ pub fn new(hittable: Arc<dyn Hittable>, angle: f64) -> Self {
+ let radians = degrees_to_radians(angle);
+ let sin_theta = radians.sin();
+ let cos_theta = radians.cos();
+ match hittable.bounding_box(0.0, 1.0) { // TODO: passing in 0.0 and 1.0 for time seems suspicious.
+ None => Self { hittable, sin_theta, cos_theta, aabb: None },
+ Some(aabb) => {
+ let mut min = Point3 { x: f64::INFINITY, y: f64::INFINITY, z: f64::INFINITY };
+ let mut max = Point3 { x: -f64::INFINITY, y: -f64::INFINITY, z: -f64::INFINITY };
+ for i in 0..2 {
+ for j in 0..2 {
+ for k in 0..2 {
+ let x = i as f64 * aabb.maximum.x + (1.0 - i as f64) * aabb.minimum.x;
+ let y = j as f64 * aabb.maximum.y + (1.0 - j as f64) * aabb.minimum.y;
+ let z = k as f64 * aabb.maximum.z + (1.0 - k as f64) * aabb.minimum.z;
+ let new_y = cos_theta * y + sin_theta * y;
+ let new_z = -sin_theta * y + cos_theta * z;
+
+ let tester = Vec3 { y: new_y, x, z: new_z };
+ for c in 0..3 {
+ *min.get_mut(c).unwrap() = min.get(c).unwrap().min(*tester.get(c).unwrap());
+ *max.get_mut(c).unwrap() = max.get(c).unwrap().max(*tester.get(c).unwrap());
+ }
+ }
+ }
+ }
+ let aabb = AABB { minimum: min, maximum: max };
+
+ Self {
+ hittable,
+ sin_theta,
+ cos_theta,
+ aabb: Some(aabb),
+ }
+ }
+ }
+ }
+}
+
+impl Hittable for RotateX {
+ fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+ let mut origin = ray.origin.clone();
+ let mut direction = ray.direction.clone();
+
+ origin.y = self.cos_theta * ray.origin.y - self.sin_theta * ray.origin.z;
+ origin.z = self.sin_theta * ray.origin.y + self.cos_theta * ray.origin.z;
+
+ direction.y = self.cos_theta * ray.direction.y - self.sin_theta * ray.direction.z;
+ direction.z = self.sin_theta * ray.direction.y + self.cos_theta * ray.direction.z;
+
+ let rotated_ray = Ray { origin, direction, time: ray.time };
+ let mut hit_record = self.hittable.hit(&rotated_ray, t_min, t_max)?;
+
+ let mut p = hit_record.p.clone();
+ let mut normal = hit_record.normal.clone();
+
+ p.y = self.cos_theta * hit_record.p.y+ self.sin_theta * hit_record.p.z;
+ p.z = -self.sin_theta * hit_record.p.y + self.cos_theta * hit_record.p.z;
+
+ normal.y = self.cos_theta * hit_record.normal.y + self.sin_theta * hit_record.normal.z;
+ normal.z = -self.sin_theta * hit_record.normal.y + self.cos_theta * hit_record.normal.z;
+
+ hit_record.p = p;
+ hit_record.set_face_normal(&ray, &normal);
+ Some(hit_record)
+ }
+
+ fn bounding_box(&self, _: f64, _: f64) -> Option<AABB> {
+ self.aabb.clone()
+ }
+}
diff --git a/src/hittable/instance/rotate_y.rs b/src/hittable/instance/rotate_y.rs
new file mode 100644
index 0000000..8611616
--- /dev/null
+++ b/src/hittable/instance/rotate_y.rs
@@ -0,0 +1,83 @@
+use std::sync::Arc;
+
+use crate::{hittable::{HitRecord, Hittable, AABB}, ray::Ray, util::degrees_to_radians, vec3::{Point3, Vec3}};
+
+pub struct RotateY {
+ hittable: Arc<dyn Hittable>,
+ sin_theta: f64,
+ cos_theta: f64,
+ aabb: Option<AABB>,
+}
+
+impl RotateY {
+ pub fn new(hittable: Arc<dyn Hittable>, angle: f64) -> Self {
+ let radians = degrees_to_radians(angle);
+ let sin_theta = radians.sin();
+ let cos_theta = radians.cos();
+ match hittable.bounding_box(0.0, 1.0) { // TODO: passing in 0.0 and 1.0 for time seems suspicious.
+ None => Self { hittable, sin_theta, cos_theta, aabb: None },
+ Some(aabb) => {
+ let mut min = Point3 { x: f64::INFINITY, y: f64::INFINITY, z: f64::INFINITY };
+ let mut max = Point3 { x: -f64::INFINITY, y: -f64::INFINITY, z: -f64::INFINITY };
+ for i in 0..2 {
+ for j in 0..2 {
+ for k in 0..2 {
+ let x = i as f64 * aabb.maximum.x + (1.0 - i as f64) * aabb.minimum.x;
+ let y = j as f64 * aabb.maximum.y + (1.0 - j as f64) * aabb.minimum.y;
+ let z = k as f64 * aabb.maximum.z + (1.0 - k as f64) * aabb.minimum.z;
+ let new_x = cos_theta * x + sin_theta * z;
+ let new_z = -sin_theta * x + cos_theta * z;
+
+ let tester = Vec3 { x: new_x, y, z: new_z };
+ for c in 0..3 {
+ *min.get_mut(c).unwrap() = min.get(c).unwrap().min(*tester.get(c).unwrap());
+ *max.get_mut(c).unwrap() = max.get(c).unwrap().max(*tester.get(c).unwrap());
+ }
+ }
+ }
+ }
+ let aabb = AABB { minimum: min, maximum: max };
+
+ Self {
+ hittable,
+ sin_theta,
+ cos_theta,
+ aabb: Some(aabb),
+ }
+ }
+ }
+ }
+}
+
+impl Hittable for RotateY {
+ fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+ let mut origin = ray.origin.clone();
+ let mut direction = ray.direction.clone();
+
+ origin.x = self.cos_theta * ray.origin.x - self.sin_theta * ray.origin.z;
+ origin.z = self.sin_theta * ray.origin.x + self.cos_theta * ray.origin.z;
+
+ direction.x = self.cos_theta * ray.direction.x - self.sin_theta * ray.direction.z;
+ direction.z = self.sin_theta * ray.direction.x + self.cos_theta * ray.direction.z;
+
+ let rotated_ray = Ray { origin, direction, time: ray.time };
+ let mut hit_record = self.hittable.hit(&rotated_ray, t_min, t_max)?;
+
+ let mut p = hit_record.p.clone();
+ let mut normal = hit_record.normal.clone();
+
+ p.x = self.cos_theta * hit_record.p.x + self.sin_theta * hit_record.p.z;
+ p.z = -self.sin_theta * hit_record.p.x + self.cos_theta * hit_record.p.z;
+
+ normal.x = self.cos_theta * hit_record.normal.x + self.sin_theta * hit_record.normal.z;
+ normal.z = -self.sin_theta * hit_record.normal.x + self.cos_theta * hit_record.normal.z;
+
+ hit_record.p = p;
+ hit_record.set_face_normal(&ray, &normal);
+ Some(hit_record)
+ }
+
+ fn bounding_box(&self, _: f64, _: f64) -> Option<AABB> {
+ self.aabb.clone()
+ }
+}
diff --git a/src/hittable/instance/rotate_z.rs b/src/hittable/instance/rotate_z.rs
new file mode 100644
index 0000000..119baca
--- /dev/null
+++ b/src/hittable/instance/rotate_z.rs
@@ -0,0 +1,83 @@
+use std::sync::Arc;
+
+use crate::{hittable::{HitRecord, Hittable, AABB}, ray::Ray, util::degrees_to_radians, vec3::{Point3, Vec3}};
+
+pub struct RotateZ {
+ hittable: Arc<dyn Hittable>,
+ sin_theta: f64,
+ cos_theta: f64,
+ aabb: Option<AABB>,
+}
+
+impl RotateZ {
+ pub fn new(hittable: Arc<dyn Hittable>, angle: f64) -> Self {
+ let radians = degrees_to_radians(angle);
+ let sin_theta = radians.sin();
+ let cos_theta = radians.cos();
+ match hittable.bounding_box(0.0, 1.0) { // TODO: passing in 0.0 and 1.0 for time seems suspicious.
+ None => Self { hittable, sin_theta, cos_theta, aabb: None },
+ Some(aabb) => {
+ let mut min = Point3 { x: f64::INFINITY, y: f64::INFINITY, z: f64::INFINITY };
+ let mut max = Point3 { x: -f64::INFINITY, y: -f64::INFINITY, z: -f64::INFINITY };
+ for i in 0..2 {
+ for j in 0..2 {
+ for k in 0..2 {
+ let x = i as f64 * aabb.maximum.x + (1.0 - i as f64) * aabb.minimum.x;
+ let y = j as f64 * aabb.maximum.y + (1.0 - j as f64) * aabb.minimum.y;
+ let z = k as f64 * aabb.maximum.z + (1.0 - k as f64) * aabb.minimum.z;
+ let new_x = cos_theta * x + sin_theta * y;
+ let new_y = -sin_theta * x + cos_theta * y;
+
+ let tester = Vec3 { x: new_x, z, y: new_y };
+ for c in 0..3 {
+ *min.get_mut(c).unwrap() = min.get(c).unwrap().min(*tester.get(c).unwrap());
+ *max.get_mut(c).unwrap() = max.get(c).unwrap().max(*tester.get(c).unwrap());
+ }
+ }
+ }
+ }
+ let aabb = AABB { minimum: min, maximum: max };
+
+ Self {
+ hittable,
+ sin_theta,
+ cos_theta,
+ aabb: Some(aabb),
+ }
+ }
+ }
+ }
+}
+
+impl Hittable for RotateZ {
+ fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+ let mut origin = ray.origin.clone();
+ let mut direction = ray.direction.clone();
+
+ origin.x = self.cos_theta * ray.origin.x - self.sin_theta * ray.origin.y;
+ origin.y = self.sin_theta * ray.origin.x + self.cos_theta * ray.origin.y;
+
+ direction.x = self.cos_theta * ray.direction.x - self.sin_theta * ray.direction.y;
+ direction.y = self.sin_theta * ray.direction.x + self.cos_theta * ray.direction.y;
+
+ let rotated_ray = Ray { origin, direction, time: ray.time };
+ let mut hit_record = self.hittable.hit(&rotated_ray, t_min, t_max)?;
+
+ let mut p = hit_record.p.clone();
+ let mut normal = hit_record.normal.clone();
+
+ p.x = self.cos_theta * hit_record.p.x + self.sin_theta * hit_record.p.y;
+ p.y = -self.sin_theta * hit_record.p.x + self.cos_theta * hit_record.p.y;
+
+ normal.x = self.cos_theta * hit_record.normal.x + self.sin_theta * hit_record.normal.y;
+ normal.y = -self.sin_theta * hit_record.normal.x + self.cos_theta * hit_record.normal.y;
+
+ hit_record.p = p;
+ hit_record.set_face_normal(&ray, &normal);
+ Some(hit_record)
+ }
+
+ fn bounding_box(&self, _: f64, _: f64) -> Option<AABB> {
+ self.aabb.clone()
+ }
+}
diff --git a/src/hittable/instance/translate.rs b/src/hittable/instance/translate.rs
new file mode 100644
index 0000000..a9c8162
--- /dev/null
+++ b/src/hittable/instance/translate.rs
@@ -0,0 +1,27 @@
+use std::sync::Arc;
+
+use crate::{hittable::{HitRecord, Hittable, AABB}, ray::Ray, vec3::Vec3};
+
+pub struct Translate {
+ pub hittable: Arc<dyn Hittable>,
+ pub offset: Vec3,
+}
+
+impl Hittable for Translate {
+ fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+ let moved_ray = Ray { origin: &ray.origin - &self.offset, direction: ray.direction.clone(), time: ray.time };
+ let mut hit_record = self.hittable.hit(&moved_ray, t_min, t_max)?;
+ hit_record.p += self.offset.clone();
+ let normal = hit_record.normal.clone();
+ hit_record.set_face_normal(&moved_ray, &normal);
+ Some(hit_record)
+ }
+
+ fn bounding_box(&self, time_start: f64, time_end: f64) -> Option<AABB> {
+ let output_box = self.hittable.bounding_box(time_start, time_end)?;
+ Some(AABB {
+ minimum: &output_box.minimum + &self.offset,
+ maximum: &output_box.maximum + &self.offset,
+ })
+ }
+}
diff --git a/src/hittable/mod.rs b/src/hittable/mod.rs
new file mode 100644
index 0000000..e888c71
--- /dev/null
+++ b/src/hittable/mod.rs
@@ -0,0 +1,76 @@
+pub mod instance;
+mod bvh_node;
+pub use bvh_node::BVHNode;
+mod constant_medium;
+pub use constant_medium::ConstantMedium;
+mod hittable_box;
+pub use hittable_box::HittableBox;
+mod hittable_list;
+pub use hittable_list::HittableList;
+mod xy_rect;
+pub use xy_rect::XYRect;
+mod xz_rect;
+pub use xz_rect::XZRect;
+mod yz_rect;
+pub use yz_rect::YZRect;
+mod sphere;
+pub use sphere::Sphere;
+mod triangle;
+pub use triangle::Triangle;
+mod model;
+pub use model::Model;
+mod aabb;
+
+use std::sync::Arc;
+
+use crate::ray::Ray;
+use crate::vec3::{Point3, Vec3};
+use crate::material::Material;
+use aabb::AABB;
+
+#[derive(Clone)]
+pub struct HitRecord {
+ pub p: Point3,
+ pub normal: Vec3,
+ pub material: Option<Arc<dyn Material>>,
+ pub t: f64,
+ pub u: f64,
+ pub v: f64,
+ pub front_face: bool,
+}
+
+impl HitRecord {
+ pub fn new() -> HitRecord {
+ HitRecord {
+ p: Point3 {
+ x: 0.0,
+ y: 0.0,
+ z: 0.0,
+ },
+ normal: Vec3 {
+ x: 0.0,
+ y: 0.0,
+ z: 0.0,
+ },
+ material: None,
+ t: 0.0,
+ u: 0.0,
+ v: 0.0,
+ front_face: false,
+ }
+ }
+
+ pub fn set_face_normal(&mut self, ray: &Ray, outward_normal: &Vec3) {
+ self.front_face = ray.direction.dot(&outward_normal) < 0.0;
+ self.normal = if self.front_face {
+ outward_normal.clone()
+ } else {
+ -outward_normal
+ };
+ }
+}
+
+pub trait Hittable: Send + Sync {
+ fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord>;
+ fn bounding_box(&self, time_start: f64, time_end: f64) -> Option<AABB>;
+}
diff --git a/src/hittable/model.rs b/src/hittable/model.rs
new file mode 100644
index 0000000..1a652be
--- /dev/null
+++ b/src/hittable/model.rs
@@ -0,0 +1,200 @@
+use std::sync::Arc;
+use std::vec::Vec;
+
+use crate::{hittable::{HitRecord, Hittable, AABB, HittableList, Triangle}, material::Material, ray::Ray, vec3::{Point3, Vec3}};
+
+pub struct Model {
+ faces: HittableList,
+}
+
+impl Model {
+ fn parse_face_triplet(triplet: &str) -> Option<(isize, Option<isize>, Option<isize>)> {
+ let mut triplet_iter = triplet.split("/");
+ Some((triplet_iter.next()?.parse::<isize>().ok()?, triplet_iter.next().and_then(|val| val.parse::<isize>().ok()) , triplet_iter.next().and_then(|val| val.parse::<isize>().ok())))
+ }
+ pub fn from_obj(obj_data: &str, material: Arc<dyn Material>) -> Self {
+ let mut geometric_vertices: Vec<Vec3> = Vec::new();
+ let mut texture_vertices: Vec<(f64, f64)> = Vec::new();
+ let mut vertex_normals: Vec<Vec3> = Vec::new();
+ // no free-form objects, so no parameter-space vertices!
+ let mut faces: Vec<Arc<dyn Hittable>> = Vec::new();
+ for entry in obj_data.lines() {
+ if entry.starts_with("#") {
+ // comment
+ continue;
+ }
+ let mut entry_iter = entry.split(' ');
+ let operator = match entry_iter.next() {
+ None => continue,
+ Some(val) => val,
+ };
+ match operator {
+ "v" | "vn" => {
+ let x = match entry_iter.next() {
+ None => {
+ eprintln!("Malformed {} entry in OBJ: Missing x!", operator);
+ continue;
+ },
+ Some(val) => {
+ match val.parse::<f64>() {
+ Err(_) => {
+ eprintln!("Malformed {} entry in OBJ: Malformed f64 x!", operator);
+ continue;
+ },
+ Ok(val) => val,
+ }
+ }
+ };
+ let y = match entry_iter.next() {
+ None => {
+ eprintln!("Malformed {} entry in OBJ: Missing y!", operator);
+ continue;
+ },
+ Some(val) => {
+ match val.parse::<f64>() {
+ Err(_) => {
+ eprintln!("Malformed {} entry in OBJ: Malformed f64 y!", operator);
+ continue;
+ },
+ Ok(val) => val,
+ }
+ }
+ };
+ let z = match entry_iter.next() {
+ None => {
+ eprintln!("Malformed {} entry in OBJ: Missing z!", operator);
+ continue;
+ },
+ Some(val) => {
+ match val.parse::<f64>() {
+ Err(_) => {
+ eprintln!("Malformed {} entry in OBJ: Malformed f64 z!", operator);
+ continue;
+ },
+ Ok(val) => val,
+ }
+ }
+ };
+ // who cares about w
+ match operator {
+ "v" => geometric_vertices.push(Vec3 {x, y, z}),
+ "vn" => vertex_normals.push(Vec3 {x, y, z}),
+ _ => panic!(),
+ }
+ },
+ "vt" => {
+ let u = match entry_iter.next() {
+ None => {
+ eprintln!("Malformed vt entry in OBJ: Missing u!");
+ continue;
+ },
+ Some(val) => {
+ match val.parse::<f64>() {
+ Err(_) => {
+ eprintln!("Malformed vt entry in OBJ: Malformed f64 u!");
+ continue;
+ },
+ Ok(val) => val,
+ }
+ }
+ };
+ let v = match entry_iter.next() {
+ None => {
+ eprintln!("Malformed vt entry in OBJ: Missing v!");
+ continue;
+ },
+ Some(val) => {
+ match val.parse::<f64>() {
+ Err(_) => {
+ eprintln!("Malformed v entry in OBJ: Malformed f64 v!");
+ continue;
+ },
+ Ok(val) => val,
+ }
+ }
+ };
+ // who cares about w
+ texture_vertices.push((u, v));
+ },
+ "f" => {
+ let mut triplets : Vec<(isize, Option<isize>, Option<isize>)> = Vec::new();
+ for triplet in entry_iter {
+ match Self::parse_face_triplet(triplet) {
+ None => {
+ eprintln!("Encountered malformed triplet in f operator!");
+ },
+ Some(val) => {
+ triplets.push(val);
+ }
+ };
+ }
+ // only support faces with *exactly* three vertices. yeah, i know.
+ if triplets.len() != 3 {
+ eprintln!("Encountered face with unsupported vertex count!");
+ continue;
+ }
+ let mut v0_index = triplets.get(0).unwrap().0;
+ if v0_index < 0 {
+ v0_index = geometric_vertices.len() as isize + v0_index;
+ } else {
+ v0_index = v0_index - 1;
+ }
+ let mut v1_index = triplets.get(1).unwrap().0;
+ if v1_index < 0 {
+ v1_index = geometric_vertices.len() as isize + v1_index;
+ } else {
+ v1_index = v1_index - 1;
+ }
+ let mut v2_index = triplets.get(2).unwrap().0;
+ if v2_index < 0 {
+ v2_index = geometric_vertices.len() as isize + v2_index;
+ } else {
+ v2_index = v2_index - 1;
+ }
+ let mut triangle = Triangle {
+ v0: geometric_vertices.get(v0_index as usize).unwrap().clone(),
+ v1: geometric_vertices.get(v1_index as usize).unwrap().clone(),
+ v2: geometric_vertices.get(v2_index as usize).unwrap().clone(),
+ material: material.clone(),
+ custom_normal: None,
+ };
+ if let Some(vn0) = triplets.get(0).unwrap().2 {
+ if let Some(vn1) = triplets.get(1).unwrap().2 {
+ if let Some(vn2) = triplets.get(2).unwrap().2 {
+ if vn0 != vn1 || vn1 != vn2 {
+ eprintln!("Unsupported geometry in OBJ file: Multiple normals for face!");
+ continue
+ }
+ let mut vn0 = vn0;
+ if vn0 < 0 {
+ vn0 = vertex_normals.len() as isize + v0_index;
+ } else {
+ vn0 = vn0 - 1;
+ }
+ triangle.custom_normal = Some(vertex_normals.get(vn0 as usize).unwrap().unit_vector());
+ }
+ }
+ }
+ faces.push(Arc::new(triangle));
+ },
+ _ => {
+ eprintln!("Ignoring unknown operator {} in OBJ!", operator);
+ continue;
+ },
+ }
+ }
+ Self {
+ faces: HittableList { objects: faces },
+ }
+ }
+}
+
+impl Hittable for Model {
+ fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+ self.faces.hit(ray, t_min, t_max)
+ }
+
+ fn bounding_box(&self, time_start: f64, time_end: f64) -> Option<AABB> {
+ self.faces.bounding_box(time_start, time_end)
+ }
+}
diff --git a/src/hittable/sphere.rs b/src/hittable/sphere.rs
new file mode 100644
index 0000000..783b788
--- /dev/null
+++ b/src/hittable/sphere.rs
@@ -0,0 +1,61 @@
+use std::sync::Arc;
+use std::f64::consts;
+
+use crate::{hittable::{HitRecord, Hittable, AABB}, material::Material, vec3::Vec3};
+use crate::ray::Ray;
+use crate::vec3::Point3;
+
+pub struct Sphere {
+ pub center: Point3,
+ pub radius: f64,
+ pub material: Arc<dyn Material>,
+}
+
+impl Sphere {
+ pub fn get_sphere_uv(p: &Point3, u: &mut f64, v: &mut f64) {
+ let theta = (-p.y).acos();
+ let phi = -p.z.atan2(p.x) + consts::PI;
+
+ *u = phi / (2.0 * consts::PI);
+ *v = theta / consts::PI;
+ }
+}
+
+impl Hittable for Sphere {
+ fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+ let oc = &ray.origin - &self.center;
+ let a = ray.direction.length_squared();
+ let half_b = oc.dot(&ray.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();
+ // Find the nearest root that lies within acceptable range
+ let mut root = (-half_b - sqrtd) / a;
+ if root < t_min || t_max < root {
+ root = (-half_b + sqrtd) / a;
+ if root < t_min || t_max < root {
+ return None;
+ }
+ }
+
+ let mut hit_record = HitRecord::new();
+ hit_record.t = root;
+ hit_record.p = ray.at(hit_record.t);
+ let outward_normal = (&hit_record.p - &self.center) / self.radius;
+ hit_record.set_face_normal(ray, &outward_normal);
+ Self::get_sphere_uv(&outward_normal, &mut hit_record.u, &mut hit_record.v);
+ hit_record.material = Some(self.material.clone());
+ Some(hit_record)
+ }
+
+ fn bounding_box(&self, _: f64, _: f64) -> Option<AABB> {
+ Some(AABB {
+ minimum: &self.center - Vec3 { x: self.radius, y: self.radius, z: self.radius },
+ maximum: &self.center + Vec3 { x: self.radius, y: self.radius, z: self.radius },
+ })
+ }
+}
diff --git a/src/hittable/triangle.rs b/src/hittable/triangle.rs
new file mode 100644
index 0000000..0fe5fc4
--- /dev/null
+++ b/src/hittable/triangle.rs
@@ -0,0 +1,79 @@
+use std::sync::Arc;
+
+use crate::{hittable::{HitRecord, Hittable, AABB}, material::Material, ray::Ray, vec3::{Point3, Vec3}};
+
+pub struct Triangle {
+ pub v0: Point3,
+ pub v1: Point3,
+ pub v2: Point3,
+ pub material: Arc<dyn Material>,
+ pub custom_normal: Option<Vec3>,
+}
+
+impl Triangle {
+ fn has_vertex_at_infinity(&self) -> bool {
+ self.v0.has_infinite_member() || self.v1.has_infinite_member() || self.v2.has_infinite_member()
+ }
+}
+
+impl Hittable for Triangle {
+ // https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm
+ fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+ let epsilon: f64 = 0.0000001;
+ let edge1 = &self.v1 - &self.v0;
+ let edge2 = &self.v2 - &self.v0;
+ let h = ray.direction.cross(&edge2);
+ let a = edge1.dot(&h);
+
+ if a > -epsilon && a < epsilon {
+ return None; // This ray is parallel to the triangle.
+ }
+ let f = 1.0 / a;
+ let s = &ray.origin - &self.v0;
+ let u = f * s.dot(&h);
+ if u < 0.0 || u > 1.0 {
+ return None;
+ }
+ let q = s.cross(&edge1);
+ let v = f * ray.direction.dot(&q);
+ if v < 0.0 || u + v > 1.0 {
+ return None;
+ }
+ // At this point, we can compute the point of intersection.
+ let t = f * edge2.dot(&q);
+ if t < t_min || t > t_max {
+ return None;
+ }
+ let mut hit_record = HitRecord::new();
+ hit_record.u = u;
+ hit_record.v = v;
+ hit_record.t = t;
+ hit_record.p = ray.at(t);
+ // TODO: i don't love this, but it allows for custom surface normals from OBJ data.
+ if let Some(normal) = &self.custom_normal {
+ hit_record.set_face_normal(ray, &normal);
+ } else {
+ let outward_normal = edge2.cross(&edge1).unit_vector();
+ hit_record.set_face_normal(ray, &outward_normal);
+ }
+ hit_record.material = Some(self.material.clone());
+ Some(hit_record)
+ }
+ fn bounding_box(&self, _: f64, _: f64) -> Option<AABB> {
+ match self.has_vertex_at_infinity() {
+ true => None,
+ false => Some(AABB {
+ minimum: Point3 {
+ x: self.v0.x.min(self.v1.x).min(self.v2.x) - 0.0001,
+ y: self.v0.y.min(self.v1.y).min(self.v2.y) - 0.0001,
+ z: self.v0.z.min(self.v1.z).min(self.v2.z) - 0.0001,
+ },
+ maximum: Point3 {
+ x: self.v0.x.max(self.v1.x).max(self.v2.x) + 0.0001,
+ y: self.v0.y.max(self.v1.y).max(self.v2.y) + 0.0001,
+ z: self.v0.z.max(self.v1.z).max(self.v2.z) + 0.0001,
+ },
+ })
+ }
+ }
+}
diff --git a/src/hittable/xy_rect.rs b/src/hittable/xy_rect.rs
new file mode 100644
index 0000000..8421bae
--- /dev/null
+++ b/src/hittable/xy_rect.rs
@@ -0,0 +1,48 @@
+use std::sync::Arc;
+
+use crate::{hittable::{HitRecord, Hittable, AABB}, material::Material, ray::Ray, vec3::{Point3, Vec3}};
+
+pub struct XYRect {
+ pub material: Arc<dyn Material>,
+ pub x0: f64,
+ pub x1: f64,
+ pub y0: f64,
+ pub y1: f64,
+ pub k: f64,
+}
+
+impl XYRect {
+ fn has_infinite_bounds(&self) -> bool {
+ self.x0.is_infinite() || self.x1.is_infinite() || self.y0.is_infinite() || self.y1.is_infinite()
+ }
+}
+
+impl Hittable for XYRect {
+ fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+ let t = (self.k - ray.origin.z) / ray.direction.z;
+ if t < t_min || t > t_max {
+ return None;
+ }
+ let x = ray.origin.x + t * ray.direction.x;
+ let y = ray.origin.y + t * ray.direction.y;
+ if x < self.x0 || x > self.x1 || y < self.y0 || y > self.y1 {
+ return None;
+ }
+ let mut hit_record = HitRecord::new();
+ hit_record.u = (x - self.x0) / (self.x1 - self.x0);
+ hit_record.v = (y - self.y0) / (self.y1 - self.y0);
+ hit_record.t = t;
+ let outward_normal = Vec3 { x: 0.0, y: 0.0, z: 1.0 };
+ hit_record.set_face_normal(ray, &outward_normal);
+ hit_record.material = Some(self.material.clone());
+ hit_record.p = ray.at(t);
+ Some(hit_record)
+ }
+
+ fn bounding_box(&self, _: f64, _: f64) -> Option<AABB> {
+ match self.has_infinite_bounds() {
+ true => None,
+ false => Some(AABB { minimum: Point3 { x: self.x0, y: self.y0, z: self.k - 0.0001 }, maximum: Point3 { x: self.x1, y: self.y1, z: self.k + 0.0001 } }),
+ }
+ }
+}
diff --git a/src/hittable/xz_rect.rs b/src/hittable/xz_rect.rs
new file mode 100644
index 0000000..4761f36
--- /dev/null
+++ b/src/hittable/xz_rect.rs
@@ -0,0 +1,48 @@
+use std::sync::Arc;
+
+use crate::{hittable::{HitRecord, Hittable, AABB}, material::Material, ray::Ray, vec3::{Point3, Vec3}};
+
+pub struct XZRect {
+ pub material: Arc<dyn Material>,
+ pub x0: f64,
+ pub x1: f64,
+ pub z0: f64,
+ pub z1: f64,
+ pub k: f64,
+}
+
+impl XZRect {
+ fn has_infinite_bounds(&self) -> bool {
+ self.x0.is_infinite() || self.x1.is_infinite() || self.z0.is_infinite() || self.z1.is_infinite()
+ }
+}
+
+impl Hittable for XZRect {
+ fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+ let t = (self.k - ray.origin.y) / ray.direction.y;
+ if t < t_min || t > t_max {
+ return None;
+ }
+ let x = ray.origin.x + t * ray.direction.x;
+ let z = ray.origin.z + t * ray.direction.z;
+ if x < self.x0 || x > self.x1 || z < self.z0 || z > self.z1 {
+ return None;
+ }
+ let mut hit_record = HitRecord::new();
+ hit_record.u = (x - self.x0) / (self.x1 - self.x0);
+ hit_record.v = (z - self.z0) / (self.z1 - self.z0);
+ hit_record.t = t;
+ let outward_normal = Vec3 { x: 0.0, y: 1.0, z: 0.0 };
+ hit_record.set_face_normal(ray, &outward_normal);
+ hit_record.material = Some(self.material.clone());
+ hit_record.p = ray.at(t);
+ Some(hit_record)
+ }
+
+ fn bounding_box(&self, _: f64, _: f64) -> Option<AABB> {
+ match self.has_infinite_bounds() {
+ true => None,
+ false => Some(AABB { minimum: Point3 { x: self.x0, y: self.k - 0.0001, z: self.z0 }, maximum: Point3 { x: self.x1, y: self.k + 0.0001, z: self.z1 } }),
+ }
+ }
+}
diff --git a/src/hittable/yz_rect.rs b/src/hittable/yz_rect.rs
new file mode 100644
index 0000000..dd90bdb
--- /dev/null
+++ b/src/hittable/yz_rect.rs
@@ -0,0 +1,48 @@
+use std::sync::Arc;
+
+use crate::{hittable::{HitRecord, Hittable, AABB}, material::Material, ray::Ray, vec3::{Point3, Vec3}};
+
+pub struct YZRect {
+ pub material: Arc<dyn Material>,
+ pub y0: f64,
+ pub y1: f64,
+ pub z0: f64,
+ pub z1: f64,
+ pub k: f64,
+}
+
+impl YZRect {
+ fn has_infinite_bounds(&self) -> bool {
+ self.y0.is_infinite() || self.y1.is_infinite() || self.z0.is_infinite() || self.z1.is_infinite()
+ }
+}
+
+impl Hittable for YZRect {
+ fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+ let t = (self.k - ray.origin.x) / ray.direction.x;
+ if t < t_min || t > t_max {
+ return None;
+ }
+ let y = ray.origin.y + t * ray.direction.y;
+ let z = ray.origin.z + t * ray.direction.z;
+ if y < self.y0 || y > self.y1 || z < self.z0 || z > self.z1 {
+ return None;
+ }
+ let mut hit_record = HitRecord::new();
+ hit_record.u = (y - self.y0) / (self.y1 - self.y0);
+ hit_record.v = (z - self.z0) / (self.z1 - self.z0);
+ hit_record.t = t;
+ let outward_normal = Vec3 { x: 1.0, y: 0.0, z: 0.0 };
+ hit_record.set_face_normal(ray, &outward_normal);
+ hit_record.material = Some(self.material.clone());
+ hit_record.p = ray.at(t);
+ Some(hit_record)
+ }
+
+ fn bounding_box(&self, _: f64, _: f64) -> Option<AABB> {
+ match self.has_infinite_bounds() {
+ true => None,
+ false => Some(AABB { minimum: Point3 { x: self.k - 0.0001, y: self.y0, z: self.z0 }, maximum: Point3 { x: self.k + 0.0001, y: self.y1, z: self.z1 } }),
+ }
+ }
+}