1use crate::build::environment_map as em;
16use alloc::vec;
17use alloc::vec::Vec;
18use concinnity_core::gfx::projection::perspective_rh;
19use concinnity_core::gfx::transform::mat4_mul;
20use concinnity_core::math::vec3::dot;
21use concinnity_core::math::{ceil, floor, powi, round, sqrt};
22use core::f32::consts::FRAC_PI_2;
23
24const FACE_BASIS: [[[f32; 3]; 3]; 6] = [
28 [[0.0, 0.0, -1.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]], [[0.0, 0.0, 1.0], [0.0, 1.0, 0.0], [-1.0, 0.0, 0.0]], [[1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0, 1.0, 0.0]], [[1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, -1.0, 0.0]], [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], [[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0]], ];
36
37fn perspective_90(near: f32, far: f32) -> [[f32; 4]; 4] {
39 perspective_rh(FRAC_PI_2, 1.0, near, far)
40}
41
42fn face_view(eye: [f32; 3], r: [f32; 3], u: [f32; 3], f: [f32; 3]) -> [[f32; 4]; 4] {
45 [
46 [r[0], u[0], -f[0], 0.0],
47 [r[1], u[1], -f[1], 0.0],
48 [r[2], u[2], -f[2], 0.0],
49 [-dot(r, eye), -dot(u, eye), dot(f, eye), 1.0],
50 ]
51}
52
53pub fn face_view_projection(eye: [f32; 3], face: usize, near: f32, far: f32) -> [[f32; 4]; 4] {
55 let b = FACE_BASIS[face];
56 let view = face_view(eye, b[0], b[1], b[2]);
57 mat4_mul(perspective_90(near, far), view)
58}
59
60pub fn face_view_matrix(eye: [f32; 3], face: usize) -> [[f32; 4]; 4] {
65 let b = FACE_BASIS[face];
66 face_view(eye, b[0], b[1], b[2])
67}
68
69#[derive(Clone, Copy, Debug, PartialEq)]
74pub struct ProbePlacement {
75 pub position: [f32; 3],
77 pub box_min: [f32; 3],
79 pub box_max: [f32; 3],
81}
82
83impl ProbePlacement {
84 pub fn from_center_extents(position: [f32; 3], half_extents: [f32; 3]) -> ProbePlacement {
87 ProbePlacement {
88 position,
89 box_min: [
90 position[0] - half_extents[0],
91 position[1] - half_extents[1],
92 position[2] - half_extents[2],
93 ],
94 box_max: [
95 position[0] + half_extents[0],
96 position[1] + half_extents[1],
97 position[2] + half_extents[2],
98 ],
99 }
100 }
101}
102
103#[derive(Clone, Copy, Debug, PartialEq, Eq)]
109pub struct ProbeBakeQueue {
110 total: usize,
111 next: usize,
112}
113
114impl ProbeBakeQueue {
115 pub fn new(total: usize) -> ProbeBakeQueue {
117 ProbeBakeQueue { total, next: 0 }
118 }
119
120 pub fn pending(&self) -> bool {
122 self.next < self.total
123 }
124
125 pub fn take_next(&mut self) -> Option<usize> {
127 (self.next < self.total).then(|| {
128 let i = self.next;
129 self.next += 1;
130 i
131 })
132 }
133
134 pub fn abort(&mut self) {
137 self.next = self.total;
138 }
139}
140
141#[derive(Clone, Copy, Debug, PartialEq, Eq)]
148pub enum BakePhase {
149 Idle,
151 Rendering,
153 Converting,
155}
156
157#[derive(Clone, Copy, Debug, PartialEq, Eq)]
165pub enum BakeAction {
166 Idle,
168 StartNext,
170 RenderFace,
172 Install,
174 Readback,
176}
177
178pub fn next_bake_action(
194 phase: BakePhase,
195 done: bool,
196 payload_ready: bool,
197 queue_pending: bool,
198 eligible: bool,
199 more_faces: bool,
200) -> BakeAction {
201 match phase {
202 BakePhase::Rendering => {
203 if more_faces {
204 BakeAction::RenderFace
205 } else if done {
206 BakeAction::Readback
207 } else {
208 BakeAction::Idle
209 }
210 }
211 BakePhase::Converting => {
212 if payload_ready {
213 BakeAction::Install
214 } else {
215 BakeAction::Idle
216 }
217 }
218 BakePhase::Idle => {
219 if queue_pending && eligible {
220 BakeAction::StartNext
221 } else {
222 BakeAction::Idle
223 }
224 }
225 }
226}
227
228pub(crate) const AUTO_SEED_BUDGET: usize = 8;
233
234const AUTO_SEED_CELL_TARGET: f32 = 12.0;
238
239const INTERIOR_VOXELS_LONG_AXIS: usize = 48;
243const INTERIOR_MAX_DIM: usize = 128;
245const INTERIOR_MIN_ENCLOSED: u8 = 5;
251const INTERIOR_MIN_CLUSTER: usize = 4;
254const INTERIOR_MIN_ROOM_SPAN: f32 = 2.0;
263
264fn fit_grid(nx: usize, nz: usize, budget: usize) -> (usize, usize) {
268 let (mut nx, mut nz) = (nx.max(1), nz.max(1));
269 if nx * nz > budget {
270 let scale = sqrt(budget as f32 / (nx * nz) as f32);
271 nx = (round(nx as f32 * scale) as usize).max(1);
272 nz = (round(nz as f32 * scale) as usize).max(1);
273 while nx * nz > budget {
274 if nx >= nz {
275 nx -= 1;
276 } else {
277 nz -= 1;
278 }
279 }
280 }
281 (nx.max(1), nz.max(1))
282}
283
284fn point_inside_any(p: [f32; 3], occupancy: &[([f32; 3], [f32; 3])]) -> bool {
286 occupancy.iter().any(|(mn, mx)| {
287 p[0] >= mn[0]
288 && p[0] <= mx[0]
289 && p[1] >= mn[1]
290 && p[1] <= mx[1]
291 && p[2] >= mn[2]
292 && p[2] <= mx[2]
293 })
294}
295
296fn open_capture_point(
303 center: [f32; 3],
304 x0: f32,
305 x1: f32,
306 z0: f32,
307 z1: f32,
308 occupancy: &[([f32; 3], [f32; 3])],
309) -> [f32; 3] {
310 if !point_inside_any(center, occupancy) {
311 return center;
312 }
313 let lerp = |a: f32, b: f32, t: f32| a + (b - a) * t;
314 for (fx, fz) in [(0.25, 0.25), (0.75, 0.25), (0.25, 0.75), (0.75, 0.75)] {
315 let p = [lerp(x0, x1, fx), center[1], lerp(z0, z1, fz)];
316 if !point_inside_any(p, occupancy) {
317 return p;
318 }
319 }
320 center
321}
322
323fn interior_voxel_grid(
329 aabb_min: [f32; 3],
330 aabb_max: [f32; 3],
331) -> Option<(f32, usize, usize, usize)> {
332 let extent = [
333 aabb_max[0] - aabb_min[0],
334 aabb_max[1] - aabb_min[1],
335 aabb_max[2] - aabb_min[2],
336 ];
337 let long = extent[0].max(extent[2]);
338 if long <= 0.0 || extent[1] <= 0.0 {
339 return None;
340 }
341 let vs = (long / INTERIOR_VOXELS_LONG_AXIS as f32).max(0.25);
342 let dim = |e: f32| (ceil(e / vs) as usize).clamp(1, INTERIOR_MAX_DIM);
343 Some((vs, dim(extent[0]), dim(extent[1]), dim(extent[2])))
344}
345
346fn solid_from_aabbs(
350 aabb_min: [f32; 3],
351 vs: f32,
352 nx: usize,
353 ny: usize,
354 nz: usize,
355 occupancy: &[([f32; 3], [f32; 3])],
356) -> Vec<bool> {
357 let idx = |x: usize, y: usize, z: usize| (z * ny + y) * nx + x;
358 let mut solid = vec![false; nx * ny * nz];
359 let to_vx =
360 |v: f32, origin: f32, hi: usize| (floor((v - origin) / vs).max(0.0) as usize).min(hi);
361 for (mn, mx) in occupancy {
362 let x0 = to_vx(mn[0], aabb_min[0], nx - 1);
363 let x1 = to_vx(mx[0], aabb_min[0], nx - 1);
364 let y0 = to_vx(mn[1], aabb_min[1], ny - 1);
365 let y1 = to_vx(mx[1], aabb_min[1], ny - 1);
366 let z0 = to_vx(mn[2], aabb_min[2], nz - 1);
367 let z1 = to_vx(mx[2], aabb_min[2], nz - 1);
368 for z in z0..=z1 {
369 for y in y0..=y1 {
370 for x in x0..=x1 {
371 solid[idx(x, y, z)] = true;
372 }
373 }
374 }
375 }
376 solid
377}
378
379fn tri_box_overlap(box_c: [f32; 3], box_h: [f32; 3], tri: &[[f32; 3]; 3]) -> bool {
387 let sub = |a: [f32; 3], b: [f32; 3]| [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
388 let dot = |a: [f32; 3], b: [f32; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
389 let cross = |a: [f32; 3], b: [f32; 3]| {
390 [
391 a[1] * b[2] - a[2] * b[1],
392 a[2] * b[0] - a[0] * b[2],
393 a[0] * b[1] - a[1] * b[0],
394 ]
395 };
396 let v = [sub(tri[0], box_c), sub(tri[1], box_c), sub(tri[2], box_c)];
398 let edges = [sub(v[1], v[0]), sub(v[2], v[1]), sub(v[0], v[2])];
399 let box_axes = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
400
401 let separated = |l: [f32; 3]| -> bool {
402 let r = box_h[0] * l[0].abs() + box_h[1] * l[1].abs() + box_h[2] * l[2].abs();
403 let p0 = dot(l, v[0]);
404 let p1 = dot(l, v[1]);
405 let p2 = dot(l, v[2]);
406 p0.min(p1).min(p2) > r || p0.max(p1).max(p2) < -r
407 };
408
409 for a in box_axes {
410 if separated(a) {
411 return false;
412 }
413 }
414 for e in edges {
415 for a in box_axes {
416 if separated(cross(e, a)) {
417 return false;
418 }
419 }
420 }
421 !separated(cross(edges[0], edges[1]))
422}
423
424fn solid_from_triangles(
430 aabb_min: [f32; 3],
431 vs: f32,
432 nx: usize,
433 ny: usize,
434 nz: usize,
435 triangles: &[[[f32; 3]; 3]],
436) -> Vec<bool> {
437 let idx = |x: usize, y: usize, z: usize| (z * ny + y) * nx + x;
438 let mut solid = vec![false; nx * ny * nz];
439 let half = [vs * 0.5 + vs * 1e-3; 3];
445 let to_vx =
446 |v: f32, origin: f32, hi: usize| (floor((v - origin) / vs).max(0.0) as usize).min(hi);
447 for tri in triangles {
448 let mut tmn = tri[0];
449 let mut tmx = tri[0];
450 for vtx in &tri[1..] {
451 for a in 0..3 {
452 tmn[a] = tmn[a].min(vtx[a]);
453 tmx[a] = tmx[a].max(vtx[a]);
454 }
455 }
456 if !tmn.iter().chain(tmx.iter()).all(|c| c.is_finite()) {
457 continue;
458 }
459 let x0 = to_vx(tmn[0], aabb_min[0], nx - 1);
460 let x1 = to_vx(tmx[0], aabb_min[0], nx - 1);
461 let y0 = to_vx(tmn[1], aabb_min[1], ny - 1);
462 let y1 = to_vx(tmx[1], aabb_min[1], ny - 1);
463 let z0 = to_vx(tmn[2], aabb_min[2], nz - 1);
464 let z1 = to_vx(tmx[2], aabb_min[2], nz - 1);
465 for z in z0..=z1 {
466 for y in y0..=y1 {
467 for x in x0..=x1 {
468 let i = idx(x, y, z);
469 if solid[i] {
470 continue;
471 }
472 let c = [
473 aabb_min[0] + (x as f32 + 0.5) * vs,
474 aabb_min[1] + (y as f32 + 0.5) * vs,
475 aabb_min[2] + (z as f32 + 0.5) * vs,
476 ];
477 if tri_box_overlap(c, half, tri) {
478 solid[i] = true;
479 }
480 }
481 }
482 }
483 }
484 solid
485}
486
487fn interior_probes_from_solid(
497 aabb_min: [f32; 3],
498 vs: f32,
499 nx: usize,
500 ny: usize,
501 nz: usize,
502 solid: &[bool],
503 budget: usize,
504) -> Vec<ProbePlacement> {
505 let n = nx * ny * nz;
506 let idx = |x: usize, y: usize, z: usize| (z * ny + y) * nx + x;
507
508 let mut enclosed = vec![0u8; n];
511 for z in 0..nz {
512 for y in 0..ny {
513 let (mut fwd, mut bwd) = (false, false);
514 for x in (0..nx).rev() {
515 let i = idx(x, y, z);
516 if solid[i] {
517 fwd = true;
518 } else if fwd {
519 enclosed[i] += 1;
520 }
521 }
522 for x in 0..nx {
523 let i = idx(x, y, z);
524 if solid[i] {
525 bwd = true;
526 } else if bwd {
527 enclosed[i] += 1;
528 }
529 }
530 }
531 }
532 for z in 0..nz {
533 for x in 0..nx {
534 let (mut fwd, mut bwd) = (false, false);
535 for y in (0..ny).rev() {
536 let i = idx(x, y, z);
537 if solid[i] {
538 fwd = true;
539 } else if fwd {
540 enclosed[i] += 1;
541 }
542 }
543 for y in 0..ny {
544 let i = idx(x, y, z);
545 if solid[i] {
546 bwd = true;
547 } else if bwd {
548 enclosed[i] += 1;
549 }
550 }
551 }
552 }
553 for y in 0..ny {
554 for x in 0..nx {
555 let (mut fwd, mut bwd) = (false, false);
556 for z in (0..nz).rev() {
557 let i = idx(x, y, z);
558 if solid[i] {
559 fwd = true;
560 } else if fwd {
561 enclosed[i] += 1;
562 }
563 }
564 for z in 0..nz {
565 let i = idx(x, y, z);
566 if solid[i] {
567 bwd = true;
568 } else if bwd {
569 enclosed[i] += 1;
570 }
571 }
572 }
573 }
574
575 let is_interior = |i: usize| !solid[i] && enclosed[i] >= INTERIOR_MIN_ENCLOSED;
576
577 let mut label = vec![usize::MAX; n];
579 let mut clusters: Vec<Vec<usize>> = Vec::new();
580 let mut stack: Vec<usize> = Vec::new();
581 for start in 0..n {
582 if !is_interior(start) || label[start] != usize::MAX {
583 continue;
584 }
585 let cid = clusters.len();
586 let mut members = Vec::new();
587 label[start] = cid;
588 stack.push(start);
589 while let Some(i) = stack.pop() {
590 members.push(i);
591 let z = i / (nx * ny);
592 let y = (i / nx) % ny;
593 let x = i % nx;
594 let neighbours = [
595 (x > 0).then(|| i - 1),
596 (x + 1 < nx).then_some(i + 1),
597 (y > 0).then(|| i - nx),
598 (y + 1 < ny).then_some(i + nx),
599 (z > 0).then(|| i - nx * ny),
600 (z + 1 < nz).then_some(i + nx * ny),
601 ];
602 for j in neighbours.into_iter().flatten() {
603 if is_interior(j) && label[j] == usize::MAX {
604 label[j] = cid;
605 stack.push(j);
606 }
607 }
608 }
609 clusters.push(members);
610 }
611
612 let voxel_center = |i: usize| {
613 let z = i / (nx * ny);
614 let y = (i / nx) % ny;
615 let x = i % nx;
616 [
617 aabb_min[0] + (x as f32 + 0.5) * vs,
618 aabb_min[1] + (y as f32 + 0.5) * vs,
619 aabb_min[2] + (z as f32 + 0.5) * vs,
620 ]
621 };
622 let cluster_span = |members: &[usize]| {
624 let mut lo = [f32::MAX; 3];
625 let mut hi = [f32::MIN; 3];
626 for &i in members {
627 let c = voxel_center(i);
628 for a in 0..3 {
629 lo[a] = lo[a].min(c[a] - vs * 0.5);
630 hi[a] = hi[a].max(c[a] + vs * 0.5);
631 }
632 }
633 [hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]]
634 };
635
636 clusters.retain(|c| {
639 c.len() >= INTERIOR_MIN_CLUSTER
640 && cluster_span(c).iter().all(|d| *d >= INTERIOR_MIN_ROOM_SPAN)
641 });
642 clusters.sort_by_key(|c| core::cmp::Reverse(c.len()));
643 clusters.truncate(budget);
644 clusters
645 .iter()
646 .map(|members| {
647 let inv = 1.0 / members.len() as f32;
650 let mut centroid = [0.0f32; 3];
651 for &i in members {
652 let c = voxel_center(i);
653 for a in 0..3 {
654 centroid[a] += c[a] * inv;
655 }
656 }
657 let dist2 = |c: [f32; 3]| {
658 powi(c[0] - centroid[0], 2)
659 + powi(c[1] - centroid[1], 2)
660 + powi(c[2] - centroid[2], 2)
661 };
662 let position = members
663 .iter()
664 .map(|&i| voxel_center(i))
665 .min_by(|a, b| dist2(*a).total_cmp(&dist2(*b)))
666 .unwrap_or(centroid);
667 let mut box_min = [f32::MAX; 3];
670 let mut box_max = [f32::MIN; 3];
671 for &i in members {
672 let c = voxel_center(i);
673 for a in 0..3 {
674 box_min[a] = box_min[a].min(c[a] - vs * 0.5);
675 box_max[a] = box_max[a].max(c[a] + vs * 0.5);
676 }
677 }
678 ProbePlacement {
679 position,
680 box_min,
681 box_max,
682 }
683 })
684 .collect()
685}
686
687fn seed_interior_probes(
691 aabb_min: [f32; 3],
692 aabb_max: [f32; 3],
693 occupancy: &[([f32; 3], [f32; 3])],
694 budget: usize,
695) -> Vec<ProbePlacement> {
696 if budget == 0 || occupancy.is_empty() {
697 return Vec::new();
698 }
699 let (vs, nx, ny, nz) = match interior_voxel_grid(aabb_min, aabb_max) {
700 Some(g) => g,
701 None => return Vec::new(),
702 };
703 let solid = solid_from_aabbs(aabb_min, vs, nx, ny, nz, occupancy);
704 interior_probes_from_solid(aabb_min, vs, nx, ny, nz, &solid, budget)
705}
706
707fn seed_interior_probes_tris(
711 aabb_min: [f32; 3],
712 aabb_max: [f32; 3],
713 triangles: &[[[f32; 3]; 3]],
714 budget: usize,
715) -> Vec<ProbePlacement> {
716 if budget == 0 || triangles.is_empty() {
717 return Vec::new();
718 }
719 let (vs, nx, ny, nz) = match interior_voxel_grid(aabb_min, aabb_max) {
720 Some(g) => g,
721 None => return Vec::new(),
722 };
723 let solid = solid_from_triangles(aabb_min, vs, nx, ny, nz, triangles);
724 interior_probes_from_solid(aabb_min, vs, nx, ny, nz, &solid, budget)
725}
726
727const REFLECTOR_BOUNDS_HALF_HEIGHT: f32 = 2.0;
733
734pub fn reflector_bounds(centre: [f32; 3], half_extents: [f32; 3]) -> ([f32; 3], [f32; 3]) {
747 let half = |a: usize| half_extents[a].abs().max(REFLECTOR_BOUNDS_HALF_HEIGHT);
748 (
749 [
750 centre[0] - half(0),
751 centre[1] - half(1),
752 centre[2] - half(2),
753 ],
754 [
755 centre[0] + half(0),
756 centre[1] + half(1),
757 centre[2] + half(2),
758 ],
759 )
760}
761
762pub fn auto_seed_probes(
766 aabb_min: [f32; 3],
767 aabb_max: [f32; 3],
768 occupancy: &[([f32; 3], [f32; 3])],
769) -> Vec<ProbePlacement> {
770 auto_seed_probes_with_geometry(aabb_min, aabb_max, occupancy, &[])
771}
772
773pub fn auto_seed_probes_with_geometry(
785 aabb_min: [f32; 3],
786 aabb_max: [f32; 3],
787 occupancy: &[([f32; 3], [f32; 3])],
788 triangles: &[[[f32; 3]; 3]],
789) -> Vec<ProbePlacement> {
790 let finite = aabb_min
791 .iter()
792 .chain(aabb_max.iter())
793 .all(|c| c.is_finite());
794 if !finite || aabb_max[0] <= aabb_min[0] || aabb_max[2] <= aabb_min[2] {
795 return Vec::new();
796 }
797 let mut out = if triangles.is_empty() {
798 seed_interior_probes(aabb_min, aabb_max, occupancy, AUTO_SEED_BUDGET)
799 } else {
800 seed_interior_probes_tris(aabb_min, aabb_max, triangles, AUTO_SEED_BUDGET)
801 };
802 let remaining = AUTO_SEED_BUDGET.saturating_sub(out.len());
803 if remaining > 0 {
804 out.extend(seed_grid_probes(aabb_min, aabb_max, occupancy, remaining));
805 }
806 out
807}
808
809fn seed_grid_probes(
815 aabb_min: [f32; 3],
816 aabb_max: [f32; 3],
817 occupancy: &[([f32; 3], [f32; 3])],
818 budget: usize,
819) -> Vec<ProbePlacement> {
820 if budget == 0 {
821 return Vec::new();
822 }
823 let dx = aabb_max[0] - aabb_min[0];
824 let dz = aabb_max[2] - aabb_min[2];
825 let nx_raw = ceil(dx / AUTO_SEED_CELL_TARGET).max(1.0) as usize;
826 let nz_raw = ceil(dz / AUTO_SEED_CELL_TARGET).max(1.0) as usize;
827 let (nx, nz) = fit_grid(nx_raw, nz_raw, budget);
828
829 let y_eye = probe_eye_point(aabb_min, aabb_max)[1];
830 let lerp = |a: f32, b: f32, t: f32| a + (b - a) * t;
831 let mut out = Vec::with_capacity(nx * nz);
832 for ix in 0..nx {
833 for iz in 0..nz {
834 let x0 = lerp(aabb_min[0], aabb_max[0], ix as f32 / nx as f32);
835 let x1 = lerp(aabb_min[0], aabb_max[0], (ix + 1) as f32 / nx as f32);
836 let z0 = lerp(aabb_min[2], aabb_max[2], iz as f32 / nz as f32);
837 let z1 = lerp(aabb_min[2], aabb_max[2], (iz + 1) as f32 / nz as f32);
838 let center = [(x0 + x1) * 0.5, y_eye, (z0 + z1) * 0.5];
839 out.push(ProbePlacement {
840 position: open_capture_point(center, x0, x1, z0, z1, occupancy),
841 box_min: [x0, aabb_min[1], z0],
842 box_max: [x1, aabb_max[1], z1],
843 });
844 }
845 }
846 out
847}
848
849pub fn fold_world_bounds(
853 boxes: impl IntoIterator<Item = ([f32; 3], [f32; 3])>,
854) -> Option<([f32; 3], [f32; 3])> {
855 let mut acc: Option<([f32; 3], [f32; 3])> = None;
856 for (mn, mx) in boxes {
857 if !mn.iter().chain(mx.iter()).all(|c| c.is_finite()) {
858 continue;
859 }
860 match &mut acc {
861 None => acc = Some((mn, mx)),
862 Some((amn, amx)) => {
863 for i in 0..3 {
864 amn[i] = amn[i].min(mn[i]);
865 amx[i] = amx[i].max(mx[i]);
866 }
867 }
868 }
869 }
870 acc
871}
872
873pub fn build_probe_payload<S: em::RowScheduler>(
882 scheduler: &S,
883 faces: &[Vec<f32>; 6],
884 face_size: u32,
885 irradiance_face: u32,
886 prefilter_samples: u32,
887 prefilter_clamp: f32,
888) -> Vec<u8> {
889 let mips = em::max_mip_count(face_size);
890 let irradiance = em::CubeBake::irradiance(
891 faces,
892 face_size,
893 irradiance_face,
894 em::DEFAULT_IRRADIANCE_PHI_SAMPLES,
895 em::DEFAULT_IRRADIANCE_THETA_SAMPLES,
896 )
897 .bake(scheduler);
898 let mut prefilter = Vec::with_capacity(mips as usize);
902 prefilter.push(em::prefilter_mip0(faces, face_size, prefilter_clamp, true));
903 for mip in 1..mips {
904 prefilter.push(
905 em::CubeBake::ggx(
906 faces,
907 face_size,
908 face_size >> mip,
909 em::prefilter_roughness(mip, mips),
910 prefilter_samples,
911 prefilter_clamp,
912 )
913 .bake(scheduler),
914 );
915 }
916 em::serialise_payload(irradiance_face, face_size, mips, &irradiance, &prefilter)
917}
918
919pub(crate) fn probe_eye_point(aabb_min: [f32; 3], aabb_max: [f32; 3]) -> [f32; 3] {
927 const EYE_HEIGHT: f32 = 1.7;
928 let cx = 0.5 * (aabb_min[0] + aabb_max[0]);
929 let cz = 0.5 * (aabb_min[2] + aabb_max[2]);
930 let floor = aabb_min[1];
931 let ceil = aabb_max[1];
932 let y = (floor + EYE_HEIGHT).min(0.5 * (floor + ceil)).max(floor);
935 [cx, y, cz]
936}
937
938#[cfg(test)]
939mod tests {
940 use super::*;
941
942 fn cube_texel_dir(face: usize, u: f32, v: f32) -> [f32; 3] {
945 match face {
946 0 => [1.0, -v, -u],
947 1 => [-1.0, -v, u],
948 2 => [u, 1.0, v],
949 3 => [u, -1.0, -v],
950 4 => [u, -v, 1.0],
951 5 => [-u, -v, -1.0],
952 _ => unreachable!(),
953 }
954 }
955
956 fn project(vp: [[f32; 4]; 4], p: [f32; 3]) -> (f32, f32, f32) {
957 let mut c = [0.0f32; 4];
958 let pv = [p[0], p[1], p[2], 1.0];
959 for row in 0..4 {
960 for k in 0..4 {
961 c[row] += vp[k][row] * pv[k];
962 }
963 }
964 (c[0] / c[3], c[1] / c[3], c[3])
965 }
966
967 #[test]
972 fn face_view_projection_matches_cube_convention() {
973 let eye = [3.0, -1.5, 2.0];
974 let samples = [
975 (0.0f32, 0.0f32),
976 (0.5, 0.0),
977 (0.0, 0.5),
978 (-0.6, 0.3),
979 (0.7, -0.4),
980 ];
981 for face in 0..6 {
982 let vp = face_view_projection(eye, face, 0.05, 100.0);
983 for &(u, v) in &samples {
984 let d = cube_texel_dir(face, u, v);
985 let p = [eye[0] + d[0], eye[1] + d[1], eye[2] + d[2]];
986 let (nx, ny, w) = project(vp, p);
987 assert!(
988 w > 0.0,
989 "face {face} sample ({u},{v}) behind camera (w={w})"
990 );
991 assert!(
992 (nx - u).abs() < 1e-4 && (ny - (-v)).abs() < 1e-4,
993 "face {face} ({u},{v}) -> ndc ({nx},{ny}), expected ({u},{})",
994 -v
995 );
996 }
997 }
998 }
999
1000 #[test]
1001 fn build_probe_payload_round_trips() {
1002 let face = 8usize;
1005 let faces: [Vec<f32>; 6] = core::array::from_fn(|f| {
1006 let mut v = vec![0.0f32; face * face * 4];
1007 for px in v.chunks_exact_mut(4) {
1008 px[0] = f as f32 * 0.1;
1009 px[1] = 0.2;
1010 px[2] = 0.3;
1011 px[3] = 1.0;
1012 }
1013 v
1014 });
1015 let bytes = build_probe_payload(&em::Serial, &faces, face as u32, 8, 16, 12.0);
1016 let view = crate::build::environment_map::deserialise(&bytes).expect("deserialise");
1017 assert_eq!(view.prefilter_face, 8);
1018 assert_eq!(view.irradiance_face, 8);
1019 assert!(view.prefilter_mip_bytes.len() >= 2);
1020 }
1021
1022 #[test]
1023 fn probe_eye_point_centres_at_eye_height() {
1024 let eye = probe_eye_point([-10.0, 0.0, -4.0], [6.0, 30.0, 12.0]);
1027 assert!((eye[0] - (-2.0)).abs() < 1e-6, "x not centred: {}", eye[0]);
1028 assert!((eye[2] - 4.0).abs() < 1e-6, "z not centred: {}", eye[2]);
1029 assert!((eye[1] - 1.7).abs() < 1e-6, "y not eye height: {}", eye[1]);
1030 }
1031
1032 #[test]
1033 fn probe_eye_point_clamps_to_a_flat_scene() {
1034 let eye = probe_eye_point([0.0, 0.0, 0.0], [2.0, 1.0, 2.0]);
1036 assert!(
1037 eye[1] >= 0.0 && eye[1] <= 1.0,
1038 "y escaped bounds: {}",
1039 eye[1]
1040 );
1041 }
1042
1043 #[test]
1044 fn face_view_matrix_composes_to_face_vp() {
1045 let eye = [1.0, 2.0, -3.0];
1049 for face in 0..6 {
1050 let vp = face_view_projection(eye, face, 0.1, 50.0);
1051 let comp = mat4_mul(perspective_90(0.1, 50.0), face_view_matrix(eye, face));
1052 for c in 0..4 {
1053 for r in 0..4 {
1054 assert!(
1055 (vp[c][r] - comp[c][r]).abs() < 1e-5,
1056 "face {face} [{c}][{r}] mismatch"
1057 );
1058 }
1059 }
1060 }
1061 }
1062
1063 #[test]
1064 fn placement_from_center_extents_builds_box() {
1065 let p = ProbePlacement::from_center_extents([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]);
1066 assert_eq!(p.box_min, [-3.0, -3.0, -3.0]);
1067 assert_eq!(p.box_max, [5.0, 7.0, 9.0]);
1068 assert_eq!(p.position, [1.0, 2.0, 3.0]);
1069 }
1070
1071 fn probe_union(probes: &[ProbePlacement]) -> ([f32; 3], [f32; 3]) {
1073 let mn = probes.iter().fold([f32::MAX; 3], |a, p| {
1074 core::array::from_fn(|i| a[i].min(p.box_min[i]))
1075 });
1076 let mx = probes.iter().fold([f32::MIN; 3], |a, p| {
1077 core::array::from_fn(|i| a[i].max(p.box_max[i]))
1078 });
1079 (mn, mx)
1080 }
1081
1082 #[test]
1083 fn auto_seed_probes_tiles_the_scene() {
1084 let probes = auto_seed_probes([-10.0, 0.0, -10.0], [10.0, 6.0, 10.0], &[]);
1086 assert_eq!(probes.len(), 4);
1087 let (union_min, union_max) = probe_union(&probes);
1088 assert_eq!(union_min, [-10.0, 0.0, -10.0]);
1089 assert_eq!(union_max, [10.0, 6.0, 10.0]);
1090 assert!(auto_seed_probes([0.0; 3], [0.0; 3], &[]).is_empty());
1092 assert!(auto_seed_probes([f32::NAN, 0.0, 0.0], [1.0, 1.0, 1.0], &[]).is_empty());
1093 }
1094
1095 #[test]
1096 fn auto_seed_scales_count_to_scene_size_and_aspect() {
1097 let small = auto_seed_probes([-3.0, 0.0, -3.0], [3.0, 3.0, 3.0], &[]);
1100 assert_eq!(small.len(), 1);
1101 let long = auto_seed_probes([0.0, 0.0, 0.0], [96.0, 4.0, 12.0], &[]);
1104 assert!(long.len() > 1 && long.len() <= AUTO_SEED_BUDGET);
1105 let nx = long.iter().filter(|p| p.box_min[2] == 0.0).count();
1106 let nz = long.len() / nx;
1107 assert!(nx > nz, "long axis (x) should have more cells: {nx}x{nz}");
1108 let (mn, mx) = probe_union(&long);
1109 assert_eq!(mn, [0.0, 0.0, 0.0]);
1110 assert_eq!(mx, [96.0, 4.0, 12.0]);
1111 let big = auto_seed_probes([0.0, 0.0, 0.0], [500.0, 4.0, 500.0], &[]);
1113 assert!(big.len() <= AUTO_SEED_BUDGET);
1114 }
1115
1116 #[test]
1117 fn auto_seed_nudges_capture_point_out_of_geometry() {
1118 let occ = [([-1.0, 0.0, -1.0], [1.0, 5.0, 1.0])];
1121 let probes = auto_seed_probes([-3.0, 0.0, -3.0], [3.0, 3.0, 3.0], &occ);
1122 assert_eq!(probes.len(), 1);
1123 let p = probes[0].position;
1124 assert!(
1125 !point_inside_any(p, &occ),
1126 "capture point {p:?} still inside the occupancy box"
1127 );
1128 assert!(p[0] >= probes[0].box_min[0] && p[0] <= probes[0].box_max[0]);
1130 assert!(p[2] >= probes[0].box_min[2] && p[2] <= probes[0].box_max[2]);
1131 let everywhere = [([-100.0, -100.0, -100.0], [100.0, 100.0, 100.0])];
1133 let trapped = auto_seed_probes([-3.0, 0.0, -3.0], [3.0, 3.0, 3.0], &everywhere);
1134 assert_eq!(trapped.len(), 1);
1135 }
1136
1137 #[test]
1138 fn fit_grid_respects_budget_and_aspect() {
1139 assert_eq!(fit_grid(1, 1, 8), (1, 1));
1140 assert_eq!(fit_grid(2, 2, 8), (2, 2)); let (nx, nz) = fit_grid(9, 2, 8); assert!(nx * nz <= 8 && nx > nz);
1143 let (nx, nz) = fit_grid(20, 20, 8); assert!(nx * nz <= 8 && nx >= 1 && nz >= 1);
1145 }
1146
1147 fn box_room(min: [f32; 3], max: [f32; 3]) -> Vec<([f32; 3], [f32; 3])> {
1150 let [x0, y0, z0] = min;
1151 let [x1, y1, z1] = max;
1152 vec![
1153 ([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]), ]
1160 }
1161
1162 #[test]
1163 fn seed_interior_probes_finds_a_sealed_room() {
1164 let room = box_room([0.0, 0.0, 0.0], [10.0, 6.0, 10.0]);
1167 let probes = seed_interior_probes([-3.0, -3.0, -3.0], [13.0, 9.0, 13.0], &room, 8);
1168 assert_eq!(probes.len(), 1, "one room -> one interior probe");
1169 let p = probes[0].position;
1170 assert!(
1171 p[0] > 0.0 && p[0] < 10.0 && p[1] > 0.0 && p[1] < 6.0 && p[2] > 0.0 && p[2] < 10.0,
1172 "probe {p:?} should sit inside the room"
1173 );
1174 assert!(probes[0].box_min[0] < 2.0 && probes[0].box_max[0] > 8.0);
1176 }
1177
1178 #[test]
1179 fn reflector_bounds_covers_the_surface_and_has_volume() {
1180 let (mn, mx) = reflector_bounds([0.0, 0.0, 0.0], [14.0, 0.0, 14.0]);
1184 assert_eq!(mn[0], -14.0);
1185 assert_eq!(mx[2], 14.0);
1186 assert!(
1187 mx[1] - mn[1] > 0.0,
1188 "flat axis is inflated, not left at zero"
1189 );
1190
1191 let (mn, mx) = reflector_bounds([5.0, 3.0, -2.0], [0.5, 6.0, 0.5]);
1193 assert_eq!(mn[1], -3.0);
1194 assert_eq!(mx[1], 9.0);
1195 assert!(mn[0] < 5.0 && mx[0] > 5.0);
1196
1197 let crate_aabb = ([-0.7, 0.6, -0.7], [0.7, 2.0, 0.7]);
1199 let pool = reflector_bounds([0.0, 0.0, 0.0], [14.0, 0.0, 14.0]);
1200 let (mn, mx) = fold_world_bounds([crate_aabb, pool]).expect("finite bounds");
1201 assert_eq!((mn[0], mx[0]), (-14.0, 14.0));
1202 assert_eq!((mn[2], mx[2]), (-14.0, 14.0));
1203 }
1204
1205 #[test]
1206 fn seed_interior_probes_ignores_a_prop_sized_hollow() {
1207 let crate_tris = box_mesh_tris([-0.7, 0.6, -0.7], [0.7, 2.0, 0.7]);
1213 let probes = seed_interior_probes_tris([-0.8, 0.5, -0.8], [0.8, 2.1, 0.8], &crate_tris, 8);
1214 assert!(
1215 probes.is_empty(),
1216 "a prop-sized hollow is not a room: {probes:?}"
1217 );
1218
1219 let room_tris = box_mesh_tris([-2.5, 0.0, -2.5], [2.5, 3.0, 2.5]);
1222 let probes = seed_interior_probes_tris([-3.0, -0.5, -3.0], [3.0, 3.5, 3.0], &room_tris, 8);
1223 assert_eq!(
1224 probes.len(),
1225 1,
1226 "a standing-height room still earns a probe"
1227 );
1228 }
1229
1230 #[test]
1231 fn seed_interior_probes_ignores_an_open_scene() {
1232 let open = vec![
1235 ([-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]), ];
1239 let probes = seed_interior_probes([-20.0, -1.0, -20.0], [20.0, 8.0, 20.0], &open, 8);
1240 assert!(
1241 probes.is_empty(),
1242 "open scene seeds no interior probes: {probes:?}"
1243 );
1244 assert!(seed_interior_probes([0.0, 0.0, 0.0], [10.0, 5.0, 10.0], &[], 8).is_empty());
1246 }
1247
1248 #[test]
1249 fn auto_seed_places_a_room_probe_then_grid() {
1250 let room = box_room([0.0, 0.0, 0.0], [10.0, 6.0, 10.0]);
1253 let probes = auto_seed_probes([-12.0, -3.0, -12.0], [22.0, 9.0, 22.0], &room);
1254 assert!(!probes.is_empty() && probes.len() <= AUTO_SEED_BUDGET);
1255 let inside_room = probes.iter().any(|p| {
1256 let q = p.position;
1257 q[0] > 0.0 && q[0] < 10.0 && q[1] > 0.0 && q[1] < 6.0 && q[2] > 0.0 && q[2] < 10.0
1258 });
1259 assert!(
1260 inside_room,
1261 "auto-seed should drop a probe inside the room: {probes:?}"
1262 );
1263 }
1264
1265 fn box_mesh_tris(min: [f32; 3], max: [f32; 3]) -> Vec<[[f32; 3]; 3]> {
1268 let [x0, y0, z0] = min;
1269 let [x1, y1, z1] = max;
1270 let c = [
1271 [x0, y0, z0],
1272 [x1, y0, z0],
1273 [x1, y1, z0],
1274 [x0, y1, z0],
1275 [x0, y0, z1],
1276 [x1, y0, z1],
1277 [x1, y1, z1],
1278 [x0, y1, z1],
1279 ];
1280 let quads = [
1282 [0, 1, 2, 3], [4, 5, 6, 7], [0, 3, 7, 4], [1, 2, 6, 5], [0, 1, 5, 4], [3, 2, 6, 7], ];
1289 let mut tris = Vec::with_capacity(12);
1290 for q in quads {
1291 tris.push([c[q[0]], c[q[1]], c[q[2]]]);
1292 tris.push([c[q[0]], c[q[2]], c[q[3]]]);
1293 }
1294 tris
1295 }
1296
1297 #[test]
1298 fn tri_box_overlap_detects_intersection_and_separation() {
1299 let h = [0.5, 0.5, 0.5];
1300 let through = [[-1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1302 assert!(tri_box_overlap([0.0, 0.0, 0.0], h, &through));
1303 assert!(!tri_box_overlap([10.0, 0.0, 0.0], h, &through));
1305 let above = [[-1.0, 5.0, -1.0], [3.0, 5.0, -1.0], [0.0, 5.0, 3.0]];
1308 assert!(!tri_box_overlap([0.0, 0.0, 0.0], h, &above));
1309 let inside = [[-0.1, 0.0, 0.0], [0.1, 0.0, 0.0], [0.0, 0.1, 0.0]];
1311 assert!(tri_box_overlap([0.0, 0.0, 0.0], h, &inside));
1312 }
1313
1314 #[test]
1315 fn surface_voxels_leave_a_watertight_mesh_hollow() {
1316 let scene_min = [-3.0, -3.0, -3.0];
1320 let scene_max = [13.0, 9.0, 13.0];
1321 let room_aabb = vec![([0.0, 0.0, 0.0], [10.0, 6.0, 10.0])];
1322 let room_tris = box_mesh_tris([0.0, 0.0, 0.0], [10.0, 6.0, 10.0]);
1323
1324 let from_aabb = seed_interior_probes(scene_min, scene_max, &room_aabb, 8);
1326 assert!(
1327 from_aabb.is_empty(),
1328 "a watertight mesh's AABB hides its interior: {from_aabb:?}"
1329 );
1330
1331 let from_tris = seed_interior_probes_tris(scene_min, scene_max, &room_tris, 8);
1333 assert_eq!(from_tris.len(), 1, "the hollow interior earns one probe");
1334 let p = from_tris[0].position;
1335 assert!(
1336 p[0] > 0.0 && p[0] < 10.0 && p[1] > 0.0 && p[1] < 6.0 && p[2] > 0.0 && p[2] < 10.0,
1337 "probe {p:?} should sit inside the watertight room"
1338 );
1339
1340 let auto = auto_seed_probes_with_geometry(scene_min, scene_max, &room_aabb, &room_tris);
1342 assert!(
1343 auto.iter().any(|q| {
1344 let q = q.position;
1345 q[0] > 0.0 && q[0] < 10.0 && q[1] > 0.0 && q[1] < 6.0 && q[2] > 0.0 && q[2] < 10.0
1346 }),
1347 "auto-seed with geometry drops a probe inside the room: {auto:?}"
1348 );
1349 assert_eq!(
1351 auto_seed_probes_with_geometry(scene_min, scene_max, &room_aabb, &[]).len(),
1352 auto_seed_probes(scene_min, scene_max, &room_aabb).len(),
1353 );
1354 }
1355
1356 #[test]
1357 fn fold_world_bounds_unions_and_skips_nonfinite() {
1358 let boxes = [
1359 ([0.0, 0.0, 0.0], [1.0, 2.0, 1.0]),
1360 ([-3.0, 1.0, -1.0], [0.5, 4.0, 2.0]),
1361 ([f32::NAN, 0.0, 0.0], [1.0, 1.0, 1.0]), ];
1363 let (mn, mx) = fold_world_bounds(boxes).expect("non-empty");
1364 assert_eq!(mn, [-3.0, 0.0, -1.0]);
1365 assert_eq!(mx, [1.0, 4.0, 2.0]);
1366 assert!(fold_world_bounds(core::iter::empty()).is_none());
1367 }
1368
1369 #[test]
1370 fn face_centres_look_down_their_axis() {
1371 let eye = [0.0, 0.0, 0.0];
1373 for face in 0..6 {
1374 let vp = face_view_projection(eye, face, 0.05, 100.0);
1375 let d = cube_texel_dir(face, 0.0, 0.0);
1376 let (nx, ny, w) = project(vp, d);
1377 assert!(w > 0.0);
1378 assert!(
1379 nx.abs() < 1e-5 && ny.abs() < 1e-5,
1380 "face {face} centre off-origin"
1381 );
1382 }
1383 }
1384
1385 #[test]
1386 fn bake_queue_hands_out_indices_in_order() {
1387 let mut q = ProbeBakeQueue::new(3);
1388 assert!(q.pending());
1389 assert_eq!(q.take_next(), Some(0));
1390 assert_eq!(q.take_next(), Some(1));
1391 assert!(q.pending());
1392 assert_eq!(q.take_next(), Some(2));
1393 assert!(!q.pending());
1394 assert_eq!(q.take_next(), None);
1395 }
1396
1397 #[test]
1398 fn bake_queue_empty_is_never_pending() {
1399 let mut q = ProbeBakeQueue::new(0);
1400 assert!(!q.pending());
1401 assert_eq!(q.take_next(), None);
1402 }
1403
1404 #[test]
1405 fn bake_queue_abort_skips_the_remainder() {
1406 let mut q = ProbeBakeQueue::new(4);
1407 assert_eq!(q.take_next(), Some(0));
1408 q.abort();
1409 assert!(!q.pending());
1410 assert_eq!(q.take_next(), None);
1411 }
1412
1413 #[test]
1414 fn bake_action_idle_starts_only_when_pending_and_eligible() {
1415 assert_eq!(
1417 next_bake_action(BakePhase::Idle, false, false, true, true, false),
1418 BakeAction::StartNext
1419 );
1420 assert_eq!(
1422 next_bake_action(BakePhase::Idle, false, false, false, true, false),
1423 BakeAction::Idle
1424 );
1425 assert_eq!(
1428 next_bake_action(BakePhase::Idle, false, false, true, false, false),
1429 BakeAction::Idle
1430 );
1431 }
1432
1433 #[test]
1434 fn bake_action_rendering_submits_faces_before_waiting_for_completion() {
1435 assert_eq!(
1438 next_bake_action(BakePhase::Rendering, false, false, true, true, true),
1439 BakeAction::RenderFace
1440 );
1441 assert_eq!(
1443 next_bake_action(BakePhase::Rendering, false, false, true, true, false),
1444 BakeAction::Idle
1445 );
1446 assert_eq!(
1448 next_bake_action(BakePhase::Rendering, true, false, true, true, false),
1449 BakeAction::Readback
1450 );
1451 }
1452
1453 #[test]
1454 fn bake_action_converting_waits_for_offthread_payload() {
1455 assert_eq!(
1457 next_bake_action(BakePhase::Converting, true, false, true, true, false),
1458 BakeAction::Idle
1459 );
1460 assert_eq!(
1461 next_bake_action(BakePhase::Converting, true, true, false, true, false),
1462 BakeAction::Install
1463 );
1464 }
1465
1466 #[test]
1467 fn bake_action_never_starts_a_second_bake_while_one_is_in_flight() {
1468 for phase in [BakePhase::Rendering, BakePhase::Converting] {
1471 assert_ne!(
1472 next_bake_action(phase, false, false, true, true, false),
1473 BakeAction::StartNext
1474 );
1475 assert_ne!(
1476 next_bake_action(phase, false, false, true, true, true),
1477 BakeAction::StartNext
1478 );
1479 }
1480 }
1481}