use nalgebra::{Matrix3, Vector3}; use ordered_float::NotNan; use crate::scene_data::{Cylinder, Sphere}; /// A normalized parametric Ray of the form (origin + direction * time) /// /// That means at any time t: f64, the point represented by origin + direction * /// time occurs on the ray. #[derive(Debug)] pub struct Ray { origin: Vector3, direction: Vector3, } impl Ray { /// Construct a ray from endpoints pub fn from_endpoints(start: Vector3, end: Vector3) -> Self { let delta = (end - start).normalize(); Ray { origin: start, direction: delta, } } /// Evaluate the ray at a certain point in time, yielding a point pub fn eval(&self, time: f64) -> Vector3 { self.origin + self.direction * time } /// Given a sphere, returns the first time at which this ray intersects the /// sphere. /// /// If there is no intersection point, returns None. pub fn intersects_sphere_at(&self, sphere: &Sphere) -> Option { let a = self.direction.x.powi(2) + self.direction.y.powi(2) + self.direction.z.powi(2); let b = 2.0 * (self.direction.x * (self.origin.x - sphere.center.x) + self.direction.y * (self.origin.y - sphere.center.y) + self.direction.z * (self.origin.z - sphere.center.z)); let c = (self.origin.x - sphere.center.x).powi(2) + (self.origin.y - sphere.center.y).powi(2) + (self.origin.z - sphere.center.z).powi(2) - sphere.radius.powi(2); let discriminant = b * b - 4.0 * a * c; match discriminant { // Discriminant < 0, means the equation has no solutions. d if d < 0.0 => return None, // Discriminant == 0 d if d == 0.0 => { return Some(-b / (2.0 * a)); } d if d > 0.0 => { let solution_1 = (-b + discriminant.sqrt()) / (2.0 * a); let solution_2 = (-b - discriminant.sqrt()) / (2.0 * a); return Some(solution_1.min(solution_2)); } // Probably hit some NaN or Infinity value due to faulty inputs... _ => unreachable!("Invalid determinant value: {discriminant}"), } } /// Given a cylinder, returns the first time at which this ray intersects the /// cylinder. /// /// If there is no intersection point, returns None. pub fn intersects_cylinder_at(&self, cylinder: &Cylinder) -> Option { // Determine rotation matrix for turning the cylinder upright along the // Z-axis let target_direction = Vector3::new(0.0, 0.0, 1.0); let rotation_matrix = compute_rotation_matrix(cylinder.direction, target_direction); // Transform all parameters according to this rotation matrix let rotated_cylinder_center = rotation_matrix * cylinder.center; let rotated_ray_origin = rotation_matrix * self.origin; let rotated_ray_direction = rotation_matrix * self.direction; // Now that we know the cylinder is upright, we can start checking against // the formula: // // (ox + t*rx - cx)^2 + (oy + t*ry - cy)^2 = r^2 // // where o{xy} is the ray origin, r{xy} is the ray direction, and c{xy} is // the cylinder center. The z will be taken care of after the fact. To // solve, we must put it into the form At^2 + Bt + c = 0. The variables // are: // // A: rx^2 + ry^2 // B: 2(rx(ox - cx) + ry(oy - cy)) // C: (cx - ox)^2 + (cy - oy)^2 - r^2 let (a, b, c) = { let o = rotated_ray_origin; let r = rotated_ray_direction; let c = rotated_cylinder_center; ( r.x.powi(2) + r.y.powi(2), 2.0 * (r.x * (o.x - c.x) + r.y * (o.y - c.y)), (c.x - o.x).powi(2) + (c.y - o.y).powi(2) - cylinder.radius.powi(2), ) }; let discriminant = b * b - 4.0 * a * c; let mut solutions = match discriminant { // Discriminant < 0, means the equation has no solutions. d if d < 0.0 => vec![], // Discriminant == 0 d if d == 0.0 => vec![-b / 2.0 * a], // Discriminant > 0, 2 solutions available. d if d > 0.0 => { vec![ (-b + discriminant.sqrt()) / (2.0 * a), (-b - discriminant.sqrt()) / (2.0 * a), ] } // Probably hit some NaN or Infinity value due to faulty inputs... _ => unreachable!("Invalid determinant value: {discriminant}"), }; // We also need to add solutions for the two ends of the cylinder, which // uses a similar method except backwards: check intersection points // with the correct z-plane and then see if the points are within the // circle. // // Luckily, this means we only need to care about one dimension at first, // and don't need to perform the quadratic equation method above. // // oz + t * rz = cz +- (len / 2) // t = (oz + cz +- (len / 2)) / rz let possible_z_intersections = { let o = rotated_ray_origin; let r = rotated_ray_direction; let c = rotated_cylinder_center; vec![ (o.z + c.z + cylinder.length / 2.0) / r.z, (o.z + c.z - cylinder.length / 2.0) / r.z, ] }; // Filter out all the solutions where the z does not lie in the circle solutions.extend(possible_z_intersections.into_iter().filter(|t| { let ray_point = self.origin + self.direction * (*t); ray_point.x.powi(2) + ray_point.y.powi(2) <= cylinder.radius.powi(2) })); // Filter out solutions that don't have a valid Z position. let solutions = solutions .into_iter() .filter(|t| { let ray_point = self.origin + self.direction * (*t); let rotated_ray_point = rotation_matrix * ray_point; let z = rotated_ray_point.z - rotated_cylinder_center.z; // Check to see if z is between -len/2 and len/2 z.abs() < cylinder.length / 2.0 }) .filter_map(|t| NotNan::new(t).ok()); // Return the minimum solution solutions.min().map(|t| t.into_inner()) } } /// Calculate the rotation matrix between the 2 given vectors /// Based on the method here: https://math.stackexchange.com/a/897677 fn compute_rotation_matrix(a: Vector3, b: Vector3) -> Matrix3 { let cos_t = a.dot(&b); let sin_t = a.cross(&b).norm(); let G = Matrix3::new(cos_t, -sin_t, 0.0, sin_t, cos_t, 0.0, 0.0, 0.0, 1.0); // Basis let u = a; let v = (b - a.dot(&b) * a).normalize(); let w = b.cross(&a); let F = Matrix3::from_columns(&[u, v, w]).try_inverse().unwrap(); F.try_inverse().unwrap() * G * F } #[cfg(test)] mod tests { use nalgebra::Vector3; use crate::scene_data::Sphere; use super::Ray; #[test] fn practice_problem_slide_154() { let ray = Ray { origin: Vector3::new(0.0, 0.0, 0.0), direction: Vector3::new(0.0, 0.0, -1.0), }; let sphere = Sphere { center: Vector3::new(0.0, 0.0, -10.0), radius: 4.0, }; let point = ray.intersects_sphere_at(&sphere).map(|t| ray.eval(t)); // the intersection point in this case is (0, 0, -6) assert_eq!(point, Some(Vector3::new(0.0, 0.0, -6.0))); } #[test] fn practice_problem_slide_158() { let ray = Ray { origin: Vector3::new(0.0, 0.0, 0.0), direction: Vector3::new(0.0, 0.5, -1.0), }; let sphere = Sphere { center: Vector3::new(0.0, 0.0, -10.0), radius: 4.0, }; // oops! In this case, the ray does not intersect the sphere. assert_eq!(ray.intersects_sphere_at(&sphere), None); } }