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 {
50 Self::from_bounds_with_vertices_iter(
51 bounds,
52 min_height,
53 max_height,
54 std::iter::empty::<[f64; 3]>(),
55 )
56 }
57
58 pub fn from_bounds_with_vertices_iter<I>(
70 bounds: &TileBounds,
71 min_height: f32,
72 max_height: f32,
73 vertices_geodetic: I,
74 ) -> Self
75 where
76 I: IntoIterator<Item = [f64; 3]>,
77 {
78 let center_lon = bounds.center_lon();
79 let center_lat = bounds.center_lat();
80 let center_height = (min_height as f64 + max_height as f64) / 2.0;
81
82 let center = geodetic_to_ecef(center_lon, center_lat, center_height);
83
84 let (bounding_sphere_center, bounding_sphere_radius) =
85 compute_bounding_sphere(bounds, min_height as f64, max_height as f64);
86
87 let horizon_occlusion_point = compute_horizon_occlusion_point(
88 &bounding_sphere_center,
89 bounds,
90 min_height as f64,
91 max_height as f64,
92 vertices_geodetic,
93 );
94
95 Self {
96 center,
97 min_height,
98 max_height,
99 bounding_sphere_center,
100 bounding_sphere_radius,
101 horizon_occlusion_point,
102 }
103 }
104
105 #[deprecated(
112 since = "0.2.1",
113 note = "Use `from_bounds_with_vertices_iter` to avoid allocating an intermediate `Vec<(f64, f64, f64)>`"
114 )]
115 pub fn from_bounds_with_vertices(
116 bounds: &TileBounds,
117 min_height: f32,
118 max_height: f32,
119 vertices_geodetic: &[(f64, f64, f64)],
120 ) -> Self {
121 Self::from_bounds_with_vertices_iter(
122 bounds,
123 min_height,
124 max_height,
125 vertices_geodetic.iter().map(|&(a, b, c)| [a, b, c]),
126 )
127 }
128
129 pub fn to_bytes(&self) -> [u8; 88] {
131 let mut bytes = [0u8; 88];
132 let mut offset = 0;
133
134 for &v in &self.center {
136 bytes[offset..offset + 8].copy_from_slice(&v.to_le_bytes());
137 offset += 8;
138 }
139
140 bytes[offset..offset + 4].copy_from_slice(&self.min_height.to_le_bytes());
142 offset += 4;
143 bytes[offset..offset + 4].copy_from_slice(&self.max_height.to_le_bytes());
144 offset += 4;
145
146 for &v in &self.bounding_sphere_center {
148 bytes[offset..offset + 8].copy_from_slice(&v.to_le_bytes());
149 offset += 8;
150 }
151
152 bytes[offset..offset + 8].copy_from_slice(&self.bounding_sphere_radius.to_le_bytes());
154 offset += 8;
155
156 for &v in &self.horizon_occlusion_point {
158 bytes[offset..offset + 8].copy_from_slice(&v.to_le_bytes());
159 offset += 8;
160 }
161
162 debug_assert_eq!(offset, 88);
163 bytes
164 }
165
166 pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
168 if bytes.len() < 88 {
169 return None;
170 }
171
172 let mut offset = 0;
173
174 let read_f64 = |bytes: &[u8], offset: &mut usize| -> f64 {
175 let v = f64::from_le_bytes(bytes[*offset..*offset + 8].try_into().unwrap());
176 *offset += 8;
177 v
178 };
179
180 let read_f32 = |bytes: &[u8], offset: &mut usize| -> f32 {
181 let v = f32::from_le_bytes(bytes[*offset..*offset + 4].try_into().unwrap());
182 *offset += 4;
183 v
184 };
185
186 let center = [
187 read_f64(bytes, &mut offset),
188 read_f64(bytes, &mut offset),
189 read_f64(bytes, &mut offset),
190 ];
191
192 let min_height = read_f32(bytes, &mut offset);
193 let max_height = read_f32(bytes, &mut offset);
194
195 let bounding_sphere_center = [
196 read_f64(bytes, &mut offset),
197 read_f64(bytes, &mut offset),
198 read_f64(bytes, &mut offset),
199 ];
200
201 let bounding_sphere_radius = read_f64(bytes, &mut offset);
202
203 let horizon_occlusion_point = [
204 read_f64(bytes, &mut offset),
205 read_f64(bytes, &mut offset),
206 read_f64(bytes, &mut offset),
207 ];
208
209 Some(Self {
210 center,
211 min_height,
212 max_height,
213 bounding_sphere_center,
214 bounding_sphere_radius,
215 horizon_occlusion_point,
216 })
217 }
218}
219
220fn compute_bounding_sphere(
227 bounds: &TileBounds,
228 min_height: f64,
229 max_height: f64,
230) -> ([f64; 3], f64) {
231 let avg_height = (min_height + max_height) / 2.0;
235 let center = geodetic_to_ecef(bounds.center_lon(), bounds.center_lat(), avg_height);
236
237 let points = [
239 geodetic_to_ecef(bounds.west, bounds.south, min_height),
241 geodetic_to_ecef(bounds.east, bounds.south, min_height),
242 geodetic_to_ecef(bounds.west, bounds.north, min_height),
243 geodetic_to_ecef(bounds.east, bounds.north, min_height),
244 geodetic_to_ecef(bounds.west, bounds.south, max_height),
246 geodetic_to_ecef(bounds.east, bounds.south, max_height),
247 geodetic_to_ecef(bounds.west, bounds.north, max_height),
248 geodetic_to_ecef(bounds.east, bounds.north, max_height),
249 geodetic_to_ecef(bounds.west, bounds.center_lat(), max_height),
251 geodetic_to_ecef(bounds.east, bounds.center_lat(), max_height),
252 geodetic_to_ecef(bounds.center_lon(), bounds.south, max_height),
253 geodetic_to_ecef(bounds.center_lon(), bounds.north, max_height),
254 ];
255
256 let mut radius = 0.0f64;
258 for p in &points {
259 let dist = vec3_distance(¢er, p);
260 radius = radius.max(dist);
261 }
262
263 (center, radius)
264}
265
266fn compute_horizon_occlusion_point<I>(
277 bounding_sphere_center: &[f64; 3],
278 bounds: &TileBounds,
279 min_height: f64,
280 max_height: f64,
281 vertices_geodetic: I,
282) -> [f64; 3]
283where
284 I: IntoIterator<Item = [f64; 3]>,
285{
286 let scaled_center = ecef_to_ellipsoid_scaled(bounding_sphere_center);
287 let dir_mag = vec3_magnitude(&scaled_center);
288 if dir_mag < 1e-10 {
289 return [0.0, 0.0, 1.0];
292 }
293 let direction = vec3_normalize(&scaled_center);
294
295 let mut max_magnitude: f64 = 0.0;
300 let mut update = |position: [f64; 3]| {
301 if let Some(m) = compute_occlusion_magnitude(&position, &direction)
302 && m > max_magnitude
303 {
304 max_magnitude = m;
305 }
306 };
307
308 let mut iter = vertices_geodetic.into_iter().peekable();
309 if iter.peek().is_none() {
310 let avg_h = (min_height + max_height) / 2.0;
311 let cx = bounds.center_lon();
312 let cy = bounds.center_lat();
313 for &h in &[min_height, max_height] {
314 update(geodetic_to_ecef(bounds.west, bounds.south, h));
315 update(geodetic_to_ecef(bounds.east, bounds.south, h));
316 update(geodetic_to_ecef(bounds.west, bounds.north, h));
317 update(geodetic_to_ecef(bounds.east, bounds.north, h));
318 }
319 update(geodetic_to_ecef(bounds.west, cy, avg_h));
320 update(geodetic_to_ecef(bounds.east, cy, avg_h));
321 update(geodetic_to_ecef(cx, bounds.south, avg_h));
322 update(geodetic_to_ecef(cx, bounds.north, avg_h));
323 update(geodetic_to_ecef(cx, cy, max_height));
324 } else {
325 for [lon, lat, h] in iter {
326 update(geodetic_to_ecef(lon, lat, h));
327 }
328 }
329
330 if !max_magnitude.is_finite() || max_magnitude <= 0.0 {
331 max_magnitude = 1.0;
334 }
335
336 [
337 direction[0] * max_magnitude,
338 direction[1] * max_magnitude,
339 direction[2] * max_magnitude,
340 ]
341}
342
343fn compute_occlusion_magnitude(position: &[f64; 3], direction: &[f64; 3]) -> Option<f64> {
352 let scaled = ecef_to_ellipsoid_scaled(position);
353 let mag_sq = (scaled[0] * scaled[0] + scaled[1] * scaled[1] + scaled[2] * scaled[2]).max(1.0);
357 let mag = mag_sq.sqrt();
358 let inv_mag = 1.0 / mag;
359 let unit = [
360 scaled[0] * inv_mag,
361 scaled[1] * inv_mag,
362 scaled[2] * inv_mag,
363 ];
364
365 let cos_alpha = unit[0] * direction[0] + unit[1] * direction[1] + unit[2] * direction[2];
366 let cx = unit[1] * direction[2] - unit[2] * direction[1];
368 let cy = unit[2] * direction[0] - unit[0] * direction[2];
369 let cz = unit[0] * direction[1] - unit[1] * direction[0];
370 let sin_alpha = (cx * cx + cy * cy + cz * cz).sqrt();
371 let cos_beta = inv_mag;
372 let sin_beta = (mag_sq - 1.0).max(0.0).sqrt() * inv_mag;
373
374 let denom = cos_alpha * cos_beta - sin_alpha * sin_beta;
375 if denom <= 0.0 {
376 None
377 } else {
378 Some(1.0 / denom)
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385 use crate::coords::WGS84_SEMI_MINOR_AXIS;
386
387 #[test]
390 #[allow(deprecated)]
391 fn from_bounds_with_vertices_iter_matches_slice_form() {
392 let bounds = TileBounds::new(-1.0, -1.0, 1.0, 1.0);
393 let verts_tuples = vec![
394 (-0.5_f64, -0.5, 10.0),
395 (0.5, -0.5, 20.0),
396 (-0.5, 0.5, 30.0),
397 (0.5, 0.5, 40.0),
398 ];
399 let from_slice =
400 QuantizedMeshHeader::from_bounds_with_vertices(&bounds, 0.0, 100.0, &verts_tuples);
401
402 let iter = verts_tuples.iter().map(|&(a, b, c)| [a, b, c]);
404 let from_iter =
405 QuantizedMeshHeader::from_bounds_with_vertices_iter(&bounds, 0.0, 100.0, iter);
406
407 assert_eq!(
408 from_slice.horizon_occlusion_point,
409 from_iter.horizon_occlusion_point
410 );
411 assert_eq!(
412 from_slice.bounding_sphere_center,
413 from_iter.bounding_sphere_center
414 );
415 assert_eq!(
416 from_slice.bounding_sphere_radius,
417 from_iter.bounding_sphere_radius
418 );
419 }
420
421 #[test]
425 fn from_bounds_with_vertices_iter_accepts_flat_f32_stream() {
426 let bounds = TileBounds::new(-1.0, -1.0, 1.0, 1.0);
427 let flat_f32: Vec<f32> = vec![
428 -0.5, -0.5, 10.0, 0.5, -0.5, 20.0, -0.5, 0.5, 30.0, 0.5, 0.5, 40.0, ];
433
434 let iter = flat_f32
435 .chunks_exact(3)
436 .map(|c| [c[0] as f64, c[1] as f64, c[2] as f64]);
437
438 let header = QuantizedMeshHeader::from_bounds_with_vertices_iter(&bounds, 0.0, 100.0, iter);
439
440 let p = header.horizon_occlusion_point;
442 assert!(p.iter().all(|v| v.is_finite()));
443 assert!(p[0].abs() + p[1].abs() + p[2].abs() > 0.0);
444 }
445
446 #[test]
447 fn test_header_serialization_roundtrip() {
448 let header = QuantizedMeshHeader {
449 center: [1.0, 2.0, 3.0],
450 min_height: 100.0,
451 max_height: 200.0,
452 bounding_sphere_center: [4.0, 5.0, 6.0],
453 bounding_sphere_radius: 1000.0,
454 horizon_occlusion_point: [0.1, 0.2, 0.3],
455 };
456
457 let bytes = header.to_bytes();
458 assert_eq!(bytes.len(), 88);
459
460 let parsed = QuantizedMeshHeader::from_bytes(&bytes).unwrap();
461
462 assert_eq!(header.center, parsed.center);
463 assert_eq!(header.min_height, parsed.min_height);
464 assert_eq!(header.max_height, parsed.max_height);
465 assert_eq!(header.bounding_sphere_center, parsed.bounding_sphere_center);
466 assert_eq!(header.bounding_sphere_radius, parsed.bounding_sphere_radius);
467 assert_eq!(
468 header.horizon_occlusion_point,
469 parsed.horizon_occlusion_point
470 );
471 }
472
473 #[test]
474 fn test_header_from_bounds() {
475 let bounds = TileBounds::new(-1.0, -1.0, 1.0, 1.0);
476 let header = QuantizedMeshHeader::from_bounds(&bounds, 0.0, 100.0);
477
478 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);
484 assert_eq!(header.max_height, 100.0);
485 assert!(header.bounding_sphere_radius > 0.0);
486 }
487
488 #[test]
489 fn test_header_default() {
490 let header = QuantizedMeshHeader::default();
491
492 assert_eq!(header.center[0], 0.0);
493 assert_eq!(header.center[1], 0.0);
494 assert!((header.center[2] - WGS84_SEMI_MAJOR_AXIS).abs() < 1.0);
495 }
496
497 #[test]
506 fn test_horizon_occlusion_visible_for_low_longitude_cameras() {
507 use crate::coords::{WGS84_SEMI_MAJOR_AXIS as A, ecef_to_ellipsoid_scaled};
508
509 let bounds = TileBounds::new(0.0, -90.0, 180.0, 90.0);
511 let header = QuantizedMeshHeader::from_bounds(&bounds, -100.0, 6000.0);
512 let p = header.horizon_occlusion_point;
513
514 let camera_ecef = geodetic_to_ecef(6.5, 46.4, 5000.0); let v = ecef_to_ellipsoid_scaled(&camera_ecef);
520 let v_dot_v = v[0] * v[0] + v[1] * v[1] + v[2] * v[2];
521 let p_dot_v = p[0] * v[0] + p[1] * v[1] + p[2] * v[2];
522 let vt_dot_vc = v_dot_v - p_dot_v;
523 let dt2 = v_dot_v - 1.0;
524 assert!(
525 vt_dot_vc <= dt2,
526 "Geneva camera should pass condition-1 visibility for level-0 east tile; vtDotVc={vt_dot_vc} dt2={dt2}"
527 );
528
529 let antipode_ecef = geodetic_to_ecef(-100.0, -30.0, 5000.0);
533 let v = ecef_to_ellipsoid_scaled(&antipode_ecef);
534 let v_dot_v = v[0] * v[0] + v[1] * v[1] + v[2] * v[2];
535 let p_dot_v = p[0] * v[0] + p[1] * v[1] + p[2] * v[2];
536 let vt_dot_vc = v_dot_v - p_dot_v;
537 let dt2 = v_dot_v - 1.0;
538 let visible = if vt_dot_vc <= dt2 {
539 true
540 } else {
541 let vt_minus_p = [p[0] - v[0], p[1] - v[1], p[2] - v[2]];
542 let lensq = vt_minus_p[0] * vt_minus_p[0]
543 + vt_minus_p[1] * vt_minus_p[1]
544 + vt_minus_p[2] * vt_minus_p[2];
545 (vt_dot_vc * vt_dot_vc) / lensq < dt2
546 };
547 assert!(
548 !visible,
549 "Antipodal camera over Pacific must be culled by level-0 east tile occluder"
550 );
551
552 let _ = A;
554 }
555
556 #[test]
557 fn test_bounding_sphere_at_pole() {
558 let bounds = TileBounds::new(-10.0, 80.0, 10.0, 90.0);
559 let header = QuantizedMeshHeader::from_bounds(&bounds, 0.0, 1000.0);
560
561 assert!(header.center[2] > WGS84_SEMI_MINOR_AXIS * 0.9);
563 assert!(header.bounding_sphere_radius > 0.0);
564 }
565}