1#![allow(dead_code)]
12#![allow(clippy::too_many_arguments)]
13
14#[inline]
18fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
19 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
20}
21
22#[inline]
24fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
25 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
26}
27
28#[inline]
30fn scale3(v: [f64; 3], s: f64) -> [f64; 3] {
31 [v[0] * s, v[1] * s, v[2] * s]
32}
33
34#[inline]
36fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
37 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
38}
39
40#[inline]
42fn length3(v: [f64; 3]) -> f64 {
43 dot3(v, v).sqrt()
44}
45
46#[inline]
48fn normalize3(v: [f64; 3]) -> [f64; 3] {
49 let len = length3(v);
50 if len < 1e-15 {
51 return [0.0; 3];
52 }
53 scale3(v, 1.0 / len)
54}
55
56#[inline]
58fn clamp(v: f64, lo: f64, hi: f64) -> f64 {
59 v.max(lo).min(hi)
60}
61
62#[inline]
64fn max3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
65 [a[0].max(b[0]), a[1].max(b[1]), a[2].max(b[2])]
66}
67
68#[inline]
70fn abs3(v: [f64; 3]) -> [f64; 3] {
71 [v[0].abs(), v[1].abs(), v[2].abs()]
72}
73
74#[derive(Debug, Clone, Copy)]
78pub struct Ray {
79 pub origin: [f64; 3],
81 pub direction: [f64; 3],
83}
84
85impl Ray {
86 pub fn new(origin: [f64; 3], direction: [f64; 3]) -> Self {
90 Self {
91 origin,
92 direction: normalize3(direction),
93 }
94 }
95
96 pub fn at(&self, t: f64) -> [f64; 3] {
98 [
99 self.origin[0] + t * self.direction[0],
100 self.origin[1] + t * self.direction[1],
101 self.origin[2] + t * self.direction[2],
102 ]
103 }
104}
105
106pub trait Sdf {
111 fn distance(&self, p: [f64; 3]) -> f64;
113
114 fn normal(&self, p: [f64; 3]) -> [f64; 3] {
119 let e = 1e-5;
120 let dx = self.distance([p[0] + e, p[1], p[2]]) - self.distance([p[0] - e, p[1], p[2]]);
121 let dy = self.distance([p[0], p[1] + e, p[2]]) - self.distance([p[0], p[1] - e, p[2]]);
122 let dz = self.distance([p[0], p[1], p[2] + e]) - self.distance([p[0], p[1], p[2] - e]);
123 normalize3([dx, dy, dz])
124 }
125}
126
127#[derive(Debug, Clone, Copy)]
131pub struct SphereSdf {
132 pub center: [f64; 3],
134 pub radius: f64,
136}
137
138impl SphereSdf {
139 pub fn new(center: [f64; 3], radius: f64) -> Self {
141 Self { center, radius }
142 }
143}
144
145impl Sdf for SphereSdf {
146 fn distance(&self, p: [f64; 3]) -> f64 {
147 let d = sub3(p, self.center);
148 length3(d) - self.radius
149 }
150}
151
152#[derive(Debug, Clone, Copy)]
156pub struct BoxSdf {
157 pub half_extents: [f64; 3],
159}
160
161impl BoxSdf {
162 pub fn new(half_extents: [f64; 3]) -> Self {
164 Self { half_extents }
165 }
166}
167
168impl Sdf for BoxSdf {
169 fn distance(&self, p: [f64; 3]) -> f64 {
170 let q = [
172 p[0].abs() - self.half_extents[0],
173 p[1].abs() - self.half_extents[1],
174 p[2].abs() - self.half_extents[2],
175 ];
176 let outside = length3(max3(q, [0.0; 3]));
177 let inside = q[0].max(q[1]).max(q[2]).min(0.0);
178 outside + inside
179 }
180}
181
182#[derive(Debug, Clone, Copy)]
186pub struct CapsuleSdf {
187 pub a: [f64; 3],
189 pub b: [f64; 3],
191 pub r: f64,
193}
194
195impl CapsuleSdf {
196 pub fn new(a: [f64; 3], b: [f64; 3], r: f64) -> Self {
198 Self { a, b, r }
199 }
200}
201
202impl Sdf for CapsuleSdf {
203 fn distance(&self, p: [f64; 3]) -> f64 {
204 let pa = sub3(p, self.a);
205 let ba = sub3(self.b, self.a);
206 let h = clamp(dot3(pa, ba) / dot3(ba, ba), 0.0, 1.0);
207 let closest = sub3(pa, scale3(ba, h));
208 length3(closest) - self.r
209 }
210}
211
212#[derive(Debug, Clone, Copy)]
216pub struct TorusSdf {
217 pub r_major: f64,
219 pub r_minor: f64,
221}
222
223impl TorusSdf {
224 pub fn new(r_major: f64, r_minor: f64) -> Self {
226 Self { r_major, r_minor }
227 }
228}
229
230impl Sdf for TorusSdf {
231 fn distance(&self, p: [f64; 3]) -> f64 {
232 let q_x = (p[0] * p[0] + p[2] * p[2]).sqrt() - self.r_major;
234 let q_y = p[1];
235 (q_x * q_x + q_y * q_y).sqrt() - self.r_minor
236 }
237}
238
239pub fn sdf_union(a: f64, b: f64) -> f64 {
245 a.min(b)
246}
247
248pub fn sdf_intersection(a: f64, b: f64) -> f64 {
253 a.max(b)
254}
255
256pub fn sdf_subtraction(a: f64, b: f64) -> f64 {
260 a.max(-b)
261}
262
263pub fn sdf_smooth_union(a: f64, b: f64, k: f64) -> f64 {
268 if k < 1e-15 {
269 return sdf_union(a, b);
270 }
271 let h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
272 let mix = b + h * (a - b);
273 mix - k * h * (1.0 - h)
274}
275
276#[derive(Debug, Clone, Copy)]
280pub struct RayMarchResult {
281 pub hit: bool,
283 pub t: f64,
285 pub steps: usize,
287 pub normal: [f64; 3],
289}
290
291impl RayMarchResult {
292 pub fn miss(t: f64, steps: usize) -> Self {
294 Self {
295 hit: false,
296 t,
297 steps,
298 normal: [0.0; 3],
299 }
300 }
301
302 pub fn hit(t: f64, steps: usize, normal: [f64; 3]) -> Self {
304 Self {
305 hit: true,
306 t,
307 steps,
308 normal,
309 }
310 }
311}
312
313pub fn ray_march(
324 ray: &Ray,
325 sdf: &dyn Sdf,
326 max_steps: usize,
327 max_dist: f64,
328 eps: f64,
329) -> RayMarchResult {
330 let mut t = 0.0_f64;
331 for step in 0..max_steps {
332 let p = ray.at(t);
333 let d = sdf.distance(p);
334 if d.abs() < eps {
335 let normal = sdf.normal(p);
336 return RayMarchResult::hit(t, step + 1, normal);
337 }
338 t += d;
339 if t >= max_dist {
340 return RayMarchResult::miss(max_dist, step + 1);
341 }
342 }
343 RayMarchResult::miss(t, max_steps)
344}
345
346#[derive(Debug, Clone, Copy)]
350pub struct AmbientOcclusion {
351 pub samples: usize,
353 pub radius: f64,
355 pub falloff: f64,
357}
358
359impl AmbientOcclusion {
360 pub fn new(samples: usize, radius: f64, falloff: f64) -> Self {
362 Self {
363 samples,
364 radius,
365 falloff,
366 }
367 }
368
369 pub fn compute(&self, pos: [f64; 3], normal: [f64; 3], sdf: &dyn Sdf) -> f64 {
374 if self.samples == 0 {
375 return 1.0;
376 }
377 let mut occ = 0.0_f64;
378 for i in 1..=self.samples {
379 let t = self.radius * (i as f64) / (self.samples as f64);
380 let sample_pos = add3(pos, scale3(normal, t));
381 let d = sdf.distance(sample_pos);
382 occ += (t - d).max(0.0) / t.powf(self.falloff);
383 }
384 (1.0 - occ / (self.samples as f64)).clamp(0.0, 1.0)
385 }
386}
387
388#[derive(Debug, Clone, Copy)]
392pub struct SoftShadow {
393 pub light_dir: [f64; 3],
395 pub k: f64,
397}
398
399impl SoftShadow {
400 pub fn new(light_dir: [f64; 3], k: f64) -> Self {
402 Self {
403 light_dir: normalize3(light_dir),
404 k,
405 }
406 }
407
408 pub fn compute(&self, pos: [f64; 3], sdf: &dyn Sdf, max_dist: f64) -> f64 {
412 let eps = 1e-4;
413 let mut res = 1.0_f64;
414 let mut t = eps;
415 while t < max_dist {
416 let p = add3(pos, scale3(self.light_dir, t));
417 let d = sdf.distance(p);
418 if d < eps {
419 return 0.0;
420 }
421 res = res.min(self.k * d / t);
422 t += d;
423 }
424 res.clamp(0.0, 1.0)
425 }
426}
427
428#[derive(Debug, Clone)]
433pub struct RayMarchRenderer {
434 pub width: usize,
436 pub height: usize,
438 pub fov: f64,
440 pub camera_pos: [f64; 3],
442 pub camera_target: [f64; 3],
444}
445
446impl RayMarchRenderer {
447 pub fn new(
449 width: usize,
450 height: usize,
451 fov: f64,
452 camera_pos: [f64; 3],
453 camera_target: [f64; 3],
454 ) -> Self {
455 Self {
456 width,
457 height,
458 fov,
459 camera_pos,
460 camera_target,
461 }
462 }
463
464 fn camera_basis(&self) -> ([f64; 3], [f64; 3], [f64; 3]) {
466 let world_up = [0.0_f64, 1.0, 0.0];
467 let forward = normalize3(sub3(self.camera_target, self.camera_pos));
468 let right = normalize3(cross3_fn(forward, world_up));
469 let up = cross3_fn(right, forward);
470 (right, up, forward)
471 }
472
473 pub fn generate_ray(&self, px: usize, py: usize) -> Ray {
477 let (right, up, forward) = self.camera_basis();
478 let aspect = self.width as f64 / self.height.max(1) as f64;
479 let half_h = (self.fov * 0.5).tan();
480 let half_w = half_h * aspect;
481
482 let u = (2.0 * (px as f64 + 0.5) / self.width as f64 - 1.0) * half_w;
484 let v = (1.0 - 2.0 * (py as f64 + 0.5) / self.height as f64) * half_h;
485
486 let dir = add3(add3(forward, scale3(right, u)), scale3(up, v));
487 Ray::new(self.camera_pos, dir)
488 }
489
490 pub fn render_depth(&self, sdf: &dyn Sdf) -> Vec<f64> {
495 let n = self.width * self.height;
496 let mut depth = Vec::with_capacity(n);
497 for py in 0..self.height {
498 for px in 0..self.width {
499 let ray = self.generate_ray(px, py);
500 let result = ray_march(&ray, sdf, 256, 1000.0, 1e-5);
501 depth.push(if result.hit { result.t } else { f64::INFINITY });
502 }
503 }
504 depth
505 }
506
507 pub fn render_normals(&self, sdf: &dyn Sdf) -> Vec<[f64; 3]> {
512 let n = self.width * self.height;
513 let mut normals = Vec::with_capacity(n);
514 for py in 0..self.height {
515 for px in 0..self.width {
516 let ray = self.generate_ray(px, py);
517 let result = ray_march(&ray, sdf, 256, 1000.0, 1e-5);
518 normals.push(result.normal);
519 }
520 }
521 normals
522 }
523}
524
525#[inline]
527fn cross3_fn(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
528 [
529 a[1] * b[2] - a[2] * b[1],
530 a[2] * b[0] - a[0] * b[2],
531 a[0] * b[1] - a[1] * b[0],
532 ]
533}
534
535pub fn subsurface_scattering_approx(depth: f64, scatter_coeff: f64, absorption: f64) -> f64 {
550 let extinction = scatter_coeff + absorption;
551 (-extinction * depth.max(0.0)).exp()
552}
553
554#[cfg(test)]
557mod tests {
558 use super::*;
559 use std::f64::consts::PI;
560
561 #[test]
564 fn ray_at_origin() {
565 let ray = Ray::new([0.0; 3], [0.0, 0.0, 1.0]);
566 let p = ray.at(0.0);
567 assert_eq!(p, [0.0; 3]);
568 }
569
570 #[test]
571 fn ray_at_t_positive() {
572 let ray = Ray::new([1.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
573 let p = ray.at(5.0);
574 assert!((p[0] - 1.0).abs() < 1e-12);
575 assert!((p[2] - 5.0).abs() < 1e-12);
576 }
577
578 #[test]
579 fn ray_direction_normalized() {
580 let ray = Ray::new([0.0; 3], [3.0, 4.0, 0.0]);
581 let len = length3(ray.direction);
582 assert!((len - 1.0).abs() < 1e-12);
583 }
584
585 #[test]
586 fn ray_at_negative_t() {
587 let ray = Ray::new([0.0; 3], [1.0, 0.0, 0.0]);
588 let p = ray.at(-2.0);
589 assert!((p[0] + 2.0).abs() < 1e-12);
590 }
591
592 #[test]
595 fn sphere_distance_outside() {
596 let s = SphereSdf::new([0.0; 3], 1.0);
597 let d = s.distance([2.0, 0.0, 0.0]);
598 assert!((d - 1.0).abs() < 1e-10);
599 }
600
601 #[test]
602 fn sphere_distance_inside() {
603 let s = SphereSdf::new([0.0; 3], 1.0);
604 let d = s.distance([0.0; 3]);
605 assert!((d + 1.0).abs() < 1e-10);
606 }
607
608 #[test]
609 fn sphere_distance_surface() {
610 let s = SphereSdf::new([0.0; 3], 1.0);
611 let d = s.distance([1.0, 0.0, 0.0]);
612 assert!(d.abs() < 1e-12);
613 }
614
615 #[test]
616 fn sphere_normal_at_x_axis() {
617 let s = SphereSdf::new([0.0; 3], 1.0);
618 let n = s.normal([1.0, 0.0, 0.0]);
619 assert!((n[0] - 1.0).abs() < 1e-4);
620 assert!(n[1].abs() < 1e-4);
621 assert!(n[2].abs() < 1e-4);
622 }
623
624 #[test]
625 fn sphere_translated_center() {
626 let s = SphereSdf::new([5.0, 0.0, 0.0], 2.0);
627 let d = s.distance([7.0, 0.0, 0.0]);
628 assert!(d.abs() < 1e-12);
629 }
630
631 #[test]
634 fn box_distance_outside_along_x() {
635 let b = BoxSdf::new([1.0; 3]);
636 let d = b.distance([3.0, 0.0, 0.0]);
637 assert!((d - 2.0).abs() < 1e-12);
638 }
639
640 #[test]
641 fn box_distance_inside() {
642 let b = BoxSdf::new([1.0; 3]);
643 let d = b.distance([0.0; 3]);
644 assert!(d < 0.0);
645 }
646
647 #[test]
648 fn box_distance_on_face() {
649 let b = BoxSdf::new([1.0, 1.0, 1.0]);
650 let d = b.distance([1.0, 0.0, 0.0]);
651 assert!(d.abs() < 1e-12);
652 }
653
654 #[test]
655 fn box_normal_x_face() {
656 let b = BoxSdf::new([1.0; 3]);
657 let n = b.normal([1.0, 0.0, 0.0]);
658 assert!((n[0] - 1.0).abs() < 1e-3);
659 }
660
661 #[test]
664 fn capsule_distance_at_midpoint() {
665 let c = CapsuleSdf::new([0.0, -1.0, 0.0], [0.0, 1.0, 0.0], 0.5);
666 let d = c.distance([0.5, 0.0, 0.0]);
667 assert!(d.abs() < 1e-10);
668 }
669
670 #[test]
671 fn capsule_distance_outside() {
672 let c = CapsuleSdf::new([0.0, -1.0, 0.0], [0.0, 1.0, 0.0], 0.5);
673 let d = c.distance([2.0, 0.0, 0.0]);
674 assert!((d - 1.5).abs() < 1e-10);
675 }
676
677 #[test]
678 fn capsule_distance_at_cap() {
679 let c = CapsuleSdf::new([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], 0.5);
680 let d = c.distance([0.0, 2.0, 0.0]);
682 assert!((d - 0.5).abs() < 1e-10);
683 }
684
685 #[test]
688 fn torus_distance_on_ring() {
689 let t = TorusSdf::new(2.0, 0.5);
691 let d = t.distance([2.0, 0.5, 0.0]);
692 assert!(d.abs() < 1e-10);
693 }
694
695 #[test]
696 fn torus_distance_centre_negative() {
697 let t = TorusSdf::new(2.0, 0.5);
698 let d = t.distance([0.0; 3]);
700 assert!((d - 1.5).abs() < 1e-10);
702 }
703
704 #[test]
705 fn torus_distance_outside_far() {
706 let t = TorusSdf::new(1.0, 0.2);
707 let d = t.distance([10.0, 0.0, 0.0]);
708 assert!(d > 0.0);
709 }
710
711 #[test]
714 fn sdf_union_picks_min() {
715 assert_eq!(sdf_union(3.0, 1.0), 1.0);
716 assert_eq!(sdf_union(-1.0, 2.0), -1.0);
717 }
718
719 #[test]
720 fn sdf_intersection_picks_max() {
721 assert_eq!(sdf_intersection(3.0, 1.0), 3.0);
722 }
723
724 #[test]
725 fn sdf_subtraction_correctness() {
726 assert_eq!(sdf_subtraction(1.0, -2.0), 2.0);
728 assert_eq!(sdf_subtraction(1.0, 3.0), 1.0);
729 }
730
731 #[test]
732 fn sdf_smooth_union_degenerate() {
733 let su = sdf_smooth_union(3.0, 1.0, 0.0);
735 assert!((su - sdf_union(3.0, 1.0)).abs() < 1e-10);
736 }
737
738 #[test]
739 fn sdf_smooth_union_blends() {
740 let su = sdf_smooth_union(0.0, 0.0, 1.0);
741 assert!(su <= 0.0 + 1e-10);
743 }
744
745 #[test]
746 fn sdf_smooth_union_between_values() {
747 let a = 2.0_f64;
749 let b = 3.0_f64;
750 let su = sdf_smooth_union(a, b, 0.5);
751 assert!(su <= a + 1e-10);
752 }
753
754 #[test]
757 fn ray_march_result_miss() {
758 let r = RayMarchResult::miss(100.0, 10);
759 assert!(!r.hit);
760 assert!((r.t - 100.0).abs() < 1e-12);
761 }
762
763 #[test]
764 fn ray_march_result_hit() {
765 let r = RayMarchResult::hit(5.0, 20, [1.0, 0.0, 0.0]);
766 assert!(r.hit);
767 assert!((r.normal[0] - 1.0).abs() < 1e-12);
768 }
769
770 #[test]
773 fn ray_march_hits_sphere() {
774 let sphere = SphereSdf::new([0.0, 0.0, 5.0], 1.0);
775 let ray = Ray::new([0.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
776 let result = ray_march(&ray, &sphere, 256, 100.0, 1e-5);
777 assert!(result.hit);
778 assert!((result.t - 4.0).abs() < 1e-3);
779 }
780
781 #[test]
782 fn ray_march_misses_sphere() {
783 let sphere = SphereSdf::new([0.0, 0.0, 5.0], 1.0);
784 let ray = Ray::new([10.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
785 let result = ray_march(&ray, &sphere, 256, 100.0, 1e-5);
786 assert!(!result.hit);
787 }
788
789 #[test]
790 fn ray_march_steps_bounded() {
791 let sphere = SphereSdf::new([0.0, 0.0, 5.0], 1.0);
792 let ray = Ray::new([0.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
793 let result = ray_march(&ray, &sphere, 128, 100.0, 1e-5);
794 assert!(result.steps <= 128);
795 }
796
797 #[test]
798 fn ray_march_hit_normal_approx_minus_z() {
799 let sphere = SphereSdf::new([0.0, 0.0, 5.0], 1.0);
800 let ray = Ray::new([0.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
801 let result = ray_march(&ray, &sphere, 256, 100.0, 1e-5);
802 assert!(result.hit);
804 assert!(result.normal[2] < 0.0);
805 }
806
807 #[test]
810 fn ao_open_space_returns_one() {
811 let sphere = SphereSdf::new([0.0; 3], 1.0);
813 let ao = AmbientOcclusion::new(8, 1.0, 1.0);
814 let pos = [100.0, 0.0, 0.0];
815 let normal = [1.0, 0.0, 0.0];
816 let v = ao.compute(pos, normal, &sphere);
817 assert!(v > 0.5);
818 }
819
820 #[test]
821 fn ao_samples_zero_returns_one() {
822 let sphere = SphereSdf::new([0.0; 3], 1.0);
823 let ao = AmbientOcclusion::new(0, 1.0, 1.0);
824 let v = ao.compute([0.0; 3], [0.0, 1.0, 0.0], &sphere);
825 assert!((v - 1.0).abs() < 1e-12);
826 }
827
828 #[test]
829 fn ao_result_clamped() {
830 let sphere = SphereSdf::new([0.0; 3], 1.0);
831 let ao = AmbientOcclusion::new(4, 0.5, 1.0);
832 let pos = [1.0, 0.0, 0.0];
833 let normal = [1.0, 0.0, 0.0];
834 let v = ao.compute(pos, normal, &sphere);
835 assert!((0.0..=1.0).contains(&v));
836 }
837
838 #[test]
841 fn soft_shadow_lit_no_occluder() {
842 let sphere = SphereSdf::new([0.0, 0.0, -100.0], 0.1);
844 let ss = SoftShadow::new([0.0, 1.0, 0.0], 8.0);
845 let pos = [0.0, 0.0, 0.0];
846 let v = ss.compute(pos, &sphere, 50.0);
847 assert!(v > 0.9);
848 }
849
850 #[test]
851 fn soft_shadow_shadow_with_occluder() {
852 let sphere = SphereSdf::new([0.0, 5.0, 0.0], 1.0);
854 let ss = SoftShadow::new([0.0, 1.0, 0.0], 8.0);
855 let pos = [0.0, 0.0, 0.0];
856 let v = ss.compute(pos, &sphere, 20.0);
857 assert!(v < 0.5);
858 }
859
860 #[test]
861 fn soft_shadow_result_in_range() {
862 let sphere = SphereSdf::new([0.0; 3], 1.0);
863 let ss = SoftShadow::new([1.0, 1.0, 1.0], 4.0);
864 let v = ss.compute([3.0, 0.0, 0.0], &sphere, 20.0);
865 assert!((0.0..=1.0).contains(&v));
866 }
867
868 #[test]
871 fn renderer_depth_buffer_size() {
872 let renderer = RayMarchRenderer::new(4, 4, PI / 3.0, [0.0, 0.0, -5.0], [0.0, 0.0, 0.0]);
873 let sphere = SphereSdf::new([0.0; 3], 1.0);
874 let depth = renderer.render_depth(&sphere);
875 assert_eq!(depth.len(), 16);
876 }
877
878 #[test]
879 fn renderer_normals_buffer_size() {
880 let renderer = RayMarchRenderer::new(2, 2, PI / 3.0, [0.0, 0.0, -5.0], [0.0, 0.0, 0.0]);
881 let sphere = SphereSdf::new([0.0; 3], 1.0);
882 let normals = renderer.render_normals(&sphere);
883 assert_eq!(normals.len(), 4);
884 }
885
886 #[test]
887 fn renderer_center_ray_hits_sphere() {
888 let renderer = RayMarchRenderer::new(11, 11, PI / 3.0, [0.0, 0.0, -5.0], [0.0, 0.0, 0.0]);
889 let sphere = SphereSdf::new([0.0; 3], 1.0);
890 let depth = renderer.render_depth(&sphere);
891 let center_t = depth[5 * 11 + 5];
893 assert!(center_t < f64::INFINITY);
894 }
895
896 #[test]
897 fn renderer_corner_ray_misses_small_sphere() {
898 let renderer = RayMarchRenderer::new(11, 11, PI / 3.0, [0.0, 0.0, -5.0], [0.0, 0.0, 0.0]);
899 let sphere = SphereSdf::new([0.0; 3], 0.01);
900 let depth = renderer.render_depth(&sphere);
901 assert_eq!(depth[0], f64::INFINITY);
903 }
904
905 #[test]
906 fn renderer_generate_ray_center() {
907 let renderer = RayMarchRenderer::new(11, 11, PI / 3.0, [0.0, 0.0, -5.0], [0.0, 0.0, 0.0]);
908 let ray = renderer.generate_ray(5, 5);
909 assert!(ray.direction[2] > 0.0);
911 }
912
913 #[test]
916 fn sss_zero_depth_returns_one() {
917 let v = subsurface_scattering_approx(0.0, 1.0, 1.0);
918 assert!((v - 1.0).abs() < 1e-12);
919 }
920
921 #[test]
922 fn sss_large_depth_near_zero() {
923 let v = subsurface_scattering_approx(1000.0, 1.0, 0.0);
924 assert!(v < 1e-10);
925 }
926
927 #[test]
928 fn sss_negative_depth_clamped() {
929 let v = subsurface_scattering_approx(-5.0, 1.0, 1.0);
930 assert!((v - 1.0).abs() < 1e-12);
931 }
932
933 #[test]
934 fn sss_result_in_range() {
935 let v = subsurface_scattering_approx(0.5, 2.0, 1.0);
936 assert!((0.0..=1.0).contains(&v));
937 }
938
939 #[test]
940 fn sss_monotone_decreasing() {
941 let v1 = subsurface_scattering_approx(1.0, 1.0, 0.5);
942 let v2 = subsurface_scattering_approx(2.0, 1.0, 0.5);
943 assert!(v1 > v2);
944 }
945
946 #[test]
949 fn test_add3() {
950 let r = add3([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]);
951 assert_eq!(r, [5.0, 7.0, 9.0]);
952 }
953
954 #[test]
955 fn test_sub3() {
956 let r = sub3([4.0, 5.0, 6.0], [1.0, 2.0, 3.0]);
957 assert_eq!(r, [3.0, 3.0, 3.0]);
958 }
959
960 #[test]
961 fn test_scale3() {
962 let r = scale3([1.0, 2.0, 3.0], 2.0);
963 assert_eq!(r, [2.0, 4.0, 6.0]);
964 }
965
966 #[test]
967 fn test_abs3() {
968 let r = abs3([-1.0, 2.0, -3.0]);
969 assert_eq!(r, [1.0, 2.0, 3.0]);
970 }
971
972 #[test]
973 fn test_max3() {
974 let r = max3([1.0, -1.0, 2.0], [0.0, 0.0, 0.0]);
975 assert_eq!(r, [1.0, 0.0, 2.0]);
976 }
977
978 #[test]
979 fn test_normalize3_zero() {
980 let n = normalize3([0.0; 3]);
981 assert_eq!(n, [0.0; 3]);
982 }
983
984 #[test]
985 fn test_clamp() {
986 assert_eq!(clamp(-1.0, 0.0, 1.0), 0.0);
987 assert_eq!(clamp(0.5, 0.0, 1.0), 0.5);
988 assert_eq!(clamp(2.0, 0.0, 1.0), 1.0);
989 }
990}