1use rand::Rng;
10
11use rand::RngExt;
12#[inline]
15fn vadd(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
16 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
17}
18
19#[inline]
20fn vsub(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
21 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
22}
23
24#[inline]
25fn vmul(a: [f32; 3], s: f32) -> [f32; 3] {
26 [a[0] * s, a[1] * s, a[2] * s]
27}
28
29#[inline]
30fn vmul3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
31 [a[0] * b[0], a[1] * b[1], a[2] * b[2]]
32}
33
34#[inline]
35fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
36 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
37}
38
39#[inline]
40fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
41 [
42 a[1] * b[2] - a[2] * b[1],
43 a[2] * b[0] - a[0] * b[2],
44 a[0] * b[1] - a[1] * b[0],
45 ]
46}
47
48#[inline]
49fn length(v: [f32; 3]) -> f32 {
50 dot(v, v).sqrt()
51}
52
53#[inline]
54fn normalize(v: [f32; 3]) -> [f32; 3] {
55 let l = length(v);
56 if l < 1e-8 {
57 return [0.0; 3];
58 }
59 vmul(v, 1.0 / l)
60}
61
62#[inline]
63fn reflect(d: [f32; 3], n: [f32; 3]) -> [f32; 3] {
64 vsub(d, vmul(n, 2.0 * dot(d, n)))
65}
66
67fn refract(uv: [f32; 3], n: [f32; 3], ni_over_nt: f32) -> Option<[f32; 3]> {
68 let cos_theta = (-dot(uv, n)).min(1.0);
69 let r_out_perp = vmul(vadd(uv, vmul(n, cos_theta)), ni_over_nt);
70 let r_out_parallel_len2 = (1.0 - dot(r_out_perp, r_out_perp)).abs();
71 let r_out_parallel = vmul(n, -(r_out_parallel_len2.sqrt()));
72 Some(vadd(r_out_perp, r_out_parallel))
73}
74
75fn schlick(cosine: f32, ref_idx: f32) -> f32 {
76 let r0 = ((1.0 - ref_idx) / (1.0 + ref_idx)).powi(2);
77 r0 + (1.0 - r0) * (1.0 - cosine).powi(5)
78}
79
80fn random_in_unit_sphere(rng: &mut impl Rng) -> [f32; 3] {
81 loop {
82 let v = [
83 rng.random_range(-1.0f32..1.0),
84 rng.random_range(-1.0f32..1.0),
85 rng.random_range(-1.0f32..1.0),
86 ];
87 if dot(v, v) < 1.0 {
88 return v;
89 }
90 }
91}
92
93fn random_unit_vector(rng: &mut impl Rng) -> [f32; 3] {
94 normalize(random_in_unit_sphere(rng))
95}
96
97#[derive(Debug, Clone, Copy)]
101pub struct Ray {
102 pub origin: [f32; 3],
104 pub direction: [f32; 3],
106}
107
108impl Ray {
109 pub fn new(origin: [f32; 3], direction: [f32; 3]) -> Self {
111 Self { origin, direction }
112 }
113
114 pub fn at(&self, t: f32) -> [f32; 3] {
116 vadd(self.origin, vmul(self.direction, t))
117 }
118}
119
120#[derive(Debug, Clone, Copy)]
124pub enum MaterialType {
125 Lambertian,
127 Metal(f32),
129 Dielectric(f32),
131}
132
133#[derive(Debug, Clone, Copy)]
135pub struct Material {
136 pub albedo: [f32; 3],
138 pub kind: MaterialType,
140}
141
142impl Material {
143 pub fn lambertian(albedo: [f32; 3]) -> Self {
145 Self {
146 albedo,
147 kind: MaterialType::Lambertian,
148 }
149 }
150
151 pub fn metal(albedo: [f32; 3], fuzz: f32) -> Self {
153 Self {
154 albedo,
155 kind: MaterialType::Metal(fuzz.clamp(0.0, 1.0)),
156 }
157 }
158
159 pub fn dielectric(ior: f32) -> Self {
161 Self {
162 albedo: [1.0; 3],
163 kind: MaterialType::Dielectric(ior),
164 }
165 }
166
167 pub fn scatter(
171 &self,
172 ray: &Ray,
173 hit: &HitRecord,
174 rng: &mut impl Rng,
175 ) -> Option<(Ray, [f32; 3])> {
176 match self.kind {
177 MaterialType::Lambertian => {
178 let target = vadd(vadd(hit.point, hit.normal), random_unit_vector(rng));
179 let scattered = Ray::new(hit.point, vsub(target, hit.point));
180 Some((scattered, self.albedo))
181 }
182 MaterialType::Metal(fuzz) => {
183 let reflected = reflect(normalize(ray.direction), hit.normal);
184 let fuzzed = vadd(reflected, vmul(random_in_unit_sphere(rng), fuzz));
185 if dot(fuzzed, hit.normal) > 0.0 {
186 Some((Ray::new(hit.point, fuzzed), self.albedo))
187 } else {
188 None
189 }
190 }
191 MaterialType::Dielectric(ior) => {
192 let attenuation = [1.0f32; 3];
193 let refraction_ratio = if hit.front_face { 1.0 / ior } else { ior };
194 let unit_dir = normalize(ray.direction);
195 let cos_theta = (-dot(unit_dir, hit.normal)).min(1.0);
196 let sin_theta = (1.0 - cos_theta * cos_theta).sqrt();
197 let cannot_refract = refraction_ratio * sin_theta > 1.0;
198 let scattered_dir = if cannot_refract
199 || schlick(cos_theta, refraction_ratio) > rng.random::<f32>()
200 {
201 reflect(unit_dir, hit.normal)
202 } else {
203 refract(unit_dir, hit.normal, refraction_ratio)
204 .unwrap_or_else(|| reflect(unit_dir, hit.normal))
205 };
206 Some((Ray::new(hit.point, scattered_dir), attenuation))
207 }
208 }
209 }
210}
211
212#[derive(Debug, Clone, Copy)]
216pub struct HitRecord {
217 pub t: f32,
219 pub point: [f32; 3],
221 pub normal: [f32; 3],
223 pub material_index: usize,
225 pub front_face: bool,
227}
228
229impl HitRecord {
230 fn new(t: f32, point: [f32; 3], outward_normal: [f32; 3], ray: &Ray, mat: usize) -> Self {
231 let front_face = dot(ray.direction, outward_normal) < 0.0;
232 let normal = if front_face {
233 outward_normal
234 } else {
235 vmul(outward_normal, -1.0)
236 };
237 Self {
238 t,
239 point,
240 normal,
241 material_index: mat,
242 front_face,
243 }
244 }
245}
246
247#[derive(Debug, Clone, Copy)]
251pub struct Sphere {
252 pub center: [f32; 3],
254 pub radius: f32,
256 pub material_index: usize,
258}
259
260impl Sphere {
261 pub fn new(center: [f32; 3], radius: f32, material_index: usize) -> Self {
263 Self {
264 center,
265 radius,
266 material_index,
267 }
268 }
269
270 pub fn hit(&self, ray: &Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
272 let oc = vsub(ray.origin, self.center);
273 let a = dot(ray.direction, ray.direction);
274 let half_b = dot(oc, ray.direction);
275 let c = dot(oc, oc) - self.radius * self.radius;
276 let discriminant = half_b * half_b - a * c;
277 if discriminant < 0.0 {
278 return None;
279 }
280 let sqrt_d = discriminant.sqrt();
281 let mut root = (-half_b - sqrt_d) / a;
282 if root < t_min || root > t_max {
283 root = (-half_b + sqrt_d) / a;
284 if root < t_min || root > t_max {
285 return None;
286 }
287 }
288 let point = ray.at(root);
289 let outward_normal = vmul(vsub(point, self.center), 1.0 / self.radius);
290 Some(HitRecord::new(
291 root,
292 point,
293 outward_normal,
294 ray,
295 self.material_index,
296 ))
297 }
298}
299
300#[derive(Debug, Clone, Copy)]
304pub struct Triangle {
305 pub v0: [f32; 3],
307 pub v1: [f32; 3],
309 pub v2: [f32; 3],
311 pub normal: [f32; 3],
313 pub material_index: usize,
315}
316
317impl Triangle {
318 pub fn new(v0: [f32; 3], v1: [f32; 3], v2: [f32; 3], material_index: usize) -> Self {
320 let edge1 = vsub(v1, v0);
321 let edge2 = vsub(v2, v0);
322 let normal = normalize(cross(edge1, edge2));
323 Self {
324 v0,
325 v1,
326 v2,
327 normal,
328 material_index,
329 }
330 }
331
332 pub fn hit(&self, ray: &Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
334 const EPSILON: f32 = 1e-7;
335 let edge1 = vsub(self.v1, self.v0);
336 let edge2 = vsub(self.v2, self.v0);
337 let h = cross(ray.direction, edge2);
338 let a = dot(edge1, h);
339 if a.abs() < EPSILON {
340 return None; }
342 let f = 1.0 / a;
343 let s = vsub(ray.origin, self.v0);
344 let u = f * dot(s, h);
345 if !(0.0..=1.0).contains(&u) {
346 return None;
347 }
348 let q = cross(s, edge1);
349 let v = f * dot(ray.direction, q);
350 if v < 0.0 || u + v > 1.0 {
351 return None;
352 }
353 let t = f * dot(edge2, q);
354 if t < t_min || t > t_max {
355 return None;
356 }
357 let point = ray.at(t);
358 Some(HitRecord::new(
359 t,
360 point,
361 self.normal,
362 ray,
363 self.material_index,
364 ))
365 }
366}
367
368#[derive(Debug, Clone, Copy)]
372pub struct PointLight {
373 pub position: [f32; 3],
375 pub color: [f32; 3],
377 pub intensity: f32,
379}
380
381impl PointLight {
382 pub fn new(position: [f32; 3], color: [f32; 3], intensity: f32) -> Self {
384 Self {
385 position,
386 color,
387 intensity,
388 }
389 }
390}
391
392#[derive(Debug, Clone, Default)]
396pub struct PathTracerScene {
397 pub spheres: Vec<Sphere>,
399 pub triangles: Vec<Triangle>,
401 pub lights: Vec<PointLight>,
403 pub materials: Vec<Material>,
405 pub sky_top: [f32; 3],
407 pub sky_bottom: [f32; 3],
409}
410
411impl PathTracerScene {
412 pub fn new() -> Self {
414 Self {
415 sky_top: [0.5, 0.7, 1.0],
416 sky_bottom: [1.0, 1.0, 1.0],
417 ..Default::default()
418 }
419 }
420
421 pub fn add_material(&mut self, mat: Material) -> usize {
423 let idx = self.materials.len();
424 self.materials.push(mat);
425 idx
426 }
427
428 pub fn add_sphere(&mut self, sphere: Sphere) {
430 self.spheres.push(sphere);
431 }
432
433 pub fn add_triangle(&mut self, triangle: Triangle) {
435 self.triangles.push(triangle);
436 }
437
438 pub fn add_light(&mut self, light: PointLight) {
440 self.lights.push(light);
441 }
442
443 pub fn hit_scene(&self, ray: &Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
445 let mut closest: Option<HitRecord> = None;
446 let mut t_closest = t_max;
447 for sphere in &self.spheres {
448 if let Some(rec) = sphere.hit(ray, t_min, t_closest) {
449 t_closest = rec.t;
450 closest = Some(rec);
451 }
452 }
453 for tri in &self.triangles {
454 if let Some(rec) = tri.hit(ray, t_min, t_closest) {
455 t_closest = rec.t;
456 closest = Some(rec);
457 }
458 }
459 closest
460 }
461
462 fn sky_color(&self, ray: &Ray) -> [f32; 3] {
464 let unit = normalize(ray.direction);
465 let t = 0.5 * (unit[1] + 1.0);
466 let a = self.sky_bottom;
467 let b = self.sky_top;
468 [
469 a[0] * (1.0 - t) + b[0] * t,
470 a[1] * (1.0 - t) + b[1] * t,
471 a[2] * (1.0 - t) + b[2] * t,
472 ]
473 }
474
475 pub fn trace(&self, ray: &Ray, max_depth: usize, rng: &mut impl Rng) -> [f32; 3] {
479 if max_depth == 0 {
480 return [0.0; 3];
481 }
482 if let Some(hit) = self.hit_scene(ray, 1e-4, f32::INFINITY) {
483 let mat = &self.materials[hit.material_index];
484 if let Some((scattered, attenuation)) = mat.scatter(ray, &hit, rng) {
485 let incoming = self.trace(&scattered, max_depth - 1, rng);
486 vmul3(attenuation, incoming)
487 } else {
488 [0.0; 3]
489 }
490 } else {
491 self.sky_color(ray)
492 }
493 }
494}
495
496#[derive(Debug, Clone)]
500pub struct PathTracerBuffer {
501 pub width: usize,
503 pub height: usize,
505 pub accumulator: Vec<[f32; 3]>,
507 pub sample_count: Vec<u32>,
509}
510
511impl PathTracerBuffer {
512 pub fn new(width: usize, height: usize) -> Self {
514 let n = width * height;
515 Self {
516 width,
517 height,
518 accumulator: vec![[0.0; 3]; n],
519 sample_count: vec![0; n],
520 }
521 }
522
523 pub fn add_sample(&mut self, x: usize, y: usize, color: [f32; 3]) {
525 let idx = y * self.width + x;
526 let acc = &mut self.accumulator[idx];
527 acc[0] += color[0];
528 acc[1] += color[1];
529 acc[2] += color[2];
530 self.sample_count[idx] += 1;
531 }
532
533 pub fn get_pixel(&self, x: usize, y: usize) -> [f32; 3] {
535 let idx = y * self.width + x;
536 let n = self.sample_count[idx] as f32;
537 if n == 0.0 {
538 return [0.0; 3];
539 }
540 let acc = self.accumulator[idx];
541 [acc[0] / n, acc[1] / n, acc[2] / n]
542 }
543
544 pub fn to_rgb8(&self) -> Vec<u8> {
546 let mut out = Vec::with_capacity(self.width * self.height * 3);
547 for y in 0..self.height {
548 for x in 0..self.width {
549 let c = self.get_pixel(x, y);
550 for ch in c.iter() {
551 let linear = ch.clamp(0.0, 1.0);
552 let gamma = linear.sqrt(); out.push((gamma * 255.999) as u8);
554 }
555 }
556 }
557 out
558 }
559
560 pub fn clear(&mut self) {
562 for acc in &mut self.accumulator {
563 *acc = [0.0; 3];
564 }
565 for s in &mut self.sample_count {
566 *s = 0;
567 }
568 }
569
570 pub fn total_samples(&self) -> u64 {
572 self.sample_count.iter().map(|&s| s as u64).sum()
573 }
574}
575
576#[derive(Debug, Clone)]
580pub struct Camera {
581 pub origin: [f32; 3],
583 lower_left_corner: [f32; 3],
584 horizontal: [f32; 3],
585 vertical: [f32; 3],
586 lens_radius: f32,
587 u: [f32; 3],
588 v: [f32; 3],
589}
590
591impl Camera {
592 pub fn new(
602 look_from: [f32; 3],
603 look_at: [f32; 3],
604 vup: [f32; 3],
605 vfov: f32,
606 aspect_ratio: f32,
607 aperture: f32,
608 focus_dist: f32,
609 ) -> Self {
610 let theta = vfov.to_radians();
611 let h = (theta / 2.0).tan();
612 let viewport_height = 2.0 * h;
613 let viewport_width = aspect_ratio * viewport_height;
614
615 let w = normalize(vsub(look_from, look_at));
616 let u = normalize(cross(vup, w));
617 let v = cross(w, u);
618
619 let horizontal = vmul(u, viewport_width * focus_dist);
620 let vertical = vmul(v, viewport_height * focus_dist);
621 let lower_left_corner = vsub(
622 vsub(vsub(look_from, vmul(horizontal, 0.5)), vmul(vertical, 0.5)),
623 vmul(w, focus_dist),
624 );
625
626 Self {
627 origin: look_from,
628 lower_left_corner,
629 horizontal,
630 vertical,
631 lens_radius: aperture / 2.0,
632 u,
633 v,
634 }
635 }
636
637 pub fn get_ray(&self, s: f32, t: f32, rng: &mut impl Rng) -> Ray {
639 let rd = vmul(self.random_in_unit_disk(rng), self.lens_radius);
640 let offset = vadd(vmul(self.u, rd[0]), vmul(self.v, rd[1]));
641 let dir = vsub(
642 vadd(
643 vadd(self.lower_left_corner, vmul(self.horizontal, s)),
644 vmul(self.vertical, t),
645 ),
646 vadd(self.origin, offset),
647 );
648 Ray::new(vadd(self.origin, offset), dir)
649 }
650
651 fn random_in_unit_disk(&self, rng: &mut impl Rng) -> [f32; 3] {
652 loop {
653 let p = [
654 rng.random_range(-1.0f32..1.0),
655 rng.random_range(-1.0f32..1.0),
656 0.0,
657 ];
658 if dot(p, p) < 1.0 {
659 return p;
660 }
661 }
662 }
663}
664
665#[derive(Debug, Clone)]
669pub struct PathTracerRenderer {
670 pub scene: PathTracerScene,
672 pub camera: Camera,
674 pub max_depth: usize,
676 pub samples_per_pass: usize,
678}
679
680impl PathTracerRenderer {
681 pub fn new(
683 scene: PathTracerScene,
684 camera: Camera,
685 max_depth: usize,
686 samples_per_pass: usize,
687 ) -> Self {
688 Self {
689 scene,
690 camera,
691 max_depth,
692 samples_per_pass,
693 }
694 }
695
696 pub fn render_pass(&self, buffer: &mut PathTracerBuffer) {
700 let w = buffer.width;
701 let h = buffer.height;
702 let mut rng = rand::rng();
703 for y in 0..h {
704 for x in 0..w {
705 let mut color = [0.0f32; 3];
706 for _ in 0..self.samples_per_pass {
707 let u = (x as f32 + rng.random::<f32>()) / (w - 1) as f32;
708 let v = (y as f32 + rng.random::<f32>()) / (h - 1) as f32;
709 let ray = self.camera.get_ray(u, v, &mut rng);
710 let c = self.scene.trace(&ray, self.max_depth, &mut rng);
711 color[0] += c[0];
712 color[1] += c[1];
713 color[2] += c[2];
714 }
715 let inv = 1.0 / self.samples_per_pass as f32;
716 buffer.add_sample(x, y, vmul(color, inv));
717 }
718 }
719 }
720}
721
722#[cfg(test)]
725mod tests {
726 use super::*;
727
728 fn make_rng() -> impl Rng {
729 rand::rng()
730 }
731
732 #[test]
735 fn test_ray_at_origin() {
736 let r = Ray::new([0.0; 3], [1.0, 0.0, 0.0]);
737 let p = r.at(0.0);
738 assert_eq!(p, [0.0; 3]);
739 }
740
741 #[test]
742 fn test_ray_at_t() {
743 let r = Ray::new([1.0, 2.0, 3.0], [1.0, 0.0, 0.0]);
744 let p = r.at(3.0);
745 assert!((p[0] - 4.0).abs() < 1e-6);
746 assert!((p[1] - 2.0).abs() < 1e-6);
747 assert!((p[2] - 3.0).abs() < 1e-6);
748 }
749
750 #[test]
751 fn test_ray_at_negative_t() {
752 let r = Ray::new([0.0; 3], [0.0, 1.0, 0.0]);
753 let p = r.at(-2.0);
754 assert!((p[1] - (-2.0)).abs() < 1e-6);
755 }
756
757 #[test]
760 fn test_sphere_hit_center() {
761 let s = Sphere::new([0.0, 0.0, -1.0], 0.5, 0);
762 let r = Ray::new([0.0; 3], [0.0, 0.0, -1.0]);
763 let hit = s.hit(&r, 0.001, f32::INFINITY);
764 assert!(hit.is_some());
765 let rec = hit.unwrap();
766 assert!(rec.t > 0.4 && rec.t < 0.6);
767 }
768
769 #[test]
770 fn test_sphere_miss() {
771 let s = Sphere::new([0.0, 0.0, -1.0], 0.5, 0);
772 let r = Ray::new([0.0; 3], [0.0, 1.0, 0.0]);
773 assert!(s.hit(&r, 0.001, f32::INFINITY).is_none());
774 }
775
776 #[test]
777 fn test_sphere_hit_from_inside() {
778 let s = Sphere::new([0.0; 3], 1.0, 0);
779 let r = Ray::new([0.0; 3], [1.0, 0.0, 0.0]);
780 let hit = s.hit(&r, 0.001, f32::INFINITY);
781 assert!(hit.is_some());
782 let rec = hit.unwrap();
783 assert!(!rec.front_face);
784 }
785
786 #[test]
787 fn test_sphere_normal_outward() {
788 let s = Sphere::new([0.0; 3], 1.0, 0);
789 let r = Ray::new([0.0, 0.0, 5.0], [0.0, 0.0, -1.0]);
790 let hit = s.hit(&r, 0.001, f32::INFINITY).unwrap();
791 assert!(hit.front_face);
792 assert!((hit.normal[2] - 1.0).abs() < 1e-5);
793 }
794
795 #[test]
796 fn test_sphere_t_range_cull() {
797 let s = Sphere::new([0.0, 0.0, -1.0], 0.5, 0);
798 let r = Ray::new([0.0; 3], [0.0, 0.0, -1.0]);
799 assert!(s.hit(&r, 0.001, 0.1).is_none());
801 }
802
803 #[test]
806 fn test_triangle_hit() {
807 let tri = Triangle::new([-1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], 0);
808 let r = Ray::new([0.0, 0.3, 1.0], [0.0, 0.0, -1.0]);
809 let hit = tri.hit(&r, 0.001, f32::INFINITY);
810 assert!(hit.is_some());
811 }
812
813 #[test]
814 fn test_triangle_miss_outside() {
815 let tri = Triangle::new([-1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], 0);
816 let r = Ray::new([5.0, 5.0, 1.0], [0.0, 0.0, -1.0]);
817 assert!(tri.hit(&r, 0.001, f32::INFINITY).is_none());
818 }
819
820 #[test]
821 fn test_triangle_miss_parallel() {
822 let tri = Triangle::new([-1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], 0);
823 let r = Ray::new([0.0, 0.0, 1.0], [1.0, 0.0, 0.0]);
825 assert!(tri.hit(&r, 0.001, f32::INFINITY).is_none());
826 }
827
828 #[test]
829 fn test_triangle_normal_direction() {
830 let tri = Triangle::new([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], 0);
831 assert!(tri.normal[2].abs() > 0.9);
833 }
834
835 #[test]
836 fn test_triangle_hit_at_vertex() {
837 let tri = Triangle::new([0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [0.0, 2.0, 0.0], 0);
838 let r = Ray::new([0.5, 0.5, 1.0], [0.0, 0.0, -1.0]);
840 assert!(tri.hit(&r, 0.001, f32::INFINITY).is_some());
841 }
842
843 #[test]
846 fn test_lambertian_scatter() {
847 let mat = Material::lambertian([0.8, 0.3, 0.3]);
848 let ray = Ray::new([0.0; 3], [0.0, 0.0, -1.0]);
849 let hit = HitRecord {
850 t: 1.0,
851 point: [0.0, 0.0, -1.0],
852 normal: [0.0, 0.0, 1.0],
853 material_index: 0,
854 front_face: true,
855 };
856 let mut rng = make_rng();
857 let result = mat.scatter(&ray, &hit, &mut rng);
858 assert!(result.is_some());
859 let (_scattered, attenuation) = result.unwrap();
860 assert!((attenuation[0] - 0.8).abs() < 1e-6);
861 }
862
863 #[test]
864 fn test_metal_scatter() {
865 let mat = Material::metal([0.8, 0.8, 0.8], 0.0);
866 let ray = Ray::new([0.0; 3], normalize([1.0, -1.0, 0.0]));
867 let hit = HitRecord {
868 t: 1.0,
869 point: [0.0; 3],
870 normal: [0.0, 1.0, 0.0],
871 material_index: 0,
872 front_face: true,
873 };
874 let mut rng = make_rng();
875 let result = mat.scatter(&ray, &hit, &mut rng);
876 assert!(result.is_some());
877 let (scattered, _attenuation) = result.unwrap();
878 assert!(scattered.direction[1] > 0.0);
880 }
881
882 #[test]
883 fn test_dielectric_scatter() {
884 let mat = Material::dielectric(1.5);
885 let ray = Ray::new([0.0, 0.0, 1.0], normalize([0.0, 0.0, -1.0]));
886 let hit = HitRecord {
887 t: 1.0,
888 point: [0.0; 3],
889 normal: [0.0, 0.0, 1.0],
890 material_index: 0,
891 front_face: true,
892 };
893 let mut rng = make_rng();
894 let result = mat.scatter(&ray, &hit, &mut rng);
895 assert!(result.is_some());
896 let (_s, attn) = result.unwrap();
897 assert!((attn[0] - 1.0).abs() < 1e-6);
898 }
899
900 #[test]
901 fn test_metal_fuzz_clamped() {
902 let mat = Material::metal([1.0; 3], 5.0);
903 if let MaterialType::Metal(f) = mat.kind {
904 assert!(f <= 1.0);
905 } else {
906 panic!("expected Metal");
907 }
908 }
909
910 #[test]
913 fn test_scene_add_material() {
914 let mut scene = PathTracerScene::new();
915 let idx = scene.add_material(Material::lambertian([1.0; 3]));
916 assert_eq!(idx, 0);
917 let idx2 = scene.add_material(Material::lambertian([0.5; 3]));
918 assert_eq!(idx2, 1);
919 }
920
921 #[test]
922 fn test_scene_hit_sphere() {
923 let mut scene = PathTracerScene::new();
924 let m = scene.add_material(Material::lambertian([0.5; 3]));
925 scene.add_sphere(Sphere::new([0.0, 0.0, -1.0], 0.5, m));
926 let r = Ray::new([0.0; 3], [0.0, 0.0, -1.0]);
927 assert!(scene.hit_scene(&r, 0.001, f32::INFINITY).is_some());
928 }
929
930 #[test]
931 fn test_scene_miss() {
932 let scene = PathTracerScene::new();
933 let r = Ray::new([0.0; 3], [0.0, 0.0, -1.0]);
934 assert!(scene.hit_scene(&r, 0.001, f32::INFINITY).is_none());
935 }
936
937 #[test]
938 fn test_scene_sky_color_up() {
939 let scene = PathTracerScene::new();
940 let r = Ray::new([0.0; 3], [0.0, 1.0, 0.0]);
941 let c = scene.sky_color(&r);
942 assert!(c[2] > 0.9);
944 }
945
946 #[test]
947 fn test_scene_trace_no_hit() {
948 let scene = PathTracerScene::new();
949 let r = Ray::new([0.0; 3], [0.0, 1.0, 0.0]);
950 let mut rng = make_rng();
951 let c = scene.trace(&r, 5, &mut rng);
952 assert!(c[2] > 0.0);
954 }
955
956 #[test]
957 fn test_scene_trace_depth_zero() {
958 let mut scene = PathTracerScene::new();
959 let m = scene.add_material(Material::lambertian([0.5; 3]));
960 scene.add_sphere(Sphere::new([0.0, 0.0, -1.0], 0.5, m));
961 let r = Ray::new([0.0; 3], [0.0, 0.0, -1.0]);
962 let mut rng = make_rng();
963 let c = scene.trace(&r, 0, &mut rng);
964 assert_eq!(c, [0.0; 3]);
965 }
966
967 #[test]
968 fn test_scene_closest_hit() {
969 let mut scene = PathTracerScene::new();
970 let m = scene.add_material(Material::lambertian([0.5; 3]));
971 scene.add_sphere(Sphere::new([0.0, 0.0, -2.0], 0.5, m));
972 scene.add_sphere(Sphere::new([0.0, 0.0, -1.0], 0.5, m));
973 let r = Ray::new([0.0; 3], [0.0, 0.0, -1.0]);
974 let hit = scene.hit_scene(&r, 0.001, f32::INFINITY).unwrap();
975 assert!(hit.t < 1.0);
977 }
978
979 #[test]
982 fn test_buffer_new() {
983 let buf = PathTracerBuffer::new(4, 4);
984 assert_eq!(buf.width, 4);
985 assert_eq!(buf.height, 4);
986 assert_eq!(buf.total_samples(), 0);
987 }
988
989 #[test]
990 fn test_buffer_add_and_get() {
991 let mut buf = PathTracerBuffer::new(4, 4);
992 buf.add_sample(1, 2, [0.6, 0.4, 0.2]);
993 buf.add_sample(1, 2, [0.4, 0.6, 0.8]);
994 let p = buf.get_pixel(1, 2);
995 assert!((p[0] - 0.5).abs() < 1e-5);
996 assert!((p[1] - 0.5).abs() < 1e-5);
997 assert!((p[2] - 0.5).abs() < 1e-5);
998 }
999
1000 #[test]
1001 fn test_buffer_zero_samples() {
1002 let buf = PathTracerBuffer::new(4, 4);
1003 let p = buf.get_pixel(0, 0);
1004 assert_eq!(p, [0.0; 3]);
1005 }
1006
1007 #[test]
1008 fn test_buffer_to_rgb8_white() {
1009 let mut buf = PathTracerBuffer::new(1, 1);
1010 buf.add_sample(0, 0, [1.0; 3]);
1011 let rgb = buf.to_rgb8();
1012 assert_eq!(rgb.len(), 3);
1013 assert_eq!(rgb[0], 255);
1014 }
1015
1016 #[test]
1017 fn test_buffer_to_rgb8_black() {
1018 let mut buf = PathTracerBuffer::new(1, 1);
1019 buf.add_sample(0, 0, [0.0; 3]);
1020 let rgb = buf.to_rgb8();
1021 assert_eq!(rgb[0], 0);
1022 }
1023
1024 #[test]
1025 fn test_buffer_total_samples() {
1026 let mut buf = PathTracerBuffer::new(2, 2);
1027 buf.add_sample(0, 0, [1.0; 3]);
1028 buf.add_sample(0, 0, [1.0; 3]);
1029 buf.add_sample(1, 1, [0.5; 3]);
1030 assert_eq!(buf.total_samples(), 3);
1031 }
1032
1033 #[test]
1034 fn test_buffer_clear() {
1035 let mut buf = PathTracerBuffer::new(2, 2);
1036 buf.add_sample(0, 0, [1.0; 3]);
1037 buf.clear();
1038 assert_eq!(buf.total_samples(), 0);
1039 assert_eq!(buf.get_pixel(0, 0), [0.0; 3]);
1040 }
1041
1042 #[test]
1043 fn test_buffer_size() {
1044 let buf = PathTracerBuffer::new(8, 6);
1045 assert_eq!(buf.accumulator.len(), 48);
1046 assert_eq!(buf.sample_count.len(), 48);
1047 }
1048
1049 #[test]
1052 fn test_camera_get_ray_center() {
1053 let cam = Camera::new(
1054 [0.0, 0.0, 0.0],
1055 [0.0, 0.0, -1.0],
1056 [0.0, 1.0, 0.0],
1057 90.0,
1058 1.0,
1059 0.0,
1060 1.0,
1061 );
1062 let mut rng = make_rng();
1063 let ray = cam.get_ray(0.5, 0.5, &mut rng);
1064 let d = normalize(ray.direction);
1066 assert!(d[2] < -0.9);
1067 }
1068
1069 #[test]
1070 fn test_camera_origin() {
1071 let cam = Camera::new(
1072 [1.0, 2.0, 3.0],
1073 [0.0, 0.0, 0.0],
1074 [0.0, 1.0, 0.0],
1075 60.0,
1076 1.5,
1077 0.0,
1078 1.0,
1079 );
1080 assert!((cam.origin[0] - 1.0).abs() < 1e-5);
1081 }
1082
1083 #[test]
1086 fn test_renderer_render_pass_small() {
1087 let mut scene = PathTracerScene::new();
1088 let m = scene.add_material(Material::lambertian([0.7, 0.3, 0.5]));
1089 scene.add_sphere(Sphere::new([0.0, 0.0, -1.0], 0.5, m));
1090 let cam = Camera::new(
1091 [0.0, 0.0, 0.0],
1092 [0.0, 0.0, -1.0],
1093 [0.0, 1.0, 0.0],
1094 90.0,
1095 1.0,
1096 0.0,
1097 1.0,
1098 );
1099 let renderer = PathTracerRenderer::new(scene, cam, 3, 2);
1100 let mut buf = PathTracerBuffer::new(4, 4);
1101 renderer.render_pass(&mut buf);
1102 assert!(buf.total_samples() > 0);
1103 assert_eq!(buf.total_samples(), (4 * 4) as u64);
1105 }
1106
1107 #[test]
1108 fn test_renderer_rgb8_output_valid() {
1109 let mut scene = PathTracerScene::new();
1110 let m = scene.add_material(Material::lambertian([0.5; 3]));
1111 scene.add_sphere(Sphere::new([0.0, 0.0, -1.0], 0.5, m));
1112 let cam = Camera::new(
1113 [0.0, 0.0, 0.0],
1114 [0.0, 0.0, -1.0],
1115 [0.0, 1.0, 0.0],
1116 90.0,
1117 1.0,
1118 0.0,
1119 1.0,
1120 );
1121 let renderer = PathTracerRenderer::new(scene, cam, 2, 1);
1122 let mut buf = PathTracerBuffer::new(8, 8);
1123 renderer.render_pass(&mut buf);
1124 let rgb = buf.to_rgb8();
1125 assert_eq!(rgb.len(), 8 * 8 * 3);
1126 for &v in &rgb {
1128 let _ = v; }
1130 }
1131
1132 #[test]
1135 fn test_vadd() {
1136 let a = [1.0, 2.0, 3.0];
1137 let b = [4.0, 5.0, 6.0];
1138 let c = vadd(a, b);
1139 assert_eq!(c, [5.0, 7.0, 9.0]);
1140 }
1141
1142 #[test]
1143 fn test_vsub() {
1144 let a = [3.0, 2.0, 1.0];
1145 let b = [1.0, 1.0, 1.0];
1146 assert_eq!(vsub(a, b), [2.0, 1.0, 0.0]);
1147 }
1148
1149 #[test]
1150 fn test_dot_orthogonal() {
1151 assert!((dot([1.0, 0.0, 0.0], [0.0, 1.0, 0.0])).abs() < 1e-7);
1152 }
1153
1154 #[test]
1155 fn test_cross_unit_vectors() {
1156 let k = cross([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
1157 assert!((k[2] - 1.0).abs() < 1e-7);
1158 }
1159
1160 #[test]
1161 fn test_normalize_length() {
1162 let v = [3.0, 4.0, 0.0];
1163 let n = normalize(v);
1164 let l = length(n);
1165 assert!((l - 1.0).abs() < 1e-6);
1166 }
1167
1168 #[test]
1169 fn test_reflect_normal_incidence() {
1170 let d = [0.0, -1.0, 0.0];
1171 let n = [0.0, 1.0, 0.0];
1172 let r = reflect(d, n);
1173 assert!((r[1] - 1.0).abs() < 1e-6);
1174 }
1175
1176 #[test]
1177 fn test_schlick_zero_angle() {
1178 let s = schlick(0.0, 1.5);
1180 assert!((s - 1.0).abs() < 1e-5);
1181 }
1182
1183 #[test]
1184 fn test_schlick_grazing() {
1185 let s = schlick(1.0, 1.5);
1187 let r0 = ((1.0 - 1.5f32) / (1.0 + 1.5)).powi(2);
1188 assert!((s - r0).abs() < 1e-5);
1189 }
1190}