1use quantized_mesh::coords::{WGS84_SEMI_MAJOR_AXIS, geodetic_to_ecef};
18use quantized_mesh::{QUANTIZED_MAX, QuantizedVertices, TileBounds};
19
20#[derive(Debug, Clone)]
38pub struct BufferedElevations {
39 pub elevations: Vec<f64>,
42 pub buffer: u32,
44 pub tile_grid_size: u32,
47}
48
49impl BufferedElevations {
50 pub fn new(elevations: Vec<f64>, tile_grid_size: u32, buffer: u32) -> Self {
57 let side = (tile_grid_size + 2 * buffer) as usize;
58 assert_eq!(
59 elevations.len(),
60 side * side,
61 "buffered grid must have {side}×{side} = {} samples, got {}",
62 side * side,
63 elevations.len()
64 );
65 Self {
66 elevations,
67 buffer,
68 tile_grid_size,
69 }
70 }
71
72 #[inline]
74 pub fn buffered_grid_size(&self) -> u32 {
75 self.tile_grid_size + 2 * self.buffer
76 }
77}
78
79pub fn face_normals(
93 vertices: &QuantizedVertices,
94 indices: &[u32],
95 bounds: &TileBounds,
96 min_height: f64,
97 max_height: f64,
98) -> Vec<[f32; 3]> {
99 let vertex_count = vertices.len();
100 let mut normals = vec![[0.0f32; 3]; vertex_count];
101
102 let positions: Vec<[f64; 3]> = (0..vertex_count)
104 .map(|i| {
105 let u = vertices.u[i] as f64 / QUANTIZED_MAX as f64;
106 let v = vertices.v[i] as f64 / QUANTIZED_MAX as f64;
107 let h = vertices.height[i] as f64 / QUANTIZED_MAX as f64;
108
109 let lon = bounds.west + u * (bounds.east - bounds.west);
110 let lat = bounds.south + v * (bounds.north - bounds.south);
111 let height = min_height + h * (max_height - min_height);
112
113 geodetic_to_ecef(lon, lat, height)
114 })
115 .collect();
116
117 for tri in indices.chunks(3) {
118 if tri.len() < 3 {
119 continue;
120 }
121
122 let i0 = tri[0] as usize;
123 let i1 = tri[1] as usize;
124 let i2 = tri[2] as usize;
125
126 let p0 = &positions[i0];
127 let p1 = &positions[i1];
128 let p2 = &positions[i2];
129
130 let v1 = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
134 let v2 = [p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
135
136 let normal = [
137 (v1[1] * v2[2] - v1[2] * v2[1]) as f32,
138 (v1[2] * v2[0] - v1[0] * v2[2]) as f32,
139 (v1[0] * v2[1] - v1[1] * v2[0]) as f32,
140 ];
141
142 for &idx in &[i0, i1, i2] {
143 normals[idx][0] += normal[0];
144 normals[idx][1] += normal[1];
145 normals[idx][2] += normal[2];
146 }
147 }
148
149 for normal in &mut normals {
150 let len = (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt();
151 if len > 1e-6 {
152 normal[0] /= len;
153 normal[1] /= len;
154 normal[2] /= len;
155 } else {
156 *normal = [0.0, 0.0, 1.0];
159 }
160 }
161
162 normals
163}
164
165pub fn buffered_gradient_normals(
182 vertices: &QuantizedVertices,
183 bounds: &TileBounds,
184 buffered: &BufferedElevations,
185) -> Vec<[f32; 3]> {
186 let vertex_count = vertices.len();
187 let buffer_cells = buffered.buffer as usize;
188 let grid_size = buffered.tile_grid_size as usize;
189 let full_grid_size = grid_size + 2 * buffer_cells;
190
191 let tile_lon_span = bounds.east - bounds.west;
192 let tile_lat_span = bounds.north - bounds.south;
193 let cell_lon_deg = tile_lon_span / (grid_size as f64 - 1.0);
197 let cell_lat_deg = tile_lat_span / (grid_size as f64 - 1.0);
198
199 let centre_lat = (bounds.south + bounds.north) * 0.5;
202 let m_per_deg_lat = WGS84_SEMI_MAJOR_AXIS * std::f64::consts::PI / 180.0;
203 let m_per_deg_lon = m_per_deg_lat * centre_lat.to_radians().cos();
204 let cell_m_x = cell_lon_deg * m_per_deg_lon;
205 let cell_m_y = cell_lat_deg * m_per_deg_lat;
206
207 let height_at = |fx: f64, fy: f64| -> f64 {
208 let max_idx = (full_grid_size - 1) as f64;
211 let fx = fx.clamp(0.0, max_idx);
212 let fy = fy.clamp(0.0, max_idx);
213 let x0 = fx.floor() as usize;
214 let y0 = fy.floor() as usize;
215 let x1 = (x0 + 1).min(full_grid_size - 1);
216 let y1 = (y0 + 1).min(full_grid_size - 1);
217 let tx = fx - x0 as f64;
218 let ty = fy - y0 as f64;
219 let h00 = buffered.elevations[y0 * full_grid_size + x0];
220 let h10 = buffered.elevations[y0 * full_grid_size + x1];
221 let h01 = buffered.elevations[y1 * full_grid_size + x0];
222 let h11 = buffered.elevations[y1 * full_grid_size + x1];
223 let bilerp = |a: f64, b: f64, t: f64| -> f64 {
225 if a.is_nan() {
226 b
227 } else if b.is_nan() {
228 a
229 } else {
230 a * (1.0 - t) + b * t
231 }
232 };
233 let top = bilerp(h00, h10, tx);
234 let bot = bilerp(h01, h11, tx);
235 let v = bilerp(top, bot, ty);
236 if v.is_nan() { 0.0 } else { v }
237 };
238
239 let mut normals = Vec::with_capacity(vertex_count);
240 for i in 0..vertex_count {
241 let u_norm = vertices.u[i] as f64 / QUANTIZED_MAX as f64;
242 let v_norm = vertices.v[i] as f64 / QUANTIZED_MAX as f64;
243 let lon = bounds.west + u_norm * tile_lon_span;
244 let lat = bounds.south + v_norm * tile_lat_span;
245
246 let fx_center = buffer_cells as f64 + u_norm * (grid_size as f64 - 1.0);
252 let fy_center = buffer_cells as f64 + (1.0 - v_norm) * (grid_size as f64 - 1.0);
253
254 let h_west = height_at(fx_center - 0.5, fy_center);
258 let h_east = height_at(fx_center + 0.5, fy_center);
259 let h_north = height_at(fx_center, fy_center - 0.5);
260 let h_south = height_at(fx_center, fy_center + 0.5);
261
262 let dh_dx = (h_east - h_west) / cell_m_x; let dh_dy = (h_north - h_south) / cell_m_y; let nx_enu = -dh_dx;
269 let ny_enu = -dh_dy;
270 let nz_enu = 1.0;
271
272 let lat_rad = lat.to_radians();
274 let lon_rad = lon.to_radians();
275 let (sin_lat, cos_lat) = lat_rad.sin_cos();
276 let (sin_lon, cos_lon) = lon_rad.sin_cos();
277 let ex = nx_enu * (-sin_lon) + ny_enu * (-sin_lat * cos_lon) + nz_enu * (cos_lat * cos_lon);
281 let ey = nx_enu * cos_lon + ny_enu * (-sin_lat * sin_lon) + nz_enu * (cos_lat * sin_lon);
282 let ez = ny_enu * cos_lat + nz_enu * sin_lat;
283
284 let len = (ex * ex + ey * ey + ez * ez).sqrt();
285 if len > 1e-12 {
286 normals.push([(ex / len) as f32, (ey / len) as f32, (ez / len) as f32]);
287 } else {
288 normals.push([
291 (cos_lat * cos_lon) as f32,
292 (cos_lat * sin_lon) as f32,
293 sin_lat as f32,
294 ]);
295 }
296 }
297
298 normals
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[test]
309 fn buffered_gradient_matches_analytical_plane() {
310 let grid_size = 65usize;
311 let buffer = 1usize;
312 let full_grid_size = grid_size + 2 * buffer;
313
314 let bounds = TileBounds::new(139.0, 35.0, 139.01, 35.01);
316 let centre_lat = 35.005_f64;
317
318 let m_per_deg_lat = WGS84_SEMI_MAJOR_AXIS * std::f64::consts::PI / 180.0;
319 let m_per_deg_lon = m_per_deg_lat * centre_lat.to_radians().cos();
320 let cell_lon_deg = (bounds.east - bounds.west) / (grid_size as f64 - 1.0);
321 let cell_lat_deg = (bounds.north - bounds.south) / (grid_size as f64 - 1.0);
322 let cell_m_x = cell_lon_deg * m_per_deg_lon;
323 let cell_m_y = cell_lat_deg * m_per_deg_lat;
324
325 let dh_dx = 5.0 / cell_m_x;
327 let dh_dy = 3.0 / cell_m_y;
328
329 let mut buffered_grid = Vec::with_capacity(full_grid_size * full_grid_size);
332 for j in 0..full_grid_size {
333 for i in 0..full_grid_size {
334 let x = (i as f64 - buffer as f64) * cell_m_x;
335 let y = ((full_grid_size - 1 - j) as f64 - buffer as f64) * cell_m_y;
336 buffered_grid.push(dh_dx * x + dh_dy * y + 100.0);
337 }
338 }
339
340 let mut vertices = QuantizedVertices::with_capacity(9);
342 for v_q in [0u16, QUANTIZED_MAX / 2, QUANTIZED_MAX] {
343 for u_q in [0u16, QUANTIZED_MAX / 2, QUANTIZED_MAX] {
344 vertices.push(u_q, v_q, 0);
345 }
346 }
347
348 let buffered_elev = BufferedElevations::new(buffered_grid, grid_size as u32, buffer as u32);
349 let normals = buffered_gradient_normals(&vertices, &bounds, &buffered_elev);
350
351 let n_enu = {
353 let v = [-dh_dx, -dh_dy, 1.0];
354 let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
355 [v[0] / len, v[1] / len, v[2] / len]
356 };
357
358 for (i, n) in normals.iter().enumerate() {
359 let u_norm = vertices.u[i] as f64 / QUANTIZED_MAX as f64;
360 let v_norm = vertices.v[i] as f64 / QUANTIZED_MAX as f64;
361 let lon = bounds.west + u_norm * (bounds.east - bounds.west);
362 let lat = bounds.south + v_norm * (bounds.north - bounds.south);
363 let lat_rad = lat.to_radians();
364 let lon_rad = lon.to_radians();
365 let (sin_lat, cos_lat) = lat_rad.sin_cos();
366 let (sin_lon, cos_lon) = lon_rad.sin_cos();
367 let nx = n[0] as f64;
369 let ny = n[1] as f64;
370 let nz = n[2] as f64;
371 let east_c = -sin_lon * nx + cos_lon * ny;
372 let north_c = -sin_lat * cos_lon * nx + -sin_lat * sin_lon * ny + cos_lat * nz;
373 let up_c = cos_lat * cos_lon * nx + cos_lat * sin_lon * ny + sin_lat * nz;
374 let diff =
375 (east_c - n_enu[0]).abs() + (north_c - n_enu[1]).abs() + (up_c - n_enu[2]).abs();
376 assert!(
377 diff < 1e-3,
378 "vertex {i}: ENU normal mismatch (got [{east_c}, {north_c}, {up_c}], expected {n_enu:?})",
379 );
380 }
381 }
382
383 #[test]
387 fn buffered_gradient_normals_match_across_tile_seam() {
388 let grid_size = 65usize;
389 let buffer = 1usize;
390 let full_grid_size = grid_size + 2 * buffer;
391
392 let bounds_west = TileBounds::new(139.00, 35.0, 139.01, 35.01);
393 let bounds_east = TileBounds::new(139.01, 35.0, 139.02, 35.01);
394
395 let height_field =
398 |lon: f64, lat: f64| -> f64 { (lon * 100.0).sin() * 50.0 + (lat * 80.0).cos() * 30.0 };
399 let cell_lon = (bounds_west.east - bounds_west.west) / (grid_size as f64 - 1.0);
400 let cell_lat = (bounds_west.north - bounds_west.south) / (grid_size as f64 - 1.0);
401
402 let make_buffered = |b: &TileBounds| -> Vec<f64> {
403 let mut out = Vec::with_capacity(full_grid_size * full_grid_size);
404 for j in 0..full_grid_size {
405 let lat = b.north + cell_lat * buffer as f64 - (j as f64) * cell_lat;
406 for i in 0..full_grid_size {
407 let lon = b.west - cell_lon * buffer as f64 + (i as f64) * cell_lon;
408 out.push(height_field(lon, lat));
409 }
410 }
411 out
412 };
413
414 let buf_w =
415 BufferedElevations::new(make_buffered(&bounds_west), grid_size as u32, buffer as u32);
416 let buf_e =
417 BufferedElevations::new(make_buffered(&bounds_east), grid_size as u32, buffer as u32);
418
419 let mut v_west = QuantizedVertices::with_capacity(3);
420 let mut v_east = QuantizedVertices::with_capacity(3);
421 for v_q in [0u16, QUANTIZED_MAX / 2, QUANTIZED_MAX] {
422 v_west.push(QUANTIZED_MAX, v_q, 0);
423 v_east.push(0, v_q, 0);
424 }
425
426 let n_west = buffered_gradient_normals(&v_west, &bounds_west, &buf_w);
427 let n_east = buffered_gradient_normals(&v_east, &bounds_east, &buf_e);
428
429 for (i, (a, b)) in n_west.iter().zip(n_east.iter()).enumerate() {
430 let dot = (a[0] * b[0] + a[1] * b[1] + a[2] * b[2]) as f64;
431 assert!(
432 dot > 0.999_999,
433 "seam vertex {i}: normals diverged (dot = {dot})\nwest = {a:?}\neast = {b:?}",
434 );
435 }
436 }
437
438 #[test]
439 fn face_normals_flat_returns_up() {
440 let bounds = TileBounds::new(-0.01, -0.01, 0.01, 0.01);
443 let mut vertices = QuantizedVertices::with_capacity(4);
444 vertices.push(0, 0, 0);
445 vertices.push(QUANTIZED_MAX, 0, 0);
446 vertices.push(0, QUANTIZED_MAX, 0);
447 vertices.push(QUANTIZED_MAX, QUANTIZED_MAX, 0);
448 let indices = vec![0u32, 1, 2, 1, 3, 2];
449
450 let normals = face_normals(&vertices, &indices, &bounds, 0.0, 1.0);
451 for n in &normals {
452 assert!(n[0] > 0.99, "expected mostly-+X outward normal, got {n:?}");
454 }
455 }
456}