use crate::anisotropic_friction::AnisotropicFriction;
use crate::heightfield::HeightField;
use crate::math::{Fix128, Vec3Fix};
use crate::raycast::Ray;
use crate::sdf_collider::SdfField;
use crate::trimesh::TriMesh;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct GroundHit {
pub distance: Fix128,
pub point: Vec3Fix,
pub normal: Vec3Fix,
}
pub trait RoadSurface {
fn probe(&self, origin: Vec3Fix, dir: Vec3Fix, max_dist: Fix128) -> Option<GroundHit>;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FlatGround {
pub height: Fix128,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct InclinedPlane {
pub point: Vec3Fix,
pub normal: Vec3Fix,
}
#[derive(Clone, Copy, Debug)]
pub struct HeightFieldRoad<'a> {
pub field: &'a HeightField,
}
#[derive(Clone, Copy)]
pub struct TriMeshRoad<'a> {
pub mesh: &'a TriMesh,
}
pub struct SdfRoad<'a> {
pub field: &'a dyn SdfField,
pub tolerance: Fix128,
pub max_steps: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Weather {
Dry,
Wet {
water_depth_mm: Fix128,
},
Snow,
Ice,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RoadCondition {
pub material: AnisotropicFriction,
pub weather: Weather,
pub rolling_resistance: Fix128,
}
pub const HYDROPLANING_MIN_DEPTH_MM: Fix128 = Fix128 {
hi: 2,
lo: 0x8000_0000_0000_0000,
};
fn hydroplaning_floor() -> Fix128 {
Fix128::from_ratio(1, 10)
}
impl RoadCondition {
#[must_use]
pub fn dry_asphalt() -> Self {
Self {
material: AnisotropicFriction::tyre_asphalt(),
weather: Weather::Dry,
rolling_resistance: Fix128::from_ratio(12, 1000),
}
}
#[must_use]
pub fn weather_factor(&self) -> Fix128 {
match self.weather {
Weather::Dry => Fix128::ONE,
Weather::Wet { .. } => Fix128::from_ratio(7, 10),
Weather::Snow => Fix128::from_ratio(6, 25),
Weather::Ice => Fix128::from_ratio(3, 25),
}
}
#[must_use]
pub fn grip(&self, speed: Fix128, tyre_pressure_kpa: Fix128) -> AnisotropicFriction {
let mut k = self.weather_factor();
if let Weather::Wet { water_depth_mm } = self.weather {
if water_depth_mm >= HYDROPLANING_MIN_DEPTH_MM {
let v = speed.abs();
let vp = hydroplaning_onset_speed(tyre_pressure_kpa);
if v > vp {
let q = vp / v;
let loss = (q * q).max(hydroplaning_floor());
k = k * loss;
}
}
}
let m = self.material;
AnisotropicFriction {
longitudinal_static: m.longitudinal_static * k,
longitudinal_kinetic: m.longitudinal_kinetic * k,
transverse_static: m.transverse_static * k,
transverse_kinetic: m.transverse_kinetic * k,
slip_threshold_m_s: m.slip_threshold_m_s,
}
}
}
#[must_use]
pub fn hydroplaning_onset_speed(tyre_pressure_kpa: Fix128) -> Fix128 {
if tyre_pressure_kpa <= Fix128::ZERO {
return Fix128::ZERO;
}
Fix128::from_ratio(635, 360) * tyre_pressure_kpa.sqrt()
}
fn probe_plane(
p: Vec3Fix,
n: Vec3Fix,
origin: Vec3Fix,
dir: Vec3Fix,
max_dist: Fix128,
) -> Option<GroundHit> {
if max_dist < Fix128::ZERO {
return None;
}
let s = (origin - p).dot(n);
if s <= Fix128::ZERO {
return Some(GroundHit {
distance: Fix128::ZERO,
point: origin - n * s,
normal: n,
});
}
let approach = -dir.dot(n);
if approach <= Fix128::ZERO {
return None;
}
if s > approach * max_dist {
return None;
}
let t = s / approach;
Some(GroundHit {
distance: t,
point: origin + dir * t,
normal: n,
})
}
impl RoadSurface for FlatGround {
fn probe(&self, origin: Vec3Fix, dir: Vec3Fix, max_dist: Fix128) -> Option<GroundHit> {
let p = Vec3Fix::new(Fix128::ZERO, self.height, Fix128::ZERO);
let mut hit = probe_plane(p, Vec3Fix::UNIT_Y, origin, dir, max_dist)?;
hit.point.y = self.height;
Some(hit)
}
}
impl RoadSurface for InclinedPlane {
fn probe(&self, origin: Vec3Fix, dir: Vec3Fix, max_dist: Fix128) -> Option<GroundHit> {
probe_plane(self.point, self.normal, origin, dir, max_dist)
}
}
const HF_STEPS_PER_CELL: i64 = 4;
const HF_BISECT_ITERS: u32 = 64;
impl HeightFieldRoad<'_> {
fn extent(&self) -> Option<(Fix128, Fix128, Fix128, Fix128)> {
let f = self.field;
if f.width < 2 || f.depth < 2 || f.spacing <= Fix128::ZERO {
return None;
}
let w = f.spacing * Fix128::from_int(i64::from(f.width - 1));
let d = f.spacing * Fix128::from_int(i64::from(f.depth - 1));
Some((f.origin.x, f.origin.x + w, f.origin.z, f.origin.z + d))
}
fn normal_at(&self, x: Fix128, z: Fix128, ext: (Fix128, Fix128, Fix128, Fix128)) -> Vec3Fix {
let f = self.field;
let eps = f.spacing.half();
let x0 = (x - eps).max(ext.0);
let x1 = (x + eps).min(ext.1);
let z0 = (z - eps).max(ext.2);
let z1 = (z + eps).min(ext.3);
let dhdx = (f.sample_height(x1, z) - f.sample_height(x0, z)) / (x1 - x0);
let dhdz = (f.sample_height(x, z1) - f.sample_height(x, z0)) / (z1 - z0);
Vec3Fix::new(-dhdx, Fix128::ONE, -dhdz).normalize()
}
fn hit_at(
&self,
origin: Vec3Fix,
dir: Vec3Fix,
t: Fix128,
ext: (Fix128, Fix128, Fix128, Fix128),
) -> GroundHit {
let p = origin + dir * t;
let y = self.field.sample_height(p.x, p.z);
GroundHit {
distance: t,
point: Vec3Fix::new(p.x, y, p.z),
normal: self.normal_at(p.x, p.z, ext),
}
}
}
fn clip_slab(
t0: Fix128,
t1: Fix128,
o: Fix128,
d: Fix128,
lo: Fix128,
hi: Fix128,
) -> Option<(Fix128, Fix128)> {
if d.is_zero() {
return if o < lo || o > hi {
None
} else {
Some((t0, t1))
};
}
let ta = (lo - o) / d;
let tb = (hi - o) / d;
let (near, far) = if ta <= tb { (ta, tb) } else { (tb, ta) };
let a = t0.max(near);
let b = t1.min(far);
if a > b {
None
} else {
Some((a, b))
}
}
impl RoadSurface for HeightFieldRoad<'_> {
fn probe(&self, origin: Vec3Fix, dir: Vec3Fix, max_dist: Fix128) -> Option<GroundHit> {
if max_dist < Fix128::ZERO {
return None;
}
let ext = self.extent()?;
let f = self.field;
let gap = |t: Fix128| {
let p = origin + dir * t;
p.y - f.sample_height(p.x, p.z)
};
let inside =
origin.x >= ext.0 && origin.x <= ext.1 && origin.z >= ext.2 && origin.z <= ext.3;
if inside && gap(Fix128::ZERO) <= Fix128::ZERO {
return Some(self.hit_at(origin, dir, Fix128::ZERO, ext));
}
let (t0, t1) = clip_slab(Fix128::ZERO, max_dist, origin.x, dir.x, ext.0, ext.1)?;
let (t0, t1) = clip_slab(t0, t1, origin.z, dir.z, ext.2, ext.3)?;
let step = f.spacing / Fix128::from_int(HF_STEPS_PER_CELL);
let mut a = t0;
if gap(a) <= Fix128::ZERO {
return None;
}
while a < t1 {
let b = (a + step).min(t1);
if gap(b) <= Fix128::ZERO {
let (mut lo, mut hi) = (a, b);
for _ in 0..HF_BISECT_ITERS {
let mid = lo + (hi - lo).half();
if gap(mid) <= Fix128::ZERO {
hi = mid;
} else {
lo = mid;
}
}
return Some(self.hit_at(origin, dir, hi, ext));
}
a = b;
}
None
}
}
impl TriMeshRoad<'_> {
fn outward(&self, i: usize) -> Vec3Fix {
self.mesh.triangles[i].unit_normal()
}
}
impl RoadSurface for TriMeshRoad<'_> {
fn probe(&self, origin: Vec3Fix, dir: Vec3Fix, max_dist: Fix128) -> Option<GroundHit> {
if max_dist < Fix128::ZERO || self.mesh.triangles.is_empty() {
return None;
}
let b = self.mesh.bounds;
let reach = |o: Fix128, lo: Fix128, hi: Fix128| (o - lo).abs().max((hi - o).abs());
let back_len = reach(origin.x, b.min.x, b.max.x)
+ reach(origin.y, b.min.y, b.max.y)
+ reach(origin.z, b.min.z, b.max.z);
let back = Ray {
origin,
direction: -dir,
};
if let Some(h) = self.mesh.raycast(&back, back_len) {
let n = self.outward(h.body_index);
if n.dot(dir) < Fix128::ZERO {
return Some(GroundHit {
distance: Fix128::ZERO,
point: h.point,
normal: n,
});
}
}
let fwd = Ray {
origin,
direction: dir,
};
let h = self.mesh.raycast(&fwd, max_dist)?;
let n = self.outward(h.body_index);
if n.dot(dir) > Fix128::ZERO {
return Some(GroundHit {
distance: Fix128::ZERO,
point: h.point,
normal: n,
});
}
Some(GroundHit {
distance: h.t,
point: h.point,
normal: n,
})
}
}
impl SdfRoad<'_> {
fn sample(&self, p: Vec3Fix, dir: Vec3Fix) -> Option<(Fix128, Vec3Fix)> {
let (x, y, z) = p.to_f32();
let (d, (nx, ny, nz)) = self.field.distance_and_normal(x, y, z);
if !d.is_finite() || !nx.is_finite() || !ny.is_finite() || !nz.is_finite() {
return None;
}
let n = Vec3Fix::from_f32(nx, ny, nz)
.try_normalize()
.unwrap_or(-dir);
Some((Fix128::from_f32(d), n))
}
}
impl RoadSurface for SdfRoad<'_> {
fn probe(&self, origin: Vec3Fix, dir: Vec3Fix, max_dist: Fix128) -> Option<GroundHit> {
if max_dist < Fix128::ZERO {
return None;
}
let (d0, n0) = self.sample(origin, dir)?;
if d0 <= Fix128::ZERO {
return Some(GroundHit {
distance: Fix128::ZERO,
point: origin - n0 * d0,
normal: n0,
});
}
let tol = self.tolerance.max(Fix128::ZERO);
let mut t = Fix128::ZERO;
for _ in 0..self.max_steps {
let p = origin + dir * t;
let (d, n) = self.sample(p, dir)?;
if d <= tol {
return Some(GroundHit {
distance: t,
point: p - n * d,
normal: n,
});
}
t = t + d;
if t > max_dist {
return None;
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sdf_collider::ClosureSdf;
use crate::trimesh::Triangle;
fn fx(n: i64) -> Fix128 {
Fix128::from_int(n)
}
fn r(n: i64, d: i64) -> Fix128 {
Fix128::from_ratio(n, d)
}
fn v(x: Fix128, y: Fix128, z: Fix128) -> Vec3Fix {
Vec3Fix::new(x, y, z)
}
fn down() -> Vec3Fix {
Vec3Fix::new(Fix128::ZERO, Fix128::NEG_ONE, Fix128::ZERO)
}
fn close(a: Fix128, b: f64, tol: f64) -> bool {
(a.to_f64() - b).abs() <= tol
}
fn close_v(a: Vec3Fix, b: [f64; 3], tol: f64) -> bool {
close(a.x, b[0], tol) && close(a.y, b[1], tol) && close(a.z, b[2], tol)
}
#[test]
fn flat_vertical_probe_is_height_difference() {
let g = FlatGround { height: fx(1) };
let hit = g.probe(v(fx(3), fx(5), fx(-2)), down(), fx(10)).unwrap();
assert_eq!(hit.distance, fx(4));
assert_eq!(hit.point, v(fx(3), fx(1), fx(-2)));
assert_eq!(hit.normal, Vec3Fix::UNIT_Y);
}
#[test]
fn flat_oblique_probe_matches_closed_form() {
let g = FlatGround { height: fx(1) };
let dir = v(r(3, 5), r(-4, 5), Fix128::ZERO);
let hit = g.probe(v(fx(0), fx(5), fx(0)), dir, fx(10)).unwrap();
assert!(close(hit.distance, 5.0, 1e-15), "{}", hit.distance);
assert!(close_v(hit.point, [3.0, 1.0, 0.0], 1e-15));
}
#[test]
fn flat_origin_below_returns_zero_and_projects() {
let g = FlatGround { height: fx(1) };
let hit = g.probe(v(fx(2), fx(-1), fx(7)), down(), fx(10)).unwrap();
assert_eq!(hit.distance, Fix128::ZERO);
assert_eq!(hit.point, v(fx(2), fx(1), fx(7)));
assert_eq!(hit.normal, Vec3Fix::UNIT_Y);
}
#[test]
fn flat_out_of_range_and_upward_miss() {
let g = FlatGround { height: fx(1) };
assert_eq!(g.probe(v(fx(0), fx(5), fx(0)), down(), fx(3)), None);
assert_eq!(
g.probe(v(fx(0), fx(5), fx(0)), Vec3Fix::UNIT_Y, fx(100)),
None
);
}
#[test]
fn degenerate_inputs_on_plane() {
let g = FlatGround { height: fx(1) };
assert_eq!(g.probe(v(fx(0), fx(5), fx(0)), down(), Fix128::ZERO), None);
let hit = g
.probe(v(fx(0), fx(0), fx(0)), down(), Fix128::ZERO)
.unwrap();
assert_eq!(hit.distance, Fix128::ZERO);
assert_eq!(
g.probe(v(fx(0), fx(5), fx(0)), Vec3Fix::UNIT_X, fx(100)),
None
);
assert_eq!(g.probe(v(fx(0), fx(0), fx(0)), down(), fx(-1)), None);
let on = g
.probe(v(fx(4), fx(1), fx(4)), Vec3Fix::UNIT_X, fx(1))
.unwrap();
assert_eq!(on.distance, Fix128::ZERO);
assert_eq!(on.point, v(fx(4), fx(1), fx(4)));
}
fn slope() -> InclinedPlane {
InclinedPlane {
point: Vec3Fix::ZERO,
normal: v(r(-3, 5), r(4, 5), Fix128::ZERO),
}
}
#[test]
fn incline_vertical_probe_matches_closed_form() {
let hit = slope()
.probe(v(fx(5), fx(10), fx(1)), down(), fx(20))
.unwrap();
assert!(close(hit.distance, 6.25, 1e-15), "{}", hit.distance);
assert!(close_v(hit.point, [5.0, 3.75, 1.0], 1e-15));
assert!(close_v(hit.normal, [-0.6, 0.8, 0.0], 1e-15));
}
#[test]
fn incline_origin_below_projects_along_normal() {
let hit = slope()
.probe(v(fx(4), fx(0), fx(0)), down(), fx(5))
.unwrap();
assert_eq!(hit.distance, Fix128::ZERO);
assert!(close_v(hit.point, [2.56, 1.92, 0.0], 1e-15));
let along = v(r(4, 5), r(3, 5), Fix128::ZERO);
assert_eq!(slope().probe(v(fx(0), fx(1), fx(0)), along, fx(100)), None);
}
fn linear_field(
a: Fix128,
b: Fix128,
c: Fix128,
n: u32,
spacing: Fix128,
origin: Vec3Fix,
) -> HeightField {
let mut hs = Vec::new();
for iz in 0..n {
for ix in 0..n {
let x = origin.x + spacing * fx(i64::from(ix));
let z = origin.z + spacing * fx(i64::from(iz));
hs.push(a * x + b * z + c);
}
}
HeightField::new(hs, n, n, spacing, origin)
}
fn linear_t(o: [f64; 3], d: [f64; 3], a: f64, b: f64, c: f64) -> f64 {
(o[1] - a * o[0] - b * o[2] - c) / (a * d[0] + b * d[2] - d[1])
}
#[test]
fn heightfield_flat_matches_flat_ground() {
let field = HeightField::flat(5, 5, fx(1), Vec3Fix::ZERO, fx(1));
let road = HeightFieldRoad { field: &field };
let o = v(r(23, 10), fx(5), r(17, 10));
let hit = road.probe(o, down(), fx(10)).unwrap();
assert!(close(hit.distance, 4.0, 1e-15), "{}", hit.distance);
assert!(close_v(hit.point, [2.3, 1.0, 1.7], 1e-15));
assert!(close_v(hit.normal, [0.0, 1.0, 0.0], 1e-15));
}
#[test]
fn heightfield_linear_oblique_matches_closed_form() {
let field = linear_field(r(1, 2), r(1, 4), fx(1), 9, r(1, 2), Vec3Fix::ZERO);
let road = HeightFieldRoad { field: &field };
let dir = v(r(3, 5), r(-4, 5), Fix128::ZERO);
let o = [0.5, 6.0, 2.0];
let hit = road.probe(v(r(1, 2), fx(6), fx(2)), dir, fx(20)).unwrap();
let t = linear_t(o, [0.6, -0.8, 0.0], 0.5, 0.25, 1.0);
assert!(close(hit.distance, t, 1e-12), "{} vs {t}", hit.distance);
let p = [o[0] + 0.6 * t, o[1] - 0.8 * t, o[2]];
assert!(close_v(hit.point, p, 1e-12));
let n = (0.25f64 + 1.0 + 0.0625).sqrt();
assert!(close_v(hit.normal, [-0.5 / n, 1.0 / n, -0.25 / n], 1e-12));
}
#[test]
fn heightfield_normal_at_grid_edge_keeps_full_slope() {
let field = linear_field(r(1, 2), r(1, 4), fx(1), 9, r(1, 2), Vec3Fix::ZERO);
let road = HeightFieldRoad { field: &field };
let hit = road
.probe(v(r(39, 10), fx(10), fx(2)), down(), fx(20))
.unwrap();
let n = (0.25f64 + 1.0 + 0.0625).sqrt();
assert!(
close_v(hit.normal, [-0.5 / n, 1.0 / n, -0.25 / n], 1e-12),
"{}",
hit.normal
);
}
#[test]
fn heightfield_xz_origin_offset_matches_closed_form() {
let origin = v(fx(10), Fix128::ZERO, fx(-4));
let field = linear_field(r(1, 2), r(1, 4), fx(1), 9, r(1, 2), origin);
let road = HeightFieldRoad { field: &field };
let hit = road
.probe(v(fx(12), fx(20), fx(-3)), down(), fx(30))
.unwrap();
assert!(close(hit.distance, 13.75, 1e-12), "{}", hit.distance);
assert_eq!(road.probe(v(fx(2), fx(20), fx(-3)), down(), fx(30)), None);
}
#[test]
fn heightfield_origin_y_follows_sample_height() {
let field = HeightField::flat(5, 5, fx(1), v(fx(0), fx(7), fx(0)), fx(1));
let road = HeightFieldRoad { field: &field };
let surface_y = field.sample_height(fx(2), fx(2));
let hit = road.probe(v(fx(2), fx(20), fx(2)), down(), fx(30)).unwrap();
assert!(close(hit.distance, (fx(20) - surface_y).to_f64(), 1e-15));
assert!(close(hit.point.y, surface_y.to_f64(), 1e-15));
}
#[test]
fn heightfield_below_and_degenerate() {
let field = HeightField::flat(5, 5, fx(1), Vec3Fix::ZERO, fx(1));
let road = HeightFieldRoad { field: &field };
let hit = road.probe(v(fx(2), fx(0), fx(2)), down(), fx(5)).unwrap();
assert_eq!(hit.distance, Fix128::ZERO);
assert!(close_v(hit.point, [2.0, 1.0, 2.0], 1e-15));
assert_eq!(
road.probe(v(fx(2), fx(3), fx(2)), down(), Fix128::ZERO),
None
);
assert_eq!(
road.probe(v(fx(0), fx(3), fx(2)), Vec3Fix::UNIT_X, fx(10)),
None
);
let empty = HeightField::new(Vec::new(), 0, 0, fx(1), Vec3Fix::ZERO);
assert_eq!(
HeightFieldRoad { field: &empty }.probe(v(fx(0), fx(3), fx(0)), down(), fx(10)),
None
);
let flat0 = HeightField::flat(5, 5, Fix128::ZERO, Vec3Fix::ZERO, fx(1));
assert_eq!(
HeightFieldRoad { field: &flat0 }.probe(v(fx(0), fx(3), fx(0)), down(), fx(10)),
None
);
}
fn quad() -> TriMesh {
let y = fx(2);
let a = v(fx(-5), y, fx(-5));
let b = v(fx(-5), y, fx(5));
let c = v(fx(5), y, fx(5));
let d = v(fx(5), y, fx(-5));
TriMesh::from_triangles(vec![Triangle::new(a, b, c), Triangle::new(a, c, d)])
}
#[test]
fn trimesh_quad_matches_closed_form() {
let mesh = quad();
let road = TriMeshRoad { mesh: &mesh };
let dir = v(Fix128::ZERO, r(-4, 5), r(3, 5));
let hit = road.probe(v(fx(1), fx(6), fx(-2)), dir, fx(10)).unwrap();
assert!(close(hit.distance, 5.0, 1e-15), "{}", hit.distance);
assert!(close_v(hit.point, [1.0, 2.0, 1.0], 1e-15));
assert!(close_v(hit.normal, [0.0, 1.0, 0.0], 1e-15));
assert_eq!(road.probe(v(fx(1), fx(6), fx(-2)), dir, fx(4)), None);
assert_eq!(road.probe(v(fx(9), fx(6), fx(0)), down(), fx(10)), None);
}
#[test]
fn trimesh_origin_below_returns_zero_with_outward_normal() {
let mesh = quad();
let road = TriMeshRoad { mesh: &mesh };
let hit = road.probe(v(fx(1), fx(0), fx(1)), down(), fx(10)).unwrap();
assert_eq!(hit.distance, Fix128::ZERO);
assert!(close_v(hit.point, [1.0, 2.0, 1.0], 1e-15));
assert!(close_v(hit.normal, [0.0, 1.0, 0.0], 1e-15));
let empty = TriMesh::from_triangles(Vec::new());
assert_eq!(
TriMeshRoad { mesh: &empty }.probe(v(fx(0), fx(3), fx(0)), down(), fx(10)),
None
);
assert_eq!(
road.probe(v(fx(0), fx(3), fx(0)), Vec3Fix::UNIT_X, fx(10)),
None
);
}
#[test]
fn trimesh_reversed_winding_is_a_road_facing_down() {
let y = fx(2);
let a = v(fx(-5), y, fx(-5));
let b = v(fx(-5), y, fx(5));
let c = v(fx(5), y, fx(5));
let d = v(fx(5), y, fx(-5));
let mesh = TriMesh::from_triangles(vec![Triangle::new(a, c, b), Triangle::new(a, d, c)]);
let road = TriMeshRoad { mesh: &mesh };
let above = road.probe(v(fx(1), fx(6), fx(1)), down(), fx(10)).unwrap();
assert_eq!(above.distance, Fix128::ZERO);
assert!(close_v(above.point, [1.0, 2.0, 1.0], 1e-15));
assert!(close_v(above.normal, [0.0, -1.0, 0.0], 1e-15));
assert_eq!(road.probe(v(fx(1), fx(0), fx(1)), down(), fx(10)), None);
}
fn plane_sdf() -> ClosureSdf {
ClosureSdf::new(|_, y, _| y - 1.0, |_, _, _| (0.0, 1.0, 0.0))
}
fn sphere_sdf() -> ClosureSdf {
ClosureSdf::new(
|x, y, z| (x * x + y * y + z * z).sqrt() - 1.0,
|x, y, z| {
let l = (x * x + y * y + z * z).sqrt();
(x / l, y / l, z / l)
},
)
}
#[test]
fn sdf_plane_matches_closed_form() {
let f = plane_sdf();
let road = SdfRoad {
field: &f,
tolerance: r(1, 10_000),
max_steps: 64,
};
let hit = road.probe(v(fx(0), fx(4), fx(0)), down(), fx(10)).unwrap();
assert!(close(hit.distance, 3.0, 1e-4), "{}", hit.distance);
assert!(close_v(hit.point, [0.0, 1.0, 0.0], 1e-6));
assert!(close_v(hit.normal, [0.0, 1.0, 0.0], 1e-12));
let below = road.probe(v(fx(2), fx(-1), fx(0)), down(), fx(10)).unwrap();
assert_eq!(below.distance, Fix128::ZERO);
assert!(close_v(below.point, [2.0, 1.0, 0.0], 1e-6));
}
#[test]
fn sdf_sphere_matches_closed_form() {
let f = sphere_sdf();
let tol = 1e-4;
let road = SdfRoad {
field: &f,
tolerance: r(1, 10_000),
max_steps: 256,
};
let dir = v(Fix128::ZERO, Fix128::ZERO, Fix128::NEG_ONE);
let hit = road.probe(v(r(1, 2), fx(0), fx(5)), dir, fx(10)).unwrap();
let t = 5.0 - 0.75f64.sqrt();
assert!(close(hit.distance, t, 2.0 * tol), "{} vs {t}", hit.distance);
assert!(close_v(hit.normal, [0.5, 0.0, 0.75f64.sqrt()], 1e-3));
let inside = road.probe(v(fx(0), fx(0), r(1, 2)), dir, fx(10)).unwrap();
assert_eq!(inside.distance, Fix128::ZERO);
assert!(close_v(inside.point, [0.0, 0.0, 1.0], 1e-6));
assert_eq!(road.probe(v(fx(3), fx(0), fx(5)), dir, fx(10)), None);
assert_eq!(road.probe(v(r(1, 2), fx(0), fx(5)), dir, fx(4)), None);
let none_steps = SdfRoad {
field: &f,
tolerance: r(1, 10_000),
max_steps: 0,
};
assert_eq!(
none_steps.probe(v(r(1, 2), fx(0), fx(5)), dir, fx(10)),
None
);
}
#[test]
fn sdf_non_finite_distance_is_no_hit() {
let f = ClosureSdf::new(|_, _, _| f32::NAN, |_, _, _| (0.0, 1.0, 0.0));
let road = SdfRoad {
field: &f,
tolerance: r(1, 10_000),
max_steps: 16,
};
assert_eq!(road.probe(v(fx(0), fx(4), fx(0)), down(), fx(10)), None);
}
fn assert_scaled(g: AnisotropicFriction, base: AnisotropicFriction, k: Fix128) {
assert_eq!(g.longitudinal_static, base.longitudinal_static * k);
assert_eq!(g.longitudinal_kinetic, base.longitudinal_kinetic * k);
assert_eq!(g.transverse_static, base.transverse_static * k);
assert_eq!(g.transverse_kinetic, base.transverse_kinetic * k);
assert_eq!(g.slip_threshold_m_s, base.slip_threshold_m_s);
}
#[test]
fn dry_asphalt_values() {
let c = RoadCondition::dry_asphalt();
assert_eq!(c.material, AnisotropicFriction::tyre_asphalt());
assert_eq!(c.weather, Weather::Dry);
assert_eq!(c.rolling_resistance, r(12, 1000));
}
#[test]
fn weather_table_ratios_are_exact() {
let base = RoadCondition::dry_asphalt();
let speed = fx(10);
let p = fx(220);
let cases = [
(Weather::Dry, Fix128::ONE),
(
Weather::Wet {
water_depth_mm: fx(1),
},
r(7, 10),
),
(Weather::Snow, r(6, 25)),
(Weather::Ice, r(3, 25)),
];
for (w, k) in cases {
let c = RoadCondition { weather: w, ..base };
assert_eq!(c.weather_factor(), k, "{w:?}");
assert_scaled(c.grip(speed, p), base.material, k);
}
}
#[test]
fn horne_onset_speed_matches_f64() {
for p in [100i64, 180, 220, 250, 900] {
let got = hydroplaning_onset_speed(fx(p));
let want = 6.35 * (p as f64).sqrt() / 3.6;
assert!(close(got, want, 1e-12), "p={p}: {got} vs {want}");
}
assert_eq!(hydroplaning_onset_speed(Fix128::ZERO), Fix128::ZERO);
assert_eq!(hydroplaning_onset_speed(fx(-50)), Fix128::ZERO);
}
#[test]
fn wet_grip_below_onset_is_speed_independent() {
let p = fx(220);
let vp = hydroplaning_onset_speed(p);
let c = RoadCondition {
weather: Weather::Wet {
water_depth_mm: fx(5),
},
..RoadCondition::dry_asphalt()
};
let g0 = c.grip(Fix128::ZERO, p);
assert_eq!(c.grip(fx(5), p), g0);
assert_eq!(c.grip(vp * r(9, 10), p), g0);
assert_eq!(c.grip(vp, p), g0);
assert_scaled(g0, c.material, r(7, 10));
}
#[test]
fn hydroplaning_above_onset_follows_model() {
let p = fx(220);
let vp = hydroplaning_onset_speed(p);
let deep = RoadCondition {
weather: Weather::Wet {
water_depth_mm: r(5, 2),
},
..RoadCondition::dry_asphalt()
};
let g = deep.grip(vp * fx(2), p);
let want = 1.1 * 0.7 * 0.25;
assert!(
close(g.longitudinal_static, want, 1e-12),
"{}",
g.longitudinal_static
);
let g_far = deep.grip(vp * fx(10), p);
assert!(
close(g_far.longitudinal_static, 1.1 * 0.7 * 0.1, 1e-12),
"{}",
g_far.longitudinal_static
);
assert_eq!(deep.grip(-(vp * fx(2)), p), g);
let shallow = RoadCondition {
weather: Weather::Wet {
water_depth_mm: r(24, 10),
},
..deep
};
assert_scaled(shallow.grip(vp * fx(2), p), shallow.material, r(7, 10));
let neg = RoadCondition {
weather: Weather::Wet {
water_depth_mm: fx(-3),
},
..deep
};
assert_scaled(neg.grip(vp * fx(2), p), neg.material, r(7, 10));
assert!(close(
deep.grip(fx(1), Fix128::ZERO).longitudinal_static,
1.1 * 0.7 * 0.1,
1e-12
));
assert_scaled(
deep.grip(Fix128::ZERO, Fix128::ZERO),
deep.material,
r(7, 10),
);
}
}