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 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 11:45:54 +00:00
parent fa31d18c12
commit 47473a3a08
4 changed files with 61 additions and 52 deletions

View File

@@ -36,36 +36,46 @@ impl AABB {
} }
//Apply a matrix transformation to a box //Apply a matrix transformation to a box
pub fn transform_mut(&mut self, mat: &Matrix4<f64>) { pub fn transform_mut(&mut self, mat: &Matrix4<f64>) {
let bln = &mut self.bln; let corners = [
let trf = &mut self.trf; Point3::new(self.bln.x, self.bln.y, self.bln.z),
let centroid = &mut self.centroid; Point3::new(self.trf.x, self.bln.y, self.bln.z),
self.bln = mat.transform_point(bln); Point3::new(self.bln.x, self.trf.y, self.bln.z),
self.trf = mat.transform_point(trf); Point3::new(self.trf.x, self.trf.y, self.bln.z),
self.centroid = mat.transform_point(centroid); 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 // Intersect bounding box exactly
pub fn intersect_ray(&self, ray: &Ray) -> bool { pub fn intersect_ray(&self, ray: &Ray) -> bool {
let bln = &self.bln; let t1 = (self.bln - ray.a).component_div(&ray.b);
let trf = &self.trf; let t2 = (self.trf - ray.a).component_div(&ray.b);
let t1 = (bln - ray.a).component_div(&ray.b);
let t2 = (trf - ray.a).component_div(&ray.b);
let tmin = t1.inf(&t2).max(); let tmin = t1.inf(&t2).max();
let tmax = t1.sup(&t2).min(); let tmax = t1.sup(&t2).min();
tmax >= tmin && tmax > 0.0 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 { pub fn intersect_ray_aprox(&self, ray: &Ray) -> bool {
let bln = &self.bln; let t1 = (self.bln - ray.a).component_div(&ray.b);
let trf = &self.trf; let t2 = (self.trf - ray.a).component_div(&ray.b);
let t1 = (bln - ray.a).component_div(&ray.b);
let t2 = (trf - ray.a).component_div(&ray.b);
let tmin = t1.inf(&t2).max(); let tmin = t1.inf(&t2).max();
let tmax = t1.sup(&t2).min(); let tmax = t1.sup(&t2).min();
tmax >= tmin - EPSILON && tmax > -EPSILON tmax >= tmin - EPSILON && tmax > 0.0
} }
// Get the center of this bounding box // Get the center of this bounding box
fn get_centroid(&self) -> Point3<f64> { fn get_centroid(&self) -> Point3<f64> {
@@ -365,7 +375,7 @@ impl BVH {
return None; return None;
} }
if bvh_node.prim_count != 0 { 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: Option<(&Node, Intersection)> = None;
let mut closest_dist = f64::MAX; let mut closest_dist = f64::MAX;
for i in 0..bvh_node.prim_count { for i in 0..bvh_node.prim_count {
@@ -374,10 +384,7 @@ impl BVH {
continue; continue;
} }
if let Some(intersect) = node.intersect_ray(&ray) { if let Some(intersect) = node.intersect_ray(&ray) {
if intersect.distance < EPSILON { if intersect.distance >= EPSILON && intersect.distance < closest_dist {
continue;
}
if intersect.distance < closest_dist {
closest_dist = intersect.distance; closest_dist = intersect.distance;
closest = Some((node, intersect)); 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(); let cost = l_count as f64 * l_aabb.area() + r_count as f64 * r_aabb.area();
match cost > 0.0 { if cost > 0.0 { cost } else { 1e30 }
true => cost,
false => 1e30,
}
} }
} }

View File

@@ -99,19 +99,19 @@ impl Node {
// Compute the inverse model matrix by inverting the model matrix // Compute the inverse model matrix by inverting the model matrix
self.inv_model = self.model.try_inverse().unwrap(); self.inv_model = self.model.try_inverse().unwrap();
self.inv_transpose_model = self.inv_model.transpose().remove_row(3).remove_column(3); 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 = self.primitive.get_aabb();
self.aabb.transform_mut(&self.model); self.aabb.transform_mut(&self.model);
} }
// Intersection of a ray, will convert to model coords and check // Intersection of a ray, will convert to model coords and check
pub fn intersect_ray(&self, ray: &Ray) -> Option<Intersection> { pub fn intersect_ray(&self, ray: &Ray) -> Option<Intersection> {
let world_origin = ray.a; // Save world-space origin before transform let local_ray = ray.transform(&self.inv_model); //Transform from world coordinates
let ray = ray.transform(&self.inv_model); //Transform from world coordinates if let Some(mut intersect) = self.primitive.intersect_ray(&local_ray) {
if let Some(mut intersect) = self.primitive.intersect_ray(&ray) {
if intersect.distance < EPSILON { if intersect.distance < EPSILON {
return None; return None;
} }
intersect.transform_mut(&self.model, &self.inv_transpose_model); //Transform to world coords 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 Some(intersect);
} }
return None; return None;

View File

@@ -47,12 +47,13 @@ impl Primitive for Sphere {
Roots::No(_) => return None, Roots::No(_) => return None,
Roots::One([x1]) => x1, Roots::One([x1]) => x1,
Roots::Two([x1, x2]) => { Roots::Two([x1, x2]) => {
if x1 > EPSILON { // roots are returned in ascending order: x1 <= x2
x1 if x1 <= 0.0 && x2 <= 0.0 {
} else if x2 > EPSILON { return None;
} else if x1 <= 0.0 {
x2 x2
} else { } else {
return None; x1
} }
} }
_ => return None, _ => return None,
@@ -122,9 +123,9 @@ impl Primitive for Circle {
let n_dot_b = ray.b.dot(&self.normal); let n_dot_b = ray.b.dot(&self.normal);
let t = (self.constant - n_dot_a) / n_dot_b; let t = (self.constant - n_dot_a) / n_dot_b;
if t < EPSILON || t > INFINITY { if t <= 0.0 || t > INFINITY {
return None; return None;
}; }
let intersect = ray.at_t(t); let intersect = ray.at_t(t);
//Distance to center of circle //Distance to center of circle
@@ -195,12 +196,13 @@ impl Primitive for Cylinder {
Roots::No(_) => return None, Roots::No(_) => return None,
Roots::One([x1]) => Some(x1), Roots::One([x1]) => Some(x1),
Roots::Two([x1, x2]) => { Roots::Two([x1, x2]) => {
if x1 > EPSILON { // roots are returned in ascending order: x1 <= x2
Some(x1) if x1 <= 0.0 && x2 <= 0.0 {
} else if x2 > EPSILON { return None;
} else if x1 <= 0.0 {
Some(x2) Some(x2)
} else { } else {
return None; Some(x1)
} }
} }
_ => return None, _ => return None,
@@ -321,12 +323,13 @@ impl Primitive for Cone {
Roots::No(_) => None, Roots::No(_) => None,
Roots::One([x1]) => Some(x1), Roots::One([x1]) => Some(x1),
Roots::Two([x1, x2]) => { Roots::Two([x1, x2]) => {
if x1 > EPSILON { // roots are returned in ascending order: x1 <= x2
Some(x1) if x1 <= 0.0 && x2 <= 0.0 {
} else if x2 > EPSILON { None
} else if x1 <= 0.0 {
Some(x2) Some(x2)
} else { } else {
None Some(x1)
} }
} }
_ => None, _ => None,
@@ -354,9 +357,9 @@ impl Primitive for Cone {
(Some(cone_intersect), None) => Some(cone_intersect), (Some(cone_intersect), None) => Some(cone_intersect),
(None, Some(circle_intersect)) => Some(circle_intersect), (None, Some(circle_intersect)) => Some(circle_intersect),
(Some(cone_intersect), Some(circle_intersect)) => { (Some(cone_intersect), Some(circle_intersect)) => {
let cone_distance = distance(&ray.a, &cone_intersect.point); let cone_dist = distance(&ray.a, &cone_intersect.point);
let circle_distance = distance(&ray.a, &circle_intersect.point); let circle_dist = distance(&ray.a, &circle_intersect.point);
if cone_distance < circle_distance { if cone_dist < circle_dist {
Some(cone_intersect) Some(cone_intersect)
} else { } else {
Some(circle_intersect) Some(circle_intersect)
@@ -397,7 +400,7 @@ impl Primitive for RectangleXY {
let az = ray.a.z; let az = ray.a.z;
let bz = ray.b.z; let bz = ray.b.z;
let t = (z - az) / bz; let t = (z - az) / bz;
if t < EPSILON || t > INFINITY { if t <= 0.0 || t > INFINITY {
return None; return None;
} }
let intersect = ray.at_t(t); let intersect = ray.at_t(t);
@@ -815,8 +818,8 @@ impl Primitive for Torus {
fn get_aabb(&self) -> AABB { fn get_aabb(&self) -> AABB {
let extent = self.inner_rad + self.outer_rad; let extent = self.inner_rad + self.outer_rad;
let bln = Point3::new(-extent, -self.outer_rad, -extent); let bln = Point3::new(-extent, -extent, -self.outer_rad);
let trf = Point3::new(extent, self.outer_rad, extent); let trf = Point3::new(extent, extent, self.outer_rad);
AABB::new(bln, trf) AABB::new(bln, trf)
} }
} }

View File

@@ -3,7 +3,11 @@ use nalgebra::{distance, Matrix3, Matrix4, Point3, Vector3};
use rand; use rand;
fn random_vec() -> Vector3<f64> { fn random_vec() -> Vector3<f64> {
Vector3::new(rand::random(), rand::random(), rand::random()) Vector3::new(
rand::random::<f64>() * 2.0 - 1.0,
rand::random::<f64>() * 2.0 - 1.0,
rand::random::<f64>() * 2.0 - 1.0,
)
} }
fn random_unit_vec() -> Vector3<f64> { fn random_unit_vec() -> Vector3<f64> {
random_vec().normalize() random_vec().normalize()
@@ -165,7 +169,6 @@ impl Ray {
let incidence = &ray.b; let incidence = &ray.b;
let material = &node.material; let material = &node.material;
// Compute the ambient light component and set it as base colour
let mut colour = Vector3::zeros(); let mut colour = Vector3::zeros();
// Reflection is view-dependent, not light-dependent — compute once // Reflection is view-dependent, not light-dependent — compute once
@@ -257,7 +260,6 @@ impl Ray {
if let Some((_, intersect)) = bvh.traverse(self, 0) { if let Some((_, intersect)) = bvh.traverse(self, 0) {
return intersect.distance < light_distance; return intersect.distance < light_distance;
} }
return false;
} }
None => { None => {
for (_, node) in &scene.nodes { for (_, node) in &scene.nodes {