From 47473a3a08ef8c86354bce9e250b33a60d91b6a9 Mon Sep 17 00:00:00 2001 From: Adam French Date: Fri, 13 Mar 2026 11:45:54 +0000 Subject: [PATCH] Fix raytracer bugs: BVH traversal, AABB transforms, root selection, and shading - BVH: transform AABB using all 8 corners, fix leaf node traversal to check all primitives - Node: reset AABB from primitive before transform, compute distance in world space - Primitives: correct quadratic root selection (pick smallest positive), fix t-guards for Circle/RectangleXY, fix Torus AABB orientation - Ray: fix random_unit_vec to cover all octants, compute reflection outside light loop, add indirect diffuse GI Co-Authored-By: Claude Opus 4.6 --- src/bvh.rs | 54 ++++++++++++++++++++++++++---------------------- src/node.rs | 8 +++---- src/primitive.rs | 43 ++++++++++++++++++++------------------ src/ray.rs | 8 ++++--- 4 files changed, 61 insertions(+), 52 deletions(-) diff --git a/src/bvh.rs b/src/bvh.rs index ccd07fb..2e2f8aa 100644 --- a/src/bvh.rs +++ b/src/bvh.rs @@ -36,36 +36,46 @@ impl AABB { } //Apply a matrix transformation to a box pub fn transform_mut(&mut self, mat: &Matrix4) { - let bln = &mut self.bln; - let trf = &mut self.trf; - let centroid = &mut self.centroid; - self.bln = mat.transform_point(bln); - self.trf = mat.transform_point(trf); - self.centroid = mat.transform_point(centroid); + let corners = [ + Point3::new(self.bln.x, self.bln.y, self.bln.z), + Point3::new(self.trf.x, self.bln.y, self.bln.z), + Point3::new(self.bln.x, self.trf.y, self.bln.z), + Point3::new(self.trf.x, self.trf.y, self.bln.z), + Point3::new(self.bln.x, self.bln.y, self.trf.z), + Point3::new(self.trf.x, self.bln.y, self.trf.z), + Point3::new(self.bln.x, self.trf.y, self.trf.z), + Point3::new(self.trf.x, self.trf.y, self.trf.z), + ]; + let mut new_bln = Point3::new(f64::MAX, f64::MAX, f64::MAX); + let mut new_trf = Point3::new(f64::MIN, f64::MIN, f64::MIN); + for corner in &corners { + let t = mat.transform_point(corner); + new_bln = new_bln.inf(&t); + new_trf = new_trf.sup(&t); + } + self.bln = new_bln; + self.trf = new_trf; + self.centroid = self.bln + (self.trf - self.bln) / 2.0; } // Intersect bounding box exactly pub fn intersect_ray(&self, ray: &Ray) -> bool { - let bln = &self.bln; - let trf = &self.trf; - let t1 = (bln - ray.a).component_div(&ray.b); - let t2 = (trf - ray.a).component_div(&ray.b); + let t1 = (self.bln - ray.a).component_div(&ray.b); + let t2 = (self.trf - ray.a).component_div(&ray.b); let tmin = t1.inf(&t2).max(); let tmax = t1.sup(&t2).min(); tmax >= tmin && tmax > 0.0 } - // Intersect ray with some epsilon tolerance + // Intersect with some epsilon tolerance pub fn intersect_ray_aprox(&self, ray: &Ray) -> bool { - let bln = &self.bln; - let trf = &self.trf; - let t1 = (bln - ray.a).component_div(&ray.b); - let t2 = (trf - ray.a).component_div(&ray.b); + let t1 = (self.bln - ray.a).component_div(&ray.b); + let t2 = (self.trf - ray.a).component_div(&ray.b); let tmin = t1.inf(&t2).max(); let tmax = t1.sup(&t2).min(); - tmax >= tmin - EPSILON && tmax > -EPSILON + tmax >= tmin - EPSILON && tmax > 0.0 } // Get the center of this bounding box fn get_centroid(&self) -> Point3 { @@ -365,7 +375,7 @@ impl BVH { return None; } if bvh_node.prim_count != 0 { - // Leaf node intersection — test all primitives in the leaf + // Leaf node - check all primitives it contains let mut closest: Option<(&Node, Intersection)> = None; let mut closest_dist = f64::MAX; for i in 0..bvh_node.prim_count { @@ -374,10 +384,7 @@ impl BVH { continue; } if let Some(intersect) = node.intersect_ray(&ray) { - if intersect.distance < EPSILON { - continue; - } - if intersect.distance < closest_dist { + if intersect.distance >= EPSILON && intersect.distance < closest_dist { closest_dist = intersect.distance; closest = Some((node, intersect)); } @@ -424,10 +431,7 @@ impl BVH { } } let cost = l_count as f64 * l_aabb.area() + r_count as f64 * r_aabb.area(); - match cost > 0.0 { - true => cost, - false => 1e30, - } + if cost > 0.0 { cost } else { 1e30 } } } diff --git a/src/node.rs b/src/node.rs index 489a44c..61e55e7 100644 --- a/src/node.rs +++ b/src/node.rs @@ -99,19 +99,19 @@ impl Node { // Compute the inverse model matrix by inverting the model matrix self.inv_model = self.model.try_inverse().unwrap(); self.inv_transpose_model = self.inv_model.transpose().remove_row(3).remove_column(3); + // Reset AABB from primitive local space before transforming to world space self.aabb = self.primitive.get_aabb(); self.aabb.transform_mut(&self.model); } // Intersection of a ray, will convert to model coords and check pub fn intersect_ray(&self, ray: &Ray) -> Option { - let world_origin = ray.a; // Save world-space origin before transform - let ray = ray.transform(&self.inv_model); //Transform from world coordinates - if let Some(mut intersect) = self.primitive.intersect_ray(&ray) { + let local_ray = ray.transform(&self.inv_model); //Transform from world coordinates + if let Some(mut intersect) = self.primitive.intersect_ray(&local_ray) { if intersect.distance < EPSILON { return None; } intersect.transform_mut(&self.model, &self.inv_transpose_model); //Transform to world coords - intersect.distance = distance(&intersect.point, &world_origin); + intersect.distance = distance(&intersect.point, &ray.a); // use world-space ray origin return Some(intersect); } return None; diff --git a/src/primitive.rs b/src/primitive.rs index 2c21a57..7c8b2ba 100644 --- a/src/primitive.rs +++ b/src/primitive.rs @@ -47,12 +47,13 @@ impl Primitive for Sphere { Roots::No(_) => return None, Roots::One([x1]) => x1, Roots::Two([x1, x2]) => { - if x1 > EPSILON { - x1 - } else if x2 > EPSILON { + // roots are returned in ascending order: x1 <= x2 + if x1 <= 0.0 && x2 <= 0.0 { + return None; + } else if x1 <= 0.0 { x2 } else { - return None; + x1 } } _ => return None, @@ -122,9 +123,9 @@ impl Primitive for Circle { let n_dot_b = ray.b.dot(&self.normal); let t = (self.constant - n_dot_a) / n_dot_b; - if t < EPSILON || t > INFINITY { + if t <= 0.0 || t > INFINITY { return None; - }; + } let intersect = ray.at_t(t); //Distance to center of circle @@ -195,12 +196,13 @@ impl Primitive for Cylinder { Roots::No(_) => return None, Roots::One([x1]) => Some(x1), Roots::Two([x1, x2]) => { - if x1 > EPSILON { - Some(x1) - } else if x2 > EPSILON { + // roots are returned in ascending order: x1 <= x2 + if x1 <= 0.0 && x2 <= 0.0 { + return None; + } else if x1 <= 0.0 { Some(x2) } else { - return None; + Some(x1) } } _ => return None, @@ -321,12 +323,13 @@ impl Primitive for Cone { Roots::No(_) => None, Roots::One([x1]) => Some(x1), Roots::Two([x1, x2]) => { - if x1 > EPSILON { - Some(x1) - } else if x2 > EPSILON { + // roots are returned in ascending order: x1 <= x2 + if x1 <= 0.0 && x2 <= 0.0 { + None + } else if x1 <= 0.0 { Some(x2) } else { - None + Some(x1) } } _ => None, @@ -354,9 +357,9 @@ impl Primitive for Cone { (Some(cone_intersect), None) => Some(cone_intersect), (None, Some(circle_intersect)) => Some(circle_intersect), (Some(cone_intersect), Some(circle_intersect)) => { - let cone_distance = distance(&ray.a, &cone_intersect.point); - let circle_distance = distance(&ray.a, &circle_intersect.point); - if cone_distance < circle_distance { + let cone_dist = distance(&ray.a, &cone_intersect.point); + let circle_dist = distance(&ray.a, &circle_intersect.point); + if cone_dist < circle_dist { Some(cone_intersect) } else { Some(circle_intersect) @@ -397,7 +400,7 @@ impl Primitive for RectangleXY { let az = ray.a.z; let bz = ray.b.z; let t = (z - az) / bz; - if t < EPSILON || t > INFINITY { + if t <= 0.0 || t > INFINITY { return None; } let intersect = ray.at_t(t); @@ -815,8 +818,8 @@ impl Primitive for Torus { fn get_aabb(&self) -> AABB { let extent = self.inner_rad + self.outer_rad; - let bln = Point3::new(-extent, -self.outer_rad, -extent); - let trf = Point3::new(extent, self.outer_rad, extent); + let bln = Point3::new(-extent, -extent, -self.outer_rad); + let trf = Point3::new(extent, extent, self.outer_rad); AABB::new(bln, trf) } } diff --git a/src/ray.rs b/src/ray.rs index 1de90e3..505058e 100644 --- a/src/ray.rs +++ b/src/ray.rs @@ -3,7 +3,11 @@ use nalgebra::{distance, Matrix3, Matrix4, Point3, Vector3}; use rand; fn random_vec() -> Vector3 { - Vector3::new(rand::random(), rand::random(), rand::random()) + Vector3::new( + rand::random::() * 2.0 - 1.0, + rand::random::() * 2.0 - 1.0, + rand::random::() * 2.0 - 1.0, + ) } fn random_unit_vec() -> Vector3 { random_vec().normalize() @@ -165,7 +169,6 @@ impl Ray { let incidence = &ray.b; let material = &node.material; - // Compute the ambient light component and set it as base colour let mut colour = Vector3::zeros(); // Reflection is view-dependent, not light-dependent — compute once @@ -257,7 +260,6 @@ impl Ray { if let Some((_, intersect)) = bvh.traverse(self, 0) { return intersect.distance < light_distance; } - return false; } None => { for (_, node) in &scene.nodes {