use glam::DVec3;
use crate::GridTransform;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Lod {
Near,
Mid,
Far,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LodThresholds {
pub r_near: f64,
pub r_mid: f64,
pub mid_mip_levels: Option<u32>,
pub mid_mip_scan_dist: Option<i32>,
}
impl LodThresholds {
#[must_use]
pub const fn always_near() -> Self {
Self {
r_near: f64::INFINITY,
r_mid: f64::INFINITY,
mid_mip_levels: None,
mid_mip_scan_dist: None,
}
}
#[must_use]
pub fn from_radius(bounding_radius: f64) -> Self {
Self {
r_near: bounding_radius,
r_mid: 10.0 * bounding_radius,
mid_mip_levels: None,
mid_mip_scan_dist: None,
}
}
#[must_use]
pub fn from_radius_with_mid_mip(
bounding_radius: f64,
mid_mip_levels: u32,
mid_mip_scan_dist: i32,
) -> Self {
Self {
r_near: bounding_radius,
r_mid: 10.0 * bounding_radius,
mid_mip_levels: Some(mid_mip_levels),
mid_mip_scan_dist: Some(mid_mip_scan_dist),
}
}
}
impl Default for LodThresholds {
fn default() -> Self {
Self::always_near()
}
}
#[must_use]
pub fn select_lod(
camera_world_pos: DVec3,
transform: &GridTransform,
thresholds: LodThresholds,
) -> Lod {
let distance = (camera_world_pos - transform.origin).length();
if distance <= thresholds.r_near {
Lod::Near
} else if distance <= thresholds.r_mid {
Lod::Mid
} else {
Lod::Far
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::*;
use crate::GridTransform;
fn at_origin() -> GridTransform {
GridTransform::at(DVec3::ZERO)
}
#[test]
fn default_thresholds_are_always_near() {
let t = LodThresholds::default();
assert_eq!(t.r_near, f64::INFINITY);
assert_eq!(t.r_mid, f64::INFINITY);
}
#[test]
fn always_near_dispatches_near_at_any_distance() {
let t = LodThresholds::always_near();
let xform = at_origin();
for &d in &[0.0, 100.0, 1_000.0, 1e6, 1e15] {
assert_eq!(
select_lod(DVec3::new(d, 0.0, 0.0), &xform, t),
Lod::Near,
"expected Near at d={d}"
);
}
}
#[test]
fn from_radius_picks_near_inside_mid_band_far_outside() {
let t = LodThresholds::from_radius(100.0);
let xform = at_origin();
let pick = |d: f64| select_lod(DVec3::new(d, 0.0, 0.0), &xform, t);
assert_eq!(pick(50.0), Lod::Near);
assert_eq!(pick(100.0), Lod::Near);
assert_eq!(pick(100.000_001), Lod::Mid);
assert_eq!(pick(500.0), Lod::Mid);
assert_eq!(pick(1000.0), Lod::Mid);
assert_eq!(pick(1000.000_001), Lod::Far);
assert_eq!(pick(1e6), Lod::Far);
}
#[test]
fn distance_is_centre_to_centre_in_world_space() {
let t = LodThresholds {
r_near: 49.0,
r_mid: 51.0,
..LodThresholds::always_near()
};
let xform = GridTransform::at(DVec3::new(100.0, 200.0, 300.0));
let cam = DVec3::new(100.0, 200.0, 350.0);
assert_eq!(select_lod(cam, &xform, t), Lod::Mid);
}
#[test]
fn rotation_does_not_affect_distance_metric() {
use glam::DQuat;
let t = LodThresholds::from_radius(10.0);
let cam = DVec3::new(15.0, 0.0, 0.0);
let xform_id = GridTransform::identity();
let xform_rot = GridTransform {
origin: DVec3::ZERO,
rotation: DQuat::from_rotation_z(std::f64::consts::FRAC_PI_3),
};
assert_eq!(select_lod(cam, &xform_id, t), Lod::Mid);
assert_eq!(select_lod(cam, &xform_rot, t), Lod::Mid);
}
#[test]
fn zero_radius_collapses_to_far_for_any_nonzero_distance() {
let t = LodThresholds::from_radius(0.0);
let xform = at_origin();
assert_eq!(select_lod(DVec3::ZERO, &xform, t), Lod::Near);
assert_eq!(select_lod(DVec3::new(0.5, 0.0, 0.0), &xform, t), Lod::Far);
}
}