use glam::{Mat4, Vec3};
use super::camera::{Projected, V3};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Projection {
Ortho { px_per_unit: [f32; 2] },
Perspective { fov_y: f32 },
}
impl Projection {
#[inline]
pub fn is_ortho(&self) -> bool {
matches!(self, Projection::Ortho { .. })
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Terrain {
Flat,
Heightfield,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct PostFx {
pub ssao: bool,
pub csm: bool,
pub bloom: bool,
pub fxaa: bool,
}
impl PostFx {
pub const ALL: Self = Self { ssao: true, csm: true, bloom: true, fxaa: true };
pub const NONE: Self = Self { ssao: false, csm: false, bloom: false, fxaa: false };
#[inline]
pub fn any(&self) -> bool {
self.ssao || self.csm || self.bloom || self.fxaa
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GroundFrame2d {
pub rot: [f64; 4],
pub foreshorten: f64,
}
impl GroundFrame2d {
pub fn from_azimuth_tilt_rad(azimuth: f64, tilt: f64) -> Self {
let (sa, ca) = azimuth.sin_cos();
Self { rot: [ca, -sa, sa, ca], foreshorten: tilt.cos() }
}
#[inline]
pub fn apply(&self, dx: f64, dz: f64) -> (f64, f64) {
let rx = self.rot[0] * dx + self.rot[1] * dz;
let ry = self.rot[2] * dx + self.rot[3] * dz;
(rx, ry * self.foreshorten)
}
#[inline]
pub fn unapply(&self, rx: f64, ry: f64) -> (f64, f64) {
let ry = if self.foreshorten.abs() < f64::EPSILON { ry } else { ry / self.foreshorten };
let dx = self.rot[0] * rx + self.rot[2] * ry;
let dz = self.rot[1] * rx + self.rot[3] * ry;
(dx, dz)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct View {
pub target: Vec3,
pub azimuth: f32,
pub tilt: f32,
pub roll: f32,
pub distance: f32,
pub projection: Projection,
pub terrain: Terrain,
pub post: PostFx,
pub near_override: Option<f32>,
}
impl View {
pub const NEAR_PLANE: f32 = 0.02;
const FAR_PAD: f32 = 4.0;
pub const FAR_MIN_DISTANCE: f32 = 2.0;
pub fn ortho_2d(target: Vec3, px_per_unit: [f32; 2]) -> Self {
Self {
target: Vec3::new(target.x, 0.0, target.z),
azimuth: 0.0,
roll: 0.0,
tilt: 0.0,
distance: 1.0,
projection: Projection::Ortho { px_per_unit },
terrain: Terrain::Flat,
post: PostFx::NONE,
near_override: None,
}
}
pub fn perspective_3d(target: Vec3, azimuth: f32, tilt: f32, distance: f32, fov_y: f32) -> Self {
Self {
target,
azimuth,
tilt,
roll: 0.0,
distance,
projection: Projection::Perspective { fov_y },
terrain: Terrain::Heightfield,
post: PostFx::ALL,
near_override: None,
}
}
#[must_use]
pub fn with_roll(mut self, roll: f32) -> Self {
self.roll = roll;
self
}
#[must_use]
pub fn with_near(mut self, near: Option<f32>) -> Self {
self.near_override = near;
self
}
#[must_use]
pub fn near_clip(&self) -> f32 {
self.near_override.unwrap_or(Self::NEAR_PLANE)
}
pub fn from_orbit(target: V3, azimuth: f32, elevation: f32, distance: f32, fov_y: f32) -> Self {
Self::perspective_3d(
Vec3::new(target.x, target.y, target.z),
azimuth,
std::f32::consts::FRAC_PI_2 - elevation,
distance,
fov_y,
)
}
pub fn ortho_2d_map(ref_xy: [f32; 2], px_per_unit: [f32; 2]) -> Self {
Self::ortho_2d(Vec3::new(ref_xy[0], 0.0, ref_xy[1]), px_per_unit)
}
#[inline]
pub fn project_ground(&self, xy: [f32; 2], centre: (f32, f32), half_h: f32) -> Projected {
self.project_world(Vec3::new(xy[0], 0.0, xy[1]), centre, half_h)
}
#[inline]
pub fn unproject_ground(&self, px: (f32, f32), centre: (f32, f32), half_h: f32) -> Option<[f32; 2]> {
self.unproject_ground_px(px, centre, half_h).map(|w| [w.x, w.z])
}
pub fn ground_frame_2d(&self) -> GroundFrame2d {
debug_assert!(self.projection.is_ortho(), "a ground frame needs an orthographic view");
GroundFrame2d::from_azimuth_tilt_rad(self.azimuth as f64, self.tilt as f64)
}
pub fn ground_frame_2d_compass_deg(compass_bearing_deg: f64, tilt_deg: f64) -> GroundFrame2d {
GroundFrame2d::from_azimuth_tilt_rad(-compass_bearing_deg.to_radians(), tilt_deg.to_radians())
}
pub fn is_constrained_2d(&self) -> bool {
self.tilt == 0.0
&& self.azimuth == 0.0
&& self.projection.is_ortho()
&& self.terrain == Terrain::Flat
}
pub fn constrain_2d(&mut self, half_h: f32) {
let px_per_unit = match self.projection {
Projection::Ortho { px_per_unit } => px_per_unit,
Projection::Perspective { fov_y } => {
let s = (half_h / (fov_y * 0.5).tan()) / self.distance.max(1e-6);
[s, s]
}
};
self.tilt = 0.0;
self.azimuth = 0.0;
self.target.y = 0.0;
self.projection = Projection::Ortho { px_per_unit };
self.terrain = Terrain::Flat;
self.post = PostFx::NONE;
}
#[inline]
pub fn terrain_height(&self, sampled: f32) -> f32 {
match self.terrain {
Terrain::Flat => 0.0,
Terrain::Heightfield => sampled,
}
}
pub fn basis(&self) -> (Vec3, Vec3, Vec3) {
let (sb, cb) = self.azimuth.sin_cos();
let (st, ct) = self.tilt.sin_cos();
let right = Vec3::new(cb, 0.0, -sb);
let up = Vec3::new(-ct * sb, st, -ct * cb);
let fwd = Vec3::new(-st * sb, -ct, -st * cb);
if self.roll == 0.0 {
return (fwd, right, up);
}
let (sr, cr) = self.roll.sin_cos();
(
fwd,
Vec3::new(
right.x * cr + up.x * sr,
right.y * cr + up.y * sr,
right.z * cr + up.z * sr,
),
Vec3::new(
up.x * cr - right.x * sr,
up.y * cr - right.y * sr,
up.z * cr - right.z * sr,
),
)
}
pub fn eye(&self) -> Vec3 {
let (fwd, _, _) = self.basis();
self.target - fwd * self.distance
}
pub fn far_plane(&self) -> f32 {
self.distance.max(Self::FAR_MIN_DISTANCE) + Self::FAR_PAD
}
pub fn view_space(&self, p: Vec3) -> Vec3 {
let (fwd, right, up) = self.basis();
let rel = p - self.eye();
Vec3::new(rel.dot(right), rel.dot(up), rel.dot(fwd))
}
pub fn project_px(&self, c: Vec3, centre: (f32, f32), half_h: f32) -> Projected {
match self.projection {
Projection::Ortho { px_per_unit } => {
let visible = c.z > 0.0;
Projected {
x: centre.0 + c.x * px_per_unit[0],
y: centre.1 - c.y * px_per_unit[1],
depth: c.z,
visible,
}
}
Projection::Perspective { fov_y } => {
if c.z <= self.near_clip() || c.z >= self.far_plane() {
return Projected { x: centre.0, y: centre.1, depth: c.z, visible: false };
}
let focal = half_h / (fov_y * 0.5).tan();
Projected {
x: centre.0 + (c.x / c.z) * focal,
y: centre.1 - (c.y / c.z) * focal,
depth: c.z,
visible: true,
}
}
}
}
pub fn project_world(&self, p: Vec3, centre: (f32, f32), half_h: f32) -> Projected {
self.project_px(self.view_space(p), centre, half_h)
}
pub fn unproject_ground_px(&self, px: (f32, f32), centre: (f32, f32), half_h: f32) -> Option<Vec3> {
match self.projection {
Projection::Ortho { px_per_unit } => {
let (dx, dz) = self.ground_frame_2d().unapply(
f64::from((px.0 - centre.0) / px_per_unit[0]),
f64::from((px.1 - centre.1) / px_per_unit[1]),
);
Some(self.target + Vec3::new(dx as f32, 0.0, dz as f32))
}
Projection::Perspective { .. } => self.unproject_plane_px(px, centre, half_h, self.target.y),
}
}
pub fn unproject_plane_px(
&self,
px: (f32, f32),
centre: (f32, f32),
half_h: f32,
plane_y: f32,
) -> Option<Vec3> {
let Projection::Perspective { fov_y } = self.projection else {
return None;
};
let (fwd, right, up) = self.basis();
let focal = half_h / (fov_y * 0.5).tan();
let dir = fwd + right * ((px.0 - centre.0) / focal) + up * (-(px.1 - centre.1) / focal);
let eye = self.eye();
let denom = dir.y;
if denom.abs() < 1e-9 {
return None; }
let t = (plane_y - eye.y) / denom;
if t <= 0.0 {
return None; }
Some(eye + dir * t)
}
pub fn view_proj(&self, aspect: f32) -> Mat4 {
match self.projection {
Projection::Perspective { fov_y } => {
let f = 1.0 / (fov_y * 0.5).tan();
self.compose(f / aspect.max(1e-6), f, true, (0.0, 0.0))
}
Projection::Ortho { px_per_unit } => {
let sy = px_per_unit[1];
let sx = px_per_unit[0] / aspect.max(1e-6);
self.compose(sx, sy, false, (0.0, 0.0))
}
}
}
pub fn view_proj_px(&self, centre_px: (f32, f32), viewport: (f32, f32)) -> Mat4 {
let (vw, vh) = (viewport.0.max(1e-6), viewport.1.max(1e-6));
let half_w = vw * 0.5;
let half_h = vh * 0.5;
let off = ((centre_px.0 - half_w) / half_w, -(centre_px.1 - half_h) / half_h);
match self.projection {
Projection::Perspective { fov_y } => {
let focal = half_h / (fov_y * 0.5).tan();
self.compose(focal / half_w, focal / half_h, true, off)
}
Projection::Ortho { px_per_unit } => {
self.compose(px_per_unit[0] / half_w, px_per_unit[1] / half_h, false, off)
}
}
}
fn compose(&self, sx: f32, sy: f32, divide: bool, off: (f32, f32)) -> Mat4 {
let (fwd, right, up) = self.basis();
let eye = self.eye();
let (tx, ty, tz) = (-right.dot(eye), -up.dot(eye), -fwd.dot(eye));
let near = if divide {
self.near_override.unwrap_or_else(|| (self.distance - 2.0).max(0.05))
} else {
0.0
};
let far = self.far_plane();
let a = far / (far - near);
let b = -far * near / (far - near);
let (wx, wy, wz, ww) = if divide { (fwd.x, fwd.y, fwd.z, tz) } else { (0.0, 0.0, 0.0, 1.0) };
Mat4::from_cols_array(&[
sx * right.x + off.0 * wx,
sy * up.x + off.1 * wx,
a * fwd.x,
wx,
sx * right.y + off.0 * wy,
sy * up.y + off.1 * wy,
a * fwd.y,
wy,
sx * right.z + off.0 * wz,
sy * up.z + off.1 * wz,
a * fwd.z,
wz,
sx * tx + off.0 * ww,
sy * ty + off.1 * ww,
a * tz + b,
ww,
])
}
pub fn tilt_deg(&self) -> f32 {
self.tilt.to_degrees()
}
pub fn azimuth_deg(&self) -> f32 {
Self::wrap360(self.azimuth.to_degrees())
}
pub fn compass_bearing_deg(&self) -> f32 {
Self::wrap360(-self.azimuth.to_degrees())
}
pub fn set_compass_bearing_deg(&mut self, deg: f32) {
self.azimuth = -deg.to_radians();
}
pub fn perspective_3d_compass(
target: Vec3,
compass_bearing_deg: f32,
tilt_deg: f32,
distance: f32,
fov_y: f32,
) -> Self {
Self::perspective_3d(
target,
-compass_bearing_deg.to_radians(),
tilt_deg.to_radians(),
distance,
fov_y,
)
}
pub fn ortho_2d_compass(
target_xy: [f32; 2],
px_per_unit: [f32; 2],
compass_bearing_deg: f32,
tilt_deg: f32,
) -> Self {
let mut v = Self::ortho_2d_map(target_xy, px_per_unit);
v.set_compass_bearing_deg(compass_bearing_deg);
v.tilt = tilt_deg.to_radians();
v
}
#[inline]
fn wrap360(d: f32) -> f32 {
let d = d % 360.0;
if d < 0.0 { d + 360.0 } else { d }
}
pub fn elevation(&self) -> f32 {
std::f32::consts::FRAC_PI_2 - self.tilt
}
}
#[cfg(test)]
mod tests {
use super::*;
fn orbit_reference_basis(azimuth: f32, elevation: f32) -> (Vec3, Vec3, Vec3) {
let (se, ce) = elevation.sin_cos();
let (sa, ca) = azimuth.sin_cos();
let fwd = -Vec3::new(ce * sa, se, ce * ca).normalize();
let world_up = Vec3::new(0.0, 1.0, 0.0);
let right = fwd.cross(world_up).normalize();
let up = right.cross(fwd).normalize();
(fwd, right, up)
}
#[test]
fn closed_form_frame_is_the_orbit_cross_product_frame() {
let poses = [
(37.0f32, 12.0f32),
(-115.0, 62.0),
(223.0, 85.0),
(91.0, 3.5),
(180.0, 45.0),
(-44.0, 89.0),
];
let mut worst = 0.0f32;
for (bearing_deg, tilt_deg) in poses {
let b = bearing_deg.to_radians();
let t = tilt_deg.to_radians();
let v = View::perspective_3d(Vec3::ZERO, b, t, 5.0, 50f32.to_radians());
let (fwd, right, up) = v.basis();
let (rf, rr, ru) = orbit_reference_basis(b, std::f32::consts::FRAC_PI_2 - t);
for (got, want, name) in
[(fwd, rf, "fwd"), (right, rr, "right"), (up, ru, "up")]
{
let d = (got - want).length();
worst = worst.max(d);
assert!(
d < 1e-6,
"{name} drifted at bearing {bearing_deg}° tilt {tilt_deg}°: \
closed form {got:?} vs orbit reference {want:?} (|Δ| = {d:e})"
);
}
assert!((fwd.length() - 1.0).abs() < 1e-6, "fwd unit");
assert!(fwd.dot(right).abs() < 1e-6 && fwd.dot(up).abs() < 1e-6, "orthogonal");
assert!(right.dot(up).abs() < 1e-6, "orthogonal");
}
let a = View::perspective_3d(Vec3::ZERO, 0.6, 0.3, 5.0, 1.0).basis().0;
let c = View::perspective_3d(Vec3::ZERO, 2.4, 1.1, 5.0, 1.0).basis().0;
assert!((a - c).length() > 0.5, "the test poses must not all give one frame");
assert!(worst > 0.0, "reference and closed form are separate computations");
}
#[test]
fn top_down_lookat_basis_silently_mirrors_east_and_the_closed_form_does_not() {
const EAST: Vec3 = Vec3::new(1.0, 0.0, 0.0);
let quarter = std::f32::consts::FRAC_PI_2;
let (_, r_at, _) = orbit_reference_basis(0.0, quarter);
assert!(r_at.is_finite(), "it does not NaN — that is the whole problem");
assert!((r_at.length() - 1.0).abs() < 1e-6, "and it is unit length: {r_at:?}");
assert!(
r_at.dot(EAST) < -0.5,
"the look-at basis must be MIRRORED at exact top-down, got {r_at:?}"
);
let (_, r_below, _) = orbit_reference_basis(0.0, quarter - 1e-7);
let (_, r_above, _) = orbit_reference_basis(0.0, quarter + 1e-7);
assert!(
r_below.dot(r_above) < -0.5,
"1e-7 rad across top-down must flip the look-at east axis: {r_below:?} vs {r_above:?}"
);
let v = View::ortho_2d(Vec3::ZERO, [1.0, 1.0]);
let (fwd, right, up) = v.basis();
assert_eq!(fwd, Vec3::new(0.0, -1.0, 0.0), "straight down");
assert_eq!(right, EAST, "east → screen +x");
assert_eq!(up, Vec3::new(0.0, 0.0, -1.0), "north → screen up");
let lo = View::perspective_3d(Vec3::ZERO, 0.0, -1e-7, 5.0, 1.0).basis().1;
let hi = View::perspective_3d(Vec3::ZERO, 0.0, 1e-7, 5.0, 1.0).basis().1;
assert!(
lo.dot(hi) > 0.999_999 && lo.dot(EAST) > 0.999_999,
"the closed form must not flip across the constraint: {lo:?} vs {hi:?}"
);
}
#[test]
fn ortho_2d_is_bit_exactly_the_map_affine() {
let (zx, zy) = (4096.0f32, 4096.0f32);
let (rx, ry) = (0.361_25f32, 0.284_75f32);
let centre = (517.5f32, 388.25f32);
let v = View::ortho_2d(Vec3::new(rx, 0.0, ry), [zx, zy]);
for pos in [[0.361_5f32, 0.284_2], [0.0, 0.0], [1.0, -0.5], [rx, ry]] {
let p = v.project_world(Vec3::new(pos[0], 0.0, pos[1]), centre, 300.0);
let want_x = (pos[0] - rx) * zx + centre.0;
let want_y = (pos[1] - ry) * zy + centre.1;
assert_eq!(p.x.to_bits(), want_x.to_bits(), "x bit-exact for {pos:?}");
assert_eq!(p.y.to_bits(), want_y.to_bits(), "y bit-exact for {pos:?}");
}
let far = v.project_world(Vec3::new(rx + 0.01, 0.0, ry), centre, 300.0);
assert!(
(far.x - centre.0 - 40.96).abs() < 1e-3,
"0.01 Mercator at zoom 4096 must move 40.96 px, moved {}",
far.x - centre.0
);
}
#[test]
fn ortho_ignores_distance_and_perspective_does_not() {
let p = Vec3::new(0.25, 0.0, 0.1);
let mut a = View::ortho_2d(Vec3::ZERO, [1000.0, 1000.0]);
let near = a.project_world(p, (0.0, 0.0), 300.0).x;
a.distance = 97.0;
let far = a.project_world(p, (0.0, 0.0), 300.0).x;
assert_eq!(near.to_bits(), far.to_bits(), "ortho scale is distance-invariant");
let mut q = View::perspective_3d(Vec3::ZERO, 0.0, 0.9, 3.0, 50f32.to_radians());
let s1 = q.project_world(p, (0.0, 0.0), 300.0);
q.distance = 6.0;
let s2 = q.project_world(p, (0.0, 0.0), 300.0);
assert!(s1.visible && s2.visible, "both poses see the point");
assert!(
(s1.x - s2.x).abs() > 1.0,
"perspective MUST foreshorten with distance ({} vs {})",
s1.x,
s2.x
);
}
#[test]
fn flat_terrain_discards_the_heightfield_sample() {
let flat = View::ortho_2d(Vec3::ZERO, [1.0, 1.0]);
let hf = View::perspective_3d(Vec3::ZERO, 0.0, 0.7, 3.0, 1.0);
assert_eq!(flat.terrain_height(37.5), 0.0, "Flat pins Z to 0");
assert_eq!(hf.terrain_height(37.5), 37.5, "Heightfield passes Z through");
assert_eq!(flat.terrain_height(-9.25), 0.0);
}
#[test]
fn the_2d_constraint_predicate_rejects_each_single_violation() {
let base = View::ortho_2d(Vec3::new(0.3, 0.0, 0.2), [512.0, 512.0]);
assert!(base.is_constrained_2d(), "the constructor is constrained");
let mut a = base;
a.tilt = 0.001;
assert!(!a.is_constrained_2d(), "non-zero tilt must fail");
let mut b = base;
b.azimuth = 0.001;
assert!(!b.is_constrained_2d(), "non-zero azimuth must fail");
let mut c = base;
c.projection = Projection::Perspective { fov_y: 0.9 };
assert!(!c.is_constrained_2d(), "perspective must fail");
let mut d = base;
d.terrain = Terrain::Heightfield;
assert!(!d.is_constrained_2d(), "heightfield must fail");
}
#[test]
fn constrain_2d_flattens_a_non_trivial_3d_view() {
let mut v = View::perspective_3d(
Vec3::new(1.5, 3.0, -2.5),
127f32.to_radians(),
58f32.to_radians(),
4.0,
50f32.to_radians(),
);
assert!(!v.is_constrained_2d(), "starts unconstrained");
v.constrain_2d(300.0);
assert!(v.is_constrained_2d(), "collapsed onto the constraint surface");
assert_eq!(v.target.y, 0.0, "ground plane");
assert!(!v.post.any(), "post-FX bypassed in 2D");
let want = (300.0 / (50f32.to_radians() * 0.5).tan()) / 4.0;
match v.projection {
Projection::Ortho { px_per_unit } => {
assert!((px_per_unit[0] - want).abs() < 1e-3, "{:?} vs {want}", px_per_unit)
}
_ => panic!("must be ortho"),
}
}
#[test]
fn view_proj_px_agrees_with_the_cpu_projection_in_both_modes() {
let viewport = (800.0f32, 600.0f32);
let cases = [
View::ortho_2d(Vec3::new(0.36, 0.0, 0.28), [1024.0, 1024.0]),
View::perspective_3d(
Vec3::new(0.2, 0.0, -0.4),
71f32.to_radians(),
49f32.to_radians(),
3.0,
50f32.to_radians(),
),
];
let centre = (417.0f32, 271.0f32);
for v in cases {
let m = v.view_proj_px(centre, viewport);
let probes = [
Vec3::new(0.30, 0.0, 0.25),
Vec3::new(0.40, 0.0, 0.31),
Vec3::new(0.36, 0.0, 0.28),
];
let mut moved = 0;
for p in probes {
let cpu = v.project_px(v.view_space(p), centre, viewport.1 * 0.5);
if !cpu.visible {
continue;
}
let clip = m * p.extend(1.0);
assert!(clip.w.abs() > 1e-9, "w must be non-degenerate");
let ndc = (clip.x / clip.w, clip.y / clip.w);
let px = (
(ndc.0 * 0.5 + 0.5) * viewport.0,
(0.5 - ndc.1 * 0.5) * viewport.1,
);
assert!(
(px.0 - cpu.x).abs() < 0.01 && (px.1 - cpu.y).abs() < 0.01,
"matrix {px:?} vs cpu ({}, {}) for {p:?} in {:?}",
cpu.x,
cpu.y,
v.projection
);
if (px.0 - centre.0).abs() > 1.0 {
moved += 1;
}
}
assert!(moved > 0, "at least one probe must land off the pane centre");
}
}
#[test]
fn the_ground_frame_must_not_fold_the_foreshorten_into_the_rotation() {
let mut state = 0x2545_F491_4F6C_DD1Du64;
let mut next = move || {
state ^= state >> 12;
state ^= state << 25;
state ^= state >> 27;
((state.wrapping_mul(0x2545_F491_4F6C_DD1D) >> 11) as f64) / ((1u64 << 53) as f64)
};
let mut checked = 0u32;
let mut folded_differs = 0u32;
for _ in 0..20_000 {
let bearing = next() * 720.0 - 360.0;
let tilt = next() * 89.0;
let dx = next() * 10_000.0 - 5_000.0;
let dz = next() * 10_000.0 - 5_000.0;
let f = View::ground_frame_2d_compass_deg(bearing, tilt);
let (_, got_y) = f.apply(dx, dz);
let separated = (f.rot[2] * dx + f.rot[3] * dz) * f.foreshorten;
assert_eq!(
got_y.to_bits(),
separated.to_bits(),
"apply must rotate then foreshorten, at bearing {bearing} tilt {tilt}"
);
let ct = f.foreshorten;
let folded = (ct * f.rot[2]) * dx + (ct * f.rot[3]) * dz;
if folded.to_bits() != separated.to_bits() {
folded_differs += 1;
}
checked += 1;
}
let pct = 100.0 * f64::from(folded_differs) / f64::from(checked);
println!("[item6] folded != separated in {folded_differs}/{checked} = {pct:.1}% of draws");
assert_eq!(checked, 20_000, "the sweep must have run");
assert!(
folded_differs > 1_000,
"the folded form must be REACHABLY different or this guard is hollow \
(differed in only {folded_differs} of {checked})"
);
}
#[test]
fn unproject_is_the_inverse_of_project_in_both_modes() {
let centre = (417.0f32, 271.0f32);
let half_h = 300.0f32;
let o = View::ortho_2d(Vec3::new(0.361_25, 0.0, 0.284_75), [4096.0, 4096.0]);
for px in [(300.0f32, 200.0f32), (700.0, 512.0), (centre.0, centre.1)] {
let w = o.unproject_ground_px(px, centre, half_h).expect("ortho always hits");
let back = o.project_world(w, centre, half_h);
assert_eq!(back.x.to_bits(), px.0.to_bits(), "ortho x round-trip for {px:?}");
assert_eq!(back.y.to_bits(), px.1.to_bits(), "ortho y round-trip for {px:?}");
}
for &(bearing, tilt) in &[(0.0f32, 50.0f32), (31.0, 50.0), (137.0, 75.0), (-44.0, 12.5), (73.0, 0.0)] {
let o = View::ortho_2d_compass([0.361_25, 0.284_75], [4096.0, 2731.0], bearing, tilt);
for px in [(300.0f32, 200.0f32), (700.0, 512.0), (120.0, 890.0), (centre.0, centre.1)] {
let w = o.unproject_ground_px(px, centre, half_h).expect("ortho always hits");
assert!(w.y.abs() < 1e-6, "a tilted ortho hit must be ON the ground plane, got y={}", w.y);
let back = o.project_world(w, centre, half_h);
assert!(
(back.x - px.0).abs() < 2e-3 && (back.y - px.1).abs() < 2e-3,
"ortho round-trip at bearing {bearing} tilt {tilt} for {px:?}: got ({}, {})",
back.x,
back.y
);
}
}
let o = View::ortho_2d_compass([0.361_25, 0.284_75], [4096.0, 2731.0], 31.0, 50.0);
let (_, right, up) = o.basis();
let px = (700.0f32, 512.0f32);
let old = o.target
+ right * ((px.0 - centre.0) / 4096.0)
+ up * (-(px.1 - centre.1) / 2731.0);
let old_back = o.project_world(Vec3::new(old.x, 0.0, old.z), centre, half_h);
let miss = ((old_back.x - px.0).powi(2) + (old_back.y - px.1).powi(2)).sqrt();
assert!(miss > 10.0, "the pre-fix ortho inverse must be reachably wrong, missed by only {miss} px");
let p = View::perspective_3d(
Vec3::new(0.2, 0.0, -0.4),
71f32.to_radians(),
49f32.to_radians(),
3.0,
50f32.to_radians(),
);
let mut hits = 0;
for px in [(417.0f32, 271.0f32), (500.0, 350.0), (330.0, 300.0)] {
let Some(w) = p.unproject_ground_px(px, centre, half_h) else { continue };
assert!(w.y.abs() < 1e-4, "the hit must be ON the ground plane, got y={}", w.y);
let back = p.project_world(w, centre, half_h);
if !back.visible {
continue;
}
assert!(
(back.x - px.0).abs() < 0.01 && (back.y - px.1).abs() < 0.01,
"perspective round-trip {px:?} -> {w:?} -> ({}, {})",
back.x,
back.y
);
hits += 1;
}
assert!(hits >= 2, "the perspective sweep must actually hit the plane, hit {hits}");
let low = View::perspective_3d(Vec3::ZERO, 0.0, 80f32.to_radians(), 3.0, 50f32.to_radians());
assert!(low.eye().y > 0.4, "the probe camera must really be above the plane");
assert!(
low.unproject_ground_px((centre.0, 0.0), centre, half_h).is_none(),
"a ray above the horizon must miss the ground plane"
);
assert!(
low.unproject_ground_px((centre.0, 600.0), centre, half_h).is_some(),
"the downward ray of the same camera must hit"
);
}
#[test]
fn perspective_depth_row_orders_and_stays_normalised() {
let v = View::perspective_3d(
Vec3::ZERO,
0.7,
0.8,
5.0,
50f32.to_radians(),
);
let m = v.view_proj(800.0 / 600.0);
let (fwd, _, _) = v.basis();
let near_p = v.eye() + fwd * 3.5;
let far_p = v.eye() + fwd * 6.5;
let (a, b) = (m * near_p.extend(1.0), m * far_p.extend(1.0));
assert!(a.w > 0.0 && b.w > 0.0, "both in front of the eye");
let (za, zb) = (a.z / a.w, b.z / b.w);
assert!(za < zb, "nearer must be smaller depth ({za} vs {zb})");
assert!((0.0..=1.0).contains(&za) && (0.0..=1.0).contains(&zb), "{za}, {zb} in [0,1]");
}
}