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