1use std::f64::consts::PI;
6
7use super::types::{
8 AreaLight, Bvh, Camera, HitRecord, Material, MaterialType, PathState, PointLight, Ray,
9 RenderConfig, Triangle,
10};
11
12pub fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
14 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
15}
16pub fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
18 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
19}
20pub fn scale3(v: [f64; 3], s: f64) -> [f64; 3] {
22 [v[0] * s, v[1] * s, v[2] * s]
23}
24pub fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
26 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
27}
28pub fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
30 [
31 a[1] * b[2] - a[2] * b[1],
32 a[2] * b[0] - a[0] * b[2],
33 a[0] * b[1] - a[1] * b[0],
34 ]
35}
36pub fn length3(v: [f64; 3]) -> f64 {
38 dot3(v, v).sqrt()
39}
40pub fn normalize3(v: [f64; 3]) -> [f64; 3] {
42 let len = length3(v);
43 if len < 1e-15 {
44 return [0.0; 3];
45 }
46 scale3(v, 1.0 / len)
47}
48pub fn reflect3(d: [f64; 3], n: [f64; 3]) -> [f64; 3] {
50 let dn2 = 2.0 * dot3(d, n);
51 sub3(d, scale3(n, dn2))
52}
53pub fn mul_color(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
55 [a[0] * b[0], a[1] * b[1], a[2] * b[2]]
56}
57pub fn clamp_color(c: [f64; 3]) -> [f64; 3] {
59 [
60 c[0].clamp(0.0, 1.0),
61 c[1].clamp(0.0, 1.0),
62 c[2].clamp(0.0, 1.0),
63 ]
64}
65pub fn phong_shading(
69 hit: &HitRecord,
70 light: &PointLight,
71 view_dir: [f64; 3],
72 mat: &Material,
73 shadow: bool,
74) -> [f64; 3] {
75 if shadow {
76 return [0.0; 3];
77 }
78 let light_vec = sub3(light.position, hit.position);
79 let dist = length3(light_vec);
80 let light_dir = normalize3(light_vec);
81 let n_dot_l = dot3(hit.normal, light_dir).max(0.0);
82 let diffuse = scale3(
83 mul_color(mat.albedo, light.color),
84 n_dot_l * light.intensity * light.attenuate(dist),
85 );
86 let reflect_dir = reflect3(scale3(light_dir, -1.0), hit.normal);
87 let r_dot_v = dot3(reflect_dir, view_dir).max(0.0);
88 let spec_factor = r_dot_v.powf(mat.shininess.max(1.0));
89 let specular = scale3(
90 mul_color(light.color, [1.0; 3]),
91 spec_factor * light.intensity * light.attenuate(dist),
92 );
93 add3(diffuse, specular)
94}
95pub fn fresnel_schlick(cos_theta: f64, f0: [f64; 3]) -> [f64; 3] {
97 let c = (1.0 - cos_theta).clamp(0.0, 1.0);
98 let c5 = c * c * c * c * c;
99 [
100 f0[0] + (1.0 - f0[0]) * c5,
101 f0[1] + (1.0 - f0[1]) * c5,
102 f0[2] + (1.0 - f0[2]) * c5,
103 ]
104}
105pub fn distribution_ggx(n_dot_h: f64, roughness: f64) -> f64 {
107 let a = roughness * roughness;
108 let a2 = a * a;
109 let n_dot_h2 = n_dot_h * n_dot_h;
110 let denom = n_dot_h2 * (a2 - 1.0) + 1.0;
111 if denom.abs() < 1e-15 {
112 return 0.0;
113 }
114 a2 / (PI * denom * denom)
115}
116pub fn geometry_smith(n_dot_v: f64, n_dot_l: f64, roughness: f64) -> f64 {
118 let r = roughness + 1.0;
119 let k = r * r / 8.0;
120 let ggx1 = n_dot_v / (n_dot_v * (1.0 - k) + k);
121 let ggx2 = n_dot_l / (n_dot_l * (1.0 - k) + k);
122 ggx1 * ggx2
123}
124pub fn pbr_shading(
126 hit: &HitRecord,
127 light: &PointLight,
128 view_dir: [f64; 3],
129 mat: &Material,
130 shadow: bool,
131) -> [f64; 3] {
132 if shadow {
133 return [0.0; 3];
134 }
135 let light_vec = sub3(light.position, hit.position);
136 let dist = length3(light_vec);
137 let l = normalize3(light_vec);
138 let n = hit.normal;
139 let v = view_dir;
140 let h = normalize3(add3(v, l));
141 let n_dot_l = dot3(n, l).max(0.0);
142 if n_dot_l < 1e-10 {
143 return [0.0; 3];
144 }
145 let n_dot_v = dot3(n, v).max(1e-4);
146 let n_dot_h = dot3(n, h).max(0.0);
147 let h_dot_v = dot3(h, v).max(0.0);
148 let f0_dielectric = [0.04; 3];
149 let f0 = [
150 f0_dielectric[0] * (1.0 - mat.metallic) + mat.albedo[0] * mat.metallic,
151 f0_dielectric[1] * (1.0 - mat.metallic) + mat.albedo[1] * mat.metallic,
152 f0_dielectric[2] * (1.0 - mat.metallic) + mat.albedo[2] * mat.metallic,
153 ];
154 let f = fresnel_schlick(h_dot_v, f0);
155 let d = distribution_ggx(n_dot_h, mat.roughness);
156 let g = geometry_smith(n_dot_v, n_dot_l, mat.roughness);
157 let denom = 4.0 * n_dot_v * n_dot_l;
158 let specular = if denom > 1e-10 {
159 scale3(f, d * g / denom)
160 } else {
161 [0.0; 3]
162 };
163 let kd = [
164 (1.0 - f[0]) * (1.0 - mat.metallic),
165 (1.0 - f[1]) * (1.0 - mat.metallic),
166 (1.0 - f[2]) * (1.0 - mat.metallic),
167 ];
168 let diffuse = [
169 kd[0] * mat.albedo[0] / PI,
170 kd[1] * mat.albedo[1] / PI,
171 kd[2] * mat.albedo[2] / PI,
172 ];
173 let radiance = scale3(light.color, light.intensity * light.attenuate(dist));
174 let result = add3(diffuse, specular);
175 [
176 result[0] * radiance[0] * n_dot_l,
177 result[1] * radiance[1] * n_dot_l,
178 result[2] * radiance[2] * n_dot_l,
179 ]
180}
181pub fn soft_shadow_factor(
184 hit_pos: [f64; 3],
185 hit_normal: [f64; 3],
186 light: &AreaLight,
187 bvh: &Bvh,
188 triangles: &[Triangle],
189 num_samples: usize,
190 samples: &[[f64; 2]],
191) -> f64 {
192 if num_samples == 0 || samples.is_empty() {
193 return 1.0;
194 }
195 let actual_samples = num_samples.min(samples.len());
196 let mut unblocked = 0u32;
197 for sample in &samples[..actual_samples] {
198 let su = sample[0] * 2.0 - 1.0;
199 let sv = sample[1] * 2.0 - 1.0;
200 let light_point = light.sample_point(su, sv);
201 let to_light = sub3(light_point, hit_pos);
202 let dist = length3(to_light);
203 if dist < 1e-10 {
204 unblocked += 1;
205 continue;
206 }
207 let dir = scale3(to_light, 1.0 / dist);
208 if dot3(hit_normal, dir) <= 0.0 {
209 continue;
210 }
211 let mut shadow_ray = Ray::new(add3(hit_pos, scale3(hit_normal, 1e-4)), dir);
212 shadow_ray.t_max = dist - 1e-4;
213 if !bvh.intersect_any(&shadow_ray, triangles) {
214 unblocked += 1;
215 }
216 }
217 unblocked as f64 / actual_samples as f64
218}
219pub fn ambient_occlusion(
223 hit_pos: [f64; 3],
224 hit_normal: [f64; 3],
225 bvh: &Bvh,
226 triangles: &[Triangle],
227 hemisphere_samples: &[[f64; 3]],
228 max_dist: f64,
229) -> f64 {
230 if hemisphere_samples.is_empty() {
231 return 1.0;
232 }
233 let tangent = if hit_normal[0].abs() < 0.9 {
234 normalize3(cross3(hit_normal, [1.0, 0.0, 0.0]))
235 } else {
236 normalize3(cross3(hit_normal, [0.0, 1.0, 0.0]))
237 };
238 let bitangent = cross3(hit_normal, tangent);
239 let mut unoccluded = 0u32;
240 let n = hemisphere_samples.len();
241 for s in hemisphere_samples {
242 let world_dir = normalize3(add3(
243 add3(scale3(tangent, s[0]), scale3(bitangent, s[1])),
244 scale3(hit_normal, s[2].abs()),
245 ));
246 if dot3(world_dir, hit_normal) <= 0.0 {
247 unoccluded += 1;
248 continue;
249 }
250 let origin = add3(hit_pos, scale3(hit_normal, 1e-4));
251 let mut ao_ray = Ray::new(origin, world_dir);
252 ao_ray.t_max = max_dist;
253 if !bvh.intersect_any(&ao_ray, triangles) {
254 unoccluded += 1;
255 }
256 }
257 unoccluded as f64 / n as f64
258}
259pub fn schlick_reflectance(cos_theta: f64, ior_ratio: f64) -> f64 {
261 let r0 = ((1.0 - ior_ratio) / (1.0 + ior_ratio)).powi(2);
262 r0 + (1.0 - r0) * (1.0 - cos_theta).powi(5)
263}
264pub fn refract(d: [f64; 3], n: [f64; 3], ior_ratio: f64) -> Option<[f64; 3]> {
268 let cos_theta = dot3(scale3(d, -1.0), n).min(1.0);
269 let sin_theta_sq = 1.0 - cos_theta * cos_theta;
270 if sin_theta_sq * ior_ratio * ior_ratio > 1.0 {
271 return None;
272 }
273 let r_out_perp = scale3(add3(d, scale3(n, cos_theta)), ior_ratio);
274 let r_out_parallel = scale3(n, -(1.0 - dot3(r_out_perp, r_out_perp)).abs().sqrt());
275 Some(normalize3(add3(r_out_perp, r_out_parallel)))
276}
277pub fn cosine_sample_hemisphere(u1: f64, u2: f64) -> [f64; 3] {
282 let r = u1.sqrt();
283 let theta = 2.0 * PI * u2;
284 let x = r * theta.cos();
285 let z = r * theta.sin();
286 let y = (1.0 - u1).max(0.0).sqrt();
287 [x, y, z]
288}
289pub fn uniform_sample_hemisphere(u1: f64, u2: f64) -> [f64; 3] {
291 let cos_theta = u1;
292 let sin_theta = (1.0 - cos_theta * cos_theta).max(0.0).sqrt();
293 let phi = 2.0 * PI * u2;
294 [sin_theta * phi.cos(), cos_theta, sin_theta * phi.sin()]
295}
296pub fn path_trace_step(
300 state: &mut PathState,
301 bvh: &Bvh,
302 triangles: &[Triangle],
303 materials: &[Material],
304 background: [f64; 3],
305 u1: f64,
306 u2: f64,
307) -> bool {
308 if !state.should_continue() {
309 if state.depth == 0 {
310 state.radiance = background;
311 }
312 return false;
313 }
314 match bvh.intersect(&state.ray, triangles) {
315 None => {
316 let contrib = [
317 state.throughput[0] * background[0],
318 state.throughput[1] * background[1],
319 state.throughput[2] * background[2],
320 ];
321 state.radiance = add3(state.radiance, contrib);
322 false
323 }
324 Some((hit, _tri)) => {
325 let mat = if (hit.material_id as usize) < materials.len() {
326 &materials[hit.material_id as usize]
327 } else {
328 &materials[0]
329 };
330 let emission_contrib = [
331 state.throughput[0] * mat.emission[0],
332 state.throughput[1] * mat.emission[1],
333 state.throughput[2] * mat.emission[2],
334 ];
335 state.radiance = add3(state.radiance, emission_contrib);
336 let local_dir = cosine_sample_hemisphere(u1, u2);
337 let tangent = if hit.normal[0].abs() < 0.9 {
338 normalize3(cross3(hit.normal, [1.0, 0.0, 0.0]))
339 } else {
340 normalize3(cross3(hit.normal, [0.0, 1.0, 0.0]))
341 };
342 let bitangent = cross3(hit.normal, tangent);
343 let world_dir = normalize3(add3(
344 add3(
345 scale3(tangent, local_dir[0]),
346 scale3(bitangent, local_dir[2]),
347 ),
348 scale3(hit.normal, local_dir[1]),
349 ));
350 let cos_theta = dot3(hit.normal, world_dir).max(0.0);
351 state.throughput = [
352 state.throughput[0] * mat.albedo[0] * cos_theta * 2.0,
353 state.throughput[1] * mat.albedo[1] * cos_theta * 2.0,
354 state.throughput[2] * mat.albedo[2] * cos_theta * 2.0,
355 ];
356 state.ray = Ray::new(add3(hit.position, scale3(hit.normal, 1e-4)), world_dir);
357 state.depth += 1;
358 true
359 }
360 }
361}
362pub fn atrous_denoise(
368 color: &[[f64; 3]],
369 normal: &[[f64; 3]],
370 position: &[[f64; 3]],
371 width: usize,
372 height: usize,
373 step_width: usize,
374 sigma_color: f64,
375 sigma_normal: f64,
376 sigma_position: f64,
377) -> Vec<[f64; 3]> {
378 let kernel = [
379 [
380 1.0f64 / 256.0,
381 1.0 / 64.0,
382 3.0 / 128.0,
383 1.0 / 64.0,
384 1.0 / 256.0,
385 ],
386 [1.0 / 64.0, 1.0 / 16.0, 3.0 / 32.0, 1.0 / 16.0, 1.0 / 64.0],
387 [3.0 / 128.0, 3.0 / 32.0, 9.0 / 64.0, 3.0 / 32.0, 3.0 / 128.0],
388 [1.0 / 64.0, 1.0 / 16.0, 3.0 / 32.0, 1.0 / 16.0, 1.0 / 64.0],
389 [
390 1.0 / 256.0,
391 1.0 / 64.0,
392 3.0 / 128.0,
393 1.0 / 64.0,
394 1.0 / 256.0,
395 ],
396 ];
397 let n = width * height;
398 let mut output = vec![[0.0f64; 3]; n];
399 for py in 0..height {
400 for px in 0..width {
401 let idx = py * width + px;
402 let c_center = color[idx];
403 let n_center = normal[idx];
404 let p_center = position[idx];
405 let mut accum = [0.0f64; 3];
406 let mut weight_sum = 0.0f64;
407 for ky in 0..5i32 {
408 for kx in 0..5i32 {
409 let oy = ky - 2;
410 let ox = kx - 2;
411 let nx = px as i32 + ox * step_width as i32;
412 let ny = py as i32 + oy * step_width as i32;
413 if nx < 0 || ny < 0 || nx >= width as i32 || ny >= height as i32 {
414 continue;
415 }
416 let sidx = ny as usize * width + nx as usize;
417 let c_s = color[sidx];
418 let n_s = normal[sidx];
419 let p_s = position[sidx];
420 let dc = [
421 c_center[0] - c_s[0],
422 c_center[1] - c_s[1],
423 c_center[2] - c_s[2],
424 ];
425 let dist_c = dc[0] * dc[0] + dc[1] * dc[1] + dc[2] * dc[2];
426 let w_c = (-dist_c / (sigma_color * sigma_color)).exp();
427 let dn_x = n_center[0] - n_s[0];
428 let dn_y = n_center[1] - n_s[1];
429 let dn_z = n_center[2] - n_s[2];
430 let dist_n = dn_x * dn_x + dn_y * dn_y + dn_z * dn_z;
431 let w_n = (-dist_n / (sigma_normal * sigma_normal)).exp();
432 let dp_x = p_center[0] - p_s[0];
433 let dp_y = p_center[1] - p_s[1];
434 let dp_z = p_center[2] - p_s[2];
435 let dist_p = dp_x * dp_x + dp_y * dp_y + dp_z * dp_z;
436 let w_p = (-dist_p / (sigma_position * sigma_position)).exp();
437 let h_weight = kernel[ky as usize][kx as usize];
438 let w = h_weight * w_c * w_n * w_p;
439 accum[0] += w * c_s[0];
440 accum[1] += w * c_s[1];
441 accum[2] += w * c_s[2];
442 weight_sum += w;
443 }
444 }
445 if weight_sum > 1e-10 {
446 output[idx] = [
447 accum[0] / weight_sum,
448 accum[1] / weight_sum,
449 accum[2] / weight_sum,
450 ];
451 } else {
452 output[idx] = c_center;
453 }
454 }
455 }
456 output
457}
458pub fn temporal_accumulate(
462 current: &[[f64; 3]],
463 history: &[[f64; 3]],
464 alpha: f64,
465) -> Vec<[f64; 3]> {
466 let n = current.len().min(history.len());
467 let mut result = Vec::with_capacity(n);
468 for (&c, &h) in current[..n].iter().zip(&history[..n]) {
469 result.push([
470 alpha * c[0] + (1.0 - alpha) * h[0],
471 alpha * c[1] + (1.0 - alpha) * h[1],
472 alpha * c[2] + (1.0 - alpha) * h[2],
473 ]);
474 }
475 result
476}
477pub fn box_filter(color: &[[f64; 3]], width: usize, height: usize, radius: usize) -> Vec<[f64; 3]> {
479 let n = width * height;
480 let mut output = vec![[0.0f64; 3]; n];
481 for py in 0..height {
482 for px in 0..width {
483 let mut accum = [0.0f64; 3];
484 let mut count = 0u32;
485 let y0 = py.saturating_sub(radius);
486 let y1 = (py + radius + 1).min(height);
487 let x0 = px.saturating_sub(radius);
488 let x1 = (px + radius + 1).min(width);
489 for sy in y0..y1 {
490 for sx in x0..x1 {
491 let sidx = sy * width + sx;
492 accum[0] += color[sidx][0];
493 accum[1] += color[sidx][1];
494 accum[2] += color[sidx][2];
495 count += 1;
496 }
497 }
498 let inv = 1.0 / count as f64;
499 output[py * width + px] = [accum[0] * inv, accum[1] * inv, accum[2] * inv];
500 }
501 }
502 output
503}
504pub fn tonemap_reinhard(color: [f64; 3]) -> [f64; 3] {
506 [
507 color[0] / (1.0 + color[0]),
508 color[1] / (1.0 + color[1]),
509 color[2] / (1.0 + color[2]),
510 ]
511}
512pub fn tonemap_filmic(color: [f64; 3]) -> [f64; 3] {
514 let f = |x: f64| {
515 let x = (x - 0.004).max(0.0);
516 (x * (6.2 * x + 0.5)) / (x * (6.2 * x + 1.7) + 0.06)
517 };
518 [f(color[0]), f(color[1]), f(color[2])]
519}
520pub fn tonemap_aces(color: [f64; 3]) -> [f64; 3] {
522 let aces = |x: f64| {
523 let a = 2.51;
524 let b = 0.03;
525 let c = 2.43;
526 let d = 0.59;
527 let e = 0.14;
528 ((x * (a * x + b)) / (x * (c * x + d) + e)).clamp(0.0, 1.0)
529 };
530 [aces(color[0]), aces(color[1]), aces(color[2])]
531}
532pub fn linear_to_srgb(c: f64) -> f64 {
534 if c <= 0.0031308 {
535 c * 12.92
536 } else {
537 1.055 * c.powf(1.0 / 2.4) - 0.055
538 }
539}
540pub fn gamma_correct(color: [f64; 3]) -> [f64; 3] {
542 [
543 linear_to_srgb(color[0].clamp(0.0, 1.0)),
544 linear_to_srgb(color[1].clamp(0.0, 1.0)),
545 linear_to_srgb(color[2].clamp(0.0, 1.0)),
546 ]
547}
548pub fn render_direct(
552 config: &RenderConfig,
553 camera: &Camera,
554 bvh: &Bvh,
555 triangles: &[Triangle],
556 materials: &[Material],
557 lights: &[PointLight],
558) -> Vec<[f64; 3]> {
559 let n = config.width * config.height;
560 let mut image = vec![[0.0f64; 3]; n];
561 let w = config.width as f64;
562 let h = config.height as f64;
563 for py in 0..config.height {
564 for px in 0..config.width {
565 let ray = camera.generate_ray(px as f64, py as f64, w, h);
566 let color = trace_direct(&ray, bvh, triangles, materials, lights, config);
567 let idx = py * config.width + px;
568 image[idx] = match config.tonemap {
569 1 => gamma_correct(tonemap_reinhard(color)),
570 2 => gamma_correct(tonemap_filmic(color)),
571 3 => gamma_correct(tonemap_aces(color)),
572 _ => gamma_correct(color),
573 };
574 }
575 }
576 image
577}
578pub(super) fn trace_direct(
580 ray: &Ray,
581 bvh: &Bvh,
582 triangles: &[Triangle],
583 materials: &[Material],
584 lights: &[PointLight],
585 config: &RenderConfig,
586) -> [f64; 3] {
587 match bvh.intersect(ray, triangles) {
588 None => config.background,
589 Some((hit, _tri)) => {
590 let mat = if (hit.material_id as usize) < materials.len() {
591 &materials[hit.material_id as usize]
592 } else {
593 return config.background;
594 };
595 if mat.mat_type == MaterialType::Emissive {
596 return mat.emission;
597 }
598 let view_dir = normalize3(scale3(ray.direction, -1.0));
599 let mut color = config.ambient;
600 for light in lights {
601 let to_light = sub3(light.position, hit.position);
602 let dist = length3(to_light);
603 let light_dir = normalize3(to_light);
604 let shadow_origin = add3(hit.position, scale3(hit.normal, 1e-4));
605 let mut shadow_ray = Ray::new(shadow_origin, light_dir);
606 shadow_ray.t_max = dist - 1e-4;
607 let in_shadow = bvh.intersect_any(&shadow_ray, triangles);
608 let contrib = if mat.mat_type == MaterialType::Pbr {
609 pbr_shading(&hit, light, view_dir, mat, in_shadow)
610 } else {
611 phong_shading(&hit, light, view_dir, mat, in_shadow)
612 };
613 color = add3(color, contrib);
614 }
615 clamp_color(color)
616 }
617 }
618}
619#[cfg(test)]
620mod tests {
621 use super::*;
622 use crate::raytracing::Aabb;
623 use crate::raytracing::Scene;
624 fn make_floor_triangle() -> Triangle {
625 Triangle::new(
626 [-5.0, 0.0, -5.0],
627 [5.0, 0.0, -5.0],
628 [0.0, 0.0, 5.0],
629 [0.0, 1.0, 0.0],
630 [0.0, 1.0, 0.0],
631 [0.0, 1.0, 0.0],
632 [0.0, 0.0],
633 [1.0, 0.0],
634 [0.5, 1.0],
635 0,
636 )
637 }
638 #[test]
639 fn test_ray_at() {
640 let ray = Ray::new([0.0; 3], [0.0, 0.0, 1.0]);
641 let p = ray.at(3.0);
642 assert!((p[2] - 3.0).abs() < 1e-12);
643 }
644 #[test]
645 fn test_triangle_intersect_hit() {
646 let tri = make_floor_triangle();
647 let ray = Ray::new([0.0, 5.0, 0.0], [0.0, -1.0, 0.0]);
648 assert!(tri.intersect(&ray).is_some());
649 }
650 #[test]
651 fn test_triangle_intersect_miss_parallel() {
652 let tri = make_floor_triangle();
653 let ray = Ray::new([0.0, 5.0, 0.0], [1.0, 0.0, 0.0]);
654 assert!(tri.intersect(&ray).is_none());
655 }
656 #[test]
657 fn test_triangle_intersect_miss_outside() {
658 let tri = make_floor_triangle();
659 let ray = Ray::new([100.0, 5.0, 0.0], [0.0, -1.0, 0.0]);
660 assert!(tri.intersect(&ray).is_none());
661 }
662 #[test]
663 fn test_triangle_geometric_normal() {
664 let tri = Triangle::new(
665 [0.0, 0.0, 0.0],
666 [1.0, 0.0, 0.0],
667 [0.0, 1.0, 0.0],
668 [0.0, 0.0, 1.0],
669 [0.0, 0.0, 1.0],
670 [0.0, 0.0, 1.0],
671 [0.0, 0.0],
672 [1.0, 0.0],
673 [0.0, 1.0],
674 0,
675 );
676 let n = tri.geometric_normal();
677 assert!((n[2] - 1.0).abs() < 1e-10 || (n[2] + 1.0).abs() < 1e-10);
678 }
679 #[test]
680 fn test_triangle_area() {
681 let tri = Triangle::new(
682 [0.0, 0.0, 0.0],
683 [2.0, 0.0, 0.0],
684 [0.0, 2.0, 0.0],
685 [0.0, 0.0, 1.0],
686 [0.0, 0.0, 1.0],
687 [0.0, 0.0, 1.0],
688 [0.0, 0.0],
689 [1.0, 0.0],
690 [0.0, 1.0],
691 0,
692 );
693 assert!((tri.area() - 2.0).abs() < 1e-10);
694 }
695 #[test]
696 fn test_aabb_merge() {
697 let a = Aabb::new([0.0; 3], [1.0; 3]);
698 let b = Aabb::new([-1.0; 3], [2.0; 3]);
699 let merged = a.merge(&b);
700 assert!((merged.min[0] + 1.0).abs() < 1e-12);
701 assert!((merged.max[0] - 2.0).abs() < 1e-12);
702 }
703 #[test]
704 fn test_aabb_ray_hit() {
705 let aabb = Aabb::new([-1.0; 3], [1.0; 3]);
706 let ray = Ray::new([0.0, 0.0, -5.0], [0.0, 0.0, 1.0]);
707 assert!(aabb.intersect_ray(&ray).is_some());
708 }
709 #[test]
710 fn test_aabb_ray_miss() {
711 let aabb = Aabb::new([-1.0; 3], [1.0; 3]);
712 let ray = Ray::new([5.0, 0.0, -5.0], [0.0, 0.0, 1.0]);
713 assert!(aabb.intersect_ray(&ray).is_none());
714 }
715 #[test]
716 fn test_aabb_longest_axis() {
717 let aabb = Aabb::new([0.0, 0.0, 0.0], [3.0, 1.0, 2.0]);
718 assert_eq!(aabb.longest_axis(), 0);
719 }
720 #[test]
721 fn test_bvh_build_empty() {
722 let bvh = Bvh::build(&[]);
723 assert_eq!(bvh.prim_count, 0);
724 }
725 #[test]
726 fn test_bvh_build_single_triangle() {
727 let tri = make_floor_triangle();
728 let bvh = Bvh::build(&[tri]);
729 assert_eq!(bvh.prim_count, 1);
730 }
731 #[test]
732 fn test_bvh_intersect_hit() {
733 let tri = make_floor_triangle();
734 let triangles = vec![tri];
735 let bvh = Bvh::build(&triangles);
736 let ray = Ray::new([0.0, 5.0, 0.0], [0.0, -1.0, 0.0]);
737 assert!(bvh.intersect(&ray, &triangles).is_some());
738 }
739 #[test]
740 fn test_bvh_intersect_miss() {
741 let tri = make_floor_triangle();
742 let triangles = vec![tri];
743 let bvh = Bvh::build(&triangles);
744 let ray = Ray::new([0.0, 5.0, 0.0], [0.0, 1.0, 0.0]);
745 assert!(bvh.intersect(&ray, &triangles).is_none());
746 }
747 #[test]
748 fn test_bvh_intersect_any_shadow() {
749 let tri = make_floor_triangle();
750 let triangles = vec![tri];
751 let bvh = Bvh::build(&triangles);
752 let ray = Ray::new([0.0, 5.0, 0.0], [0.0, -1.0, 0.0]);
753 assert!(bvh.intersect_any(&ray, &triangles));
754 }
755 #[test]
756 fn test_camera_generate_ray() {
757 let cam = Camera::look_at(
758 [0.0, 0.0, 5.0],
759 [0.0, 0.0, 0.0],
760 [0.0, 1.0, 0.0],
761 60.0,
762 16.0 / 9.0,
763 0.0,
764 5.0,
765 );
766 let ray = cam.generate_ray(400.0, 300.0, 800.0, 600.0);
767 assert!(ray.direction[2] < 0.0);
768 }
769 #[test]
770 fn test_fresnel_schlick_zero_angle() {
771 let f0 = [0.04; 3];
772 let f = fresnel_schlick(0.0, f0);
773 assert!((f[0] - 1.0).abs() < 1e-10);
774 }
775 #[test]
776 fn test_fresnel_schlick_one_angle() {
777 let f0 = [0.04; 3];
778 let f = fresnel_schlick(1.0, f0);
779 assert!((f[0] - 0.04).abs() < 1e-10);
780 }
781 #[test]
782 fn test_distribution_ggx_smooth() {
783 let d = distribution_ggx(1.0, 0.01);
784 assert!(d > 100.0);
785 }
786 #[test]
787 fn test_refract_no_tir() {
788 let d = normalize3([0.0, -1.0, 0.0]);
789 let n = [0.0, 1.0, 0.0];
790 let result = refract(d, n, 1.0 / 1.5);
791 assert!(result.is_some());
792 }
793 #[test]
794 fn test_refract_tir() {
795 let d = normalize3([0.9, -0.1, 0.0]);
796 let n = [0.0, 1.0, 0.0];
797 let result = refract(d, n, 1.5);
798 assert!(result.is_none());
799 }
800 #[test]
801 fn test_cosine_sample_hemisphere() {
802 let s = cosine_sample_hemisphere(0.5, 0.5);
803 let len = (s[0] * s[0] + s[1] * s[1] + s[2] * s[2]).sqrt();
804 assert!((len - 1.0).abs() < 1e-10);
805 assert!(s[1] >= 0.0);
806 }
807 #[test]
808 fn test_tonemap_reinhard() {
809 let c = tonemap_reinhard([2.0, 1.0, 0.5]);
810 assert!((c[0] - 2.0 / 3.0).abs() < 1e-10);
811 }
812 #[test]
813 fn test_tonemap_aces_clamp() {
814 let c = tonemap_aces([100.0, 100.0, 100.0]);
815 assert!(c[0] <= 1.0);
816 assert!(c[0] >= 0.0);
817 }
818 #[test]
819 fn test_linear_to_srgb_zero() {
820 assert!((linear_to_srgb(0.0)).abs() < 1e-10);
821 }
822 #[test]
823 fn test_linear_to_srgb_one() {
824 assert!((linear_to_srgb(1.0) - 1.0).abs() < 1e-6);
825 }
826 #[test]
827 fn test_box_filter_single_pixel() {
828 let image = vec![[1.0f64, 0.0, 0.0]];
829 let result = box_filter(&image, 1, 1, 1);
830 assert_eq!(result.len(), 1);
831 assert!((result[0][0] - 1.0).abs() < 1e-10);
832 }
833 #[test]
834 fn test_temporal_accumulate() {
835 let current = vec![[1.0f64; 3]];
836 let history = vec![[0.0f64; 3]];
837 let result = temporal_accumulate(¤t, &history, 0.5);
838 assert!((result[0][0] - 0.5).abs() < 1e-10);
839 }
840 #[test]
841 fn test_scene_add_box() {
842 let mut scene = Scene::new();
843 let mid = scene.add_material(Material::diffuse([0.8, 0.2, 0.2]));
844 scene.add_box([0.0; 3], [1.0; 3], mid);
845 assert_eq!(scene.triangles.len(), 12);
846 }
847 #[test]
848 fn test_scene_build_and_intersect() {
849 let mut scene = Scene::new();
850 let mid = scene.add_material(Material::diffuse([0.8, 0.8, 0.8]));
851 scene.add_box([0.0; 3], [1.0; 3], mid);
852 scene.add_light(PointLight::new([5.0, 5.0, 5.0], [1.0; 3], 1.0));
853 scene.build_bvh();
854 let ray = Ray::new([0.0, 0.0, -5.0], [0.0, 0.0, 1.0]);
855 assert!(scene.intersect(&ray).is_some());
856 }
857 #[test]
858 fn test_render_direct_small() {
859 let mut scene = Scene::new();
860 let mid = scene.add_material(Material::diffuse([0.8, 0.8, 0.8]));
861 scene.add_box([0.0; 3], [1.0; 3], mid);
862 scene.add_light(PointLight::new([5.0, 5.0, 5.0], [1.0; 3], 1.0));
863 scene.build_bvh();
864 let cam = Camera::look_at(
865 [0.0, 0.0, 5.0],
866 [0.0, 0.0, 0.0],
867 [0.0, 1.0, 0.0],
868 60.0,
869 4.0 / 3.0,
870 0.0,
871 5.0,
872 );
873 let config = RenderConfig {
874 width: 4,
875 height: 3,
876 ..Default::default()
877 };
878 let image = render_direct(
879 &config,
880 &cam,
881 scene.bvh.as_ref().unwrap(),
882 &scene.triangles,
883 &scene.materials,
884 &scene.lights,
885 );
886 assert_eq!(image.len(), 12);
887 for pixel in &image {
888 for c in pixel {
889 assert!(*c >= 0.0 && *c <= 1.0);
890 }
891 }
892 }
893 #[test]
894 fn test_path_state_should_continue() {
895 let ray = Ray::new([0.0; 3], [0.0, 0.0, 1.0]);
896 let mut state = PathState::new(ray, 4);
897 assert!(state.should_continue());
898 state.depth = 4;
899 assert!(!state.should_continue());
900 }
901 #[test]
902 fn test_reflect3_down_up_normal() {
903 let d = [0.0, -1.0, 0.0];
904 let n = [0.0, 1.0, 0.0];
905 let r = reflect3(d, n);
906 assert!((r[1] - 1.0).abs() < 1e-10);
907 }
908 #[test]
909 fn test_point_light_attenuation() {
910 let light = PointLight::new([0.0; 3], [1.0; 3], 1.0);
911 let att = light.attenuate(0.0);
912 assert!((att - 1.0).abs() < 1e-10);
913 }
914 #[test]
915 fn test_pbr_material_creation() {
916 let mat = Material::pbr([0.5; 3], 0.0, 0.5, 1.0);
917 assert_eq!(mat.mat_type, MaterialType::Pbr);
918 assert!((mat.roughness - 0.5).abs() < 1e-10);
919 }
920 #[test]
921 fn test_atrous_denoise_flat_image() {
922 let n = 4 * 4;
923 let color = vec![[0.5f64, 0.5, 0.5]; n];
924 let normal = vec![[0.0f64, 1.0, 0.0]; n];
925 let position = vec![[0.0f64; 3]; n];
926 let result = atrous_denoise(&color, &normal, &position, 4, 4, 1, 0.1, 0.1, 0.1);
927 assert_eq!(result.len(), n);
928 for p in &result {
929 assert!((p[0] - 0.5).abs() < 0.01);
930 }
931 }
932}