use crate::gfx::transform::{Mat4, mat4_inverse, mat4_mul};
use crate::math::sqrt;
use alloc::vec::Vec;
type Vec4 = [f32; 4];
pub const MAX_PLANAR_PLANES: usize = 4;
fn mat_vec(m: Mat4, v: Vec4) -> Vec4 {
let mut out = [0.0f32; 4];
for row in 0..4 {
for k in 0..4 {
out[row] += m[k][row] * v[k];
}
}
out
}
fn transpose(m: Mat4) -> Mat4 {
let mut out = [[0.0f32; 4]; 4];
for col in 0..4 {
for row in 0..4 {
out[col][row] = m[row][col];
}
}
out
}
fn dot4(a: Vec4, b: Vec4) -> f32 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]
}
pub(crate) fn normalize_plane(plane: Vec4) -> Vec4 {
let len = sqrt(plane[0] * plane[0] + plane[1] * plane[1] + plane[2] * plane[2]);
if len < 1e-12 {
return plane;
}
let inv = 1.0 / len;
[
plane[0] * inv,
plane[1] * inv,
plane[2] * inv,
plane[3] * inv,
]
}
pub(crate) fn reflection_matrix(plane: Vec4) -> Mat4 {
let [nx, ny, nz, d] = plane;
[
[1.0 - 2.0 * nx * nx, -2.0 * ny * nx, -2.0 * nz * nx, 0.0],
[-2.0 * nx * ny, 1.0 - 2.0 * ny * ny, -2.0 * nz * ny, 0.0],
[-2.0 * nx * nz, -2.0 * ny * nz, 1.0 - 2.0 * nz * nz, 0.0],
[-2.0 * nx * d, -2.0 * ny * d, -2.0 * nz * d, 1.0],
]
}
pub(crate) fn reflect_point(p: [f32; 3], plane: Vec4) -> [f32; 3] {
let dist = plane[0] * p[0] + plane[1] * p[1] + plane[2] * p[2] + plane[3];
[
p[0] - 2.0 * dist * plane[0],
p[1] - 2.0 * dist * plane[1],
p[2] - 2.0 * dist * plane[2],
]
}
pub(crate) fn reflected_view(view: Mat4, plane: Vec4) -> Mat4 {
mat4_mul(view, reflection_matrix(plane))
}
pub(crate) fn plane_in_view(plane_world: Vec4, view: Mat4) -> Vec4 {
mat_vec(transpose(mat4_inverse(view)), plane_world)
}
pub(crate) fn oblique_projection(proj: Mat4, clip_plane: Vec4) -> Mat4 {
let xs = proj[0][0];
let ys = proj[1][1];
let zs = proj[2][2]; let zs_near = proj[3][2]; if xs.abs() < 1e-12 || ys.abs() < 1e-12 || zs_near.abs() < 1e-12 {
return proj;
}
let sgn = |v: f32| {
if v > 0.0 {
1.0
} else if v < 0.0 {
-1.0
} else {
0.0
}
};
let q: Vec4 = [
sgn(clip_plane[0]) / xs,
sgn(clip_plane[1]) / ys,
-1.0,
(1.0 + zs) / zs_near,
];
let denom = dot4(clip_plane, q);
if denom.abs() < 1e-12 {
return proj;
}
let alpha = 1.0 / denom;
let mut out = proj;
out[0][2] = alpha * clip_plane[0];
out[1][2] = alpha * clip_plane[1];
out[2][2] = alpha * clip_plane[2];
out[3][2] = alpha * clip_plane[3];
out
}
pub fn orient_plane_toward(plane: Vec4, point: [f32; 3]) -> Vec4 {
let signed = plane[0] * point[0] + plane[1] * point[1] + plane[2] * point[2] + plane[3];
if signed < 0.0 {
[-plane[0], -plane[1], -plane[2], -plane[3]]
} else {
plane
}
}
pub struct PlanarAssignment {
pub slots: Vec<Option<usize>>,
pub representatives: Vec<Vec4>,
}
pub fn assign_planar_slots(planes: &[Vec4], max_slots: usize) -> PlanarAssignment {
const NORMAL_DOT_EPS: f32 = 0.999;
const OFFSET_EPS: f32 = 0.1;
let mut representatives: Vec<Vec4> = Vec::new();
let mut slots: Vec<Option<usize>> = Vec::with_capacity(planes.len());
for &raw in planes {
let p = normalize_plane(raw);
let nlen = sqrt(p[0] * p[0] + p[1] * p[1] + p[2] * p[2]);
if nlen < 1e-6 {
slots.push(None);
continue;
}
let mut found = None;
for (i, r) in representatives.iter().enumerate() {
let d = p[0] * r[0] + p[1] * r[1] + p[2] * r[2];
if d.abs() >= NORMAL_DOT_EPS {
let rd_aligned = if d < 0.0 { -r[3] } else { r[3] };
if (p[3] - rd_aligned).abs() <= OFFSET_EPS {
found = Some(i);
break;
}
}
}
match found {
Some(i) => slots.push(Some(i)),
None => {
if representatives.len() < max_slots {
representatives.push(p);
slots.push(Some(representatives.len() - 1));
} else {
slots.push(None);
}
}
}
}
PlanarAssignment {
slots,
representatives,
}
}
pub fn planar_pass_needed(
has_targets: bool,
water_has_slot: bool,
rt_transparent_active: bool,
) -> bool {
has_targets && (water_has_slot || !rt_transparent_active)
}
pub struct PlanarMatrices {
pub view: Mat4,
pub view_proj: Mat4,
pub eye: [f32; 3],
}
pub fn planar_matrices(
view: Mat4,
proj: Mat4,
cam_pos: [f32; 3],
plane_world: Vec4,
clip_bias: f32,
) -> PlanarMatrices {
let plane = normalize_plane(plane_world);
let r_view = reflected_view(view, plane);
let clip_world = [plane[0], plane[1], plane[2], plane[3] + clip_bias];
let clip_view = plane_in_view(clip_world, r_view);
let r_proj = oblique_projection(proj, clip_view);
PlanarMatrices {
view: r_view,
view_proj: mat4_mul(r_proj, r_view),
eye: reflect_point(cam_pos, plane),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gfx::projection::perspective_rh;
fn xform(m: Mat4, p: [f32; 4]) -> [f32; 4] {
mat_vec(m, p)
}
fn approx(a: f32, b: f32, eps: f32) -> bool {
(a - b).abs() <= eps
}
#[test]
fn reflection_is_an_involution() {
let plane = normalize_plane([0.0, 1.0, 0.0, -2.0]); let p = [3.0, 5.0, -1.0];
let once = reflect_point(p, plane);
let twice = reflect_point(once, plane);
assert!(approx(twice[0], p[0], 1e-5));
assert!(approx(twice[1], p[1], 1e-5));
assert!(approx(twice[2], p[2], 1e-5));
}
#[test]
fn reflection_across_y_plane_flips_height_about_it() {
let plane = normalize_plane([0.0, 1.0, 0.0, -2.0]);
let r = reflect_point([3.0, 5.0, -1.0], plane);
assert!(approx(r[0], 3.0, 1e-5));
assert!(approx(r[1], -1.0, 1e-5));
assert!(approx(r[2], -1.0, 1e-5));
}
#[test]
fn reflection_matrix_matches_point_reflection() {
let plane = normalize_plane([0.2, 0.9, -0.3, 1.4]);
let m = reflection_matrix(plane);
let p = [1.3, -2.1, 0.7];
let via_matrix = xform(m, [p[0], p[1], p[2], 1.0]);
let via_point = reflect_point(p, plane);
for i in 0..3 {
assert!(approx(via_matrix[i], via_point[i], 1e-4), "component {i}");
}
assert!(approx(via_matrix[3], 1.0, 1e-5));
}
#[test]
fn inverse_round_trips() {
let m = perspective_rh(1.1, 1.7, 0.2, 80.0);
let id = mat4_mul(m, mat4_inverse(m));
for (c, col) in id.iter().enumerate() {
for (r, &val) in col.iter().enumerate() {
let expect = if c == r { 1.0 } else { 0.0 };
assert!(approx(val, expect, 1e-4), "[{c}][{r}]");
}
}
}
#[test]
fn oblique_clip_puts_the_plane_at_the_near_depth() {
let proj = perspective_rh(1.2, 1.0, 0.1, 100.0);
let c = [0.0, 0.0, -1.0, -5.0];
let pobl = oblique_projection(proj, c);
let ndc_z = |z: f32| {
let clip = xform(pobl, [0.0, 0.0, z, 1.0]);
clip[2] / clip[3]
};
assert!(approx(ndc_z(-5.0), 0.0, 1e-3), "on-plane ndc.z");
let front = ndc_z(-50.0);
assert!(front > 0.0 && front < 1.0, "far side in [0,1]: {front}");
assert!(ndc_z(-2.0) < 0.0, "near side clipped");
}
#[test]
fn oblique_clip_preserves_x_and_y_projection() {
let proj = perspective_rh(1.0, 1.5, 0.1, 50.0);
let c = [0.0, 0.0, -1.0, -8.0];
let pobl = oblique_projection(proj, c);
let p = [2.0, 1.5, -20.0, 1.0];
let a = xform(proj, p);
let b = xform(pobl, p);
assert!(approx(a[0] / a[3], b[0] / b[3], 1e-5), "ndc.x");
assert!(approx(a[1] / a[3], b[1] / b[3], 1e-5), "ndc.y");
}
#[test]
fn planar_matrices_clip_below_the_water_plane() {
let plane = [0.0, 1.0, 0.0, 0.0]; let view = [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, -3.0, -6.0, 1.0],
];
let proj = perspective_rh(1.2, 1.6, 0.1, 100.0);
let m = planar_matrices(view, proj, [0.0, 3.0, 6.0], plane, 0.0);
let ndc_z = |p: [f32; 3]| {
let clip = xform(m.view_proj, [p[0], p[1], p[2], 1.0]);
clip[2] / clip[3]
};
let above = ndc_z([0.0, 2.0, -4.0]);
assert!(above > 0.0 && above < 1.0, "above-water visible: {above}");
let below = ndc_z([0.0, -2.0, -4.0]);
assert!(below < 0.0, "below-water clipped: {below}");
assert!(approx(m.eye[1], -3.0, 1e-5), "reflected eye height");
}
#[test]
fn orient_plane_faces_the_camera() {
let plane = normalize_plane([0.0, 0.0, -1.0, -3.0]); let cam = [0.0, 1.0, 0.0]; let oriented = orient_plane_toward(plane, cam);
let signed =
oriented[0] * cam[0] + oriented[1] * cam[1] + oriented[2] * cam[2] + oriented[3];
assert!(signed > 0.0, "camera must be on the +normal (kept) side");
assert!(approx(oriented[2], 1.0, 1e-5), "normal flipped toward +z");
}
#[test]
fn orient_plane_is_noop_for_water_above_camera() {
let plane = normalize_plane([0.0, 1.0, 0.0, -2.0]);
let cam = [3.0, 5.0, -1.0]; let oriented = orient_plane_toward(plane, cam);
for i in 0..4 {
assert!(
approx(oriented[i], plane[i], 1e-6),
"component {i} unchanged"
);
}
}
#[test]
fn assign_slots_dedups_coplanar_and_caps_distinct() {
let wall_a0 = [0.0, 0.0, 1.0, -3.0];
let wall_a1 = [0.0, 0.0, 1.0, -3.05]; let wall_b = [1.0, 0.0, 0.0, -5.0];
let wall_c = [0.0, 1.0, 0.0, -1.0];
let a = assign_planar_slots(&[wall_a0, wall_a1, wall_b, wall_c], 2);
assert_eq!(a.representatives.len(), 2, "two slots allocated");
assert_eq!(a.slots[0], Some(0));
assert_eq!(a.slots[1], Some(0), "coplanar pane reuses slot 0");
assert_eq!(a.slots[2], Some(1));
assert_eq!(
a.slots[3], None,
"third distinct plane overflows the budget"
);
}
#[test]
fn assign_slots_is_sign_invariant() {
let front = [0.0, 0.0, 1.0, -3.0];
let back = [0.0, 0.0, -1.0, 3.0];
let a = assign_planar_slots(&[front, back], 4);
assert_eq!(a.representatives.len(), 1, "flip is the same surface");
assert_eq!(a.slots[0], Some(0));
assert_eq!(a.slots[1], Some(0));
}
#[test]
fn reflected_frustum_captures_geometry_behind_the_camera() {
let plane = [0.0, 0.0, 1.0, 5.0]; let view = [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
];
let proj = perspective_rh(1.2, 1.6, 0.1, 100.0);
let cam_pos = [0.0, 0.0, 0.0];
let m = planar_matrices(view, proj, cam_pos, plane, 0.0);
let bb_min = [-0.5, -0.5, 2.5];
let bb_max = [0.5, 0.5, 3.5];
let main_frustum = crate::gfx::frustum::Frustum::from_view_projection(proj);
assert!(
!main_frustum.intersects_aabb(bb_min, bb_max),
"object behind the camera must be outside the main frustum"
);
let reflected_frustum = crate::gfx::frustum::Frustum::from_view_projection(m.view_proj);
assert!(
reflected_frustum.intersects_aabb(bb_min, bb_max),
"object behind the camera must be visible in the reflection"
);
}
#[test]
fn assign_slots_overflow_still_reuses_existing_slot() {
let a = assign_planar_slots(
&[
[0.0, 0.0, 1.0, -3.0],
[1.0, 0.0, 0.0, -5.0], [0.0, 0.0, 1.0, -3.0], ],
1,
);
assert_eq!(a.representatives.len(), 1);
assert_eq!(a.slots[0], Some(0));
assert_eq!(a.slots[1], None);
assert_eq!(a.slots[2], Some(0));
}
#[test]
fn planar_runs_for_water_even_while_ray_tracing() {
assert!(planar_pass_needed(true, true, true));
assert!(!planar_pass_needed(true, false, true));
assert!(planar_pass_needed(true, false, false));
assert!(!planar_pass_needed(false, true, true));
assert!(!planar_pass_needed(false, false, false));
}
}