From 78ddaff5855bf8446adef9e18eb0d7b7ddcee52a Mon Sep 17 00:00:00 2001 From: lamp Date: Sun, 5 Mar 2023 21:45:56 +0000 Subject: init --- src/hittable/aabb.rs | 46 +++++++++ src/hittable/bvh_node.rs | 90 +++++++++++++++++ src/hittable/constant_medium.rs | 65 ++++++++++++ src/hittable/hittable_box.rs | 40 ++++++++ src/hittable/hittable_list.rs | 52 ++++++++++ src/hittable/instance/mod.rs | 10 ++ src/hittable/instance/moving.rs | 40 ++++++++ src/hittable/instance/rotate_x.rs | 83 +++++++++++++++ src/hittable/instance/rotate_y.rs | 83 +++++++++++++++ src/hittable/instance/rotate_z.rs | 83 +++++++++++++++ src/hittable/instance/translate.rs | 27 +++++ src/hittable/mod.rs | 76 ++++++++++++++ src/hittable/model.rs | 200 +++++++++++++++++++++++++++++++++++++ src/hittable/sphere.rs | 61 +++++++++++ src/hittable/triangle.rs | 79 +++++++++++++++ src/hittable/xy_rect.rs | 48 +++++++++ src/hittable/xz_rect.rs | 48 +++++++++ src/hittable/yz_rect.rs | 48 +++++++++ 18 files changed, 1179 insertions(+) create mode 100644 src/hittable/aabb.rs create mode 100644 src/hittable/bvh_node.rs create mode 100644 src/hittable/constant_medium.rs create mode 100644 src/hittable/hittable_box.rs create mode 100644 src/hittable/hittable_list.rs create mode 100644 src/hittable/instance/mod.rs create mode 100644 src/hittable/instance/moving.rs create mode 100644 src/hittable/instance/rotate_x.rs create mode 100644 src/hittable/instance/rotate_y.rs create mode 100644 src/hittable/instance/rotate_z.rs create mode 100644 src/hittable/instance/translate.rs create mode 100644 src/hittable/mod.rs create mode 100644 src/hittable/model.rs create mode 100644 src/hittable/sphere.rs create mode 100644 src/hittable/triangle.rs create mode 100644 src/hittable/xy_rect.rs create mode 100644 src/hittable/xz_rect.rs create mode 100644 src/hittable/yz_rect.rs (limited to 'src/hittable') 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, + right: Arc, + 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>, start: usize, end: usize, time_start: f64, time_end: f64) -> BVHNode { + let mut objects = src_objects.clone(); + let comparator = [ + |a: &Arc, b: &Arc| Self::box_compare(a.clone(), b.clone(), Axis::X), + |a: &Arc, b: &Arc| Self::box_compare(a.clone(), b.clone(), Axis::Y), + |a: &Arc, b: &Arc| 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, + Arc::new(BVHNode::from_objects(&objects, mid, end, time_start, time_end)) as Arc) + + } + }; + 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, b: Arc, 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 { + 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 { + 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, + phase_function: Arc, + neg_inv_density: f64, +} + +impl ConstantMedium { + pub fn new(boundary: Arc, density: f64, texture: Arc) -> 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 { + 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::().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 { + 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) -> 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 { + self.sides.hit(ray, t_min, t_max) + } + + fn bounding_box(&self, _: f64, _: f64) -> Option { + 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>, +} + +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) { + self.objects.push(object); + } +} + +impl Hittable for HittableList { + fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option { + 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 { + let mut output_box: Option = 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, + 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 { + 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 { + 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, + sin_theta: f64, + cos_theta: f64, + aabb: Option, +} + +impl RotateX { + pub fn new(hittable: Arc, 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 { + 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 { + 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, + sin_theta: f64, + cos_theta: f64, + aabb: Option, +} + +impl RotateY { + pub fn new(hittable: Arc, 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 { + 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 { + 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, + sin_theta: f64, + cos_theta: f64, + aabb: Option, +} + +impl RotateZ { + pub fn new(hittable: Arc, 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 { + 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 { + 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, + pub offset: Vec3, +} + +impl Hittable for Translate { + fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option { + 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 { + 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>, + 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; + fn bounding_box(&self, time_start: f64, time_end: f64) -> Option; +} 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, Option)> { + let mut triplet_iter = triplet.split("/"); + Some((triplet_iter.next()?.parse::().ok()?, triplet_iter.next().and_then(|val| val.parse::().ok()) , triplet_iter.next().and_then(|val| val.parse::().ok()))) + } + pub fn from_obj(obj_data: &str, material: Arc) -> Self { + let mut geometric_vertices: Vec = Vec::new(); + let mut texture_vertices: Vec<(f64, f64)> = Vec::new(); + let mut vertex_normals: Vec = Vec::new(); + // no free-form objects, so no parameter-space vertices! + let mut faces: Vec> = 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::() { + 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::() { + 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::() { + 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::() { + 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::() { + 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, Option)> = 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 { + self.faces.hit(ray, t_min, t_max) + } + + fn bounding_box(&self, time_start: f64, time_end: f64) -> Option { + 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, +} + +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 { + 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 { + 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, + pub custom_normal: Option, +} + +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 { + 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 { + 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, + 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 { + 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 { + 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, + 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 { + 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 { + 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, + 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 { + 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 { + 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 } }), + } + } +} -- cgit v1.2.3