1use crate::build::environment_map as em;
13use crate::gfx::cubemap::FACE_BASIS;
14use crate::gfx::projection::{perspective_rh, view_from_basis};
15use crate::gfx::transform::mat4_mul;
16use crate::math::{ceil, floor, powi, round, sqrt};
17use alloc::vec;
18use alloc::vec::Vec;
19use core::f32::consts::FRAC_PI_2;
20
21fn perspective_90(near: f32, far: f32) -> [[f32; 4]; 4] {
23 perspective_rh(FRAC_PI_2, 1.0, near, far)
24}
25
26pub fn face_view_projection(eye: [f32; 3], face: usize, near: f32, far: f32) -> [[f32; 4]; 4] {
28 let b = FACE_BASIS[face];
29 let view = view_from_basis(eye, b[0], b[1], b[2]);
30 mat4_mul(perspective_90(near, far), view)
31}
32
33pub fn face_view_matrix(eye: [f32; 3], face: usize) -> [[f32; 4]; 4] {
38 let b = FACE_BASIS[face];
39 view_from_basis(eye, b[0], b[1], b[2])
40}
41
42#[derive(Clone, Copy, Debug, PartialEq)]
47pub struct ProbePlacement {
48 pub position: [f32; 3],
50 pub box_min: [f32; 3],
52 pub box_max: [f32; 3],
54}
55
56impl ProbePlacement {
57 pub fn from_center_extents(position: [f32; 3], half_extents: [f32; 3]) -> ProbePlacement {
60 ProbePlacement {
61 position,
62 box_min: [
63 position[0] - half_extents[0],
64 position[1] - half_extents[1],
65 position[2] - half_extents[2],
66 ],
67 box_max: [
68 position[0] + half_extents[0],
69 position[1] + half_extents[1],
70 position[2] + half_extents[2],
71 ],
72 }
73 }
74}
75
76#[derive(Clone, Copy, Debug, PartialEq, Eq)]
82pub struct ProbeBakeQueue {
83 total: usize,
84 next: usize,
85}
86
87impl ProbeBakeQueue {
88 pub fn new(total: usize) -> ProbeBakeQueue {
90 ProbeBakeQueue { total, next: 0 }
91 }
92
93 pub fn pending(&self) -> bool {
95 self.next < self.total
96 }
97
98 pub fn take_next(&mut self) -> Option<usize> {
100 (self.next < self.total).then(|| {
101 let i = self.next;
102 self.next += 1;
103 i
104 })
105 }
106
107 pub fn abort(&mut self) {
110 self.next = self.total;
111 }
112}
113
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
121pub enum BakePhase {
122 Idle,
124 Rendering,
126 Converting,
128}
129
130#[derive(Clone, Copy, Debug, PartialEq, Eq)]
138pub enum BakeAction {
139 Idle,
141 StartNext,
143 RenderFace,
145 Install,
147 Readback,
149}
150
151pub fn next_bake_action(
167 phase: BakePhase,
168 done: bool,
169 payload_ready: bool,
170 queue_pending: bool,
171 eligible: bool,
172 more_faces: bool,
173) -> BakeAction {
174 match phase {
175 BakePhase::Rendering => {
176 if more_faces {
177 BakeAction::RenderFace
178 } else if done {
179 BakeAction::Readback
180 } else {
181 BakeAction::Idle
182 }
183 }
184 BakePhase::Converting => {
185 if payload_ready {
186 BakeAction::Install
187 } else {
188 BakeAction::Idle
189 }
190 }
191 BakePhase::Idle => {
192 if queue_pending && eligible {
193 BakeAction::StartNext
194 } else {
195 BakeAction::Idle
196 }
197 }
198 }
199}
200
201pub(crate) const AUTO_SEED_BUDGET: usize = 8;
206
207const AUTO_SEED_CELL_TARGET: f32 = 12.0;
211
212const INTERIOR_VOXELS_LONG_AXIS: usize = 48;
216const INTERIOR_MAX_DIM: usize = 128;
218const INTERIOR_MIN_ENCLOSED: u8 = 5;
224const INTERIOR_MIN_CLUSTER: usize = 4;
227const INTERIOR_MIN_ROOM_SPAN: f32 = 2.0;
236
237fn fit_grid(nx: usize, nz: usize, budget: usize) -> (usize, usize) {
241 let (mut nx, mut nz) = (nx.max(1), nz.max(1));
242 if nx * nz > budget {
243 let scale = sqrt(budget as f32 / (nx * nz) as f32);
244 nx = (round(nx as f32 * scale) as usize).max(1);
245 nz = (round(nz as f32 * scale) as usize).max(1);
246 while nx * nz > budget {
247 if nx >= nz {
248 nx -= 1;
249 } else {
250 nz -= 1;
251 }
252 }
253 }
254 (nx.max(1), nz.max(1))
255}
256
257fn point_inside_any(p: [f32; 3], occupancy: &[([f32; 3], [f32; 3])]) -> bool {
259 occupancy.iter().any(|(mn, mx)| {
260 p[0] >= mn[0]
261 && p[0] <= mx[0]
262 && p[1] >= mn[1]
263 && p[1] <= mx[1]
264 && p[2] >= mn[2]
265 && p[2] <= mx[2]
266 })
267}
268
269fn open_capture_point(
276 center: [f32; 3],
277 x0: f32,
278 x1: f32,
279 z0: f32,
280 z1: f32,
281 occupancy: &[([f32; 3], [f32; 3])],
282) -> [f32; 3] {
283 if !point_inside_any(center, occupancy) {
284 return center;
285 }
286 let lerp = |a: f32, b: f32, t: f32| a + (b - a) * t;
287 for (fx, fz) in [(0.25, 0.25), (0.75, 0.25), (0.25, 0.75), (0.75, 0.75)] {
288 let p = [lerp(x0, x1, fx), center[1], lerp(z0, z1, fz)];
289 if !point_inside_any(p, occupancy) {
290 return p;
291 }
292 }
293 center
294}
295
296fn interior_voxel_grid(
302 aabb_min: [f32; 3],
303 aabb_max: [f32; 3],
304) -> Option<(f32, usize, usize, usize)> {
305 let extent = [
306 aabb_max[0] - aabb_min[0],
307 aabb_max[1] - aabb_min[1],
308 aabb_max[2] - aabb_min[2],
309 ];
310 let long = extent[0].max(extent[2]);
311 if long <= 0.0 || extent[1] <= 0.0 {
312 return None;
313 }
314 let vs = (long / INTERIOR_VOXELS_LONG_AXIS as f32).max(0.25);
315 let dim = |e: f32| (ceil(e / vs) as usize).clamp(1, INTERIOR_MAX_DIM);
316 Some((vs, dim(extent[0]), dim(extent[1]), dim(extent[2])))
317}
318
319fn solid_from_aabbs(
323 aabb_min: [f32; 3],
324 vs: f32,
325 nx: usize,
326 ny: usize,
327 nz: usize,
328 occupancy: &[([f32; 3], [f32; 3])],
329) -> Vec<bool> {
330 let idx = |x: usize, y: usize, z: usize| (z * ny + y) * nx + x;
331 let mut solid = vec![false; nx * ny * nz];
332 let to_vx =
333 |v: f32, origin: f32, hi: usize| (floor((v - origin) / vs).max(0.0) as usize).min(hi);
334 for (mn, mx) in occupancy {
335 let x0 = to_vx(mn[0], aabb_min[0], nx - 1);
336 let x1 = to_vx(mx[0], aabb_min[0], nx - 1);
337 let y0 = to_vx(mn[1], aabb_min[1], ny - 1);
338 let y1 = to_vx(mx[1], aabb_min[1], ny - 1);
339 let z0 = to_vx(mn[2], aabb_min[2], nz - 1);
340 let z1 = to_vx(mx[2], aabb_min[2], nz - 1);
341 for z in z0..=z1 {
342 for y in y0..=y1 {
343 for x in x0..=x1 {
344 solid[idx(x, y, z)] = true;
345 }
346 }
347 }
348 }
349 solid
350}
351
352fn tri_box_overlap(box_c: [f32; 3], box_h: [f32; 3], tri: &[[f32; 3]; 3]) -> bool {
360 let sub = |a: [f32; 3], b: [f32; 3]| [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
361 let dot = |a: [f32; 3], b: [f32; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
362 let cross = |a: [f32; 3], b: [f32; 3]| {
363 [
364 a[1] * b[2] - a[2] * b[1],
365 a[2] * b[0] - a[0] * b[2],
366 a[0] * b[1] - a[1] * b[0],
367 ]
368 };
369 let v = [sub(tri[0], box_c), sub(tri[1], box_c), sub(tri[2], box_c)];
371 let edges = [sub(v[1], v[0]), sub(v[2], v[1]), sub(v[0], v[2])];
372 let box_axes = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
373
374 let separated = |l: [f32; 3]| -> bool {
375 let r = box_h[0] * l[0].abs() + box_h[1] * l[1].abs() + box_h[2] * l[2].abs();
376 let p0 = dot(l, v[0]);
377 let p1 = dot(l, v[1]);
378 let p2 = dot(l, v[2]);
379 p0.min(p1).min(p2) > r || p0.max(p1).max(p2) < -r
380 };
381
382 for a in box_axes {
383 if separated(a) {
384 return false;
385 }
386 }
387 for e in edges {
388 for a in box_axes {
389 if separated(cross(e, a)) {
390 return false;
391 }
392 }
393 }
394 !separated(cross(edges[0], edges[1]))
395}
396
397fn solid_from_triangles(
403 aabb_min: [f32; 3],
404 vs: f32,
405 nx: usize,
406 ny: usize,
407 nz: usize,
408 triangles: &[[[f32; 3]; 3]],
409) -> Vec<bool> {
410 let idx = |x: usize, y: usize, z: usize| (z * ny + y) * nx + x;
411 let mut solid = vec![false; nx * ny * nz];
412 let half = [vs * 0.5 + vs * 1e-3; 3];
418 let to_vx =
419 |v: f32, origin: f32, hi: usize| (floor((v - origin) / vs).max(0.0) as usize).min(hi);
420 for tri in triangles {
421 let mut tmn = tri[0];
422 let mut tmx = tri[0];
423 for vtx in &tri[1..] {
424 for a in 0..3 {
425 tmn[a] = tmn[a].min(vtx[a]);
426 tmx[a] = tmx[a].max(vtx[a]);
427 }
428 }
429 if !tmn.iter().chain(tmx.iter()).all(|c| c.is_finite()) {
430 continue;
431 }
432 let x0 = to_vx(tmn[0], aabb_min[0], nx - 1);
433 let x1 = to_vx(tmx[0], aabb_min[0], nx - 1);
434 let y0 = to_vx(tmn[1], aabb_min[1], ny - 1);
435 let y1 = to_vx(tmx[1], aabb_min[1], ny - 1);
436 let z0 = to_vx(tmn[2], aabb_min[2], nz - 1);
437 let z1 = to_vx(tmx[2], aabb_min[2], nz - 1);
438 for z in z0..=z1 {
439 for y in y0..=y1 {
440 for x in x0..=x1 {
441 let i = idx(x, y, z);
442 if solid[i] {
443 continue;
444 }
445 let c = [
446 aabb_min[0] + (x as f32 + 0.5) * vs,
447 aabb_min[1] + (y as f32 + 0.5) * vs,
448 aabb_min[2] + (z as f32 + 0.5) * vs,
449 ];
450 if tri_box_overlap(c, half, tri) {
451 solid[i] = true;
452 }
453 }
454 }
455 }
456 }
457 solid
458}
459
460fn interior_probes_from_solid(
470 aabb_min: [f32; 3],
471 vs: f32,
472 nx: usize,
473 ny: usize,
474 nz: usize,
475 solid: &[bool],
476 budget: usize,
477) -> Vec<ProbePlacement> {
478 let n = nx * ny * nz;
479 let idx = |x: usize, y: usize, z: usize| (z * ny + y) * nx + x;
480
481 let mut enclosed = vec![0u8; n];
484 for z in 0..nz {
485 for y in 0..ny {
486 let (mut fwd, mut bwd) = (false, false);
487 for x in (0..nx).rev() {
488 let i = idx(x, y, z);
489 if solid[i] {
490 fwd = true;
491 } else if fwd {
492 enclosed[i] += 1;
493 }
494 }
495 for x in 0..nx {
496 let i = idx(x, y, z);
497 if solid[i] {
498 bwd = true;
499 } else if bwd {
500 enclosed[i] += 1;
501 }
502 }
503 }
504 }
505 for z in 0..nz {
506 for x in 0..nx {
507 let (mut fwd, mut bwd) = (false, false);
508 for y in (0..ny).rev() {
509 let i = idx(x, y, z);
510 if solid[i] {
511 fwd = true;
512 } else if fwd {
513 enclosed[i] += 1;
514 }
515 }
516 for y in 0..ny {
517 let i = idx(x, y, z);
518 if solid[i] {
519 bwd = true;
520 } else if bwd {
521 enclosed[i] += 1;
522 }
523 }
524 }
525 }
526 for y in 0..ny {
527 for x in 0..nx {
528 let (mut fwd, mut bwd) = (false, false);
529 for z in (0..nz).rev() {
530 let i = idx(x, y, z);
531 if solid[i] {
532 fwd = true;
533 } else if fwd {
534 enclosed[i] += 1;
535 }
536 }
537 for z in 0..nz {
538 let i = idx(x, y, z);
539 if solid[i] {
540 bwd = true;
541 } else if bwd {
542 enclosed[i] += 1;
543 }
544 }
545 }
546 }
547
548 let is_interior = |i: usize| !solid[i] && enclosed[i] >= INTERIOR_MIN_ENCLOSED;
549
550 let mut label = vec![usize::MAX; n];
552 let mut clusters: Vec<Vec<usize>> = Vec::new();
553 let mut stack: Vec<usize> = Vec::new();
554 for start in 0..n {
555 if !is_interior(start) || label[start] != usize::MAX {
556 continue;
557 }
558 let cid = clusters.len();
559 let mut members = Vec::new();
560 label[start] = cid;
561 stack.push(start);
562 while let Some(i) = stack.pop() {
563 members.push(i);
564 let z = i / (nx * ny);
565 let y = (i / nx) % ny;
566 let x = i % nx;
567 let neighbours = [
568 (x > 0).then(|| i - 1),
569 (x + 1 < nx).then_some(i + 1),
570 (y > 0).then(|| i - nx),
571 (y + 1 < ny).then_some(i + nx),
572 (z > 0).then(|| i - nx * ny),
573 (z + 1 < nz).then_some(i + nx * ny),
574 ];
575 for j in neighbours.into_iter().flatten() {
576 if is_interior(j) && label[j] == usize::MAX {
577 label[j] = cid;
578 stack.push(j);
579 }
580 }
581 }
582 clusters.push(members);
583 }
584
585 let voxel_center = |i: usize| {
586 let z = i / (nx * ny);
587 let y = (i / nx) % ny;
588 let x = i % nx;
589 [
590 aabb_min[0] + (x as f32 + 0.5) * vs,
591 aabb_min[1] + (y as f32 + 0.5) * vs,
592 aabb_min[2] + (z as f32 + 0.5) * vs,
593 ]
594 };
595 let cluster_span = |members: &[usize]| {
597 let mut lo = [f32::MAX; 3];
598 let mut hi = [f32::MIN; 3];
599 for &i in members {
600 let c = voxel_center(i);
601 for a in 0..3 {
602 lo[a] = lo[a].min(c[a] - vs * 0.5);
603 hi[a] = hi[a].max(c[a] + vs * 0.5);
604 }
605 }
606 [hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]]
607 };
608
609 clusters.retain(|c| {
612 c.len() >= INTERIOR_MIN_CLUSTER
613 && cluster_span(c).iter().all(|d| *d >= INTERIOR_MIN_ROOM_SPAN)
614 });
615 clusters.sort_by_key(|c| core::cmp::Reverse(c.len()));
616 clusters.truncate(budget);
617 clusters
618 .iter()
619 .map(|members| {
620 let inv = 1.0 / members.len() as f32;
623 let mut centroid = [0.0f32; 3];
624 for &i in members {
625 let c = voxel_center(i);
626 for a in 0..3 {
627 centroid[a] += c[a] * inv;
628 }
629 }
630 let dist2 = |c: [f32; 3]| {
631 powi(c[0] - centroid[0], 2)
632 + powi(c[1] - centroid[1], 2)
633 + powi(c[2] - centroid[2], 2)
634 };
635 let position = members
636 .iter()
637 .map(|&i| voxel_center(i))
638 .min_by(|a, b| dist2(*a).total_cmp(&dist2(*b)))
639 .unwrap_or(centroid);
640 let mut box_min = [f32::MAX; 3];
643 let mut box_max = [f32::MIN; 3];
644 for &i in members {
645 let c = voxel_center(i);
646 for a in 0..3 {
647 box_min[a] = box_min[a].min(c[a] - vs * 0.5);
648 box_max[a] = box_max[a].max(c[a] + vs * 0.5);
649 }
650 }
651 ProbePlacement {
652 position,
653 box_min,
654 box_max,
655 }
656 })
657 .collect()
658}
659
660fn seed_interior_probes(
664 aabb_min: [f32; 3],
665 aabb_max: [f32; 3],
666 occupancy: &[([f32; 3], [f32; 3])],
667 budget: usize,
668) -> Vec<ProbePlacement> {
669 if budget == 0 || occupancy.is_empty() {
670 return Vec::new();
671 }
672 let (vs, nx, ny, nz) = match interior_voxel_grid(aabb_min, aabb_max) {
673 Some(g) => g,
674 None => return Vec::new(),
675 };
676 let solid = solid_from_aabbs(aabb_min, vs, nx, ny, nz, occupancy);
677 interior_probes_from_solid(aabb_min, vs, nx, ny, nz, &solid, budget)
678}
679
680fn seed_interior_probes_tris(
684 aabb_min: [f32; 3],
685 aabb_max: [f32; 3],
686 triangles: &[[[f32; 3]; 3]],
687 budget: usize,
688) -> Vec<ProbePlacement> {
689 if budget == 0 || triangles.is_empty() {
690 return Vec::new();
691 }
692 let (vs, nx, ny, nz) = match interior_voxel_grid(aabb_min, aabb_max) {
693 Some(g) => g,
694 None => return Vec::new(),
695 };
696 let solid = solid_from_triangles(aabb_min, vs, nx, ny, nz, triangles);
697 interior_probes_from_solid(aabb_min, vs, nx, ny, nz, &solid, budget)
698}
699
700const REFLECTOR_BOUNDS_HALF_HEIGHT: f32 = 2.0;
706
707pub fn reflector_bounds(centre: [f32; 3], half_extents: [f32; 3]) -> ([f32; 3], [f32; 3]) {
720 let half = |a: usize| half_extents[a].abs().max(REFLECTOR_BOUNDS_HALF_HEIGHT);
721 (
722 [
723 centre[0] - half(0),
724 centre[1] - half(1),
725 centre[2] - half(2),
726 ],
727 [
728 centre[0] + half(0),
729 centre[1] + half(1),
730 centre[2] + half(2),
731 ],
732 )
733}
734
735pub fn auto_seed_probes(
739 aabb_min: [f32; 3],
740 aabb_max: [f32; 3],
741 occupancy: &[([f32; 3], [f32; 3])],
742) -> Vec<ProbePlacement> {
743 auto_seed_probes_with_geometry(aabb_min, aabb_max, occupancy, &[])
744}
745
746pub fn auto_seed_probes_with_geometry(
758 aabb_min: [f32; 3],
759 aabb_max: [f32; 3],
760 occupancy: &[([f32; 3], [f32; 3])],
761 triangles: &[[[f32; 3]; 3]],
762) -> Vec<ProbePlacement> {
763 let finite = aabb_min
764 .iter()
765 .chain(aabb_max.iter())
766 .all(|c| c.is_finite());
767 if !finite || aabb_max[0] <= aabb_min[0] || aabb_max[2] <= aabb_min[2] {
768 return Vec::new();
769 }
770 let mut out = if triangles.is_empty() {
771 seed_interior_probes(aabb_min, aabb_max, occupancy, AUTO_SEED_BUDGET)
772 } else {
773 seed_interior_probes_tris(aabb_min, aabb_max, triangles, AUTO_SEED_BUDGET)
774 };
775 let remaining = AUTO_SEED_BUDGET.saturating_sub(out.len());
776 if remaining > 0 {
777 out.extend(seed_grid_probes(aabb_min, aabb_max, occupancy, remaining));
778 }
779 out
780}
781
782fn seed_grid_probes(
788 aabb_min: [f32; 3],
789 aabb_max: [f32; 3],
790 occupancy: &[([f32; 3], [f32; 3])],
791 budget: usize,
792) -> Vec<ProbePlacement> {
793 if budget == 0 {
794 return Vec::new();
795 }
796 let dx = aabb_max[0] - aabb_min[0];
797 let dz = aabb_max[2] - aabb_min[2];
798 let nx_raw = ceil(dx / AUTO_SEED_CELL_TARGET).max(1.0) as usize;
799 let nz_raw = ceil(dz / AUTO_SEED_CELL_TARGET).max(1.0) as usize;
800 let (nx, nz) = fit_grid(nx_raw, nz_raw, budget);
801
802 let y_eye = probe_eye_point(aabb_min, aabb_max)[1];
803 let lerp = |a: f32, b: f32, t: f32| a + (b - a) * t;
804 let mut out = Vec::with_capacity(nx * nz);
805 for ix in 0..nx {
806 for iz in 0..nz {
807 let x0 = lerp(aabb_min[0], aabb_max[0], ix as f32 / nx as f32);
808 let x1 = lerp(aabb_min[0], aabb_max[0], (ix + 1) as f32 / nx as f32);
809 let z0 = lerp(aabb_min[2], aabb_max[2], iz as f32 / nz as f32);
810 let z1 = lerp(aabb_min[2], aabb_max[2], (iz + 1) as f32 / nz as f32);
811 let center = [(x0 + x1) * 0.5, y_eye, (z0 + z1) * 0.5];
812 out.push(ProbePlacement {
813 position: open_capture_point(center, x0, x1, z0, z1, occupancy),
814 box_min: [x0, aabb_min[1], z0],
815 box_max: [x1, aabb_max[1], z1],
816 });
817 }
818 }
819 out
820}
821
822pub fn fold_world_bounds(
826 boxes: impl IntoIterator<Item = ([f32; 3], [f32; 3])>,
827) -> Option<([f32; 3], [f32; 3])> {
828 let mut acc: Option<([f32; 3], [f32; 3])> = None;
829 for (mn, mx) in boxes {
830 if !mn.iter().chain(mx.iter()).all(|c| c.is_finite()) {
831 continue;
832 }
833 match &mut acc {
834 None => acc = Some((mn, mx)),
835 Some((amn, amx)) => {
836 for i in 0..3 {
837 amn[i] = amn[i].min(mn[i]);
838 amx[i] = amx[i].max(mx[i]);
839 }
840 }
841 }
842 }
843 acc
844}
845
846pub fn build_probe_payload<S: em::RowScheduler>(
855 scheduler: &S,
856 faces: &[Vec<f32>; 6],
857 face_size: u32,
858 irradiance_face: u32,
859 prefilter_samples: u32,
860 prefilter_clamp: f32,
861) -> Vec<u8> {
862 let mips = em::max_mip_count(face_size);
863 let irradiance = em::CubeBake::irradiance(
864 faces,
865 face_size,
866 irradiance_face,
867 em::DEFAULT_IRRADIANCE_PHI_SAMPLES,
868 em::DEFAULT_IRRADIANCE_THETA_SAMPLES,
869 )
870 .bake(scheduler);
871 let mut prefilter = Vec::with_capacity(mips as usize);
875 prefilter.push(em::prefilter_mip0(faces, face_size, prefilter_clamp, true));
876 for mip in 1..mips {
877 prefilter.push(
878 em::CubeBake::ggx(
879 faces,
880 face_size,
881 face_size >> mip,
882 em::prefilter_roughness(mip, mips),
883 prefilter_samples,
884 prefilter_clamp,
885 )
886 .bake(scheduler),
887 );
888 }
889 em::serialise_payload(irradiance_face, face_size, mips, &irradiance, &prefilter)
890}
891
892pub(crate) fn probe_eye_point(aabb_min: [f32; 3], aabb_max: [f32; 3]) -> [f32; 3] {
900 const EYE_HEIGHT: f32 = 1.7;
901 let cx = 0.5 * (aabb_min[0] + aabb_max[0]);
902 let cz = 0.5 * (aabb_min[2] + aabb_max[2]);
903 let floor = aabb_min[1];
904 let ceil = aabb_max[1];
905 let y = (floor + EYE_HEIGHT).min(0.5 * (floor + ceil)).max(floor);
908 [cx, y, cz]
909}
910
911#[cfg(test)]
912mod tests {
913 use super::*;
914 use crate::gfx::cubemap::face_dir;
915
916 fn project(vp: [[f32; 4]; 4], p: [f32; 3]) -> (f32, f32, f32) {
917 let mut c = [0.0f32; 4];
918 let pv = [p[0], p[1], p[2], 1.0];
919 for row in 0..4 {
920 for k in 0..4 {
921 c[row] += vp[k][row] * pv[k];
922 }
923 }
924 (c[0] / c[3], c[1] / c[3], c[3])
925 }
926
927 #[test]
932 fn face_view_projection_matches_cube_convention() {
933 let eye = [3.0, -1.5, 2.0];
934 let samples = [
935 (0.0f32, 0.0f32),
936 (0.5, 0.0),
937 (0.0, 0.5),
938 (-0.6, 0.3),
939 (0.7, -0.4),
940 ];
941 for face in 0..6 {
942 let vp = face_view_projection(eye, face, 0.05, 100.0);
943 for &(u, v) in &samples {
944 let d = face_dir(face, u, v);
945 let p = [eye[0] + d[0], eye[1] + d[1], eye[2] + d[2]];
946 let (nx, ny, w) = project(vp, p);
947 assert!(
948 w > 0.0,
949 "face {face} sample ({u},{v}) behind camera (w={w})"
950 );
951 assert!(
952 (nx - u).abs() < 1e-4 && (ny - (-v)).abs() < 1e-4,
953 "face {face} ({u},{v}) -> ndc ({nx},{ny}), expected ({u},{})",
954 -v
955 );
956 }
957 }
958 }
959
960 #[test]
961 fn build_probe_payload_round_trips() {
962 let face = 8usize;
965 let faces: [Vec<f32>; 6] = core::array::from_fn(|f| {
966 let mut v = vec![0.0f32; face * face * 4];
967 for px in v.chunks_exact_mut(4) {
968 px[0] = f as f32 * 0.1;
969 px[1] = 0.2;
970 px[2] = 0.3;
971 px[3] = 1.0;
972 }
973 v
974 });
975 let bytes = build_probe_payload(&em::Serial, &faces, face as u32, 8, 16, 12.0);
976 let view = crate::build::environment_map::deserialise(&bytes).expect("deserialise");
977 assert_eq!(view.prefilter_face, 8);
978 assert_eq!(view.irradiance_face, 8);
979 assert!(view.prefilter_mip_bytes.len() >= 2);
980 }
981
982 #[test]
983 fn probe_eye_point_centres_at_eye_height() {
984 let eye = probe_eye_point([-10.0, 0.0, -4.0], [6.0, 30.0, 12.0]);
987 assert!((eye[0] - (-2.0)).abs() < 1e-6, "x not centred: {}", eye[0]);
988 assert!((eye[2] - 4.0).abs() < 1e-6, "z not centred: {}", eye[2]);
989 assert!((eye[1] - 1.7).abs() < 1e-6, "y not eye height: {}", eye[1]);
990 }
991
992 #[test]
993 fn probe_eye_point_clamps_to_a_flat_scene() {
994 let eye = probe_eye_point([0.0, 0.0, 0.0], [2.0, 1.0, 2.0]);
996 assert!(
997 eye[1] >= 0.0 && eye[1] <= 1.0,
998 "y escaped bounds: {}",
999 eye[1]
1000 );
1001 }
1002
1003 #[test]
1004 fn face_view_matrix_composes_to_face_vp() {
1005 let eye = [1.0, 2.0, -3.0];
1009 for face in 0..6 {
1010 let vp = face_view_projection(eye, face, 0.1, 50.0);
1011 let comp = mat4_mul(perspective_90(0.1, 50.0), face_view_matrix(eye, face));
1012 for c in 0..4 {
1013 for r in 0..4 {
1014 assert!(
1015 (vp[c][r] - comp[c][r]).abs() < 1e-5,
1016 "face {face} [{c}][{r}] mismatch"
1017 );
1018 }
1019 }
1020 }
1021 }
1022
1023 #[test]
1024 fn placement_from_center_extents_builds_box() {
1025 let p = ProbePlacement::from_center_extents([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]);
1026 assert_eq!(p.box_min, [-3.0, -3.0, -3.0]);
1027 assert_eq!(p.box_max, [5.0, 7.0, 9.0]);
1028 assert_eq!(p.position, [1.0, 2.0, 3.0]);
1029 }
1030
1031 fn probe_union(probes: &[ProbePlacement]) -> ([f32; 3], [f32; 3]) {
1033 let mn = probes.iter().fold([f32::MAX; 3], |a, p| {
1034 core::array::from_fn(|i| a[i].min(p.box_min[i]))
1035 });
1036 let mx = probes.iter().fold([f32::MIN; 3], |a, p| {
1037 core::array::from_fn(|i| a[i].max(p.box_max[i]))
1038 });
1039 (mn, mx)
1040 }
1041
1042 #[test]
1043 fn auto_seed_probes_tiles_the_scene() {
1044 let probes = auto_seed_probes([-10.0, 0.0, -10.0], [10.0, 6.0, 10.0], &[]);
1046 assert_eq!(probes.len(), 4);
1047 let (union_min, union_max) = probe_union(&probes);
1048 assert_eq!(union_min, [-10.0, 0.0, -10.0]);
1049 assert_eq!(union_max, [10.0, 6.0, 10.0]);
1050 assert!(auto_seed_probes([0.0; 3], [0.0; 3], &[]).is_empty());
1052 assert!(auto_seed_probes([f32::NAN, 0.0, 0.0], [1.0, 1.0, 1.0], &[]).is_empty());
1053 }
1054
1055 #[test]
1056 fn auto_seed_scales_count_to_scene_size_and_aspect() {
1057 let small = auto_seed_probes([-3.0, 0.0, -3.0], [3.0, 3.0, 3.0], &[]);
1060 assert_eq!(small.len(), 1);
1061 let long = auto_seed_probes([0.0, 0.0, 0.0], [96.0, 4.0, 12.0], &[]);
1064 assert!(long.len() > 1 && long.len() <= AUTO_SEED_BUDGET);
1065 let nx = long.iter().filter(|p| p.box_min[2] == 0.0).count();
1066 let nz = long.len() / nx;
1067 assert!(nx > nz, "long axis (x) should have more cells: {nx}x{nz}");
1068 let (mn, mx) = probe_union(&long);
1069 assert_eq!(mn, [0.0, 0.0, 0.0]);
1070 assert_eq!(mx, [96.0, 4.0, 12.0]);
1071 let big = auto_seed_probes([0.0, 0.0, 0.0], [500.0, 4.0, 500.0], &[]);
1073 assert!(big.len() <= AUTO_SEED_BUDGET);
1074 }
1075
1076 #[test]
1077 fn auto_seed_nudges_capture_point_out_of_geometry() {
1078 let occ = [([-1.0, 0.0, -1.0], [1.0, 5.0, 1.0])];
1081 let probes = auto_seed_probes([-3.0, 0.0, -3.0], [3.0, 3.0, 3.0], &occ);
1082 assert_eq!(probes.len(), 1);
1083 let p = probes[0].position;
1084 assert!(
1085 !point_inside_any(p, &occ),
1086 "capture point {p:?} still inside the occupancy box"
1087 );
1088 assert!(p[0] >= probes[0].box_min[0] && p[0] <= probes[0].box_max[0]);
1090 assert!(p[2] >= probes[0].box_min[2] && p[2] <= probes[0].box_max[2]);
1091 let everywhere = [([-100.0, -100.0, -100.0], [100.0, 100.0, 100.0])];
1093 let trapped = auto_seed_probes([-3.0, 0.0, -3.0], [3.0, 3.0, 3.0], &everywhere);
1094 assert_eq!(trapped.len(), 1);
1095 }
1096
1097 #[test]
1098 fn fit_grid_respects_budget_and_aspect() {
1099 assert_eq!(fit_grid(1, 1, 8), (1, 1));
1100 assert_eq!(fit_grid(2, 2, 8), (2, 2)); let (nx, nz) = fit_grid(9, 2, 8); assert!(nx * nz <= 8 && nx > nz);
1103 let (nx, nz) = fit_grid(20, 20, 8); assert!(nx * nz <= 8 && nx >= 1 && nz >= 1);
1105 }
1106
1107 fn box_room(min: [f32; 3], max: [f32; 3]) -> Vec<([f32; 3], [f32; 3])> {
1110 let [x0, y0, z0] = min;
1111 let [x1, y1, z1] = max;
1112 vec![
1113 ([x0, y0 - 1.0, z0], [x1, y0, z1]), ([x0, y1, z0], [x1, y1 + 1.0, z1]), ([x0 - 1.0, y0, z0], [x0, y1, z1]), ([x1, y0, z0], [x1 + 1.0, y1, z1]), ([x0, y0, z0 - 1.0], [x1, y1, z0]), ([x0, y0, z1], [x1, y1, z1 + 1.0]), ]
1120 }
1121
1122 #[test]
1123 fn seed_interior_probes_finds_a_sealed_room() {
1124 let room = box_room([0.0, 0.0, 0.0], [10.0, 6.0, 10.0]);
1127 let probes = seed_interior_probes([-3.0, -3.0, -3.0], [13.0, 9.0, 13.0], &room, 8);
1128 assert_eq!(probes.len(), 1, "one room -> one interior probe");
1129 let p = probes[0].position;
1130 assert!(
1131 p[0] > 0.0 && p[0] < 10.0 && p[1] > 0.0 && p[1] < 6.0 && p[2] > 0.0 && p[2] < 10.0,
1132 "probe {p:?} should sit inside the room"
1133 );
1134 assert!(probes[0].box_min[0] < 2.0 && probes[0].box_max[0] > 8.0);
1136 }
1137
1138 #[test]
1139 fn reflector_bounds_covers_the_surface_and_has_volume() {
1140 let (mn, mx) = reflector_bounds([0.0, 0.0, 0.0], [14.0, 0.0, 14.0]);
1144 assert_eq!(mn[0], -14.0);
1145 assert_eq!(mx[2], 14.0);
1146 assert!(
1147 mx[1] - mn[1] > 0.0,
1148 "flat axis is inflated, not left at zero"
1149 );
1150
1151 let (mn, mx) = reflector_bounds([5.0, 3.0, -2.0], [0.5, 6.0, 0.5]);
1153 assert_eq!(mn[1], -3.0);
1154 assert_eq!(mx[1], 9.0);
1155 assert!(mn[0] < 5.0 && mx[0] > 5.0);
1156
1157 let crate_aabb = ([-0.7, 0.6, -0.7], [0.7, 2.0, 0.7]);
1159 let pool = reflector_bounds([0.0, 0.0, 0.0], [14.0, 0.0, 14.0]);
1160 let (mn, mx) = fold_world_bounds([crate_aabb, pool]).expect("finite bounds");
1161 assert_eq!((mn[0], mx[0]), (-14.0, 14.0));
1162 assert_eq!((mn[2], mx[2]), (-14.0, 14.0));
1163 }
1164
1165 #[test]
1166 fn seed_interior_probes_ignores_a_prop_sized_hollow() {
1167 let crate_tris = box_mesh_tris([-0.7, 0.6, -0.7], [0.7, 2.0, 0.7]);
1173 let probes = seed_interior_probes_tris([-0.8, 0.5, -0.8], [0.8, 2.1, 0.8], &crate_tris, 8);
1174 assert!(
1175 probes.is_empty(),
1176 "a prop-sized hollow is not a room: {probes:?}"
1177 );
1178
1179 let room_tris = box_mesh_tris([-2.5, 0.0, -2.5], [2.5, 3.0, 2.5]);
1182 let probes = seed_interior_probes_tris([-3.0, -0.5, -3.0], [3.0, 3.5, 3.0], &room_tris, 8);
1183 assert_eq!(
1184 probes.len(),
1185 1,
1186 "a standing-height room still earns a probe"
1187 );
1188 }
1189
1190 #[test]
1191 fn seed_interior_probes_ignores_an_open_scene() {
1192 let open = vec![
1195 ([-20.0, -1.0, -20.0], [20.0, 0.0, 20.0]), ([-5.0, 0.0, -5.0], [-3.0, 6.0, -3.0]), ([3.0, 0.0, 3.0], [5.0, 6.0, 5.0]), ];
1199 let probes = seed_interior_probes([-20.0, -1.0, -20.0], [20.0, 8.0, 20.0], &open, 8);
1200 assert!(
1201 probes.is_empty(),
1202 "open scene seeds no interior probes: {probes:?}"
1203 );
1204 assert!(seed_interior_probes([0.0, 0.0, 0.0], [10.0, 5.0, 10.0], &[], 8).is_empty());
1206 }
1207
1208 #[test]
1209 fn auto_seed_places_a_room_probe_then_grid() {
1210 let room = box_room([0.0, 0.0, 0.0], [10.0, 6.0, 10.0]);
1213 let probes = auto_seed_probes([-12.0, -3.0, -12.0], [22.0, 9.0, 22.0], &room);
1214 assert!(!probes.is_empty() && probes.len() <= AUTO_SEED_BUDGET);
1215 let inside_room = probes.iter().any(|p| {
1216 let q = p.position;
1217 q[0] > 0.0 && q[0] < 10.0 && q[1] > 0.0 && q[1] < 6.0 && q[2] > 0.0 && q[2] < 10.0
1218 });
1219 assert!(
1220 inside_room,
1221 "auto-seed should drop a probe inside the room: {probes:?}"
1222 );
1223 }
1224
1225 fn box_mesh_tris(min: [f32; 3], max: [f32; 3]) -> Vec<[[f32; 3]; 3]> {
1228 let [x0, y0, z0] = min;
1229 let [x1, y1, z1] = max;
1230 let c = [
1231 [x0, y0, z0],
1232 [x1, y0, z0],
1233 [x1, y1, z0],
1234 [x0, y1, z0],
1235 [x0, y0, z1],
1236 [x1, y0, z1],
1237 [x1, y1, z1],
1238 [x0, y1, z1],
1239 ];
1240 let quads = [
1242 [0, 1, 2, 3], [4, 5, 6, 7], [0, 3, 7, 4], [1, 2, 6, 5], [0, 1, 5, 4], [3, 2, 6, 7], ];
1249 let mut tris = Vec::with_capacity(12);
1250 for q in quads {
1251 tris.push([c[q[0]], c[q[1]], c[q[2]]]);
1252 tris.push([c[q[0]], c[q[2]], c[q[3]]]);
1253 }
1254 tris
1255 }
1256
1257 #[test]
1258 fn tri_box_overlap_detects_intersection_and_separation() {
1259 let h = [0.5, 0.5, 0.5];
1260 let through = [[-1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1262 assert!(tri_box_overlap([0.0, 0.0, 0.0], h, &through));
1263 assert!(!tri_box_overlap([10.0, 0.0, 0.0], h, &through));
1265 let above = [[-1.0, 5.0, -1.0], [3.0, 5.0, -1.0], [0.0, 5.0, 3.0]];
1268 assert!(!tri_box_overlap([0.0, 0.0, 0.0], h, &above));
1269 let inside = [[-0.1, 0.0, 0.0], [0.1, 0.0, 0.0], [0.0, 0.1, 0.0]];
1271 assert!(tri_box_overlap([0.0, 0.0, 0.0], h, &inside));
1272 }
1273
1274 #[test]
1275 fn surface_voxels_leave_a_watertight_mesh_hollow() {
1276 let scene_min = [-3.0, -3.0, -3.0];
1280 let scene_max = [13.0, 9.0, 13.0];
1281 let room_aabb = vec![([0.0, 0.0, 0.0], [10.0, 6.0, 10.0])];
1282 let room_tris = box_mesh_tris([0.0, 0.0, 0.0], [10.0, 6.0, 10.0]);
1283
1284 let from_aabb = seed_interior_probes(scene_min, scene_max, &room_aabb, 8);
1286 assert!(
1287 from_aabb.is_empty(),
1288 "a watertight mesh's AABB hides its interior: {from_aabb:?}"
1289 );
1290
1291 let from_tris = seed_interior_probes_tris(scene_min, scene_max, &room_tris, 8);
1293 assert_eq!(from_tris.len(), 1, "the hollow interior earns one probe");
1294 let p = from_tris[0].position;
1295 assert!(
1296 p[0] > 0.0 && p[0] < 10.0 && p[1] > 0.0 && p[1] < 6.0 && p[2] > 0.0 && p[2] < 10.0,
1297 "probe {p:?} should sit inside the watertight room"
1298 );
1299
1300 let auto = auto_seed_probes_with_geometry(scene_min, scene_max, &room_aabb, &room_tris);
1302 assert!(
1303 auto.iter().any(|q| {
1304 let q = q.position;
1305 q[0] > 0.0 && q[0] < 10.0 && q[1] > 0.0 && q[1] < 6.0 && q[2] > 0.0 && q[2] < 10.0
1306 }),
1307 "auto-seed with geometry drops a probe inside the room: {auto:?}"
1308 );
1309 assert_eq!(
1311 auto_seed_probes_with_geometry(scene_min, scene_max, &room_aabb, &[]).len(),
1312 auto_seed_probes(scene_min, scene_max, &room_aabb).len(),
1313 );
1314 }
1315
1316 #[test]
1317 fn fold_world_bounds_unions_and_skips_nonfinite() {
1318 let boxes = [
1319 ([0.0, 0.0, 0.0], [1.0, 2.0, 1.0]),
1320 ([-3.0, 1.0, -1.0], [0.5, 4.0, 2.0]),
1321 ([f32::NAN, 0.0, 0.0], [1.0, 1.0, 1.0]), ];
1323 let (mn, mx) = fold_world_bounds(boxes).expect("non-empty");
1324 assert_eq!(mn, [-3.0, 0.0, -1.0]);
1325 assert_eq!(mx, [1.0, 4.0, 2.0]);
1326 assert!(fold_world_bounds(core::iter::empty()).is_none());
1327 }
1328
1329 #[test]
1330 fn face_centres_look_down_their_axis() {
1331 let eye = [0.0, 0.0, 0.0];
1333 for face in 0..6 {
1334 let vp = face_view_projection(eye, face, 0.05, 100.0);
1335 let d = face_dir(face, 0.0, 0.0);
1336 let (nx, ny, w) = project(vp, d);
1337 assert!(w > 0.0);
1338 assert!(
1339 nx.abs() < 1e-5 && ny.abs() < 1e-5,
1340 "face {face} centre off-origin"
1341 );
1342 }
1343 }
1344
1345 #[test]
1346 fn bake_queue_hands_out_indices_in_order() {
1347 let mut q = ProbeBakeQueue::new(3);
1348 assert!(q.pending());
1349 assert_eq!(q.take_next(), Some(0));
1350 assert_eq!(q.take_next(), Some(1));
1351 assert!(q.pending());
1352 assert_eq!(q.take_next(), Some(2));
1353 assert!(!q.pending());
1354 assert_eq!(q.take_next(), None);
1355 }
1356
1357 #[test]
1358 fn bake_queue_empty_is_never_pending() {
1359 let mut q = ProbeBakeQueue::new(0);
1360 assert!(!q.pending());
1361 assert_eq!(q.take_next(), None);
1362 }
1363
1364 #[test]
1365 fn bake_queue_abort_skips_the_remainder() {
1366 let mut q = ProbeBakeQueue::new(4);
1367 assert_eq!(q.take_next(), Some(0));
1368 q.abort();
1369 assert!(!q.pending());
1370 assert_eq!(q.take_next(), None);
1371 }
1372
1373 #[test]
1374 fn bake_action_idle_starts_only_when_pending_and_eligible() {
1375 assert_eq!(
1377 next_bake_action(BakePhase::Idle, false, false, true, true, false),
1378 BakeAction::StartNext
1379 );
1380 assert_eq!(
1382 next_bake_action(BakePhase::Idle, false, false, false, true, false),
1383 BakeAction::Idle
1384 );
1385 assert_eq!(
1388 next_bake_action(BakePhase::Idle, false, false, true, false, false),
1389 BakeAction::Idle
1390 );
1391 }
1392
1393 #[test]
1394 fn bake_action_rendering_submits_faces_before_waiting_for_completion() {
1395 assert_eq!(
1398 next_bake_action(BakePhase::Rendering, false, false, true, true, true),
1399 BakeAction::RenderFace
1400 );
1401 assert_eq!(
1403 next_bake_action(BakePhase::Rendering, false, false, true, true, false),
1404 BakeAction::Idle
1405 );
1406 assert_eq!(
1408 next_bake_action(BakePhase::Rendering, true, false, true, true, false),
1409 BakeAction::Readback
1410 );
1411 }
1412
1413 #[test]
1414 fn bake_action_converting_waits_for_offthread_payload() {
1415 assert_eq!(
1417 next_bake_action(BakePhase::Converting, true, false, true, true, false),
1418 BakeAction::Idle
1419 );
1420 assert_eq!(
1421 next_bake_action(BakePhase::Converting, true, true, false, true, false),
1422 BakeAction::Install
1423 );
1424 }
1425
1426 #[test]
1427 fn bake_action_never_starts_a_second_bake_while_one_is_in_flight() {
1428 for phase in [BakePhase::Rendering, BakePhase::Converting] {
1431 assert_ne!(
1432 next_bake_action(phase, false, false, true, true, false),
1433 BakeAction::StartNext
1434 );
1435 assert_ne!(
1436 next_bake_action(phase, false, false, true, true, true),
1437 BakeAction::StartNext
1438 );
1439 }
1440 }
1441}