Skip to main content

pfx_geom/
detail.rs

1use crate::mesh::Bounds;
2
3pub const SILHOUETTE_PIXELS: f32 = 0.5;
4pub const CURVATURE_PIXELS: f32 = 1.0;
5pub const HYSTERESIS: f32 = 0.2;
6
7pub fn projected_pixels(world: f32, distance: f32, fov_y: f32, height: f32) -> f32 {
8    let half = (fov_y * 0.5).tan();
9    if half <= 1e-8 || height <= 0.0 {
10        return f32::MAX;
11    }
12    world.abs() * height / (2.0 * distance.max(1e-4) * half)
13}
14
15pub fn world_error(pixels: f32, distance: f32, fov_y: f32, height: f32) -> f32 {
16    let half = (fov_y * 0.5).tan();
17    if half <= 1e-8 || height <= 1.0 {
18        return 0.0;
19    }
20    pixels.abs() * 2.0 * distance.max(1e-4) * half / height
21}
22
23pub fn detail_level(
24    bound: Bounds,
25    errors: &[f32],
26    distance: f32,
27    fov_y: f32,
28    height: f32,
29    previous: Option<usize>,
30) -> usize {
31    if errors.is_empty() {
32        return 0;
33    }
34    let closest = (distance - bound.radius()).max(1e-4);
35    let project = |error: f32| projected_pixels(error, closest, fov_y, height);
36    let mut ideal = 0;
37    for (index, error) in errors.iter().enumerate() {
38        if project(*error) <= SILHOUETTE_PIXELS {
39            ideal = index;
40        }
41    }
42    let Some(previous) = previous else {
43        return ideal;
44    };
45    if previous >= errors.len() || previous == ideal {
46        return ideal.min(errors.len() - 1);
47    }
48    let loosen = 1.0 + HYSTERESIS;
49    let tighten = 1.0 - HYSTERESIS;
50    if ideal > previous {
51        let mut chosen = previous;
52        while chosen + 1 < errors.len()
53            && project(errors[chosen + 1]) <= SILHOUETTE_PIXELS * tighten
54        {
55            chosen += 1;
56        }
57        chosen
58    } else {
59        let mut chosen = previous;
60        while chosen > 0 && project(errors[chosen]) > SILHOUETTE_PIXELS * loosen {
61            chosen -= 1;
62        }
63        chosen
64    }
65}