Skip to main content

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}