1#[inline]
15fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
16 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
17}
18
19#[inline]
21fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
22 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
23}
24
25#[inline]
27fn scale3(v: [f64; 3], s: f64) -> [f64; 3] {
28 [v[0] * s, v[1] * s, v[2] * s]
29}
30
31#[inline]
33fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
34 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
35}
36
37#[inline]
39fn length3(v: [f64; 3]) -> f64 {
40 dot3(v, v).sqrt()
41}
42
43#[inline]
45fn normalize3(v: [f64; 3]) -> [f64; 3] {
46 let len = length3(v);
47 if len < 1e-15 {
48 return [0.0; 3];
49 }
50 scale3(v, 1.0 / len)
51}
52
53#[inline]
55fn clamp(v: f64, lo: f64, hi: f64) -> f64 {
56 v.max(lo).min(hi)
57}
58
59#[inline]
61fn max3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
62 [a[0].max(b[0]), a[1].max(b[1]), a[2].max(b[2])]
63}
64
65#[cfg(test)]
67#[inline]
68fn abs3(v: [f64; 3]) -> [f64; 3] {
69 [v[0].abs(), v[1].abs(), v[2].abs()]
70}
71
72#[derive(Debug, Clone, Copy)]
76pub struct Ray {
77 pub origin: [f64; 3],
79 pub direction: [f64; 3],
81}
82
83impl Ray {
84 pub fn new(origin: [f64; 3], direction: [f64; 3]) -> Self {
88 Self {
89 origin,
90 direction: normalize3(direction),
91 }
92 }
93
94 pub fn at(&self, t: f64) -> [f64; 3] {
96 [
97 self.origin[0] + t * self.direction[0],
98 self.origin[1] + t * self.direction[1],
99 self.origin[2] + t * self.direction[2],
100 ]
101 }
102}
103
104pub trait Sdf {
109 fn distance(&self, p: [f64; 3]) -> f64;
111
112 fn normal(&self, p: [f64; 3]) -> [f64; 3] {
117 let e = 1e-5;
118 let dx = self.distance([p[0] + e, p[1], p[2]]) - self.distance([p[0] - e, p[1], p[2]]);
119 let dy = self.distance([p[0], p[1] + e, p[2]]) - self.distance([p[0], p[1] - e, p[2]]);
120 let dz = self.distance([p[0], p[1], p[2] + e]) - self.distance([p[0], p[1], p[2] - e]);
121 normalize3([dx, dy, dz])
122 }
123}
124
125#[derive(Debug, Clone, Copy)]
129pub struct SphereSdf {
130 pub center: [f64; 3],
132 pub radius: f64,
134}
135
136impl SphereSdf {
137 pub fn new(center: [f64; 3], radius: f64) -> Self {
139 Self { center, radius }
140 }
141}
142
143impl Sdf for SphereSdf {
144 fn distance(&self, p: [f64; 3]) -> f64 {
145 let d = sub3(p, self.center);
146 length3(d) - self.radius
147 }
148}
149
150#[derive(Debug, Clone, Copy)]
154pub struct BoxSdf {
155 pub half_extents: [f64; 3],
157}
158
159impl BoxSdf {
160 pub fn new(half_extents: [f64; 3]) -> Self {
162 Self { half_extents }
163 }
164}
165
166impl Sdf for BoxSdf {
167 fn distance(&self, p: [f64; 3]) -> f64 {
168 let q = [
170 p[0].abs() - self.half_extents[0],
171 p[1].abs() - self.half_extents[1],
172 p[2].abs() - self.half_extents[2],
173 ];
174 let outside = length3(max3(q, [0.0; 3]));
175 let inside = q[0].max(q[1]).max(q[2]).min(0.0);
176 outside + inside
177 }
178}
179
180#[derive(Debug, Clone, Copy)]
184pub struct CapsuleSdf {
185 pub a: [f64; 3],
187 pub b: [f64; 3],
189 pub r: f64,
191}
192
193impl CapsuleSdf {
194 pub fn new(a: [f64; 3], b: [f64; 3], r: f64) -> Self {
196 Self { a, b, r }
197 }
198}
199
200impl Sdf for CapsuleSdf {
201 fn distance(&self, p: [f64; 3]) -> f64 {
202 let pa = sub3(p, self.a);
203 let ba = sub3(self.b, self.a);
204 let h = clamp(dot3(pa, ba) / dot3(ba, ba), 0.0, 1.0);
205 let closest = sub3(pa, scale3(ba, h));
206 length3(closest) - self.r
207 }
208}
209
210#[derive(Debug, Clone, Copy)]
214pub struct TorusSdf {
215 pub r_major: f64,
217 pub r_minor: f64,
219}
220
221impl TorusSdf {
222 pub fn new(r_major: f64, r_minor: f64) -> Self {
224 Self { r_major, r_minor }
225 }
226}
227
228impl Sdf for TorusSdf {
229 fn distance(&self, p: [f64; 3]) -> f64 {
230 let q_x = (p[0] * p[0] + p[2] * p[2]).sqrt() - self.r_major;
232 let q_y = p[1];
233 (q_x * q_x + q_y * q_y).sqrt() - self.r_minor
234 }
235}
236
237pub fn sdf_union(a: f64, b: f64) -> f64 {
243 a.min(b)
244}
245
246pub fn sdf_intersection(a: f64, b: f64) -> f64 {
251 a.max(b)
252}
253
254pub fn sdf_subtraction(a: f64, b: f64) -> f64 {
258 a.max(-b)
259}
260
261pub fn sdf_smooth_union(a: f64, b: f64, k: f64) -> f64 {
266 if k < 1e-15 {
267 return sdf_union(a, b);
268 }
269 let h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
270 let mix = b + h * (a - b);
271 mix - k * h * (1.0 - h)
272}
273
274#[derive(Debug, Clone, Copy)]
278pub struct RayMarchResult {
279 pub hit: bool,
281 pub t: f64,
283 pub steps: usize,
285 pub normal: [f64; 3],
287}
288
289impl RayMarchResult {
290 pub fn miss(t: f64, steps: usize) -> Self {
292 Self {
293 hit: false,
294 t,
295 steps,
296 normal: [0.0; 3],
297 }
298 }
299
300 pub fn hit(t: f64, steps: usize, normal: [f64; 3]) -> Self {
302 Self {
303 hit: true,
304 t,
305 steps,
306 normal,
307 }
308 }
309}
310
311pub fn ray_march(
322 ray: &Ray,
323 sdf: &dyn Sdf,
324 max_steps: usize,
325 max_dist: f64,
326 eps: f64,
327) -> RayMarchResult {
328 let mut t = 0.0_f64;
329 for step in 0..max_steps {
330 let p = ray.at(t);
331 let d = sdf.distance(p);
332 if d.abs() < eps {
333 let normal = sdf.normal(p);
334 return RayMarchResult::hit(t, step + 1, normal);
335 }
336 t += d;
337 if t >= max_dist {
338 return RayMarchResult::miss(max_dist, step + 1);
339 }
340 }
341 RayMarchResult::miss(t, max_steps)
342}
343
344#[derive(Debug, Clone, Copy)]
348pub struct AmbientOcclusion {
349 pub samples: usize,
351 pub radius: f64,
353 pub falloff: f64,
355}
356
357impl AmbientOcclusion {
358 pub fn new(samples: usize, radius: f64, falloff: f64) -> Self {
360 Self {
361 samples,
362 radius,
363 falloff,
364 }
365 }
366
367 pub fn compute(&self, pos: [f64; 3], normal: [f64; 3], sdf: &dyn Sdf) -> f64 {
372 if self.samples == 0 {
373 return 1.0;
374 }
375 let mut occ = 0.0_f64;
376 for i in 1..=self.samples {
377 let t = self.radius * (i as f64) / (self.samples as f64);
378 let sample_pos = add3(pos, scale3(normal, t));
379 let d = sdf.distance(sample_pos);
380 occ += (t - d).max(0.0) / t.powf(self.falloff);
381 }
382 (1.0 - occ / (self.samples as f64)).clamp(0.0, 1.0)
383 }
384}
385
386#[derive(Debug, Clone, Copy)]
390pub struct SoftShadow {
391 pub light_dir: [f64; 3],
393 pub k: f64,
395}
396
397impl SoftShadow {
398 pub fn new(light_dir: [f64; 3], k: f64) -> Self {
400 Self {
401 light_dir: normalize3(light_dir),
402 k,
403 }
404 }
405
406 pub fn compute(&self, pos: [f64; 3], sdf: &dyn Sdf, max_dist: f64) -> f64 {
410 let eps = 1e-4;
411 let mut res = 1.0_f64;
412 let mut t = eps;
413 while t < max_dist {
414 let p = add3(pos, scale3(self.light_dir, t));
415 let d = sdf.distance(p);
416 if d < eps {
417 return 0.0;
418 }
419 res = res.min(self.k * d / t);
420 t += d;
421 }
422 res.clamp(0.0, 1.0)
423 }
424}
425
426#[derive(Debug, Clone)]
431pub struct RayMarchRenderer {
432 pub width: usize,
434 pub height: usize,
436 pub fov: f64,
438 pub camera_pos: [f64; 3],
440 pub camera_target: [f64; 3],
442}
443
444impl RayMarchRenderer {
445 pub fn new(
447 width: usize,
448 height: usize,
449 fov: f64,
450 camera_pos: [f64; 3],
451 camera_target: [f64; 3],
452 ) -> Self {
453 Self {
454 width,
455 height,
456 fov,
457 camera_pos,
458 camera_target,
459 }
460 }
461
462 fn camera_basis(&self) -> ([f64; 3], [f64; 3], [f64; 3]) {
464 let world_up = [0.0_f64, 1.0, 0.0];
465 let forward = normalize3(sub3(self.camera_target, self.camera_pos));
466 let right = normalize3(cross3_fn(forward, world_up));
467 let up = cross3_fn(right, forward);
468 (right, up, forward)
469 }
470
471 pub fn generate_ray(&self, px: usize, py: usize) -> Ray {
475 let (right, up, forward) = self.camera_basis();
476 let aspect = self.width as f64 / self.height.max(1) as f64;
477 let half_h = (self.fov * 0.5).tan();
478 let half_w = half_h * aspect;
479
480 let u = (2.0 * (px as f64 + 0.5) / self.width as f64 - 1.0) * half_w;
482 let v = (1.0 - 2.0 * (py as f64 + 0.5) / self.height as f64) * half_h;
483
484 let dir = add3(add3(forward, scale3(right, u)), scale3(up, v));
485 Ray::new(self.camera_pos, dir)
486 }
487
488 pub fn render_depth(&self, sdf: &dyn Sdf) -> Vec<f64> {
493 let n = self.width * self.height;
494 let mut depth = Vec::with_capacity(n);
495 for py in 0..self.height {
496 for px in 0..self.width {
497 let ray = self.generate_ray(px, py);
498 let result = ray_march(&ray, sdf, 256, 1000.0, 1e-5);
499 depth.push(if result.hit { result.t } else { f64::INFINITY });
500 }
501 }
502 depth
503 }
504
505 pub fn render_normals(&self, sdf: &dyn Sdf) -> Vec<[f64; 3]> {
510 let n = self.width * self.height;
511 let mut normals = Vec::with_capacity(n);
512 for py in 0..self.height {
513 for px in 0..self.width {
514 let ray = self.generate_ray(px, py);
515 let result = ray_march(&ray, sdf, 256, 1000.0, 1e-5);
516 normals.push(result.normal);
517 }
518 }
519 normals
520 }
521}
522
523#[inline]
525fn cross3_fn(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
526 [
527 a[1] * b[2] - a[2] * b[1],
528 a[2] * b[0] - a[0] * b[2],
529 a[0] * b[1] - a[1] * b[0],
530 ]
531}
532
533pub fn subsurface_scattering_approx(depth: f64, scatter_coeff: f64, absorption: f64) -> f64 {
548 let extinction = scatter_coeff + absorption;
549 (-extinction * depth.max(0.0)).exp()
550}
551
552#[cfg(test)]
555mod tests {
556 use super::*;
557 use std::f64::consts::PI;
558
559 #[test]
562 fn ray_at_origin() {
563 let ray = Ray::new([0.0; 3], [0.0, 0.0, 1.0]);
564 let p = ray.at(0.0);
565 assert_eq!(p, [0.0; 3]);
566 }
567
568 #[test]
569 fn ray_at_t_positive() {
570 let ray = Ray::new([1.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
571 let p = ray.at(5.0);
572 assert!((p[0] - 1.0).abs() < 1e-12);
573 assert!((p[2] - 5.0).abs() < 1e-12);
574 }
575
576 #[test]
577 fn ray_direction_normalized() {
578 let ray = Ray::new([0.0; 3], [3.0, 4.0, 0.0]);
579 let len = length3(ray.direction);
580 assert!((len - 1.0).abs() < 1e-12);
581 }
582
583 #[test]
584 fn ray_at_negative_t() {
585 let ray = Ray::new([0.0; 3], [1.0, 0.0, 0.0]);
586 let p = ray.at(-2.0);
587 assert!((p[0] + 2.0).abs() < 1e-12);
588 }
589
590 #[test]
593 fn sphere_distance_outside() {
594 let s = SphereSdf::new([0.0; 3], 1.0);
595 let d = s.distance([2.0, 0.0, 0.0]);
596 assert!((d - 1.0).abs() < 1e-10);
597 }
598
599 #[test]
600 fn sphere_distance_inside() {
601 let s = SphereSdf::new([0.0; 3], 1.0);
602 let d = s.distance([0.0; 3]);
603 assert!((d + 1.0).abs() < 1e-10);
604 }
605
606 #[test]
607 fn sphere_distance_surface() {
608 let s = SphereSdf::new([0.0; 3], 1.0);
609 let d = s.distance([1.0, 0.0, 0.0]);
610 assert!(d.abs() < 1e-12);
611 }
612
613 #[test]
614 fn sphere_normal_at_x_axis() {
615 let s = SphereSdf::new([0.0; 3], 1.0);
616 let n = s.normal([1.0, 0.0, 0.0]);
617 assert!((n[0] - 1.0).abs() < 1e-4);
618 assert!(n[1].abs() < 1e-4);
619 assert!(n[2].abs() < 1e-4);
620 }
621
622 #[test]
623 fn sphere_translated_center() {
624 let s = SphereSdf::new([5.0, 0.0, 0.0], 2.0);
625 let d = s.distance([7.0, 0.0, 0.0]);
626 assert!(d.abs() < 1e-12);
627 }
628
629 #[test]
632 fn box_distance_outside_along_x() {
633 let b = BoxSdf::new([1.0; 3]);
634 let d = b.distance([3.0, 0.0, 0.0]);
635 assert!((d - 2.0).abs() < 1e-12);
636 }
637
638 #[test]
639 fn box_distance_inside() {
640 let b = BoxSdf::new([1.0; 3]);
641 let d = b.distance([0.0; 3]);
642 assert!(d < 0.0);
643 }
644
645 #[test]
646 fn box_distance_on_face() {
647 let b = BoxSdf::new([1.0, 1.0, 1.0]);
648 let d = b.distance([1.0, 0.0, 0.0]);
649 assert!(d.abs() < 1e-12);
650 }
651
652 #[test]
653 fn box_normal_x_face() {
654 let b = BoxSdf::new([1.0; 3]);
655 let n = b.normal([1.0, 0.0, 0.0]);
656 assert!((n[0] - 1.0).abs() < 1e-3);
657 }
658
659 #[test]
662 fn capsule_distance_at_midpoint() {
663 let c = CapsuleSdf::new([0.0, -1.0, 0.0], [0.0, 1.0, 0.0], 0.5);
664 let d = c.distance([0.5, 0.0, 0.0]);
665 assert!(d.abs() < 1e-10);
666 }
667
668 #[test]
669 fn capsule_distance_outside() {
670 let c = CapsuleSdf::new([0.0, -1.0, 0.0], [0.0, 1.0, 0.0], 0.5);
671 let d = c.distance([2.0, 0.0, 0.0]);
672 assert!((d - 1.5).abs() < 1e-10);
673 }
674
675 #[test]
676 fn capsule_distance_at_cap() {
677 let c = CapsuleSdf::new([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], 0.5);
678 let d = c.distance([0.0, 2.0, 0.0]);
680 assert!((d - 0.5).abs() < 1e-10);
681 }
682
683 #[test]
686 fn torus_distance_on_ring() {
687 let t = TorusSdf::new(2.0, 0.5);
689 let d = t.distance([2.0, 0.5, 0.0]);
690 assert!(d.abs() < 1e-10);
691 }
692
693 #[test]
694 fn torus_distance_centre_negative() {
695 let t = TorusSdf::new(2.0, 0.5);
696 let d = t.distance([0.0; 3]);
698 assert!((d - 1.5).abs() < 1e-10);
700 }
701
702 #[test]
703 fn torus_distance_outside_far() {
704 let t = TorusSdf::new(1.0, 0.2);
705 let d = t.distance([10.0, 0.0, 0.0]);
706 assert!(d > 0.0);
707 }
708
709 #[test]
712 fn sdf_union_picks_min() {
713 assert_eq!(sdf_union(3.0, 1.0), 1.0);
714 assert_eq!(sdf_union(-1.0, 2.0), -1.0);
715 }
716
717 #[test]
718 fn sdf_intersection_picks_max() {
719 assert_eq!(sdf_intersection(3.0, 1.0), 3.0);
720 }
721
722 #[test]
723 fn sdf_subtraction_correctness() {
724 assert_eq!(sdf_subtraction(1.0, -2.0), 2.0);
726 assert_eq!(sdf_subtraction(1.0, 3.0), 1.0);
727 }
728
729 #[test]
730 fn sdf_smooth_union_degenerate() {
731 let su = sdf_smooth_union(3.0, 1.0, 0.0);
733 assert!((su - sdf_union(3.0, 1.0)).abs() < 1e-10);
734 }
735
736 #[test]
737 fn sdf_smooth_union_blends() {
738 let su = sdf_smooth_union(0.0, 0.0, 1.0);
739 assert!(su <= 0.0 + 1e-10);
741 }
742
743 #[test]
744 fn sdf_smooth_union_between_values() {
745 let a = 2.0_f64;
747 let b = 3.0_f64;
748 let su = sdf_smooth_union(a, b, 0.5);
749 assert!(su <= a + 1e-10);
750 }
751
752 #[test]
755 fn ray_march_result_miss() {
756 let r = RayMarchResult::miss(100.0, 10);
757 assert!(!r.hit);
758 assert!((r.t - 100.0).abs() < 1e-12);
759 }
760
761 #[test]
762 fn ray_march_result_hit() {
763 let r = RayMarchResult::hit(5.0, 20, [1.0, 0.0, 0.0]);
764 assert!(r.hit);
765 assert!((r.normal[0] - 1.0).abs() < 1e-12);
766 }
767
768 #[test]
771 fn ray_march_hits_sphere() {
772 let sphere = SphereSdf::new([0.0, 0.0, 5.0], 1.0);
773 let ray = Ray::new([0.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
774 let result = ray_march(&ray, &sphere, 256, 100.0, 1e-5);
775 assert!(result.hit);
776 assert!((result.t - 4.0).abs() < 1e-3);
777 }
778
779 #[test]
780 fn ray_march_misses_sphere() {
781 let sphere = SphereSdf::new([0.0, 0.0, 5.0], 1.0);
782 let ray = Ray::new([10.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
783 let result = ray_march(&ray, &sphere, 256, 100.0, 1e-5);
784 assert!(!result.hit);
785 }
786
787 #[test]
788 fn ray_march_steps_bounded() {
789 let sphere = SphereSdf::new([0.0, 0.0, 5.0], 1.0);
790 let ray = Ray::new([0.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
791 let result = ray_march(&ray, &sphere, 128, 100.0, 1e-5);
792 assert!(result.steps <= 128);
793 }
794
795 #[test]
796 fn ray_march_hit_normal_approx_minus_z() {
797 let sphere = SphereSdf::new([0.0, 0.0, 5.0], 1.0);
798 let ray = Ray::new([0.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
799 let result = ray_march(&ray, &sphere, 256, 100.0, 1e-5);
800 assert!(result.hit);
802 assert!(result.normal[2] < 0.0);
803 }
804
805 #[test]
808 fn ao_open_space_returns_one() {
809 let sphere = SphereSdf::new([0.0; 3], 1.0);
811 let ao = AmbientOcclusion::new(8, 1.0, 1.0);
812 let pos = [100.0, 0.0, 0.0];
813 let normal = [1.0, 0.0, 0.0];
814 let v = ao.compute(pos, normal, &sphere);
815 assert!(v > 0.5);
816 }
817
818 #[test]
819 fn ao_samples_zero_returns_one() {
820 let sphere = SphereSdf::new([0.0; 3], 1.0);
821 let ao = AmbientOcclusion::new(0, 1.0, 1.0);
822 let v = ao.compute([0.0; 3], [0.0, 1.0, 0.0], &sphere);
823 assert!((v - 1.0).abs() < 1e-12);
824 }
825
826 #[test]
827 fn ao_result_clamped() {
828 let sphere = SphereSdf::new([0.0; 3], 1.0);
829 let ao = AmbientOcclusion::new(4, 0.5, 1.0);
830 let pos = [1.0, 0.0, 0.0];
831 let normal = [1.0, 0.0, 0.0];
832 let v = ao.compute(pos, normal, &sphere);
833 assert!((0.0..=1.0).contains(&v));
834 }
835
836 #[test]
839 fn soft_shadow_lit_no_occluder() {
840 let sphere = SphereSdf::new([0.0, 0.0, -100.0], 0.1);
842 let ss = SoftShadow::new([0.0, 1.0, 0.0], 8.0);
843 let pos = [0.0, 0.0, 0.0];
844 let v = ss.compute(pos, &sphere, 50.0);
845 assert!(v > 0.9);
846 }
847
848 #[test]
849 fn soft_shadow_shadow_with_occluder() {
850 let sphere = SphereSdf::new([0.0, 5.0, 0.0], 1.0);
852 let ss = SoftShadow::new([0.0, 1.0, 0.0], 8.0);
853 let pos = [0.0, 0.0, 0.0];
854 let v = ss.compute(pos, &sphere, 20.0);
855 assert!(v < 0.5);
856 }
857
858 #[test]
859 fn soft_shadow_result_in_range() {
860 let sphere = SphereSdf::new([0.0; 3], 1.0);
861 let ss = SoftShadow::new([1.0, 1.0, 1.0], 4.0);
862 let v = ss.compute([3.0, 0.0, 0.0], &sphere, 20.0);
863 assert!((0.0..=1.0).contains(&v));
864 }
865
866 #[test]
869 fn renderer_depth_buffer_size() {
870 let renderer = RayMarchRenderer::new(4, 4, PI / 3.0, [0.0, 0.0, -5.0], [0.0, 0.0, 0.0]);
871 let sphere = SphereSdf::new([0.0; 3], 1.0);
872 let depth = renderer.render_depth(&sphere);
873 assert_eq!(depth.len(), 16);
874 }
875
876 #[test]
877 fn renderer_normals_buffer_size() {
878 let renderer = RayMarchRenderer::new(2, 2, PI / 3.0, [0.0, 0.0, -5.0], [0.0, 0.0, 0.0]);
879 let sphere = SphereSdf::new([0.0; 3], 1.0);
880 let normals = renderer.render_normals(&sphere);
881 assert_eq!(normals.len(), 4);
882 }
883
884 #[test]
885 fn renderer_center_ray_hits_sphere() {
886 let renderer = RayMarchRenderer::new(11, 11, PI / 3.0, [0.0, 0.0, -5.0], [0.0, 0.0, 0.0]);
887 let sphere = SphereSdf::new([0.0; 3], 1.0);
888 let depth = renderer.render_depth(&sphere);
889 let center_t = depth[5 * 11 + 5];
891 assert!(center_t < f64::INFINITY);
892 }
893
894 #[test]
895 fn renderer_corner_ray_misses_small_sphere() {
896 let renderer = RayMarchRenderer::new(11, 11, PI / 3.0, [0.0, 0.0, -5.0], [0.0, 0.0, 0.0]);
897 let sphere = SphereSdf::new([0.0; 3], 0.01);
898 let depth = renderer.render_depth(&sphere);
899 assert_eq!(depth[0], f64::INFINITY);
901 }
902
903 #[test]
904 fn renderer_generate_ray_center() {
905 let renderer = RayMarchRenderer::new(11, 11, PI / 3.0, [0.0, 0.0, -5.0], [0.0, 0.0, 0.0]);
906 let ray = renderer.generate_ray(5, 5);
907 assert!(ray.direction[2] > 0.0);
909 }
910
911 #[test]
914 fn sss_zero_depth_returns_one() {
915 let v = subsurface_scattering_approx(0.0, 1.0, 1.0);
916 assert!((v - 1.0).abs() < 1e-12);
917 }
918
919 #[test]
920 fn sss_large_depth_near_zero() {
921 let v = subsurface_scattering_approx(1000.0, 1.0, 0.0);
922 assert!(v < 1e-10);
923 }
924
925 #[test]
926 fn sss_negative_depth_clamped() {
927 let v = subsurface_scattering_approx(-5.0, 1.0, 1.0);
928 assert!((v - 1.0).abs() < 1e-12);
929 }
930
931 #[test]
932 fn sss_result_in_range() {
933 let v = subsurface_scattering_approx(0.5, 2.0, 1.0);
934 assert!((0.0..=1.0).contains(&v));
935 }
936
937 #[test]
938 fn sss_monotone_decreasing() {
939 let v1 = subsurface_scattering_approx(1.0, 1.0, 0.5);
940 let v2 = subsurface_scattering_approx(2.0, 1.0, 0.5);
941 assert!(v1 > v2);
942 }
943
944 #[test]
947 fn test_add3() {
948 let r = add3([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]);
949 assert_eq!(r, [5.0, 7.0, 9.0]);
950 }
951
952 #[test]
953 fn test_sub3() {
954 let r = sub3([4.0, 5.0, 6.0], [1.0, 2.0, 3.0]);
955 assert_eq!(r, [3.0, 3.0, 3.0]);
956 }
957
958 #[test]
959 fn test_scale3() {
960 let r = scale3([1.0, 2.0, 3.0], 2.0);
961 assert_eq!(r, [2.0, 4.0, 6.0]);
962 }
963
964 #[test]
965 fn test_abs3() {
966 let r = abs3([-1.0, 2.0, -3.0]);
967 assert_eq!(r, [1.0, 2.0, 3.0]);
968 }
969
970 #[test]
971 fn test_max3() {
972 let r = max3([1.0, -1.0, 2.0], [0.0, 0.0, 0.0]);
973 assert_eq!(r, [1.0, 0.0, 2.0]);
974 }
975
976 #[test]
977 fn test_normalize3_zero() {
978 let n = normalize3([0.0; 3]);
979 assert_eq!(n, [0.0; 3]);
980 }
981
982 #[test]
983 fn test_clamp() {
984 assert_eq!(clamp(-1.0, 0.0, 1.0), 0.0);
985 assert_eq!(clamp(0.5, 0.0, 1.0), 0.5);
986 assert_eq!(clamp(2.0, 0.0, 1.0), 1.0);
987 }
988}