Skip to main content

omp_tui/scene/
geometry.rs

1//! Analytic geometry and a compact bounding-volume hierarchy for ray tracing.
2
3use 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/// An axis-aligned bounding box represented by inclusive minimum and maximum
14/// corners.
15#[derive(Clone, Copy, Debug, PartialEq)]
16pub struct Aabb {
17	/// Smallest coordinate on each axis.
18	pub min: Vec3,
19	/// Largest coordinate on each axis.
20	pub max: Vec3,
21}
22
23impl Aabb {
24	/// Creates a bounding box from already ordered corners.
25	pub const fn new(min: Vec3, max: Vec3) -> Self {
26		Self { min, max }
27	}
28
29	/// Creates the smallest box containing both points.
30	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	/// Returns whether every coordinate is finite and the corners are ordered.
38	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	/// Returns the smallest box containing both input boxes.
47	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	/// Returns the midpoint of the box without overflowing finite coordinates.
63	pub fn centroid(self) -> Vec3 {
64		self.min * 0.5 + self.max * 0.5
65	}
66
67	/// Expands thin axes symmetrically to at least `minimum_extent` wide.
68	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	/// Clips a ray interval against this box using the slab method.
93	///
94	/// Parallel rays are accepted only when their origin lies inside that slab.
95	/// Invalid boxes, rays, or intervals return `None` rather than propagating
96	/// NaNs.
97	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/// Geometry-local intersection data before a material and face orientation are
147/// applied.
148#[derive(Clone, Copy, Debug, PartialEq)]
149pub struct GeometryHit {
150	/// Ray parameter at the intersection.
151	pub t:                f32,
152	/// World-space intersection point.
153	pub point:            Vec3,
154	/// Outward geometric surface normal.
155	pub geometric_normal: Vec3,
156	/// Outward smooth or perturbed normal used for shading.
157	pub shading_normal:   Vec3,
158	/// Surface coordinates in the unit square where the primitive defines them.
159	pub uv:               (f32, f32),
160}
161
162/// A bounded, thread-safe ray-intersectable shape.
163pub trait Geometry: Send + Sync {
164	/// Returns a finite world-space bound for the shape.
165	fn bounds(&self) -> Aabb;
166
167	/// Returns the closest intersection within the inclusive ray interval.
168	fn intersect(&self, ray: Ray, t_min: f32, t_max: f32) -> Option<GeometryHit>;
169}
170
171/// An analytic sphere.
172#[derive(Clone, Copy, Debug, PartialEq)]
173pub struct Sphere {
174	/// Sphere center in world space.
175	pub center: Vec3,
176	/// Sphere radius; non-positive or non-finite radii never intersect.
177	pub radius: f32,
178}
179
180impl Sphere {
181	/// Creates a sphere from its center and radius.
182	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/// A finite parallelogram spanned from one corner by two edge vectors.
234#[derive(Clone, Copy, Debug, PartialEq)]
235pub struct Quad {
236	/// Corner corresponding to UV coordinate `(0, 0)`.
237	pub origin: Vec3,
238	/// Edge from `(0, 0)` to `(1, 0)`.
239	pub u:      Vec3,
240	/// Edge from `(0, 0)` to `(0, 1)`.
241	pub v:      Vec3,
242}
243
244impl Quad {
245	/// Creates a finite quad from a corner and two non-collinear edge vectors.
246	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/// A circular disk in an arbitrarily oriented plane.
311#[derive(Clone, Copy, Debug, PartialEq)]
312pub struct Disk {
313	/// Disk center in world space.
314	pub center: Vec3,
315	/// Disk plane normal; it is normalized when queried.
316	pub normal: Vec3,
317	/// Disk radius; non-positive or non-finite radii never intersect.
318	pub radius: f32,
319}
320
321impl Disk {
322	/// Creates an oriented disk from its center, normal, and radius.
323	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
381/// Common geometry stored inline, with a boxed fallback for user-defined
382/// shapes.
383pub enum Primitive {
384	/// An inline analytic sphere.
385	Sphere(Sphere),
386	/// An inline finite quad.
387	Quad(Quad),
388	/// An inline oriented disk.
389	Disk(Disk),
390	/// A custom shape allocated once when the scene is constructed.
391	Custom(Box<dyn Geometry>),
392}
393
394impl Primitive {
395	/// Boxes a custom shape for storage alongside the built-in primitives.
396	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
439/// A scene primitive paired with its surface material.
440pub struct Object {
441	/// Shape used for bounds and intersection queries.
442	pub primitive: Primitive,
443	/// Surface material returned with intersections.
444	pub material:  Material,
445}
446
447impl Object {
448	/// Creates a renderable object from any built-in or explicit [`Primitive`].
449	pub fn new(primitive: impl Into<Primitive>, material: Material) -> Self {
450		Self { primitive: primitive.into(), material }
451	}
452
453	/// Returns the object's world-space bounds.
454	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/// A fully oriented scene intersection borrowing its object's material.
486#[derive(Clone, Copy, Debug)]
487pub struct Hit<'a> {
488	/// World-space intersection point.
489	pub point:            Vec3,
490	/// Geometric normal oriented against the incoming ray.
491	pub geometric_normal: Vec3,
492	/// Shading normal oriented against the incoming ray and geometric normal.
493	pub normal:           Vec3,
494	/// Surface coordinates supplied by the primitive.
495	pub uv:               (f32, f32),
496	/// Positive ray parameter at the intersection.
497	pub t:                f32,
498	/// Whether the ray arrived on the outward-facing side of the surface.
499	pub front_face:       bool,
500	/// Material owned by the intersected object.
501	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
516/// An owning bounding-volume hierarchy over scene objects.
517///
518/// Construction discards objects with invalid bounds, orders the remaining
519/// objects in place by median centroid splits, and stores no per-ray state.
520pub struct Bvh {
521	objects: Vec<Object>,
522	nodes:   Vec<BvhNode>,
523	root:    Option<usize>,
524}
525
526impl Bvh {
527	/// Builds a balanced hierarchy from owned objects.
528	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	/// Returns the objects in BVH leaf order.
541	pub fn objects(&self) -> &[Object] {
542		&self.objects
543	}
544
545	/// Finds the nearest valid intersection inside the inclusive ray interval.
546	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	/// Returns as soon as any object intersects inside the inclusive interval.
598	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	// Median splitting bounds the pending-node count by the machine word width.
695	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}