diff options
Diffstat (limited to 'src')
37 files changed, 2327 insertions, 0 deletions
diff --git a/src/camera.rs b/src/camera.rs new file mode 100644 index 0000000..55bf387 --- /dev/null +++ b/src/camera.rs @@ -0,0 +1,58 @@ +use crate::ray::Ray; +use crate::vec3::{Point3, Vec3}; +use crate::util::degrees_to_radians; + +pub struct Camera { + origin: Point3, + lower_left_corner: Point3, + horizontal: Vec3, + vertical: Vec3, + w: Vec3, + u: Vec3, + v: Vec3, + lens_radius: f64, + time_start: f64, + time_end: f64, +} + +impl Camera { + pub fn new(lookfrom: Point3, lookat: Point3, vup: Vec3, vfov: f64, aspect_ratio: f64, aperture: f64, focus_dist: f64, time_start: f64, time_end: f64) -> Camera { + let theta = degrees_to_radians(vfov); + let h = (theta / 2.0).tan(); + let viewport_height = 2.0 * h; + let viewport_width = aspect_ratio * viewport_height; + + let w = (&lookfrom - &lookat).unit_vector(); + let u = vup.cross(&w).unit_vector(); + let v = w.cross(&u); + + let origin = lookfrom; + let horizontal = focus_dist * viewport_width * &u; + let vertical = focus_dist * viewport_height * &v; + Camera { + lower_left_corner: &origin + - &horizontal / 2.0 + - &vertical / 2.0 + - focus_dist * &w, + origin, + horizontal, + vertical, + w, + u, + v, + lens_radius: aperture / 2.0, + time_start, + time_end, + } + } + + pub fn get_ray(&self, s: f64, t: f64) -> Ray { + let rd = self.lens_radius * Vec3::random_in_unit_disk(); + let offset = &self.u * rd.x + &self.v * rd.y; + Ray { + origin: &self.origin + &offset, + direction: &self.lower_left_corner + s * &self.horizontal + t * &self.vertical - &self.origin - &offset, + time: self.time_start + (self.time_end - self.time_start) * rand::random::<f64>(), + } + } +} 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 } }), + } + } +} diff --git a/src/image.rs b/src/image.rs new file mode 100644 index 0000000..e24c14b --- /dev/null +++ b/src/image.rs @@ -0,0 +1,77 @@ +use std::io::Write; + +use crate::vec3::Color; + +pub struct Image { + width: usize, + height: usize, + data: Vec<Pixel>, +} + +#[derive(Clone)] +struct Pixel { + color: Color, + sample_count: u32, +} + +impl Image { + pub fn new(width: usize, height: usize) -> Image { + let data = vec![ + Pixel { + color: Color { + x: 0.0, + y: 0.0, + z: 0.0 + }, + sample_count: 0 + }; + width * height + ]; + Image { + width, + height, + data, + } + } + + pub fn add_sample(&mut self, x: usize, y: usize, color: Color) { + self.data + .get_mut((y * self.width) + x) + .unwrap() + .update(color); + } + + pub fn write(&self, output: &mut impl Write) { + output.write_fmt(format_args!("P3\n{} {}\n255\n", self.width, self.height)).unwrap(); + for y in (0..self.height).rev() { + for x in 0..self.width { + let pixel = self.data.get((y * self.width) + x).unwrap(); + let mut r = pixel.color.x; + let mut g = pixel.color.y; + let mut b = pixel.color.z; + + // Divide by the number of samples and perform gamma correction for gamma 2 + let scale = 1.0 / pixel.sample_count as f64; + r = (r * scale).sqrt(); + g = (g * scale).sqrt(); + b = (b * scale).sqrt(); + + output + .write_fmt(format_args!( + "{} {} {}\n", + (256.0 * r.clamp(0.0, 0.999)) as u32, + (256.0 * g.clamp(0.0, 0.999)) as u32, + (256.0 * b.clamp(0.0, 0.999)) as u32, + )) + .unwrap(); + } + } + } +} + +impl Pixel { + pub fn update(&mut self, color: Color) { + self.color += color; + self.sample_count += 1; + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..7ec82e6 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,115 @@ +mod camera; +mod hittable; +mod material; +mod ray; +mod util; +mod vec3; +mod image; +mod texture; +mod scenes; + +use std::{sync::{Arc, mpsc}, thread}; + +use camera::Camera; +use hittable::Hittable; +use image::Image; +use ray::Ray; +use vec3::{Vec3, Color}; +use scenes::get_scene; + +struct PixelUpdate { + color: Color, + x: usize, + y: usize, +} + +fn ray_color(ray: &Ray, background: &Color, world: &dyn Hittable, depth: u32) -> Color { + if depth <= 0 { + return Color { + x: 0.0, + y: 0.0, + z: 0.0, + }; + } + match world.hit(ray, 0.001, f64::INFINITY) { + None => background.clone(), + Some(rec) => { + let mut scattered = Ray::new(); + let mut attenuation = Color::new(); + if let Some(material) = &rec.material { + let emitted = material.emitted(rec.u, rec.v, &rec.p); + if !material.scatter(&ray, &rec, &mut attenuation, &mut scattered) { + emitted + } else { + emitted + attenuation * ray_color(&scattered, background, world, depth - 1) + } + } else { + Color { x: 0.0, y: 0.0, z: 0.0 } + } + }, + } +} + +fn render(image_width: u32, image_height: u32, samples_per_pixel: u32, max_depth: u32, world: Arc<dyn Hittable>, background: Color, camera: Arc<Camera>, tx: mpsc::Sender<PixelUpdate>) { + for j in (0..image_height).rev() { + for i in 0..image_width { + for _ in 0..samples_per_pixel { + let u = ((i as f64) + rand::random::<f64>()) / ((image_width - 1) as f64); + let v = ((j as f64) + rand::random::<f64>()) / ((image_height - 1) as f64); + let ray = camera.get_ray(u, v); + + tx.send(PixelUpdate { color: ray_color(&ray, &background, world.as_ref(), max_depth), x: i as usize, y: j as usize}).unwrap(); + } + } + } +} + +fn main() { + // Image + const ASPECT_RATIO: f64 = 16.0 / 9.0; + //const ASPECT_RATIO: f64 = 1.0; + const IMAGE_WIDTH: u32 = 600; + const IMAGE_HEIGHT: u32 = (IMAGE_WIDTH as f64 / ASPECT_RATIO) as u32; + const SAMPLES_PER_PIXEL: u32 = 30; + const MAX_DEPTH: u32 = 50; + const THREAD_COUNT: u32 = 8; + const TIME_START: f64 = 0.0; + const TIME_END: f64 = 1.0; + // World + let (world, lookfrom, lookat, vfov, aperture, background) = get_scene(std::env::args().nth(1).unwrap_or("0".to_string()).trim().parse().unwrap_or(0)); + + // Camera + let vup = Vec3 { x: 0.0, y: 1.0, z: 0.0 }; + let dist_to_focus = 10.0; + let cam = Arc::new(Camera::new(lookfrom, lookat, vup, vfov, ASPECT_RATIO, aperture, dist_to_focus, TIME_START, TIME_END)); + // Render + let mut final_image = Image::new(IMAGE_WIDTH as usize, IMAGE_HEIGHT as usize); + let (tx, rx) = mpsc::channel::<PixelUpdate>(); + for _ in 0..THREAD_COUNT { + let sender = tx.clone(); + let world_ref = world.clone(); + let camera_ref = cam.clone(); + let background_clone = background.clone(); + thread::spawn( || { + render(IMAGE_WIDTH, IMAGE_HEIGHT, SAMPLES_PER_PIXEL / THREAD_COUNT, MAX_DEPTH, world_ref, background_clone, camera_ref, sender); + }); + } + let expected_updates: u64 = (SAMPLES_PER_PIXEL / THREAD_COUNT) as u64 * THREAD_COUNT as u64 * IMAGE_HEIGHT as u64 * IMAGE_WIDTH as u64; + let print_frequency: u64 = (SAMPLES_PER_PIXEL / THREAD_COUNT) as u64 * THREAD_COUNT as u64 * IMAGE_WIDTH as u64; + let mut update_count: u64 = 0; + loop { + if let Ok(update) = rx.try_recv() { + update_count += 1; + final_image.add_sample(update.x, update.y, update.color); + if update_count % print_frequency == 0 { + eprint!("\rCurrent completion: {:.2}%", (update_count as f64 / expected_updates as f64) * 100.0) + } + } else { + if Arc::strong_count(&world) == 1 { + break + } + } + } + final_image.write(&mut std::io::stdout()); + eprintln!("\nDone."); +} diff --git a/src/material/dielectric.rs b/src/material/dielectric.rs new file mode 100644 index 0000000..bcacb78 --- /dev/null +++ b/src/material/dielectric.rs @@ -0,0 +1,39 @@ +use super::Material; +use crate::{hittable::HitRecord, vec3::Vec3}; +use crate::vec3::Color; +use crate::ray::Ray; + +pub struct Dielectric { + pub index_of_refraction: f64, +} + +impl Dielectric { + fn reflectance(cosine: f64, ref_idx: f64) -> f64 { + // Using Schlick's Approximation: + let mut r0 = (1.0 - ref_idx) / (1.0 + ref_idx); + r0 *= r0; + r0 + (1.0 - r0) * (1.0 - cosine).powi(5) + } +} + +impl Material for Dielectric { + fn scatter(&self, ray_in: &Ray, hit_record: &HitRecord, attenuation: &mut Color, scattered: &mut Ray) -> bool { + *attenuation = Color { x: 1.0, y: 1.0, z: 1.0 }; + let refraction_ratio = if hit_record.front_face { 1.0 / self.index_of_refraction } else { self.index_of_refraction }; + let unit_direction = ray_in.direction.unit_vector(); + let cos_theta = hit_record.normal.dot(&-&unit_direction).min(1.0); + let sin_theta = (1.0 - cos_theta * cos_theta).sqrt(); + + let cannot_refract = refraction_ratio * sin_theta > 1.0; + let direction: Vec3; + + if cannot_refract || Self::reflectance(cos_theta, refraction_ratio) > rand::random::<f64>() { + direction = unit_direction.reflect(&hit_record.normal) + } else { + direction = unit_direction.refract(&hit_record.normal, refraction_ratio) + } + + *scattered = Ray { origin: hit_record.p.clone(), direction, time: ray_in.time }; + true + } +} diff --git a/src/material/diffuse_light.rs b/src/material/diffuse_light.rs new file mode 100644 index 0000000..fecbcff --- /dev/null +++ b/src/material/diffuse_light.rs @@ -0,0 +1,29 @@ +use std::sync::Arc; + +use super::Material; +use crate::{hittable::HitRecord, texture::Texture, vec3::Point3}; +use crate::vec3::Color; +use crate::texture::SolidColor; +use crate::ray::Ray; + +pub struct DiffuseLight { + emit: Arc<dyn Texture>, +} + +impl DiffuseLight { + pub fn from_color(color: Color) -> Self { + Self { + emit: Arc::new(SolidColor::from_color(color)), + } + } +} + +impl Material for DiffuseLight { + fn scatter(&self, _: &Ray, _: &HitRecord, _: &mut Color, _: &mut Ray) -> bool { + false + } + + fn emitted(&self, u: f64, v: f64, point: &Point3) -> Color { + self.emit.value(u, v, point) + } +} diff --git a/src/material/isotropic.rs b/src/material/isotropic.rs new file mode 100644 index 0000000..f59fa7d --- /dev/null +++ b/src/material/isotropic.rs @@ -0,0 +1,33 @@ +use std::sync::Arc; + +use super::Material; +use crate::{hittable::HitRecord, texture::Texture, vec3::Vec3}; +use crate::vec3::Color; +use crate::texture::SolidColor; +use crate::ray::Ray; + +pub struct Isotropic { + albedo: Arc<dyn Texture>, +} + +impl Isotropic { + pub fn from_color(color: Color) -> Self { + Self { + albedo: Arc::new(SolidColor::from_color(color)), + } + } + + pub fn from_texture(texture: Arc<dyn Texture>) -> Self { + Self { + albedo: texture, + } + } +} + +impl Material for Isotropic { + fn scatter(&self, ray_in: &Ray, hit_record: &HitRecord, attenuation: &mut Color, scattered: &mut Ray) -> bool { + *scattered = Ray { origin: hit_record.p.clone(), direction: Vec3::random_in_unit_sphere(), time: ray_in.time }; + *attenuation = self.albedo.value(hit_record.u, hit_record.v, &hit_record.p); + true + } +} diff --git a/src/material/lambertian.rs b/src/material/lambertian.rs new file mode 100644 index 0000000..95f698e --- /dev/null +++ b/src/material/lambertian.rs @@ -0,0 +1,34 @@ +use std::sync::Arc; + +use super::Material; +use crate::{hittable::HitRecord, texture::Texture, vec3::Vec3}; +use crate::vec3::Color; +use crate::texture::SolidColor; +use crate::ray::Ray; + +pub struct Lambertian { + pub albedo: Arc<dyn Texture>, +} + +impl Lambertian { + pub fn from_color(color: Color) -> Self { + Self { + albedo: Arc::new(SolidColor { color_value: color.clone() }), + } + } +} + +impl Material for Lambertian { + fn scatter(&self, ray_in : &Ray, hit_record: &HitRecord, attenuation: &mut Color, scattered: &mut Ray) -> bool { + let mut scatter_direction = &hit_record.normal + Vec3::random_unit_vector(); + + // Catch zero-vector scatter directions that will generate issues later + if scatter_direction.near_zero() { + scatter_direction = hit_record.normal.clone(); + } + + *scattered = Ray { origin: hit_record.p.clone(), direction: scatter_direction, time: ray_in.time }; + *attenuation = self.albedo.value(hit_record.u, hit_record.v, &hit_record.p); + true + } +} diff --git a/src/material/metal.rs b/src/material/metal.rs new file mode 100644 index 0000000..2865a2f --- /dev/null +++ b/src/material/metal.rs @@ -0,0 +1,18 @@ +use super::Material; +use crate::{hittable::HitRecord, vec3::Vec3}; +use crate::vec3::Color; +use crate::ray::Ray; + +pub struct Metal { + pub albedo: Color, + pub fuzz: f64, // TODO: This should have value 0.0 - 1.0; this is not enforced +} + +impl Material for Metal { + fn scatter(&self, ray_in: &Ray, hit_record: &HitRecord, attenuation: &mut Color, scattered: &mut Ray) -> bool { + let reflected = ray_in.direction.unit_vector().reflect(&hit_record.normal); + *scattered = Ray { origin: hit_record.p.clone(), direction: reflected + self.fuzz * Vec3::random_in_unit_sphere(), time: ray_in.time }; + *attenuation = self.albedo.clone(); + scattered.direction.dot(&hit_record.normal) > 0.0 + } +} diff --git a/src/material/mod.rs b/src/material/mod.rs new file mode 100644 index 0000000..bf1eb24 --- /dev/null +++ b/src/material/mod.rs @@ -0,0 +1,21 @@ +mod lambertian; +pub use lambertian::Lambertian; +mod metal; +pub use metal::Metal; +mod dielectric; +pub use dielectric::Dielectric; +mod diffuse_light; +pub use diffuse_light::DiffuseLight; +mod isotropic; +pub use isotropic::Isotropic; + +use crate::{hittable::HitRecord, vec3::Point3}; +use crate::vec3::Color; +use crate::ray::Ray; + +pub trait Material: Send + Sync { + fn emitted(&self, _: f64, _: f64, _: &Point3) -> Color { + Color { x: 0.0, y: 0.0, z: 0.0 } + } + fn scatter(&self, ray_in: &Ray, hit_record: &HitRecord, attenuation: &mut Color, scattered: &mut Ray) -> bool; +} diff --git a/src/ray.rs b/src/ray.rs new file mode 100644 index 0000000..0a4fc3c --- /dev/null +++ b/src/ray.rs @@ -0,0 +1,21 @@ +use crate::vec3::{Point3, Vec3}; + +pub struct Ray { + pub origin: Point3, + pub direction: Vec3, + pub time: f64, +} + +impl Ray { + pub fn new() -> Ray { + Ray { + origin: Point3 { x: 0.0, y: 0.0, z: 0.0 }, + direction: Vec3 { x: 0.0, y:0.0, z: 0.0}, + time: 0.0 + } + } + + pub fn at(&self, t: f64) -> Point3 { + &self.origin + t * &self.direction + } +} diff --git a/src/scenes.rs b/src/scenes.rs new file mode 100644 index 0000000..7cdce86 --- /dev/null +++ b/src/scenes.rs @@ -0,0 +1,252 @@ +use std::sync::Arc; + +use crate::hittable::{ConstantMedium, Hittable}; +use crate::hittable::HittableBox; +use crate::hittable::HittableList; +use crate::material::{Dielectric, DiffuseLight, Lambertian, Material, Metal}; +use crate::hittable::instance::RotateY; +use crate::hittable::instance::RotateX; +use crate::hittable::instance::RotateZ; +use crate::hittable::Sphere; +use crate::hittable::instance::Translate; +use crate::vec3::{Point3, Vec3, Color}; +use crate::hittable::BVHNode; +use crate::texture::{CheckerTexture, ImageTexture, NoiseTexture, SolidColor}; +use crate::hittable::XYRect; +use crate::hittable::XZRect; +use crate::hittable::YZRect; +use crate::hittable::Triangle; +use crate::hittable::Model; +use crate::hittable::instance::Moving; + +pub fn get_scene(id: u32) -> (Arc<dyn Hittable>, Point3, Point3, f64, f64, Color) { + match id { + 2 => two_spheres(), + 3 => two_perlin_spheres(), + 4 => earth(), + 5 => simple_light(), + 6 => cornell_box(), + 7 => cornell_smoke(), + 8 => final_scene(), + 9 => test_scene(), + 10 => triangle_scene(), + _ => random_scene(), + } +} + +fn random_scene() -> (Arc<dyn Hittable>, Point3, Point3, f64, f64, Color) { + let mut world = HittableList::new(); + + let checker = Arc::new(CheckerTexture::from_colors(Color { x: 0.2, y: 0.3, z: 0.1 } , Color { x: 0.9, y: 0.9, z: 0.9 })); + let ground_material = Arc::new(Lambertian { albedo: checker }); + world.add(Arc::new(Sphere{ center: Point3 { x: 0.0, y: -1000.0, z: 0.0 }, radius: 1000.0, material: ground_material })); + + for a in -11..11 { + for b in -11..11 { + let choose_mat = rand::random::<f64>(); + let center = Point3 { x: (a as f64) + 0.9 * rand::random::<f64>(), y: 0.2, z: (b as f64) + 0.9 * rand::random::<f64>() }; + + if (¢er - Point3 { x: 4.0, y: 0.3, z: 0.0 }).length() > 0.9 { + let sphere_material: Arc<dyn Material>; + + if choose_mat < 0.8 { + let albedo = Arc::new(SolidColor::from_color(Color::random() * Color::random())); + sphere_material = Arc::new(Lambertian { albedo }); + world.add(Arc::new(Sphere { center, radius: 0.2, material: sphere_material })); + } else if choose_mat < 0.95 { + let albedo = Color::random_in_range(0.5, 1.0); + let fuzz = rand::random::<f64>() / 2.0; + sphere_material = Arc::new(Metal { albedo, fuzz }); + world.add(Arc::new(Sphere { center, radius: 0.2, material: sphere_material })); + } else { + sphere_material = Arc::new(Dielectric { index_of_refraction: 1.5 }); + world.add(Arc::new(Sphere { center, radius: 0.2, material: sphere_material })); + } + } + } + } + + let material1 = Arc::new(Dielectric { index_of_refraction: 1.5 }); + world.add(Arc::new(Sphere { center: Point3 { x: 0.0, y: 1.0, z: 0.0 }, radius: 1.0, material: material1 })); + + let material2 = Arc::new(Lambertian { albedo: Arc::new(SolidColor::from_color(Color { x: 0.4, y: 0.2, z: 0.1 })) }); + world.add(Arc::new(Sphere { center: Point3 { x: -4.0, y: 1.0, z: 0.0 }, radius: 1.0, material: material2 })); + + let material3 = Arc::new(Metal { albedo: Color { x: 0.7, y: 0.6, z: 0.5 }, fuzz: 0.0 }); + world.add(Arc::new(Sphere { center: Point3 { x: 4.0, y: 1.0, z: 0.0 }, radius: 1.0, material: material3 })); + + (Arc::new(BVHNode::new(&world, 0.0, 1.0)), Point3 { x: 13.0, y: 2.0, z: 3.0}, Point3::new(), 20.0, 0.1, Color { x: 0.7, y: 0.8, z: 1.0 }) +} + +fn two_spheres() -> (Arc<dyn Hittable>, Point3, Point3, f64, f64, Color) { + let mut objects = HittableList::new(); + let checker = Arc::new(Lambertian { albedo: Arc::new(CheckerTexture::from_colors(Color { x: 0.2, y: 0.3, z: 0.1 } , Color { x: 0.9, y: 0.9, z: 0.9 })) }); + objects.add(Arc::new(Sphere { center: Point3 { x: 0.0, y: -10.0, z: 0.0 }, radius: 10.0, material: checker.clone() })); + objects.add(Arc::new(Sphere { center: Point3 { x: 0.0, y: 10.0, z: 0.0 }, radius: 10.0, material: checker })); + (Arc::new(BVHNode::new(&objects, 0.0, 1.0)), Point3 { x: 13.0, y: 2.0, z: 3.0}, Point3::new(), 20.0, 0.0, Color { x: 0.7, y: 0.8, z: 1.0 }) +} + +fn two_perlin_spheres() -> (Arc<dyn Hittable>, Point3, Point3, f64, f64, Color) { + let mut objects = HittableList::new(); + let pertext = Arc::new(Lambertian { albedo: Arc::new(NoiseTexture::new(4.0)) }); + objects.add(Arc::new(Sphere { center: Point3 { x: 0.0, y: -1000.0, z: 0.0 }, radius: 1000.0, material: pertext.clone() })); + objects.add(Arc::new(Sphere { center: Point3 { x: 0.0, y: 2.0, z: 0.0 }, radius: 2.0, material: pertext.clone() })); + (Arc::new(BVHNode::new(&objects, 0.0, 1.0)), Point3 { x: 13.0, y: 2.0, z: 3.0}, Point3::new(), 20.0, 0.0, Color { x: 0.7, y: 0.8, z: 1.0 }) +} + +fn earth() -> (Arc<dyn Hittable>, Point3, Point3, f64, f64, Color) { + let mut objects = HittableList::new(); + let earth_texture = Arc::new(ImageTexture::from_bmp_data(&include_bytes!("../res/earthmap.bmp").to_vec())); + let earth_surface = Arc::new(Lambertian { albedo: earth_texture }); + objects.add(Arc::new(Sphere { center: Point3 { x: 0.0, y: 0.0, z: 0.0 }, radius: 2.0, material: earth_surface })); + (Arc::new(BVHNode::new(&objects, 0.0, 1.0)), Point3 { x: 13.0, y: 2.0, z: 3.0}, Point3::new(), 20.0, 0.0, Color { x: 0.7, y: 0.8, z: 1.0 }) +} + +fn simple_light() -> (Arc<dyn Hittable>, Point3, Point3, f64, f64, Color) { + let mut objects = HittableList::new(); + let pertext = Arc::new(Lambertian { albedo: Arc::new(NoiseTexture::new(4.0)) }); + objects.add(Arc::new(Sphere { center: Point3 { x: 0.0, y: -1000.0, z: 0.0 }, radius: 1000.0, material: pertext.clone() })); + objects.add(Arc::new(Sphere { center: Point3 { x: 0.0, y: 2.0, z: 0.0 }, radius: 2.0, material: pertext.clone() })); + let diff_light = Arc::new(DiffuseLight::from_color(Color { x: 4.0, y: 4.0, z: 4.0 })); + objects.add(Arc::new(XYRect { material: diff_light, x0: 3.0, x1: 5.0, y0: 1.0, y1: 3.0, k: -2.0 })); + (Arc::new(BVHNode::new(&objects, 0.0, 1.0)), Point3 { x: 26.0, y: 3.0, z: 6.0}, Point3 { x: 0.0, y: 2.0, z: 0.0}, 20.0, 0.0, Color::new()) +} + +fn cornell_box() -> (Arc<dyn Hittable>, Point3, Point3, f64, f64, Color) { + let mut objects = HittableList::new(); + let red = Arc::new(Lambertian::from_color(Color { x: 0.65, y: 0.05, z: 0.05 })); + let white = Arc::new(Lambertian::from_color(Color { x: 0.73, y: 0.73, z: 0.73 })); + let green = Arc::new(Lambertian::from_color(Color { x: 0.12, y: 0.45, z: 0.15 })); + let light = Arc::new(DiffuseLight::from_color(Color { x: 15.0, y: 15.0, z: 15.0 })); + + objects.add(Arc::new(YZRect { material: green, y0: 0.0, y1: 555.0, z0: 0.0, z1: 555.0, k: 555.0 })); + objects.add(Arc::new(YZRect { material: red, y0: 0.0, y1: 555.0, z0: 0.0, z1: 555.0, k: 0.0 })); + objects.add(Arc::new(XZRect { material: light, x0: 213.0, x1: 343.0, z0: 227.0, z1: 332.0, k: 554.0 })); + objects.add(Arc::new(XZRect { material: white.clone(), x0: 0.0, x1: 555.0, z0: 0.0, z1: 555.0, k: 0.0 })); + objects.add(Arc::new(XZRect { material: white.clone(), x0: 0.0, x1: 555.0, z0: 0.0, z1: 555.0, k: 555.0 })); + objects.add(Arc::new(XYRect { material: white.clone(), x0: 0.0, x1: 555.0, y0: 0.0, y1: 555.0, k: 555.0 })); + + //objects.add(Arc::new(HittableBox::new(Point3 { x: 130.0, y: 0.0, z: 65.0 }, Point3 { x: 295.0, y: 165.0, z: 230.0 }, white.clone()))); + //objects.add(Arc::new(HittableBox::new(Point3 { x: 265.0, y: 0.0, z: 295.0 }, Point3 { x: 430.0, y: 330.0, z: 460.0 }, white.clone()))); + let box_1 = Arc::new(HittableBox::new(Point3 { x: 0.0, y: 0.0, z: 0.0 }, Point3 { x: 165.0, y: 330.0, z: 165.0 }, white.clone())); + let box_1 = Arc::new(RotateY::new(box_1, 15.0)); + let box_1 = Arc::new(Translate { hittable: box_1, offset: Point3 { x: 265.0, y: 0.0, z: 295.0 } }); + objects.add(box_1); + let box_2 = Arc::new(HittableBox::new(Point3 { x: 0.0, y: 0.0, z: 0.0 }, Point3 { x: 165.0, y: 165.0, z: 165.0 }, white.clone())); + let box_2 = Arc::new(RotateY::new(box_2, -18.0)); + let box_2 = Arc::new(Translate { hittable: box_2, offset: Point3 { x: 130.0, y: 0.0, z: 65.0 } }); + objects.add(box_2); + + (Arc::new(BVHNode::new(&objects, 0.0, 1.0)), Point3 { x: 278.0, y: 278.0, z: -800.0}, Point3 { x: 278.0, y: 278.0, z: 0.0}, 40.0, 0.0, Color::new()) +} + +fn cornell_smoke() -> (Arc<dyn Hittable>, Point3, Point3, f64, f64, Color) { + let mut objects = HittableList::new(); + let red = Arc::new(Lambertian::from_color(Color { x: 0.65, y: 0.05, z: 0.05 })); + let white = Arc::new(Lambertian::from_color(Color { x: 0.73, y: 0.73, z: 0.73 })); + let green = Arc::new(Lambertian::from_color(Color { x: 0.12, y: 0.45, z: 0.15 })); + let light = Arc::new(DiffuseLight::from_color(Color { x: 7.0, y: 7.0, z: 7.0 })); + + objects.add(Arc::new(YZRect { material: green, y0: 0.0, y1: 555.0, z0: 0.0, z1: 555.0, k: 555.0 })); + objects.add(Arc::new(YZRect { material: red, y0: 0.0, y1: 555.0, z0: 0.0, z1: 555.0, k: 0.0 })); + objects.add(Arc::new(XZRect { material: light, x0: 113.0, x1: 443.0, z0: 127.0, z1: 432.0, k: 554.0 })); + objects.add(Arc::new(XZRect { material: white.clone(), x0: 0.0, x1: 555.0, z0: 0.0, z1: 555.0, k: 0.0 })); + objects.add(Arc::new(XZRect { material: white.clone(), x0: 0.0, x1: 555.0, z0: 0.0, z1: 555.0, k: 555.0 })); + objects.add(Arc::new(XYRect { material: white.clone(), x0: 0.0, x1: 555.0, y0: 0.0, y1: 555.0, k: 555.0 })); + + //objects.add(Arc::new(HittableBox::new(Point3 { x: 130.0, y: 0.0, z: 65.0 }, Point3 { x: 295.0, y: 165.0, z: 230.0 }, white.clone()))); + //objects.add(Arc::new(HittableBox::new(Point3 { x: 265.0, y: 0.0, z: 295.0 }, Point3 { x: 430.0, y: 330.0, z: 460.0 }, white.clone()))); + let box_1 = Arc::new(HittableBox::new(Point3 { x: 0.0, y: 0.0, z: 0.0 }, Point3 { x: 165.0, y: 330.0, z: 165.0 }, white.clone())); + let box_1 = Arc::new(RotateY::new(box_1, 15.0)); + let box_1 = Arc::new(Translate { hittable: box_1, offset: Point3 { x: 265.0, y: 0.0, z: 295.0 } }); + let box_2 = Arc::new(HittableBox::new(Point3 { x: 0.0, y: 0.0, z: 0.0 }, Point3 { x: 165.0, y: 165.0, z: 165.0 }, white.clone())); + let box_2 = Arc::new(RotateY::new(box_2, -18.0)); + let box_2 = Arc::new(Translate { hittable: box_2, offset: Point3 { x: 130.0, y: 0.0, z: 65.0 } }); + + objects.add(Arc::new(ConstantMedium::new(box_1, 0.01, Arc::new(SolidColor::from_color(Color { x: 0.0, y: 0.0, z: 0.0 }))))); + objects.add(Arc::new(ConstantMedium::new(box_2, 0.01, Arc::new(SolidColor::from_color(Color { x: 1.0, y: 1.0, z: 1.0 }))))); + + (Arc::new(BVHNode::new(&objects, 0.0, 1.0)), Point3 { x: 278.0, y: 278.0, z: -800.0}, Point3 { x: 278.0, y: 278.0, z: 0.0}, 40.0, 0.0, Color::new()) +} + +fn final_scene() -> (Arc<dyn Hittable>, Point3, Point3, f64, f64, Color) { + let mut boxes_1 = HittableList::new(); + let ground = Arc::new(Lambertian::from_color(Color { x: 0.48, y: 0.83, z: 0.53 })); + + const BOXES_PER_SIDE: usize = 20; + for i in 0..BOXES_PER_SIDE { + for j in 0..BOXES_PER_SIDE { + let w = 100.0; + let x0 = -1000.0 + (i as f64) * w; + let z0 = -1000.0 + (j as f64) * w; + let y0 = 0.0; + let x1 = x0 + w; + let y1 = 1.0 + 100.0 * rand::random::<f64>(); + let z1 = z0 + w; + boxes_1.add(Arc::new(HittableBox::new(Point3 { x: x0, y: y0, z: z0 }, Point3 { x: x1, y: y1, z: z1 }, ground.clone()))); + } + } + + let mut objects = HittableList::new(); + objects.add(Arc::new(BVHNode::new(&boxes_1, 0.0, 1.0))); + + let light = Arc::new(DiffuseLight::from_color(Color { x: 7.0, y: 7.0, z: 7.0 })); + objects.add(Arc::new(XZRect { material: light.clone(), x0: 123.0, x1: 423.0, z0: 147.0, z1: 412.0, k: 554.0 })); + + let center_1 = Point3 { x: 400.0, y: 400.0, z: 200.0 }; + let center_2 = ¢er_1 + Vec3 { x: 30.0, y: 0.0, z: 0.0 }; + + let moving_sphere_material = Arc::new(Lambertian::from_color(Color { x: 0.7, y: 0.3, z: 0.1 })); + objects.add(Arc::new(Moving { hittable: Arc::new(Sphere { center: Point3::new(), radius: 50.0, material: moving_sphere_material }), offset_start: center_1, offset_end: center_2, time_start: 0.0, time_end: 1.0, })); + + objects.add(Arc::new(Sphere { center: Point3 { x: 260.0, y: 150.0, z: 45.0 }, radius: 50.0, material: Arc::new(Dielectric { index_of_refraction: 1.5 }) })); + objects.add(Arc::new(Sphere { center: Point3 { x: 0.0, y: 150.0, z: 145.0 }, radius: 50.0, material: Arc::new(Metal { albedo: Color { x: 0.8, y: 0.8, z: 0.9 }, fuzz: 1.0 }) })); + + let boundary = Arc::new(Sphere { center: Point3 { x: 360.0, y: 150.0, z: 145.0 }, radius: 70.0, material: Arc::new(Dielectric { index_of_refraction: 1.5 }) }); + objects.add(boundary.clone()); + objects.add(Arc::new(ConstantMedium::new(boundary.clone(), 0.2, Arc::new(SolidColor::from_color(Color { x: 0.2, y: 0.4, z: 0.9 }))))); + let boundary = Arc::new(Sphere { center: Point3 { x: 0.0, y: 0.0, z: 0.0 }, radius: 5000.0, material: Arc::new(Dielectric { index_of_refraction: 1.5 }) }); + objects.add(Arc::new(ConstantMedium::new(boundary.clone(), 0.0001, Arc::new(SolidColor::from_color(Color { x: 1.0, y: 1.0, z: 1.0 }))))); + + let emat = Arc::new(Lambertian { albedo: Arc::new(ImageTexture::from_bmp_data(&include_bytes!("../res/earthmap.bmp").to_vec())) }); + objects.add(Arc::new(Sphere { center: Point3 { x: 400.0, y: 200.0, z: 400.0 }, radius: 100.0, material: emat })); + let pertext = Arc::new(Lambertian { albedo: Arc::new(NoiseTexture::new(0.1)) }); + objects.add(Arc::new(Sphere { center: Point3 { x: 220.0, y: 280.0, z: 300.0 }, radius: 80.0, material: pertext })); + + let mut boxes_2 = HittableList::new(); + let white = Arc::new(Lambertian::from_color(Color { x: 0.73, y: 0.73, z: 0.73 })); + for _ in 0..1000 { + boxes_2.add(Arc::new(Sphere { center: Point3::random_in_range(0.0, 165.0), radius: 10.0, material: white.clone() })); + } + + objects.add(Arc::new(Translate { hittable: Arc::new(RotateY::new(Arc::new(BVHNode::new(&boxes_2, 0.0, 1.0)), 15.0)), offset: Vec3 { x: -100.0, y: 270.0, z: 395.0 } })); + (Arc::new(objects), Point3 { x: 478.0, y: 278.0, z: -600.0}, Point3 { x: 278.0, y: 278.0, z: 0.0}, 40.0, 0.0, Color::new()) +} + +fn test_scene() -> (Arc<dyn Hittable>, Point3, Point3, f64, f64, Color) { + let mut objects = HittableList::new(); + //let earth_texture = Arc::new(ImageTexture::from_bmp_data(&include_bytes!("../res/earthmap.bmp").to_vec())); + //let earth_surface = Arc::new(Lambertian { albedo: earth_texture }); + let pertext = Arc::new(Lambertian { albedo: Arc::new(NoiseTexture::new(4.0)) }); + objects.add(Arc::new(XZRect { material: pertext, x0: -f64::INFINITY, x1: f64::INFINITY, z0: -f64::INFINITY, z1: f64::INFINITY, k: 0.0 })); + //(Arc::new(BVHNode::new(&objects, 0.0, 1.0)), Point3 { x: 13.0, y: 2.0, z: 3.0}, Point3::new(), 20.0, 0.0, Color { x: 0.7, y: 0.8, z: 1.0 }) + (Arc::new(objects), Point3 { x: 13.0, y: 2.0, z: 3.0}, Point3::new(), 20.0, 0.0, Color { x: 0.7, y: 0.8, z: 1.0 }) +} + +fn triangle_scene() -> (Arc<dyn Hittable>, Point3, Point3, f64, f64, Color) { + let mut objects = HittableList::new(); + let checker = Arc::new(CheckerTexture::from_colors(Color { x: 0.2, y: 0.3, z: 0.1 } , Color { x: 0.9, y: 0.9, z: 0.9 })); + let ground_material = Arc::new(Lambertian { albedo: checker }); + objects.add(Arc::new(Sphere{ center: Point3 { x: 0.0, y: -1001.0, z: 0.0 }, radius: 1000.0, material: ground_material })); + + //let pertext = Arc::new(Lambertian { albedo: Arc::new(NoiseTexture::new(4.0)) }); + //let pertext = Arc::new(Lambertian { albedo: Arc::new(SolidColor::from_color(Color {x: 0.0, y: 0.0, z: 0.0}) ) }); + //objects.add(Arc::new(Triangle { material: Arc::new(Metal { albedo: Color { x: 0.7, y: 0.6, z: 0.5 }, fuzz: 0.0 }), v0 : Vec3::new(), v1: Vec3 { x: 0.0, y: 0.0, z: 1.0 }, v2: Vec3 { x: 0.0, y: 1.0, z: 0.5 } })); + //let monkey = Arc::new(Model::from_obj(include_str!("../res/monkey.obj"), Arc::new(Dielectric { index_of_refraction: 1.5 }))); + let monkey = Arc::new(Model::from_obj(include_str!("../res/monkey.obj"), Arc::new(Metal { albedo: Color { x: 0.7, y: 0.6, z: 0.5 }, fuzz: 0.5 }))); + let monkey = Arc::new(RotateX::new(monkey, 45.0)); + let monkey = Arc::new(RotateZ::new(monkey, 45.0)); + objects.add(monkey); + //(Arc::new(objects), Point3 { x: 5.0, y: 5.0, z: 5.0}, Point3::new(), 20.0, 0.0, Color { x: 0.7, y: 0.8, z: 1.0 }) + (Arc::new(BVHNode::new(&objects, 0.0, 1.0)), Point3 { x: 5.0, y: 5.0, z: 5.0}, Point3 { x: 0.0, y: 0.0, z: 0.0}, 20.0, 0.0, Color { x: 0.7, y: 0.8, z: 1.0 }) +} diff --git a/src/texture/checker_texture.rs b/src/texture/checker_texture.rs new file mode 100644 index 0000000..385017b --- /dev/null +++ b/src/texture/checker_texture.rs @@ -0,0 +1,29 @@ +use std::sync::Arc; + +use super::{Texture, SolidColor}; +use crate::{vec3::Color, vec3::Point3}; + +pub struct CheckerTexture { + odd: Arc<dyn Texture>, + even: Arc<dyn Texture>, +} + +impl CheckerTexture { + pub fn from_colors(odd: Color, even: Color) -> Self { + Self { + odd: Arc::new(SolidColor::from_color(odd)), + even: Arc::new(SolidColor::from_color(even)), + } + } +} + +impl Texture for CheckerTexture { + fn value(&self, u: f64, v: f64, p: &Point3) -> Color { + let sines = (10.0 * p.x).sin() * (10.0 * p.y).sin() * (10.0 * p.z).sin(); + if sines < 0.0 { + self.odd.value(u, v, p) + } else { + self.even.value(u, v, p) + } + } +} diff --git a/src/texture/image_texture.rs b/src/texture/image_texture.rs new file mode 100644 index 0000000..763c3f0 --- /dev/null +++ b/src/texture/image_texture.rs @@ -0,0 +1,63 @@ +use super::Texture; +use crate::{vec3::Color, vec3::Point3}; + +// assume 24 bit depth +const BYTES_PER_PIXEL: usize = 3; +pub struct ImageTexture { + data: Vec<u8>, + width: usize, + height: usize, + bytes_per_scanline: usize, +} + +impl ImageTexture { + pub fn from_bmp_data(bmp_data: &Vec<u8>) -> Self { + let data_position = u32::from_le_bytes([ + bmp_data[0x0A], + bmp_data[0x0B], + bmp_data[0x0C], + bmp_data[0x0D], + ]); + // assuming windows BITMAPINFOHEADER, these are i32 + let width = i32::from_le_bytes([ + bmp_data[0x12], + bmp_data[0x13], + bmp_data[0x14], + bmp_data[0x15], + ]) as usize; + let height = i32::from_le_bytes([ + bmp_data[0x16], + bmp_data[0x17], + bmp_data[0x18], + bmp_data[0x19], + ]) as usize; + Self { + data: bmp_data[(data_position as usize)..bmp_data.len()].to_vec(), + height, + width, + bytes_per_scanline: BYTES_PER_PIXEL * width, + } + } +} + +impl Texture for ImageTexture { + fn value(&self, u: f64, v: f64, _: &Point3) -> Color { + let u = u.clamp(0.0, 1.0); + // This is a deviation from the book, where v gets flipped. + // This is probably because the BMP loader loads in stuff upside down. + //let v = 1.0 - v.clamp(0.0, 1.0); + let v = v.clamp(0.0, 1.0); + let mut i = (u * self.width as f64) as usize; + let mut j = (v * self.height as f64) as usize; + + if i >= self.width { i = self.width - 1 }; + if j >= self.height { j = self.height - 1 }; + let color_scale = 1.0 / 255.0; + let pixel = j * self.bytes_per_scanline + i * BYTES_PER_PIXEL; + Color { + x: color_scale * *self.data.get(pixel + 2).unwrap() as f64, + y: color_scale * *self.data.get(pixel + 1).unwrap() as f64, + z: color_scale * *self.data.get(pixel).unwrap() as f64, + } + } +} diff --git a/src/texture/mod.rs b/src/texture/mod.rs new file mode 100644 index 0000000..55bc9cf --- /dev/null +++ b/src/texture/mod.rs @@ -0,0 +1,15 @@ +mod perlin; +mod solid_color; +pub use solid_color::SolidColor; +mod checker_texture; +pub use checker_texture::CheckerTexture; +mod noise_texture; +pub use noise_texture::NoiseTexture; +mod image_texture; +pub use image_texture::ImageTexture; + +use crate::{vec3::Color, vec3::Point3}; + +pub trait Texture: Send + Sync { + fn value(&self, u: f64, v: f64, p: &Point3) -> Color; +} diff --git a/src/texture/noise_texture.rs b/src/texture/noise_texture.rs new file mode 100644 index 0000000..92ac166 --- /dev/null +++ b/src/texture/noise_texture.rs @@ -0,0 +1,22 @@ +use super::{Texture, perlin::Perlin}; +use crate::{vec3::Color, vec3::Point3}; + +pub struct NoiseTexture { + noise: Perlin, + scale: f64, +} + +impl NoiseTexture { + pub fn new(scale: f64) -> Self { + Self { + noise: Perlin::new(), + scale, + } + } +} + +impl Texture for NoiseTexture { + fn value(&self, _: f64, _: f64, p: &Point3) -> Color { + Color { x: 1.0, y: 1.0, z: 1.0 } * 0.5 * (1.0 + (self.scale * p.z + 10.0 * self.noise.turb(p, 7)).sin()) + } +} diff --git a/src/texture/perlin.rs b/src/texture/perlin.rs new file mode 100644 index 0000000..43d8f64 --- /dev/null +++ b/src/texture/perlin.rs @@ -0,0 +1,98 @@ +use crate::vec3::{Vec3, Point3}; + +const POINT_COUNT: usize = 256; + +pub struct Perlin { + ranvec: Vec<Vec3>, + perm_x: Vec<usize>, + perm_y: Vec<usize>, + perm_z: Vec<usize>, +} + +impl Perlin { + pub fn new() -> Self { + let mut ranvec = Vec::with_capacity(POINT_COUNT); + for _ in 0..POINT_COUNT { + ranvec.push(Vec3::random_in_range(-1.0, 1.0).unit_vector()); + } + Self { + ranvec, + perm_x: Self::generate_perm(), + perm_y: Self::generate_perm(), + perm_z: Self::generate_perm(), + } + } + + pub fn turb(&self, point: &Point3, depth: u32) -> f64 { + let mut accum = 0.0; + let mut temp_point = point.clone(); + let mut weight = 1.0; + + for _ in 0..depth { + accum += weight * self.noise(&temp_point); + weight *= 0.5; + temp_point *= 2.0; + } + accum.abs() + } + + pub fn noise(&self, point: &Point3) -> f64 { + let u = point.x - point.x.floor(); + let v = point.y - point.y.floor(); + let w = point.z - point.z.floor(); + let i = point.x.floor() as i32; + let j = point.y.floor() as i32; + let k = point.z.floor() as i32; + let mut c: [[[Vec3; 2]; 2]; 2] = Default::default(); + for di in 0..2 { + for dj in 0..2 { + for dk in 0..2 { + c[di][dj][dk] = self + .ranvec + .get( + (self.perm_x.get(((i + di as i32) & 255) as usize).unwrap() + ^ self.perm_y.get(((j + dj as i32) & 255) as usize).unwrap() + ^ self.perm_z.get(((k + dk as i32) & 255) as usize).unwrap()) + as usize, + ) + .unwrap().clone(); + } + } + } + Self::trilinear_interpolate(c, u, v, w) + } + + fn trilinear_interpolate(c: [[[Vec3; 2]; 2]; 2], u: f64, v: f64, w: f64) -> f64 { + let uu = u * u * (3.0 - 2.0 * u); + let vv = v * v * (3.0 - 2.0 * v); + let ww = w * w * (3.0 - 2.0 * w); + let mut accum: f64 = 0.0; + for i in 0..2 { + for j in 0..2 { + for k in 0..2 { + let i_f = i as f64; + let j_f = j as f64; + let k_f = k as f64; + let weight_v = Vec3 { x: u - i_f, y: v - j_f, z: w - k_f }; + accum += (i_f * uu + (1.0 - i_f) * (1.0 - uu)) * + (j_f * vv + (1.0 - j_f) * (1.0 - vv)) * + (k_f * ww + (1.0 - k_f) * (1.0 - ww)) * + c[i][j][k].dot(&weight_v); + } + } + } + accum + } + + fn generate_perm() -> Vec<usize> { + let mut p = (0..POINT_COUNT).collect(); + Self::permute(&mut p, POINT_COUNT); + p + } + + fn permute(p: &mut Vec<usize>, n: usize) { + for i in (1..n).rev() { + p.swap(i, rand::random::<usize>() % i); + } + } +} diff --git a/src/texture/solid_color.rs b/src/texture/solid_color.rs new file mode 100644 index 0000000..3af46ca --- /dev/null +++ b/src/texture/solid_color.rs @@ -0,0 +1,20 @@ +use super::Texture; +use crate::{vec3::Color, vec3::Point3}; + +pub struct SolidColor { + pub color_value: Color, +} + +impl SolidColor { + pub fn from_color(color_value: Color) -> Self { + Self { + color_value, + } + } +} + +impl Texture for SolidColor { + fn value(&self, _: f64, _: f64, _: &Point3) -> Color { + self.color_value.clone() + } +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..cbd5813 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,3 @@ +pub fn degrees_to_radians(degrees: f64) -> f64 { + degrees * std::f64::consts::PI / 180.0 +} diff --git a/src/vec3.rs b/src/vec3.rs new file mode 100644 index 0000000..8525fdd --- /dev/null +++ b/src/vec3.rs @@ -0,0 +1,201 @@ +use auto_ops::{impl_op_ex, impl_op_ex_commutative}; +use std::fmt; + +pub type Point3 = Vec3; +pub type Color = Vec3; + +#[derive(Clone, Default)] +pub struct Vec3 { + pub x: f64, + pub y: f64, + pub z: f64, +} + +impl Vec3 { + pub fn new() -> Vec3 { + Vec3 { + x: 0.0, + y: 0.0, + z: 0.0, + } + } + + pub fn get(&self, index: usize) -> Option<&f64> { + match index { + 0 => Some(&self.x), + 1 => Some(&self.y), + 2 => Some(&self.z), + _ => None, + } + } + + pub fn get_mut(&mut self, index: usize) -> Option<&mut f64> { + match index { + 0 => Some(&mut self.x), + 1 => Some(&mut self.y), + 2 => Some(&mut self.z), + _ => None, + } + } + + pub fn length(&self) -> f64 { + self.length_squared().sqrt() + } + + pub fn length_squared(&self) -> f64 { + self.x * self.x + self.y * self.y + self.z * self.z + } + + pub fn dot(&self, other: &Vec3) -> f64 { + self.x * other.x + self.y * other.y + self.z * other.z + } + + pub fn cross(&self, other: &Vec3) -> Vec3 { + Vec3 { + x: self.y * other.z - self.z * other.y, + y: self.z * other.x - self.x * other.z, + z: self.x * other.y - self.y * other.x, + } + } + + pub fn unit_vector(&self) -> Vec3 { + self / self.length() + } + + pub fn random() -> Vec3 { + Vec3 { + x: rand::random::<f64>(), + y: rand::random::<f64>(), + z: rand::random::<f64>(), + } + } + + pub fn random_in_range(min: f64, max: f64) -> Vec3 { + Vec3 { + x: min + (max - min) * rand::random::<f64>(), + y: min + (max - min) * rand::random::<f64>(), + z: min + (max - min) * rand::random::<f64>(), + } + } + + pub fn random_in_unit_sphere() -> Vec3 { + loop { + let p = Vec3 { + x: 2.0 * rand::random::<f64>() - 1.0, + y: 2.0 * rand::random::<f64>() - 1.0, + z: 2.0 * rand::random::<f64>() - 1.0, + }; + if p.length_squared() < 1.0 { + return p; + } + }; + } + + pub fn random_unit_vector() -> Vec3 { + Self::random_in_unit_sphere().unit_vector() + } + + pub fn random_in_unit_disk() -> Vec3 { + loop { + let p = Vec3 { + x: 2.0 * rand::random::<f64>() -1.0, + y: 2.0 * rand::random::<f64>() -1.0, + z: 0.0, + }; + if p.length_squared() < 1.0 { + return p; + } + } + } + + pub fn near_zero(&self) -> bool { + const S: f64 = 1e-8; + self.x.abs() < S && self.y.abs() < S && self.z.abs() < S + } + + pub fn reflect(&self, normal: &Vec3) -> Vec3 { + self - 2.0 * self.dot(normal) * normal + } + + pub fn refract(&self, normal: &Vec3, etai_over_etat: f64) -> Vec3 { + let cos_theta = normal.dot(&-self).min(1.0); + let r_out_perp = etai_over_etat * (self + cos_theta * normal); + let r_out_parallel = -((1.0 - r_out_perp.length_squared()).abs().sqrt()) * normal; + r_out_perp + r_out_parallel + } + + pub fn has_infinite_member(&self) -> bool { + self.x.is_infinite() || self.y.is_infinite() || self.z.is_infinite() + } +} + +impl fmt::Display for Vec3 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} {} {}", self.x, self.y, self.z) + } +} + +impl<'a> IntoIterator for &'a Vec3 { + type Item = f64; + type IntoIter = Vec3Iterator<'a>; + + fn into_iter(self) -> Self::IntoIter { + Vec3Iterator { + vec3: self, + index: 0, + } + } +} + +pub struct Vec3Iterator<'a> { + vec3: &'a Vec3, + index: usize, +} + +impl<'a> Iterator for Vec3Iterator<'a> { + type Item = f64; + fn next(&mut self) -> Option<f64> { + let result = match self.index { + 0 => self.vec3.x, + 1 => self.vec3.y, + 2 => self.vec3.z, + _ => return None, + }; + self.index += 1; + Some(result) + } +} + +impl_op_ex!(- |a: &Vec3| -> Vec3 { + Vec3 { + x: -a.x, + y: -a.y, + z: -a.z, + } +}); +impl_op_ex!(+= |lhs: &mut Vec3, rhs: Vec3| { *lhs = Vec3 { x: lhs.x + rhs.x, y: lhs.y + rhs.y, z: lhs.z + rhs.z } }); +impl_op_ex!(*= |lhs: &mut Vec3, rhs: &f64| { *lhs = Vec3 { x: lhs.x * rhs, y: lhs.y * rhs, z: lhs.z * rhs } }); +impl_op_ex!(/= |lhs: &mut Vec3, rhs: &f64| { *lhs *= 1.0 / rhs }); +impl_op_ex!(+ |lhs: &Vec3, rhs: &Vec3| -> Vec3 { Vec3 { x: lhs.x + rhs.x, y: lhs.y + rhs.y, z: lhs.z + rhs.z } }); +impl_op_ex!(-|lhs: &Vec3, rhs: &Vec3| -> Vec3 { + Vec3 { + x: lhs.x - rhs.x, + y: lhs.y - rhs.y, + z: lhs.z - rhs.z, + } +}); +impl_op_ex!(*|lhs: &Vec3, rhs: &Vec3| -> Vec3 { + Vec3 { + x: lhs.x * rhs.x, + y: lhs.y * rhs.y, + z: lhs.z * rhs.z, + } +}); +impl_op_ex_commutative!(*|lhs: &Vec3, rhs: &f64| -> Vec3 { + Vec3 { + x: lhs.x * rhs, + y: lhs.y * rhs, + z: lhs.z * rhs, + } +}); +impl_op_ex!(/ |lhs: &Vec3, rhs: &f64| -> Vec3 { lhs * (1.0/rhs) }); |