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}