1use crate::TileBounds;
4use crate::coords::{
5 WGS84_SEMI_MAJOR_AXIS, ecef_to_ellipsoid_scaled, geodetic_to_ecef, vec3_distance,
6 vec3_magnitude, vec3_normalize,
7};
8
9#[derive(Debug, Clone, Copy)]
13pub struct QuantizedMeshHeader {
14 pub center: [f64; 3],
16 pub min_height: f32,
18 pub max_height: f32,
20 pub bounding_sphere_center: [f64; 3],
22 pub bounding_sphere_radius: f64,
24 pub horizon_occlusion_point: [f64; 3],
26}
27
28impl Default for QuantizedMeshHeader {
29 fn default() -> Self {
30 Self {
31 center: [0.0, 0.0, WGS84_SEMI_MAJOR_AXIS],
32 min_height: 0.0,
33 max_height: 0.0,
34 bounding_sphere_center: [0.0, 0.0, WGS84_SEMI_MAJOR_AXIS],
35 bounding_sphere_radius: 0.0,
36 horizon_occlusion_point: [0.0, 0.0, 1.0],
37 }
38 }
39}
40
41impl QuantizedMeshHeader {
42 pub fn from_bounds(bounds: &TileBounds, min_height: f32, max_height: f32) -> Self {
49 Self::from_bounds_with_vertices(bounds, min_height, max_height, &[])
50 }
51
52 pub fn from_bounds_with_vertices(
56 bounds: &TileBounds,
57 min_height: f32,
58 max_height: f32,
59 vertices_geodetic: &[(f64, f64, f64)],
60 ) -> Self {
61 let center_lon = bounds.center_lon();
62 let center_lat = bounds.center_lat();
63 let center_height = (min_height as f64 + max_height as f64) / 2.0;
64
65 let center = geodetic_to_ecef(center_lon, center_lat, center_height);
66
67 let (bounding_sphere_center, bounding_sphere_radius) =
68 compute_bounding_sphere(bounds, min_height as f64, max_height as f64);
69
70 let horizon_occlusion_point = compute_horizon_occlusion_point(
71 &bounding_sphere_center,
72 bounds,
73 min_height as f64,
74 max_height as f64,
75 vertices_geodetic,
76 );
77
78 Self {
79 center,
80 min_height,
81 max_height,
82 bounding_sphere_center,
83 bounding_sphere_radius,
84 horizon_occlusion_point,
85 }
86 }
87
88 pub fn to_bytes(&self) -> [u8; 88] {
90 let mut bytes = [0u8; 88];
91 let mut offset = 0;
92
93 for &v in &self.center {
95 bytes[offset..offset + 8].copy_from_slice(&v.to_le_bytes());
96 offset += 8;
97 }
98
99 bytes[offset..offset + 4].copy_from_slice(&self.min_height.to_le_bytes());
101 offset += 4;
102 bytes[offset..offset + 4].copy_from_slice(&self.max_height.to_le_bytes());
103 offset += 4;
104
105 for &v in &self.bounding_sphere_center {
107 bytes[offset..offset + 8].copy_from_slice(&v.to_le_bytes());
108 offset += 8;
109 }
110
111 bytes[offset..offset + 8].copy_from_slice(&self.bounding_sphere_radius.to_le_bytes());
113 offset += 8;
114
115 for &v in &self.horizon_occlusion_point {
117 bytes[offset..offset + 8].copy_from_slice(&v.to_le_bytes());
118 offset += 8;
119 }
120
121 debug_assert_eq!(offset, 88);
122 bytes
123 }
124
125 pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
127 if bytes.len() < 88 {
128 return None;
129 }
130
131 let mut offset = 0;
132
133 let read_f64 = |bytes: &[u8], offset: &mut usize| -> f64 {
134 let v = f64::from_le_bytes(bytes[*offset..*offset + 8].try_into().unwrap());
135 *offset += 8;
136 v
137 };
138
139 let read_f32 = |bytes: &[u8], offset: &mut usize| -> f32 {
140 let v = f32::from_le_bytes(bytes[*offset..*offset + 4].try_into().unwrap());
141 *offset += 4;
142 v
143 };
144
145 let center = [
146 read_f64(bytes, &mut offset),
147 read_f64(bytes, &mut offset),
148 read_f64(bytes, &mut offset),
149 ];
150
151 let min_height = read_f32(bytes, &mut offset);
152 let max_height = read_f32(bytes, &mut offset);
153
154 let bounding_sphere_center = [
155 read_f64(bytes, &mut offset),
156 read_f64(bytes, &mut offset),
157 read_f64(bytes, &mut offset),
158 ];
159
160 let bounding_sphere_radius = read_f64(bytes, &mut offset);
161
162 let horizon_occlusion_point = [
163 read_f64(bytes, &mut offset),
164 read_f64(bytes, &mut offset),
165 read_f64(bytes, &mut offset),
166 ];
167
168 Some(Self {
169 center,
170 min_height,
171 max_height,
172 bounding_sphere_center,
173 bounding_sphere_radius,
174 horizon_occlusion_point,
175 })
176 }
177}
178
179fn compute_bounding_sphere(
186 bounds: &TileBounds,
187 min_height: f64,
188 max_height: f64,
189) -> ([f64; 3], f64) {
190 let avg_height = (min_height + max_height) / 2.0;
194 let center = geodetic_to_ecef(bounds.center_lon(), bounds.center_lat(), avg_height);
195
196 let points = [
198 geodetic_to_ecef(bounds.west, bounds.south, min_height),
200 geodetic_to_ecef(bounds.east, bounds.south, min_height),
201 geodetic_to_ecef(bounds.west, bounds.north, min_height),
202 geodetic_to_ecef(bounds.east, bounds.north, min_height),
203 geodetic_to_ecef(bounds.west, bounds.south, max_height),
205 geodetic_to_ecef(bounds.east, bounds.south, max_height),
206 geodetic_to_ecef(bounds.west, bounds.north, max_height),
207 geodetic_to_ecef(bounds.east, bounds.north, max_height),
208 geodetic_to_ecef(bounds.west, bounds.center_lat(), max_height),
210 geodetic_to_ecef(bounds.east, bounds.center_lat(), max_height),
211 geodetic_to_ecef(bounds.center_lon(), bounds.south, max_height),
212 geodetic_to_ecef(bounds.center_lon(), bounds.north, max_height),
213 ];
214
215 let mut radius = 0.0f64;
217 for p in &points {
218 let dist = vec3_distance(¢er, p);
219 radius = radius.max(dist);
220 }
221
222 (center, radius)
223}
224
225fn compute_horizon_occlusion_point(
236 bounding_sphere_center: &[f64; 3],
237 bounds: &TileBounds,
238 min_height: f64,
239 max_height: f64,
240 vertices_geodetic: &[(f64, f64, f64)],
241) -> [f64; 3] {
242 let scaled_center = ecef_to_ellipsoid_scaled(bounding_sphere_center);
243 let dir_mag = vec3_magnitude(&scaled_center);
244 if dir_mag < 1e-10 {
245 return [0.0, 0.0, 1.0];
248 }
249 let direction = vec3_normalize(&scaled_center);
250
251 let mut max_magnitude: f64 = 0.0;
256 let mut update = |position: [f64; 3]| {
257 if let Some(m) = compute_occlusion_magnitude(&position, &direction)
258 && m > max_magnitude
259 {
260 max_magnitude = m;
261 }
262 };
263
264 if vertices_geodetic.is_empty() {
265 let avg_h = (min_height + max_height) / 2.0;
266 let cx = bounds.center_lon();
267 let cy = bounds.center_lat();
268 for &h in &[min_height, max_height] {
269 update(geodetic_to_ecef(bounds.west, bounds.south, h));
270 update(geodetic_to_ecef(bounds.east, bounds.south, h));
271 update(geodetic_to_ecef(bounds.west, bounds.north, h));
272 update(geodetic_to_ecef(bounds.east, bounds.north, h));
273 }
274 update(geodetic_to_ecef(bounds.west, cy, avg_h));
275 update(geodetic_to_ecef(bounds.east, cy, avg_h));
276 update(geodetic_to_ecef(cx, bounds.south, avg_h));
277 update(geodetic_to_ecef(cx, bounds.north, avg_h));
278 update(geodetic_to_ecef(cx, cy, max_height));
279 } else {
280 for &(lon, lat, h) in vertices_geodetic {
281 update(geodetic_to_ecef(lon, lat, h));
282 }
283 }
284
285 if !max_magnitude.is_finite() || max_magnitude <= 0.0 {
286 max_magnitude = 1.0;
289 }
290
291 [
292 direction[0] * max_magnitude,
293 direction[1] * max_magnitude,
294 direction[2] * max_magnitude,
295 ]
296}
297
298fn compute_occlusion_magnitude(position: &[f64; 3], direction: &[f64; 3]) -> Option<f64> {
307 let scaled = ecef_to_ellipsoid_scaled(position);
308 let mag_sq = (scaled[0] * scaled[0] + scaled[1] * scaled[1] + scaled[2] * scaled[2]).max(1.0);
312 let mag = mag_sq.sqrt();
313 let inv_mag = 1.0 / mag;
314 let unit = [
315 scaled[0] * inv_mag,
316 scaled[1] * inv_mag,
317 scaled[2] * inv_mag,
318 ];
319
320 let cos_alpha = unit[0] * direction[0] + unit[1] * direction[1] + unit[2] * direction[2];
321 let cx = unit[1] * direction[2] - unit[2] * direction[1];
323 let cy = unit[2] * direction[0] - unit[0] * direction[2];
324 let cz = unit[0] * direction[1] - unit[1] * direction[0];
325 let sin_alpha = (cx * cx + cy * cy + cz * cz).sqrt();
326 let cos_beta = inv_mag;
327 let sin_beta = (mag_sq - 1.0).max(0.0).sqrt() * inv_mag;
328
329 let denom = cos_alpha * cos_beta - sin_alpha * sin_beta;
330 if denom <= 0.0 {
331 None
332 } else {
333 Some(1.0 / denom)
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340 use crate::coords::WGS84_SEMI_MINOR_AXIS;
341
342 #[test]
343 fn test_header_serialization_roundtrip() {
344 let header = QuantizedMeshHeader {
345 center: [1.0, 2.0, 3.0],
346 min_height: 100.0,
347 max_height: 200.0,
348 bounding_sphere_center: [4.0, 5.0, 6.0],
349 bounding_sphere_radius: 1000.0,
350 horizon_occlusion_point: [0.1, 0.2, 0.3],
351 };
352
353 let bytes = header.to_bytes();
354 assert_eq!(bytes.len(), 88);
355
356 let parsed = QuantizedMeshHeader::from_bytes(&bytes).unwrap();
357
358 assert_eq!(header.center, parsed.center);
359 assert_eq!(header.min_height, parsed.min_height);
360 assert_eq!(header.max_height, parsed.max_height);
361 assert_eq!(header.bounding_sphere_center, parsed.bounding_sphere_center);
362 assert_eq!(header.bounding_sphere_radius, parsed.bounding_sphere_radius);
363 assert_eq!(
364 header.horizon_occlusion_point,
365 parsed.horizon_occlusion_point
366 );
367 }
368
369 #[test]
370 fn test_header_from_bounds() {
371 let bounds = TileBounds::new(-1.0, -1.0, 1.0, 1.0);
372 let header = QuantizedMeshHeader::from_bounds(&bounds, 0.0, 100.0);
373
374 assert!(header.center[0] > 0.0); assert!(header.center[1].abs() < 1000.0); assert!(header.center[2].abs() < 1000.0); assert_eq!(header.min_height, 0.0);
380 assert_eq!(header.max_height, 100.0);
381 assert!(header.bounding_sphere_radius > 0.0);
382 }
383
384 #[test]
385 fn test_header_default() {
386 let header = QuantizedMeshHeader::default();
387
388 assert_eq!(header.center[0], 0.0);
389 assert_eq!(header.center[1], 0.0);
390 assert!((header.center[2] - WGS84_SEMI_MAJOR_AXIS).abs() < 1.0);
391 }
392
393 #[test]
402 fn test_horizon_occlusion_visible_for_low_longitude_cameras() {
403 use crate::coords::{WGS84_SEMI_MAJOR_AXIS as A, ecef_to_ellipsoid_scaled};
404
405 let bounds = TileBounds::new(0.0, -90.0, 180.0, 90.0);
407 let header = QuantizedMeshHeader::from_bounds(&bounds, -100.0, 6000.0);
408 let p = header.horizon_occlusion_point;
409
410 let camera_ecef = geodetic_to_ecef(6.5, 46.4, 5000.0); let v = ecef_to_ellipsoid_scaled(&camera_ecef);
416 let v_dot_v = v[0] * v[0] + v[1] * v[1] + v[2] * v[2];
417 let p_dot_v = p[0] * v[0] + p[1] * v[1] + p[2] * v[2];
418 let vt_dot_vc = v_dot_v - p_dot_v;
419 let dt2 = v_dot_v - 1.0;
420 assert!(
421 vt_dot_vc <= dt2,
422 "Geneva camera should pass condition-1 visibility for level-0 east tile; vtDotVc={vt_dot_vc} dt2={dt2}"
423 );
424
425 let antipode_ecef = geodetic_to_ecef(-100.0, -30.0, 5000.0);
429 let v = ecef_to_ellipsoid_scaled(&antipode_ecef);
430 let v_dot_v = v[0] * v[0] + v[1] * v[1] + v[2] * v[2];
431 let p_dot_v = p[0] * v[0] + p[1] * v[1] + p[2] * v[2];
432 let vt_dot_vc = v_dot_v - p_dot_v;
433 let dt2 = v_dot_v - 1.0;
434 let visible = if vt_dot_vc <= dt2 {
435 true
436 } else {
437 let vt_minus_p = [p[0] - v[0], p[1] - v[1], p[2] - v[2]];
438 let lensq = vt_minus_p[0] * vt_minus_p[0]
439 + vt_minus_p[1] * vt_minus_p[1]
440 + vt_minus_p[2] * vt_minus_p[2];
441 (vt_dot_vc * vt_dot_vc) / lensq < dt2
442 };
443 assert!(
444 !visible,
445 "Antipodal camera over Pacific must be culled by level-0 east tile occluder"
446 );
447
448 let _ = A;
450 }
451
452 #[test]
453 fn test_bounding_sphere_at_pole() {
454 let bounds = TileBounds::new(-10.0, 80.0, 10.0, 90.0);
455 let header = QuantizedMeshHeader::from_bounds(&bounds, 0.0, 1000.0);
456
457 assert!(header.center[2] > WGS84_SEMI_MINOR_AXIS * 0.9);
459 assert!(header.bounding_sphere_radius > 0.0);
460 }
461}