1use std::f32::consts::PI;
4
5use super::{Material, Ray, Vec3, vec3};
6
7const BOUNDS_PADDING: f32 = 1.0e-4;
8const PARALLEL_EPSILON: f32 = 1.0e-8;
9const VECTOR_EPSILON_SQUARED: f32 = PARALLEL_EPSILON * PARALLEL_EPSILON;
10const LEAF_SIZE: usize = 4;
11const TRAVERSAL_STACK_SIZE: usize = usize::BITS as usize;
12
13#[derive(Clone, Copy, Debug, PartialEq)]
16pub struct Aabb {
17 pub min: Vec3,
19 pub max: Vec3,
21}
22
23impl Aabb {
24 pub const fn new(min: Vec3, max: Vec3) -> Self {
26 Self { min, max }
27 }
28
29 pub const fn from_points(a: Vec3, b: Vec3) -> Self {
31 Self {
32 min: vec3(a.x.min(b.x), a.y.min(b.y), a.z.min(b.z)),
33 max: vec3(a.x.max(b.x), a.y.max(b.y), a.z.max(b.z)),
34 }
35 }
36
37 pub fn is_valid(self) -> bool {
39 is_finite_vec(self.min)
40 && is_finite_vec(self.max)
41 && self.min.x <= self.max.x
42 && self.min.y <= self.max.y
43 && self.min.z <= self.max.z
44 }
45
46 pub const fn union(self, other: Self) -> Self {
48 Self {
49 min: vec3(
50 self.min.x.min(other.min.x),
51 self.min.y.min(other.min.y),
52 self.min.z.min(other.min.z),
53 ),
54 max: vec3(
55 self.max.x.max(other.max.x),
56 self.max.y.max(other.max.y),
57 self.max.z.max(other.max.z),
58 ),
59 }
60 }
61
62 pub fn centroid(self) -> Vec3 {
64 self.min * 0.5 + self.max * 0.5
65 }
66
67 pub fn padded(self, minimum_extent: f32) -> Self {
69 if !self.is_valid() || !minimum_extent.is_finite() || minimum_extent <= 0.0 {
70 return self;
71 }
72 let mut min = self.min;
73 let mut max = self.max;
74 if max.x - min.x < minimum_extent {
75 let padding = (minimum_extent - (max.x - min.x)) * 0.5;
76 min.x -= padding;
77 max.x += padding;
78 }
79 if max.y - min.y < minimum_extent {
80 let padding = (minimum_extent - (max.y - min.y)) * 0.5;
81 min.y -= padding;
82 max.y += padding;
83 }
84 if max.z - min.z < minimum_extent {
85 let padding = (minimum_extent - (max.z - min.z)) * 0.5;
86 min.z -= padding;
87 max.z += padding;
88 }
89 Self { min, max }
90 }
91
92 pub fn hit_interval(self, ray: Ray, mut t_min: f32, mut t_max: f32) -> Option<(f32, f32)> {
98 if !self.is_valid()
99 || !is_finite_vec(ray.origin)
100 || !is_finite_vec(ray.dir)
101 || !t_min.is_finite()
102 || t_max.is_nan()
103 || t_min > t_max
104 {
105 return None;
106 }
107
108 for (origin, direction, slab_min, slab_max) in [
109 (ray.origin.x, ray.dir.x, self.min.x, self.max.x),
110 (ray.origin.y, ray.dir.y, self.min.y, self.max.y),
111 (ray.origin.z, ray.dir.z, self.min.z, self.max.z),
112 ] {
113 if direction.abs() <= PARALLEL_EPSILON {
114 if origin < slab_min || origin > slab_max {
115 return None;
116 }
117 continue;
118 }
119 let inverse = 1.0 / direction;
120 let mut near = (slab_min - origin) * inverse;
121 let mut far = (slab_max - origin) * inverse;
122 if near > far {
123 std::mem::swap(&mut near, &mut far);
124 }
125 t_min = t_min.max(near);
126 t_max = t_max.min(far);
127 if t_max < t_min {
128 return None;
129 }
130 }
131 Some((t_min, t_max))
132 }
133
134 fn longest_axis(self) -> usize {
135 let extent = self.max - self.min;
136 if extent.x >= extent.y && extent.x >= extent.z {
137 0
138 } else if extent.y >= extent.z {
139 1
140 } else {
141 2
142 }
143 }
144}
145
146#[derive(Clone, Copy, Debug, PartialEq)]
149pub struct GeometryHit {
150 pub t: f32,
152 pub point: Vec3,
154 pub geometric_normal: Vec3,
156 pub shading_normal: Vec3,
158 pub uv: (f32, f32),
160}
161
162pub trait Geometry: Send + Sync {
164 fn bounds(&self) -> Aabb;
166
167 fn intersect(&self, ray: Ray, t_min: f32, t_max: f32) -> Option<GeometryHit>;
169}
170
171#[derive(Clone, Copy, Debug, PartialEq)]
173pub struct Sphere {
174 pub center: Vec3,
176 pub radius: f32,
178}
179
180impl Sphere {
181 pub const fn new(center: Vec3, radius: f32) -> Self {
183 Self { center, radius }
184 }
185}
186
187impl Geometry for Sphere {
188 fn bounds(&self) -> Aabb {
189 if !is_finite_vec(self.center) || !self.radius.is_finite() || self.radius <= 0.0 {
190 return invalid_bounds();
191 }
192 let radius = vec3(self.radius, self.radius, self.radius);
193 Aabb::new(self.center - radius, self.center + radius).padded(BOUNDS_PADDING)
194 }
195
196 fn intersect(&self, ray: Ray, t_min: f32, t_max: f32) -> Option<GeometryHit> {
197 if !valid_query(ray, t_min, t_max)
198 || !is_finite_vec(self.center)
199 || !self.radius.is_finite()
200 || self.radius <= 0.0
201 {
202 return None;
203 }
204 let offset = ray.origin - self.center;
205 let a = ray.dir.dot(ray.dir);
206 if !a.is_finite() || a <= PARALLEL_EPSILON {
207 return None;
208 }
209 let half_b = offset.dot(ray.dir);
210 let c = offset.dot(offset) - self.radius * self.radius;
211 let discriminant = half_b.mul_add(half_b, -a * c);
212 if !discriminant.is_finite() || discriminant < 0.0 {
213 return None;
214 }
215 let root = discriminant.sqrt();
216 let mut distance = (-half_b - root) / a;
217 if distance < t_min || distance > t_max {
218 distance = (-half_b + root) / a;
219 if distance < t_min || distance > t_max {
220 return None;
221 }
222 }
223 let point = ray.origin + ray.dir * distance;
224 let normal = (point - self.center) * (1.0 / self.radius);
225 if !is_finite_vec(point) || !is_finite_vec(normal) {
226 return None;
227 }
228 let uv = sphere_uv(normal);
229 Some(GeometryHit { t: distance, point, geometric_normal: normal, shading_normal: normal, uv })
230 }
231}
232
233#[derive(Clone, Copy, Debug, PartialEq)]
235pub struct Quad {
236 pub origin: Vec3,
238 pub u: Vec3,
240 pub v: Vec3,
242}
243
244impl Quad {
245 pub const fn new(origin: Vec3, u: Vec3, v: Vec3) -> Self {
247 Self { origin, u, v }
248 }
249}
250
251impl Geometry for Quad {
252 fn bounds(&self) -> Aabb {
253 if !is_finite_vec(self.origin) || !is_finite_vec(self.u) || !is_finite_vec(self.v) {
254 return invalid_bounds();
255 }
256 if quad_determinant(self.u, self.v).is_none() {
257 return invalid_bounds();
258 }
259 let opposite = self.origin + self.u + self.v;
260 let a = Aabb::from_points(self.origin, opposite);
261 let b = Aabb::from_points(self.origin + self.u, self.origin + self.v);
262 a.union(b).padded(BOUNDS_PADDING)
263 }
264
265 fn intersect(&self, ray: Ray, t_min: f32, t_max: f32) -> Option<GeometryHit> {
266 if !valid_query(ray, t_min, t_max)
267 || !is_finite_vec(self.origin)
268 || !is_finite_vec(self.u)
269 || !is_finite_vec(self.v)
270 {
271 return None;
272 }
273 let cross = self.u.cross(self.v);
274 let determinant = quad_determinant(self.u, self.v)?;
275 let normal = unit_vector(cross)?;
276 let denominator = normal.dot(ray.dir);
277 if !denominator.is_finite() || denominator.abs() <= PARALLEL_EPSILON {
278 return None;
279 }
280 let distance = normal.dot(self.origin - ray.origin) / denominator;
281 if !distance.is_finite() || distance < t_min || distance > t_max {
282 return None;
283 }
284 let point = ray.origin + ray.dir * distance;
285 if !is_finite_vec(point) {
286 return None;
287 }
288 let relative = point - self.origin;
289 let uu = self.u.dot(self.u);
290 let uv = self.u.dot(self.v);
291 let vv = self.v.dot(self.v);
292 let ru = relative.dot(self.u);
293 let rv = relative.dot(self.v);
294 let s = (ru * vv - rv * uv) / determinant;
295 let t = (rv * uu - ru * uv) / determinant;
296 if !s.is_finite() || !t.is_finite() || !(0.0..=1.0).contains(&s) || !(0.0..=1.0).contains(&t)
297 {
298 return None;
299 }
300 Some(GeometryHit {
301 t: distance,
302 point,
303 geometric_normal: normal,
304 shading_normal: normal,
305 uv: (s, t),
306 })
307 }
308}
309
310#[derive(Clone, Copy, Debug, PartialEq)]
312pub struct Disk {
313 pub center: Vec3,
315 pub normal: Vec3,
317 pub radius: f32,
319}
320
321impl Disk {
322 pub const fn new(center: Vec3, normal: Vec3, radius: f32) -> Self {
324 Self { center, normal, radius }
325 }
326}
327
328impl Geometry for Disk {
329 fn bounds(&self) -> Aabb {
330 let Some(normal) = unit_vector(self.normal) else {
331 return invalid_bounds();
332 };
333 if !is_finite_vec(self.center) || !self.radius.is_finite() || self.radius <= 0.0 {
334 return invalid_bounds();
335 }
336 let extent = vec3(
337 self.radius * (1.0 - normal.x * normal.x).max(0.0).sqrt(),
338 self.radius * (1.0 - normal.y * normal.y).max(0.0).sqrt(),
339 self.radius * (1.0 - normal.z * normal.z).max(0.0).sqrt(),
340 );
341 Aabb::new(self.center - extent, self.center + extent).padded(BOUNDS_PADDING)
342 }
343
344 fn intersect(&self, ray: Ray, t_min: f32, t_max: f32) -> Option<GeometryHit> {
345 if !valid_query(ray, t_min, t_max)
346 || !is_finite_vec(self.center)
347 || !self.radius.is_finite()
348 || self.radius <= 0.0
349 {
350 return None;
351 }
352 let normal = unit_vector(self.normal)?;
353 let denominator = normal.dot(ray.dir);
354 if !denominator.is_finite() || denominator.abs() <= PARALLEL_EPSILON {
355 return None;
356 }
357 let distance = normal.dot(self.center - ray.origin) / denominator;
358 if !distance.is_finite() || distance < t_min || distance > t_max {
359 return None;
360 }
361 let point = ray.origin + ray.dir * distance;
362 if !is_finite_vec(point) {
363 return None;
364 }
365 let radial = point - self.center;
366 let scaled_radial = vec3(
367 radial.x / self.radius,
368 radial.y / self.radius,
369 radial.z / self.radius,
370 );
371 let radial_squared = scaled_radial.dot(scaled_radial);
372 if !radial_squared.is_finite() || radial_squared > 1.0 {
373 return None;
374 }
375 let (tangent, bitangent) = disk_basis(normal);
376 let uv = (0.5 + 0.5 * scaled_radial.dot(tangent), 0.5 + 0.5 * scaled_radial.dot(bitangent));
377 Some(GeometryHit { t: distance, point, geometric_normal: normal, shading_normal: normal, uv })
378 }
379}
380
381pub enum Primitive {
384 Sphere(Sphere),
386 Quad(Quad),
388 Disk(Disk),
390 Custom(Box<dyn Geometry>),
392}
393
394impl Primitive {
395 pub fn custom(geometry: impl Geometry + 'static) -> Self {
397 Self::Custom(Box::new(geometry))
398 }
399}
400
401impl From<Sphere> for Primitive {
402 fn from(value: Sphere) -> Self {
403 Self::Sphere(value)
404 }
405}
406
407impl From<Quad> for Primitive {
408 fn from(value: Quad) -> Self {
409 Self::Quad(value)
410 }
411}
412
413impl From<Disk> for Primitive {
414 fn from(value: Disk) -> Self {
415 Self::Disk(value)
416 }
417}
418
419impl Geometry for Primitive {
420 fn bounds(&self) -> Aabb {
421 match self {
422 Self::Sphere(shape) => shape.bounds(),
423 Self::Quad(shape) => shape.bounds(),
424 Self::Disk(shape) => shape.bounds(),
425 Self::Custom(shape) => shape.bounds(),
426 }
427 }
428
429 fn intersect(&self, ray: Ray, t_min: f32, t_max: f32) -> Option<GeometryHit> {
430 match self {
431 Self::Sphere(shape) => shape.intersect(ray, t_min, t_max),
432 Self::Quad(shape) => shape.intersect(ray, t_min, t_max),
433 Self::Disk(shape) => shape.intersect(ray, t_min, t_max),
434 Self::Custom(shape) => shape.intersect(ray, t_min, t_max),
435 }
436 }
437}
438
439pub struct Object {
441 pub primitive: Primitive,
443 pub material: Material,
445}
446
447impl Object {
448 pub fn new(primitive: impl Into<Primitive>, material: Material) -> Self {
450 Self { primitive: primitive.into(), material }
451 }
452
453 pub fn bounds(&self) -> Aabb {
455 self.primitive.bounds()
456 }
457
458 fn hit(&self, ray: Ray, t_min: f32, t_max: f32) -> Option<Hit<'_>> {
459 let geometry_hit = self.primitive.intersect(ray, t_min, t_max)?;
460 if !valid_geometry_hit(geometry_hit, t_min, t_max) {
461 return None;
462 }
463 let geometric_outward = unit_vector(geometry_hit.geometric_normal)?;
464 let mut shading_outward =
465 unit_vector(geometry_hit.shading_normal).unwrap_or(geometric_outward);
466 if shading_outward.dot(geometric_outward) < 0.0 {
467 shading_outward *= -1.0;
468 }
469 let front_face = ray.dir.dot(geometric_outward) < 0.0;
470 let orientation = if front_face { 1.0 } else { -1.0 };
471 let geometric_normal = geometric_outward * orientation;
472 let normal = shading_outward * orientation;
473 Some(Hit {
474 point: geometry_hit.point,
475 geometric_normal,
476 normal,
477 uv: geometry_hit.uv,
478 t: geometry_hit.t,
479 front_face,
480 material: &self.material,
481 })
482 }
483}
484
485#[derive(Clone, Copy, Debug)]
487pub struct Hit<'a> {
488 pub point: Vec3,
490 pub geometric_normal: Vec3,
492 pub normal: Vec3,
494 pub uv: (f32, f32),
496 pub t: f32,
498 pub front_face: bool,
500 pub material: &'a Material,
502}
503
504#[derive(Clone, Copy, Debug)]
505struct BvhNode {
506 bounds: Aabb,
507 kind: BvhNodeKind,
508}
509
510#[derive(Clone, Copy, Debug)]
511enum BvhNodeKind {
512 Leaf { start: usize, len: usize },
513 Branch { left: usize, right: usize },
514}
515
516pub struct Bvh {
521 objects: Vec<Object>,
522 nodes: Vec<BvhNode>,
523 root: Option<usize>,
524}
525
526impl Bvh {
527 pub fn new(mut objects: Vec<Object>) -> Self {
529 objects.retain(|object| object.bounds().is_valid());
530 let mut nodes = Vec::with_capacity(objects.len().saturating_mul(2));
531 let root = if objects.is_empty() {
532 None
533 } else {
534 let count = objects.len();
535 Some(build_node(&mut objects, &mut nodes, 0, count))
536 };
537 Self { objects, nodes, root }
538 }
539
540 pub fn objects(&self) -> &[Object] {
542 &self.objects
543 }
544
545 pub fn hit(&self, ray: Ray, t_min: f32, t_max: f32) -> Option<Hit<'_>> {
547 if !valid_query(ray, t_min, t_max) {
548 return None;
549 }
550 let root = self.root?;
551 self.nodes[root].bounds.hit_interval(ray, t_min, t_max)?;
552 let mut stack = [0usize; TRAVERSAL_STACK_SIZE];
553 let mut stack_len = 1;
554 stack[0] = root;
555 let mut closest = t_max;
556 let mut result = None;
557
558 while stack_len > 0 {
559 stack_len -= 1;
560 let node_index = stack[stack_len];
561 let node = self.nodes[node_index];
562 if node.bounds.hit_interval(ray, t_min, closest).is_none() {
563 continue;
564 }
565 match node.kind {
566 BvhNodeKind::Leaf { start, len } => {
567 for object in &self.objects[start..start + len] {
568 if let Some(hit) = object.hit(ray, t_min, closest) {
569 closest = hit.t;
570 result = Some(hit);
571 }
572 }
573 },
574 BvhNodeKind::Branch { left, right } => {
575 let left_interval = self.nodes[left].bounds.hit_interval(ray, t_min, closest);
576 let right_interval = self.nodes[right].bounds.hit_interval(ray, t_min, closest);
577 match (left_interval, right_interval) {
578 (Some(left_hit), Some(right_hit)) => {
579 let (near, far) = if left_hit.0 <= right_hit.0 {
580 (left, right)
581 } else {
582 (right, left)
583 };
584 push_node(&mut stack, &mut stack_len, far);
585 push_node(&mut stack, &mut stack_len, near);
586 },
587 (Some(_), None) => push_node(&mut stack, &mut stack_len, left),
588 (None, Some(_)) => push_node(&mut stack, &mut stack_len, right),
589 (None, None) => {},
590 }
591 },
592 }
593 }
594 result
595 }
596
597 pub fn occluded(&self, ray: Ray, t_min: f32, t_max: f32) -> bool {
599 if !valid_query(ray, t_min, t_max) {
600 return false;
601 }
602 let Some(root) = self.root else {
603 return false;
604 };
605 if self.nodes[root]
606 .bounds
607 .hit_interval(ray, t_min, t_max)
608 .is_none()
609 {
610 return false;
611 }
612 let mut stack = [0usize; TRAVERSAL_STACK_SIZE];
613 let mut stack_len = 1;
614 stack[0] = root;
615
616 while stack_len > 0 {
617 stack_len -= 1;
618 let node = self.nodes[stack[stack_len]];
619 if node.bounds.hit_interval(ray, t_min, t_max).is_none() {
620 continue;
621 }
622 match node.kind {
623 BvhNodeKind::Leaf { start, len } => {
624 if self.objects[start..start + len]
625 .iter()
626 .any(|object| object.hit(ray, t_min, t_max).is_some())
627 {
628 return true;
629 }
630 },
631 BvhNodeKind::Branch { left, right } => {
632 let left_interval = self.nodes[left].bounds.hit_interval(ray, t_min, t_max);
633 let right_interval = self.nodes[right].bounds.hit_interval(ray, t_min, t_max);
634 match (left_interval, right_interval) {
635 (Some(left_hit), Some(right_hit)) => {
636 let (near, far) = if left_hit.0 <= right_hit.0 {
637 (left, right)
638 } else {
639 (right, left)
640 };
641 push_node(&mut stack, &mut stack_len, far);
642 push_node(&mut stack, &mut stack_len, near);
643 },
644 (Some(_), None) => push_node(&mut stack, &mut stack_len, left),
645 (None, Some(_)) => push_node(&mut stack, &mut stack_len, right),
646 (None, None) => {},
647 }
648 },
649 }
650 }
651 false
652 }
653}
654
655fn build_node(objects: &mut [Object], nodes: &mut Vec<BvhNode>, start: usize, end: usize) -> usize {
656 let bounds = bounds_of(&objects[start..end]);
657 let node_index = nodes.len();
658 nodes.push(BvhNode { bounds, kind: BvhNodeKind::Leaf { start, len: end - start } });
659 if end - start <= LEAF_SIZE {
660 return node_index;
661 }
662
663 let centroid_bounds = centroid_bounds_of(&objects[start..end]);
664 let axis = centroid_bounds.longest_axis();
665 objects[start..end].sort_unstable_by(|a, b| {
666 component(a.bounds().centroid(), axis).total_cmp(&component(b.bounds().centroid(), axis))
667 });
668 let middle = start + (end - start) / 2;
669 let left = build_node(objects, nodes, start, middle);
670 let right = build_node(objects, nodes, middle, end);
671 nodes[node_index].kind = BvhNodeKind::Branch { left, right };
672 node_index
673}
674
675fn bounds_of(objects: &[Object]) -> Aabb {
676 let mut bounds = objects[0].bounds();
677 for object in &objects[1..] {
678 bounds = bounds.union(object.bounds());
679 }
680 bounds.padded(BOUNDS_PADDING)
681}
682
683fn centroid_bounds_of(objects: &[Object]) -> Aabb {
684 let first = objects[0].bounds().centroid();
685 let mut bounds = Aabb::new(first, first);
686 for object in &objects[1..] {
687 let centroid = object.bounds().centroid();
688 bounds = bounds.union(Aabb::new(centroid, centroid));
689 }
690 bounds
691}
692
693const fn push_node(stack: &mut [usize; TRAVERSAL_STACK_SIZE], len: &mut usize, node: usize) {
694 if *len < stack.len() {
696 stack[*len] = node;
697 *len += 1;
698 }
699}
700
701const fn component(value: Vec3, axis: usize) -> f32 {
702 match axis {
703 0 => value.x,
704 1 => value.y,
705 _ => value.z,
706 }
707}
708
709fn valid_query(ray: Ray, t_min: f32, t_max: f32) -> bool {
710 is_finite_vec(ray.origin)
711 && is_finite_vec(ray.dir)
712 && ray.dir.dot(ray.dir) > VECTOR_EPSILON_SQUARED
713 && t_min.is_finite()
714 && !t_max.is_nan()
715 && t_min > 0.0
716 && t_max >= t_min
717}
718
719fn valid_geometry_hit(hit: GeometryHit, t_min: f32, t_max: f32) -> bool {
720 hit.t.is_finite()
721 && hit.t >= t_min
722 && hit.t <= t_max
723 && is_finite_vec(hit.point)
724 && is_finite_vec(hit.geometric_normal)
725 && hit.uv.0.is_finite()
726 && hit.uv.1.is_finite()
727}
728
729const fn is_finite_vec(value: Vec3) -> bool {
730 value.x.is_finite() && value.y.is_finite() && value.z.is_finite()
731}
732
733fn unit_vector(value: Vec3) -> Option<Vec3> {
734 if !is_finite_vec(value) {
735 return None;
736 }
737 let length_squared = value.dot(value);
738 if !length_squared.is_finite() || length_squared <= VECTOR_EPSILON_SQUARED {
739 return None;
740 }
741 Some(value * (1.0 / length_squared.sqrt()))
742}
743
744fn quad_determinant(u: Vec3, v: Vec3) -> Option<f32> {
745 let uu = u.dot(u);
746 let uv = u.dot(v);
747 let vv = v.dot(v);
748 let determinant = uu.mul_add(vv, -uv * uv);
749 if !uu.is_finite()
750 || !vv.is_finite()
751 || !determinant.is_finite()
752 || uu <= VECTOR_EPSILON_SQUARED
753 || vv <= VECTOR_EPSILON_SQUARED
754 || determinant <= f32::EPSILON * uu * vv
755 {
756 None
757 } else {
758 Some(determinant)
759 }
760}
761
762const fn invalid_bounds() -> Aabb {
763 Aabb::new(vec3(1.0, 1.0, 1.0), vec3(-1.0, -1.0, -1.0))
764}
765
766fn sphere_uv(normal: Vec3) -> (f32, f32) {
767 let u = 0.5 + normal.z.atan2(normal.x) / (2.0 * PI);
768 let v = 0.5 - normal.y.clamp(-1.0, 1.0).asin() / PI;
769 (u, v)
770}
771
772fn disk_basis(normal: Vec3) -> (Vec3, Vec3) {
773 let helper = if normal.x.abs() < 0.9 {
774 vec3(1.0, 0.0, 0.0)
775 } else {
776 vec3(0.0, 1.0, 0.0)
777 };
778 let tangent = normal.cross(helper).normalize();
779 let bitangent = normal.cross(tangent);
780 (tangent, bitangent)
781}
782
783#[cfg(test)]
784mod tests {
785 use super::*;
786
787 const EPSILON: f32 = 1.0e-4;
788
789 fn ray(origin: Vec3, direction: Vec3) -> Ray {
790 Ray { origin, dir: direction.normalize() }
791 }
792
793 fn assert_near(actual: f32, expected: f32) {
794 assert!((actual - expected).abs() < EPSILON, "expected {expected}, got {actual}");
795 }
796
797 fn material() -> Material {
798 Material::diffuse(vec3(0.7, 0.7, 0.7))
799 }
800
801 fn object(center: Vec3, radius: f32) -> Object {
802 Object::new(Sphere::new(center, radius), material())
803 }
804
805 #[test]
806 fn aabb_slabs_handle_parallel_and_bounded_intervals() {
807 let bounds = Aabb::new(vec3(-1.0, -1.0, -1.0), vec3(1.0, 1.0, 1.0));
808 let interval = bounds
809 .hit_interval(ray(vec3(0.0, 0.0, -3.0), vec3(0.0, 0.0, 1.0)), 0.001, 10.0)
810 .unwrap();
811 assert_near(interval.0, 2.0);
812 assert_near(interval.1, 4.0);
813 assert!(
814 bounds
815 .hit_interval(ray(vec3(2.0, 0.0, -3.0), vec3(0.0, 0.0, 1.0)), 0.001, 10.0)
816 .is_none()
817 );
818 assert!(
819 bounds
820 .hit_interval(ray(vec3(0.0, 0.0, -3.0), vec3(0.0, 0.0, 1.0)), 0.001, 1.5)
821 .is_none()
822 );
823 }
824
825 #[test]
826 fn sphere_intersects_from_outside_and_inside() {
827 let sphere = Sphere::new(Vec3::ZERO, 1.0);
828 let outside = sphere
829 .intersect(ray(vec3(0.0, 0.0, -3.0), vec3(0.0, 0.0, 1.0)), 0.001, 100.0)
830 .unwrap();
831 assert_near(outside.t, 2.0);
832 assert_near(outside.point.z, -1.0);
833 let inside = sphere
834 .intersect(ray(Vec3::ZERO, vec3(1.0, 0.0, 0.0)), 0.001, 100.0)
835 .unwrap();
836 assert_near(inside.t, 1.0);
837 assert!(
838 Sphere::new(Vec3::ZERO, 0.0)
839 .intersect(ray(vec3(0.0, 0.0, -3.0), vec3(0.0, 0.0, 1.0)), 0.001, 100.0)
840 .is_none()
841 );
842 }
843
844 #[test]
845 fn finite_quad_intersects_only_inside_edges() {
846 let quad = Quad::new(vec3(-1.0, -1.0, 0.0), vec3(2.0, 0.0, 0.0), vec3(0.0, 2.0, 0.0));
847 let hit = quad
848 .intersect(ray(vec3(0.0, 0.0, -2.0), vec3(0.0, 0.0, 1.0)), 0.001, 10.0)
849 .unwrap();
850 assert_near(hit.t, 2.0);
851 assert_near(hit.uv.0, 0.5);
852 assert_near(hit.uv.1, 0.5);
853 assert!(
854 quad
855 .intersect(ray(vec3(2.0, 0.0, -2.0), vec3(0.0, 0.0, 1.0)), 0.001, 10.0)
856 .is_none()
857 );
858 }
859
860 #[test]
861 fn oriented_disk_uses_its_plane_and_radius() {
862 let disk = Disk::new(Vec3::ZERO, vec3(0.0, 1.0, 1.0), 2.0);
863 let normal = vec3(0.0, 1.0, 1.0).normalize();
864 let hit = disk
865 .intersect(ray(normal * -3.0, normal), 0.001, 10.0)
866 .unwrap();
867 assert_near(hit.t, 3.0);
868 assert_near(hit.uv.0, 0.5);
869 assert_near(hit.uv.1, 0.5);
870 let tangent = disk_basis(normal).0;
871 assert!(
872 disk
873 .intersect(ray(tangent * 2.1 - normal * 3.0, normal), 0.001, 10.0)
874 .is_none()
875 );
876 }
877
878 #[test]
879 fn subnormal_disk_center_remains_intersectable() {
880 let disk = Disk::new(Vec3::ZERO, vec3(0.0, 0.0, 1.0), f32::from_bits(1));
881 let hit = disk
882 .intersect(
883 ray(vec3(0.0, 0.0, -1.0), vec3(0.0, 0.0, 1.0)),
884 0.001,
885 10.0,
886 )
887 .unwrap();
888 assert_eq!(hit.uv, (0.5, 0.5));
889 }
890
891 #[test]
892 fn object_orients_both_faces_against_the_ray() {
893 let object = object(Vec3::ZERO, 1.0);
894 let front = object
895 .hit(ray(vec3(0.0, 0.0, -3.0), vec3(0.0, 0.0, 1.0)), 0.001, 10.0)
896 .unwrap();
897 assert!(front.front_face);
898 assert_near(front.normal.z, -1.0);
899 let back = object
900 .hit(ray(Vec3::ZERO, vec3(0.0, 0.0, 1.0)), 0.001, 10.0)
901 .unwrap();
902 assert!(!back.front_face);
903 assert_near(back.geometric_normal.z, -1.0);
904 assert!(back.normal.dot(vec3(0.0, 0.0, 1.0)) < 0.0);
905 }
906
907 #[test]
908 fn bvh_selects_nearest_independent_of_insertion_order() {
909 let trace = ray(vec3(0.0, 0.0, -10.0), vec3(0.0, 0.0, 1.0));
910 let forward = Bvh::new(vec![object(vec3(0.0, 0.0, 2.0), 1.0), object(Vec3::ZERO, 1.0)]);
911 let reverse = Bvh::new(vec![object(Vec3::ZERO, 1.0), object(vec3(0.0, 0.0, 2.0), 1.0)]);
912 assert_near(forward.hit(trace, 0.001, f32::INFINITY).unwrap().t, 9.0);
913 assert_near(reverse.hit(trace, 0.001, 100.0).unwrap().t, 9.0);
914 }
915
916 #[test]
917 fn occlusion_respects_maximum_distance() {
918 let bvh = Bvh::new(vec![object(Vec3::ZERO, 1.0)]);
919 let trace = ray(vec3(0.0, 0.0, -5.0), vec3(0.0, 0.0, 1.0));
920 assert!(!bvh.occluded(trace, 0.001, 3.99));
921 assert!(bvh.occluded(trace, 0.001, 4.01));
922 }
923
924 #[test]
925 fn bvh_matches_linear_reference_for_deterministic_rays() {
926 let mut objects = Vec::new();
927 for z in -2..=2 {
928 for x in -3..=3 {
929 objects.push(object(vec3(x as f32 * 1.3, (x * z) as f32 * 0.07, z as f32 * 1.4), 0.42));
930 }
931 }
932 let bvh = Bvh::new(objects);
933 for index in 0..257 {
934 let x = ((index * 73 % 257) as f32 / 128.0 - 1.0) * 0.8;
935 let y = ((index * 151 % 263) as f32 / 131.0 - 1.0) * 0.55;
936 let trace = ray(vec3(0.0, 0.0, -9.0), vec3(x, y, 1.0));
937 let accelerated = bvh.hit(trace, 0.001, 100.0).map(|hit| hit.t);
938 let mut linear: Option<f32> = None;
939 for object in bvh.objects() {
940 let limit = linear.unwrap_or(100.0);
941 if let Some(hit) = object.hit(trace, 0.001, limit) {
942 linear = Some(hit.t);
943 }
944 }
945 match (accelerated, linear) {
946 (Some(a), Some(b)) => assert_near(a, b),
947 (None, None) => {},
948 pair => panic!("BVH and linear traversal disagree: {pair:?}"),
949 }
950 }
951 }
952}