concinnity_core/render/planar_reflection.rs
1//! Planar reflection math: mirror a camera across a world plane and oblique-clip
2//! the projection so geometry behind the plane never leaks into the reflection.
3//!
4//! Backend-agnostic and pure (mirrors gfx/reflection_probe.rs). A reflective flat
5//! surface (water, a mirror floor) renders the scene a second time from the
6//! camera reflected across its plane; the reflective surface then samples that
7//! render projectively. This module produces the matrices that pass needs.
8//!
9//! Conventions match [`crate::gfx::projection`]: column-major storage
10//! `m[col][row]`, a right-handed view looking down -z, and a perspective
11//! projection mapping depth to [0, 1] (Metal / D3D clip space). A plane is
12//! `[nx, ny, nz, d]` with `n` unit-length, satisfying `n . p + d = 0` for points
13//! on it; `n . p + d > 0` is the side the normal points toward.
14
15use crate::gfx::transform::{Mat4, mat4_inverse, mat4_mul};
16use crate::math::sqrt;
17use alloc::vec::Vec;
18
19type Vec4 = [f32; 4];
20
21/// The engine-wide capacity ceiling for distinct reflection planes: water
22/// surfaces and glass panes combined. Each plane is a full render-resolution MSAA scene
23/// re-render, so this bounds the per-frame planar cost and the reserved mirror
24/// target VRAM; reflectors past the active budget fall back to the box-projected
25/// probe cube. This is the CAPACITY every backend sizes its mirror targets / ICB
26/// slots / resolve SRVs against, so the three `planar::MAX_PLANAR_PLANES` alias it
27/// and stay in lockstep by construction. The per-frame budget passed to
28/// `assign_planar_slots` can be lower (scaled down under a quality preset / GPU
29/// tier) but never higher.
30pub const MAX_PLANAR_PLANES: usize = 4;
31
32fn mat_vec(m: Mat4, v: Vec4) -> Vec4 {
33 let mut out = [0.0f32; 4];
34 for row in 0..4 {
35 for k in 0..4 {
36 out[row] += m[k][row] * v[k];
37 }
38 }
39 out
40}
41
42fn transpose(m: Mat4) -> Mat4 {
43 let mut out = [[0.0f32; 4]; 4];
44 for col in 0..4 {
45 for row in 0..4 {
46 out[col][row] = m[row][col];
47 }
48 }
49 out
50}
51
52fn dot4(a: Vec4, b: Vec4) -> f32 {
53 a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]
54}
55
56// Normalise a plane so its normal is unit length (scaling d to match). A zero
57// normal is returned unchanged (degenerate, callers guard separately).
58pub(crate) fn normalize_plane(plane: Vec4) -> Vec4 {
59 let len = sqrt(plane[0] * plane[0] + plane[1] * plane[1] + plane[2] * plane[2]);
60 if len < 1e-12 {
61 return plane;
62 }
63 let inv = 1.0 / len;
64 [
65 plane[0] * inv,
66 plane[1] * inv,
67 plane[2] * inv,
68 plane[3] * inv,
69 ]
70}
71
72// Householder reflection of world points across `plane` (unit normal). A point
73// p maps to p - 2 (n.p + d) n; this 4x4 applies that to homogeneous points.
74pub(crate) fn reflection_matrix(plane: Vec4) -> Mat4 {
75 let [nx, ny, nz, d] = plane;
76 [
77 [1.0 - 2.0 * nx * nx, -2.0 * ny * nx, -2.0 * nz * nx, 0.0],
78 [-2.0 * nx * ny, 1.0 - 2.0 * ny * ny, -2.0 * nz * ny, 0.0],
79 [-2.0 * nx * nz, -2.0 * ny * nz, 1.0 - 2.0 * nz * nz, 0.0],
80 [-2.0 * nx * d, -2.0 * ny * d, -2.0 * nz * d, 1.0],
81 ]
82}
83
84// Reflect a single world point across the plane (the reflected camera eye).
85pub(crate) fn reflect_point(p: [f32; 3], plane: Vec4) -> [f32; 3] {
86 let dist = plane[0] * p[0] + plane[1] * p[1] + plane[2] * p[2] + plane[3];
87 [
88 p[0] - 2.0 * dist * plane[0],
89 p[1] - 2.0 * dist * plane[1],
90 p[2] - 2.0 * dist * plane[2],
91 ]
92}
93
94// The reflected view matrix: reflect a world point across the plane, then apply
95// the camera view. Equivalent to rendering the scene from the mirrored camera.
96pub(crate) fn reflected_view(view: Mat4, plane: Vec4) -> Mat4 {
97 mat4_mul(view, reflection_matrix(plane))
98}
99
100// Transform a world-space plane into a view space. Planes transform by the
101// inverse-transpose of the world->view matrix: plane_view = (V^-1)^T . plane.
102pub(crate) fn plane_in_view(plane_world: Vec4, view: Mat4) -> Vec4 {
103 mat_vec(transpose(mat4_inverse(view)), plane_world)
104}
105
106// Oblique near-plane clipping (Lengyel) for a [0, 1]-depth perspective matrix.
107// Replaces the projection's z (depth) row so the near clip plane coincides with
108// `clip_plane` (given in the projection's view space), clipping everything on the
109// negative side of that plane. The far plane is preserved by scaling against the
110// frustum corner the plane faces.
111//
112// Derivation for this depth convention: the near plane is `z_row . p = 0`, so the
113// new z-row is `alpha * C` for the clip plane C (any alpha keeps the near plane at
114// C). Picking the far frustum corner q = inv(P) . (sgn(Cx), sgn(Cy), 1, 1) and
115// requiring it to land on the far plane (ndc.z = 1, i.e. z_row.q = w_row.q = -q.z)
116// gives alpha = -q.z / (C . q). For this projection q has the closed form below
117// (q.z = -1), so alpha = 1 / (C . q).
118pub(crate) fn oblique_projection(proj: Mat4, clip_plane: Vec4) -> Mat4 {
119 let xs = proj[0][0];
120 let ys = proj[1][1];
121 let zs = proj[2][2]; // z-row's z component
122 let zs_near = proj[3][2]; // z-row's w component (= zs * near)
123 if xs.abs() < 1e-12 || ys.abs() < 1e-12 || zs_near.abs() < 1e-12 {
124 return proj;
125 }
126
127 let sgn = |v: f32| {
128 if v > 0.0 {
129 1.0
130 } else if v < 0.0 {
131 -1.0
132 } else {
133 0.0
134 }
135 };
136 // Back-projected far frustum corner toward the clip plane.
137 let q: Vec4 = [
138 sgn(clip_plane[0]) / xs,
139 sgn(clip_plane[1]) / ys,
140 -1.0,
141 (1.0 + zs) / zs_near,
142 ];
143 let denom = dot4(clip_plane, q);
144 if denom.abs() < 1e-12 {
145 return proj;
146 }
147 let alpha = 1.0 / denom;
148
149 let mut out = proj;
150 // Replace the z (depth) row: row index 2 across all four columns.
151 out[0][2] = alpha * clip_plane[0];
152 out[1][2] = alpha * clip_plane[1];
153 out[2][2] = alpha * clip_plane[2];
154 out[3][2] = alpha * clip_plane[3];
155 out
156}
157
158/// Flip a plane so its normal points toward `point` (the kept side faces the
159/// camera). The reflection matrix is sign-invariant, but the oblique near-plane
160/// clip is not: it keeps the +n side, so the normal must face the viewer or the
161/// mirror render clips the wrong half. A no-op when `point` already lies on the
162/// +n side, which is the horizontal-water-above-camera case, so water renders
163/// identically with or without this orientation.
164pub fn orient_plane_toward(plane: Vec4, point: [f32; 3]) -> Vec4 {
165 let signed = plane[0] * point[0] + plane[1] * point[1] + plane[2] * point[2] + plane[3];
166 if signed < 0.0 {
167 [-plane[0], -plane[1], -plane[2], -plane[3]]
168 } else {
169 plane
170 }
171}
172
173/// The result of grouping a list of reflection planes into a bounded number of
174/// distinct slots: `slots[i]` is the slot a plane maps to (`None` when the budget
175/// is exhausted by earlier distinct planes, i.e. it falls back to the probe cube),
176/// and `representatives` is the deduplicated plane per slot (`representatives.len()`
177/// is the number of mirror renders the frame needs).
178pub struct PlanarAssignment {
179 /// Per-reflector plane slot, `None` when the reflector fell back to a probe.
180 pub slots: Vec<Option<usize>>,
181 /// One representative plane per assigned slot.
182 pub representatives: Vec<Vec4>,
183}
184
185/// Group near-coplanar reflection planes so each distinct plane renders one mirror
186/// pass, capped at `max_slots`. Planes are matched sign-invariantly (a plane and
187/// its flip are the same surface): two planes share a slot when their unit normals
188/// are near-parallel and their offset along the normal matches. A plane coplanar
189/// with an already-assigned slot always reuses it (even past the budget); only a
190/// NEW distinct plane beyond `max_slots` overflows to `None`. Input order sets slot
191/// priority, so callers list higher-priority planes (e.g. water) first.
192pub fn assign_planar_slots(planes: &[Vec4], max_slots: usize) -> PlanarAssignment {
193 // ~2.6 degrees of normal divergence and 0.1 world units of offset still count
194 // as the same plane: tight enough to keep separate walls distinct, loose
195 // enough to merge co-planar panes authored with slight slop.
196 const NORMAL_DOT_EPS: f32 = 0.999;
197 const OFFSET_EPS: f32 = 0.1;
198
199 let mut representatives: Vec<Vec4> = Vec::new();
200 let mut slots: Vec<Option<usize>> = Vec::with_capacity(planes.len());
201 for &raw in planes {
202 let p = normalize_plane(raw);
203 let nlen = sqrt(p[0] * p[0] + p[1] * p[1] + p[2] * p[2]);
204 if nlen < 1e-6 {
205 // Degenerate normal: no usable plane, fall back to the probe cube.
206 slots.push(None);
207 continue;
208 }
209 let mut found = None;
210 for (i, r) in representatives.iter().enumerate() {
211 let d = p[0] * r[0] + p[1] * r[1] + p[2] * r[2];
212 if d.abs() >= NORMAL_DOT_EPS {
213 // Align the representative to p's sign, then the two are the same
214 // surface iff their plane constants match.
215 let rd_aligned = if d < 0.0 { -r[3] } else { r[3] };
216 if (p[3] - rd_aligned).abs() <= OFFSET_EPS {
217 found = Some(i);
218 break;
219 }
220 }
221 }
222 match found {
223 Some(i) => slots.push(Some(i)),
224 None => {
225 if representatives.len() < max_slots {
226 representatives.push(p);
227 slots.push(Some(representatives.len() - 1));
228 } else {
229 slots.push(None);
230 }
231 }
232 }
233 }
234 PlanarAssignment {
235 slots,
236 representatives,
237 }
238}
239
240/// Whether the transparent pass has to render its planar mirrors this frame.
241///
242/// Water prefers the mirror to the per-pixel ray trace: a water surface is a
243/// plane, so one mirrored scene render resolves it exactly, at a fraction of the
244/// cost of a ray per pixel, and the wave normal only has to perturb the lookup.
245/// A trace off the per-fragment wave normal is hypersensitive at grazing angles
246/// and lands on the probe / sky fallback wherever it misses, which reads as a
247/// chrome sheet rather than water. Glass keeps the trace (a pane shows what is
248/// genuinely behind it), so a world of glass alone still skips the re-render
249/// while ray tracing is live.
250///
251/// `has_targets` is whether any mirror target exists at all, `water_has_slot`
252/// whether a visible water surface holds one, and `rt_transparent_active`
253/// whether the pass would otherwise trace.
254pub fn planar_pass_needed(
255 has_targets: bool,
256 water_has_slot: bool,
257 rt_transparent_active: bool,
258) -> bool {
259 has_targets && (water_has_slot || !rt_transparent_active)
260}
261
262/// The matrices a planar reflection pass needs for one mirror plane.
263pub struct PlanarMatrices {
264 /// Reflected view matrix (world -> mirrored view).
265 pub view: Mat4,
266 /// Reflected view-projection with oblique near-plane clipping applied.
267 pub view_proj: Mat4,
268 /// The camera eye reflected across the plane (LOD / view-direction anchor).
269 pub eye: [f32; 3],
270}
271
272/// Build the mirror view + oblique-clipped view-projection + reflected eye for a
273/// camera (`view` / `proj` / `cam_pos`) reflecting across `plane_world`. The clip
274/// plane is nudged a hair below the surface (`clip_bias`, world units along the
275/// normal) so fragments exactly on the surface are not clipped by precision.
276pub fn planar_matrices(
277 view: Mat4,
278 proj: Mat4,
279 cam_pos: [f32; 3],
280 plane_world: Vec4,
281 clip_bias: f32,
282) -> PlanarMatrices {
283 let plane = normalize_plane(plane_world);
284 let r_view = reflected_view(view, plane);
285 // Push the clip plane slightly toward the kept (normal) side so geometry
286 // right at the waterline survives the near-plane test.
287 let clip_world = [plane[0], plane[1], plane[2], plane[3] + clip_bias];
288 let clip_view = plane_in_view(clip_world, r_view);
289 let r_proj = oblique_projection(proj, clip_view);
290 PlanarMatrices {
291 view: r_view,
292 view_proj: mat4_mul(r_proj, r_view),
293 eye: reflect_point(cam_pos, plane),
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300 use crate::gfx::projection::perspective_rh;
301
302 // Apply a column-major transform to a homogeneous point.
303 fn xform(m: Mat4, p: [f32; 4]) -> [f32; 4] {
304 mat_vec(m, p)
305 }
306
307 fn approx(a: f32, b: f32, eps: f32) -> bool {
308 (a - b).abs() <= eps
309 }
310
311 #[test]
312 fn reflection_is_an_involution() {
313 // Reflecting twice returns the original point (R * R = I).
314 let plane = normalize_plane([0.0, 1.0, 0.0, -2.0]); // y = 2
315 let p = [3.0, 5.0, -1.0];
316 let once = reflect_point(p, plane);
317 let twice = reflect_point(once, plane);
318 assert!(approx(twice[0], p[0], 1e-5));
319 assert!(approx(twice[1], p[1], 1e-5));
320 assert!(approx(twice[2], p[2], 1e-5));
321 }
322
323 #[test]
324 fn reflection_across_y_plane_flips_height_about_it() {
325 // y = 2 plane: a point at y=5 reflects to y=-1 (mirror about 2).
326 let plane = normalize_plane([0.0, 1.0, 0.0, -2.0]);
327 let r = reflect_point([3.0, 5.0, -1.0], plane);
328 assert!(approx(r[0], 3.0, 1e-5));
329 assert!(approx(r[1], -1.0, 1e-5));
330 assert!(approx(r[2], -1.0, 1e-5));
331 }
332
333 #[test]
334 fn reflection_matrix_matches_point_reflection() {
335 let plane = normalize_plane([0.2, 0.9, -0.3, 1.4]);
336 let m = reflection_matrix(plane);
337 let p = [1.3, -2.1, 0.7];
338 let via_matrix = xform(m, [p[0], p[1], p[2], 1.0]);
339 let via_point = reflect_point(p, plane);
340 for i in 0..3 {
341 assert!(approx(via_matrix[i], via_point[i], 1e-4), "component {i}");
342 }
343 assert!(approx(via_matrix[3], 1.0, 1e-5));
344 }
345
346 #[test]
347 fn inverse_round_trips() {
348 let m = perspective_rh(1.1, 1.7, 0.2, 80.0);
349 let id = mat4_mul(m, mat4_inverse(m));
350 for (c, col) in id.iter().enumerate() {
351 for (r, &val) in col.iter().enumerate() {
352 let expect = if c == r { 1.0 } else { 0.0 };
353 assert!(approx(val, expect, 1e-4), "[{c}][{r}]");
354 }
355 }
356 }
357
358 #[test]
359 fn oblique_clip_puts_the_plane_at_the_near_depth() {
360 // A view-space plane at z = -5 facing the camera (kept side is farther,
361 // z < -5). After oblique clipping the projection, a point ON the plane
362 // maps to ndc.z ~= 0, a point in front (far side) to ndc.z in (0, 1), and
363 // a point behind the plane to ndc.z < 0 (clipped).
364 let proj = perspective_rh(1.2, 1.0, 0.1, 100.0);
365 // Plane z = -5: n.p + d = 0 with kept side n.p + d > 0 toward -z (far).
366 // Choose C so the far/kept side is positive: C = (0,0,-1,-5) -> for
367 // p=(0,0,-50): -(-50)-5 = 45 > 0 (kept); p=(0,0,-2): 2-5 = -3 < 0 (clip).
368 let c = [0.0, 0.0, -1.0, -5.0];
369 let pobl = oblique_projection(proj, c);
370
371 let ndc_z = |z: f32| {
372 let clip = xform(pobl, [0.0, 0.0, z, 1.0]);
373 clip[2] / clip[3]
374 };
375 assert!(approx(ndc_z(-5.0), 0.0, 1e-3), "on-plane ndc.z");
376 let front = ndc_z(-50.0);
377 assert!(front > 0.0 && front < 1.0, "far side in [0,1]: {front}");
378 assert!(ndc_z(-2.0) < 0.0, "near side clipped");
379 }
380
381 #[test]
382 fn oblique_clip_preserves_x_and_y_projection() {
383 // Only the depth row changes; x/y of a projected point are untouched.
384 let proj = perspective_rh(1.0, 1.5, 0.1, 50.0);
385 let c = [0.0, 0.0, -1.0, -8.0];
386 let pobl = oblique_projection(proj, c);
387 let p = [2.0, 1.5, -20.0, 1.0];
388 let a = xform(proj, p);
389 let b = xform(pobl, p);
390 assert!(approx(a[0] / a[3], b[0] / b[3], 1e-5), "ndc.x");
391 assert!(approx(a[1] / a[3], b[1] / b[3], 1e-5), "ndc.y");
392 }
393
394 #[test]
395 fn planar_matrices_clip_below_the_water_plane() {
396 // A camera above a horizontal water plane (y = 0, normal up). The mirror
397 // pass must clip world geometry BELOW the plane (it would otherwise leak
398 // into the reflection). Verify a below-water point lands at ndc.z < 0 and
399 // an above-water point stays in [0, 1].
400 let plane = [0.0, 1.0, 0.0, 0.0]; // y = 0, normal +y (kept side: above)
401 // Simple camera at (0, 3, 6) looking toward -z and slightly down. Build a
402 // view that just translates (identity rotation is enough for the depth
403 // sign test since reflection + projection handle the rest).
404 let view = [
405 [1.0, 0.0, 0.0, 0.0],
406 [0.0, 1.0, 0.0, 0.0],
407 [0.0, 0.0, 1.0, 0.0],
408 [0.0, -3.0, -6.0, 1.0],
409 ];
410 let proj = perspective_rh(1.2, 1.6, 0.1, 100.0);
411 let m = planar_matrices(view, proj, [0.0, 3.0, 6.0], plane, 0.0);
412
413 let ndc_z = |p: [f32; 3]| {
414 let clip = xform(m.view_proj, [p[0], p[1], p[2], 1.0]);
415 clip[2] / clip[3]
416 };
417 // Above water, in front of the camera: visible (0..1).
418 let above = ndc_z([0.0, 2.0, -4.0]);
419 assert!(above > 0.0 && above < 1.0, "above-water visible: {above}");
420 // Below water, in front of the camera: clipped (ndc.z < 0).
421 let below = ndc_z([0.0, -2.0, -4.0]);
422 assert!(below < 0.0, "below-water clipped: {below}");
423 // The reflected eye sits below the plane (mirror of y = 3).
424 assert!(approx(m.eye[1], -3.0, 1e-5), "reflected eye height");
425 }
426
427 #[test]
428 fn orient_plane_faces_the_camera() {
429 // A vertical pane at z = -3 with normal pointing toward -z. A camera in
430 // front of it (at +z relative to the pane) must flip the normal so the
431 // kept (oblique-clip) side faces the viewer.
432 let plane = normalize_plane([0.0, 0.0, -1.0, -3.0]); // n.p + d = 0 -> z = -3
433 let cam = [0.0, 1.0, 0.0]; // in front of the pane (z = 0 > -3 side)
434 let oriented = orient_plane_toward(plane, cam);
435 let signed =
436 oriented[0] * cam[0] + oriented[1] * cam[1] + oriented[2] * cam[2] + oriented[3];
437 assert!(signed > 0.0, "camera must be on the +normal (kept) side");
438 // It flipped the original (which faced away from the camera).
439 assert!(approx(oriented[2], 1.0, 1e-5), "normal flipped toward +z");
440 }
441
442 #[test]
443 fn orient_plane_is_noop_for_water_above_camera() {
444 // Horizontal water plane y = 2, normal +y. A camera above it keeps the
445 // plane unchanged, so water renders identically with the orientation step.
446 let plane = normalize_plane([0.0, 1.0, 0.0, -2.0]);
447 let cam = [3.0, 5.0, -1.0]; // above the plane
448 let oriented = orient_plane_toward(plane, cam);
449 for i in 0..4 {
450 assert!(
451 approx(oriented[i], plane[i], 1e-6),
452 "component {i} unchanged"
453 );
454 }
455 }
456
457 #[test]
458 fn assign_slots_dedups_coplanar_and_caps_distinct() {
459 // Two coplanar panes (same wall), one distinct wall, plus a third distinct
460 // wall that overflows a budget of 2. The coplanar pair shares slot 0, the
461 // second wall takes slot 1, the third overflows to None.
462 let wall_a0 = [0.0, 0.0, 1.0, -3.0];
463 let wall_a1 = [0.0, 0.0, 1.0, -3.05]; // within OFFSET_EPS of a0
464 let wall_b = [1.0, 0.0, 0.0, -5.0];
465 let wall_c = [0.0, 1.0, 0.0, -1.0];
466 let a = assign_planar_slots(&[wall_a0, wall_a1, wall_b, wall_c], 2);
467 assert_eq!(a.representatives.len(), 2, "two slots allocated");
468 assert_eq!(a.slots[0], Some(0));
469 assert_eq!(a.slots[1], Some(0), "coplanar pane reuses slot 0");
470 assert_eq!(a.slots[2], Some(1));
471 assert_eq!(
472 a.slots[3], None,
473 "third distinct plane overflows the budget"
474 );
475 }
476
477 #[test]
478 fn assign_slots_is_sign_invariant() {
479 // A plane and its flip (opposite normal, opposite offset) are the same
480 // surface and must share a slot.
481 let front = [0.0, 0.0, 1.0, -3.0];
482 let back = [0.0, 0.0, -1.0, 3.0];
483 let a = assign_planar_slots(&[front, back], 4);
484 assert_eq!(a.representatives.len(), 1, "flip is the same surface");
485 assert_eq!(a.slots[0], Some(0));
486 assert_eq!(a.slots[1], Some(0));
487 }
488
489 #[test]
490 fn reflected_frustum_captures_geometry_behind_the_camera() {
491 // A vertical mirror at z = -5 (unit normal +z, facing the camera). The
492 // camera sits at the origin looking down -z, toward the mirror. An object
493 // BEHIND the camera (z = +3) is outside the main frustum, but its
494 // reflection is visible in the mirror -- so the reflected-frustum cull must
495 // capture it where the main-camera set would miss it (the V1 gap).
496 let plane = [0.0, 0.0, 1.0, 5.0]; // n.p + d = 0 -> z = -5
497 let view = [
498 [1.0, 0.0, 0.0, 0.0],
499 [0.0, 1.0, 0.0, 0.0],
500 [0.0, 0.0, 1.0, 0.0],
501 [0.0, 0.0, 0.0, 1.0],
502 ];
503 let proj = perspective_rh(1.2, 1.6, 0.1, 100.0);
504 let cam_pos = [0.0, 0.0, 0.0];
505 let m = planar_matrices(view, proj, cam_pos, plane, 0.0);
506
507 // One object behind the camera.
508 let bb_min = [-0.5, -0.5, 2.5];
509 let bb_max = [0.5, 0.5, 3.5];
510
511 // The main camera rejects it (behind the near plane).
512 let main_frustum = crate::gfx::frustum::Frustum::from_view_projection(proj);
513 assert!(
514 !main_frustum.intersects_aabb(bb_min, bb_max),
515 "object behind the camera must be outside the main frustum"
516 );
517
518 // The reflected frustum captures it. This is the frustum the GPU mirror
519 // cull tests each record against.
520 let reflected_frustum = crate::gfx::frustum::Frustum::from_view_projection(m.view_proj);
521 assert!(
522 reflected_frustum.intersects_aabb(bb_min, bb_max),
523 "object behind the camera must be visible in the reflection"
524 );
525 }
526
527 #[test]
528 fn assign_slots_overflow_still_reuses_existing_slot() {
529 // With a budget of 1, a second distinct plane overflows, but a later plane
530 // coplanar with slot 0 still maps to slot 0 (dedup precedes the cap).
531 let a = assign_planar_slots(
532 &[
533 [0.0, 0.0, 1.0, -3.0],
534 [1.0, 0.0, 0.0, -5.0], // overflow
535 [0.0, 0.0, 1.0, -3.0], // coplanar with slot 0
536 ],
537 1,
538 );
539 assert_eq!(a.representatives.len(), 1);
540 assert_eq!(a.slots[0], Some(0));
541 assert_eq!(a.slots[1], None);
542 assert_eq!(a.slots[2], Some(0));
543 }
544
545 #[test]
546 fn planar_runs_for_water_even_while_ray_tracing() {
547 // The whole point of the gate: a water surface holding a mirror slot keeps
548 // the re-render alive under a live trace.
549 assert!(planar_pass_needed(true, true, true));
550 // Glass alone under a live trace still skips it.
551 assert!(!planar_pass_needed(true, false, true));
552 // With no trace, any reflector needs the mirror.
553 assert!(planar_pass_needed(true, false, false));
554 // No mirror target, nothing to render.
555 assert!(!planar_pass_needed(false, true, true));
556 assert!(!planar_pass_needed(false, false, false));
557 }
558}