1use std::io::{self, Write};
76
77use martini::Martini;
78use quantized_mesh::{
79 EdgeIndices, EncodeOptions, QUANTIZED_MAX, QuantizedMeshEncoder, QuantizedMeshHeader,
80 QuantizedVertices, TileBounds, TileMetadata, WaterMask,
81};
82
83use crate::normals::{BufferedElevations, buffered_gradient_normals, face_normals};
84
85#[derive(Debug, Clone, Default)]
88pub enum NormalMode {
89 #[default]
91 None,
92 FaceNormals,
95 BufferedGradient(BufferedElevations),
105}
106
107#[derive(Debug, Clone)]
110pub struct TerrainOptions {
111 pub max_error: f64,
114 pub compression_level: u32,
117 pub normals: NormalMode,
119 pub water_mask: Option<WaterMask>,
121 pub metadata: Option<TileMetadata>,
123}
124
125impl Default for TerrainOptions {
126 fn default() -> Self {
127 Self {
128 max_error: 1.0,
129 compression_level: 6,
130 normals: NormalMode::None,
131 water_mask: None,
132 metadata: None,
133 }
134 }
135}
136
137pub fn encode_terrain_from_fn<F>(
155 grid_size: u32,
156 bounds: &TileBounds,
157 get_height: F,
158 options: &TerrainOptions,
159) -> Vec<u8>
160where
161 F: Fn(u32, u32) -> f64,
162{
163 let (encoder, encode_opts) = build(grid_size, bounds, get_height, options);
164 encoder.encode_with_options(&encode_opts)
165}
166
167pub fn encode_terrain_from_fn_to<F, W>(
174 grid_size: u32,
175 bounds: &TileBounds,
176 get_height: F,
177 options: &TerrainOptions,
178 writer: W,
179) -> io::Result<()>
180where
181 F: Fn(u32, u32) -> f64,
182 W: Write,
183{
184 let (encoder, encode_opts) = build(grid_size, bounds, get_height, options);
185 encoder.encode_to_with_options(writer, &encode_opts)
186}
187
188pub fn encode_terrain(
198 elevations: &[f32],
199 grid_size: u32,
200 bounds: &TileBounds,
201 options: &TerrainOptions,
202) -> Vec<u8> {
203 assert_grid_len(elevations.len(), grid_size);
204 let gs = grid_size as usize;
205 encode_terrain_from_fn(
206 grid_size,
207 bounds,
208 |x, y| elevations[y as usize * gs + x as usize] as f64,
209 options,
210 )
211}
212
213pub fn encode_terrain_to<W: Write>(
219 elevations: &[f32],
220 grid_size: u32,
221 bounds: &TileBounds,
222 options: &TerrainOptions,
223 writer: W,
224) -> io::Result<()> {
225 assert_grid_len(elevations.len(), grid_size);
226 let gs = grid_size as usize;
227 encode_terrain_from_fn_to(
228 grid_size,
229 bounds,
230 |x, y| elevations[y as usize * gs + x as usize] as f64,
231 options,
232 writer,
233 )
234}
235
236fn assert_grid_len(len: usize, grid_size: u32) {
237 let expected = (grid_size as usize) * (grid_size as usize);
238 assert_eq!(
239 len, expected,
240 "elevations length mismatch: expected {expected} ({grid_size}×{grid_size}), got {len}"
241 );
242}
243
244fn build<F>(
247 grid_size: u32,
248 bounds: &TileBounds,
249 get_height: F,
250 options: &TerrainOptions,
251) -> (QuantizedMeshEncoder, EncodeOptions)
252where
253 F: Fn(u32, u32) -> f64,
254{
255 if let NormalMode::BufferedGradient(buf) = &options.normals {
256 assert_eq!(
257 buf.tile_grid_size, grid_size,
258 "BufferedGradient tile_grid_size ({}) must equal encode grid_size ({grid_size})",
259 buf.tile_grid_size
260 );
261 }
262
263 let mut martini = Martini::new(grid_size);
264 let max = (grid_size - 1) as f64;
265 let tile = martini.create_terrain(|x, y| get_height(x as u32, y as u32));
266
267 let (positions, indices, _uvs) =
272 tile.construct_mesh(&mut martini, options.max_error, &mut |(u, v)| {
273 let gx = (u * max).round();
274 let gy = ((1.0 - v) * max).round();
275 (u, v, get_height(gx as u32, gy as u32))
276 });
277
278 let vertex_count = positions.len() / 3;
279
280 let mut min_h = f64::INFINITY;
283 let mut max_h = f64::NEG_INFINITY;
284 for i in 0..vertex_count {
285 let h = positions[i * 3 + 2] as f64;
286 min_h = min_h.min(h);
287 max_h = max_h.max(h);
288 }
289 if vertex_count == 0 {
290 min_h = 0.0;
291 max_h = 0.0;
292 }
293 let height_span = max_h - min_h;
294
295 let quant_max = QUANTIZED_MAX as f64;
297 let mut vertices = QuantizedVertices::with_capacity(vertex_count);
298 for i in 0..vertex_count {
299 let u = positions[i * 3] as f64;
300 let v = positions[i * 3 + 1] as f64;
301 let h = positions[i * 3 + 2] as f64;
302 let uq = (u * quant_max).round().clamp(0.0, quant_max) as u16;
303 let vq = (v * quant_max).round().clamp(0.0, quant_max) as u16;
304 let hq = if height_span > 0.0 {
305 (((h - min_h) / height_span) * quant_max)
306 .round()
307 .clamp(0.0, quant_max) as u16
308 } else {
309 0
310 };
311 vertices.push(uq, vq, hq);
312 }
313
314 let edge_indices = EdgeIndices::from_vertices(&vertices);
315
316 let lon_span = bounds.east - bounds.west;
319 let lat_span = bounds.north - bounds.south;
320 let geodetic = (0..vertex_count).map(|i| {
321 let u = positions[i * 3] as f64;
322 let v = positions[i * 3 + 1] as f64;
323 let h = positions[i * 3 + 2] as f64;
324 [bounds.west + u * lon_span, bounds.south + v * lat_span, h]
325 });
326 let header = QuantizedMeshHeader::from_bounds_with_vertices_iter(
327 bounds,
328 min_h as f32,
329 max_h as f32,
330 geodetic,
331 );
332
333 let normals = match &options.normals {
334 NormalMode::None => None,
335 NormalMode::FaceNormals => Some(face_normals(&vertices, &indices, bounds, min_h, max_h)),
336 NormalMode::BufferedGradient(buf) => {
337 Some(buffered_gradient_normals(&vertices, bounds, buf))
338 }
339 };
340
341 let encode_opts = EncodeOptions {
342 include_normals: normals.is_some(),
343 normals,
344 include_water_mask: options.water_mask.is_some(),
345 water_mask: options.water_mask.clone(),
346 include_metadata: options.metadata.is_some(),
347 metadata: options.metadata.clone(),
348 compression_level: options.compression_level,
349 };
350
351 let encoder = QuantizedMeshEncoder::new(header, vertices, indices, edge_indices);
352 (encoder, encode_opts)
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358 use quantized_mesh::DecodedMesh;
359
360 fn bumpy(x: u32, y: u32) -> f64 {
361 ((x as f64) / 8.0).sin() * 50.0 + ((y as f64) / 8.0).cos() * 30.0
362 }
363
364 #[test]
365 fn flat_tile_roundtrips_to_two_triangles() {
366 let bounds = TileBounds::new(139.0, 35.0, 139.01, 35.01);
367 let bytes = encode_terrain_from_fn(
368 65,
369 &bounds,
370 |_, _| 0.0,
371 &TerrainOptions {
372 max_error: 0.0,
373 compression_level: 0,
374 ..Default::default()
375 },
376 );
377
378 let mesh = DecodedMesh::decode(&bytes).expect("decode");
379 assert_eq!(mesh.indices.len(), 6);
381 assert_eq!(mesh.header.min_height, 0.0);
382 assert_eq!(mesh.header.max_height, 0.0);
383 assert!(mesh.vertices.height.iter().all(|&h| h == 0));
385 }
386
387 #[test]
388 fn default_options_gzip_compress() {
389 let bounds = TileBounds::new(139.0, 35.0, 139.01, 35.01);
390 let bytes = encode_terrain_from_fn(65, &bounds, bumpy, &TerrainOptions::default());
391 assert_eq!(&bytes[0..2], &[0x1f, 0x8b]); }
393
394 #[test]
395 fn height_range_matches_decoded_extremes() {
396 let bounds = TileBounds::new(139.0, 35.0, 139.01, 35.01);
397 let bytes = encode_terrain_from_fn(
398 129,
399 &bounds,
400 bumpy,
401 &TerrainOptions {
402 max_error: 0.5,
403 compression_level: 0,
404 ..Default::default()
405 },
406 );
407 let mesh = DecodedMesh::decode(&bytes).expect("decode");
408
409 assert_eq!(*mesh.vertices.height.iter().min().unwrap(), 0);
412 assert_eq!(*mesh.vertices.height.iter().max().unwrap(), QUANTIZED_MAX);
413 assert!(mesh.header.max_height > mesh.header.min_height);
414 }
415
416 #[test]
417 fn slice_and_closure_agree() {
418 let grid_size = 65u32;
419 let gs = grid_size as usize;
420 let elevations: Vec<f32> = (0..gs * gs)
421 .map(|i| bumpy((i % gs) as u32, (i / gs) as u32) as f32)
422 .collect();
423 let bounds = TileBounds::new(139.0, 35.0, 139.01, 35.01);
424 let opts = TerrainOptions {
425 max_error: 1.0,
426 compression_level: 0,
427 ..Default::default()
428 };
429
430 let from_slice = encode_terrain(&elevations, grid_size, &bounds, &opts);
431 let from_fn = encode_terrain_from_fn(
432 grid_size,
433 &bounds,
434 |x, y| elevations[y as usize * gs + x as usize] as f64,
435 &opts,
436 );
437 assert_eq!(from_slice, from_fn);
438 }
439
440 #[test]
441 fn writer_form_matches_vec_form() {
442 let bounds = TileBounds::new(139.0, 35.0, 139.01, 35.01);
443 let opts = TerrainOptions {
444 max_error: 1.0,
445 compression_level: 6,
446 ..Default::default()
447 };
448 let vec_form = encode_terrain_from_fn(129, &bounds, bumpy, &opts);
449
450 let mut writer_form = Vec::new();
451 encode_terrain_from_fn_to(129, &bounds, bumpy, &opts, &mut writer_form).unwrap();
452 assert_eq!(vec_form, writer_form);
453 }
454
455 #[test]
456 fn face_normals_are_emitted_and_unit_length() {
457 let bounds = TileBounds::new(139.0, 35.0, 139.01, 35.01);
458 let bytes = encode_terrain_from_fn(
459 65,
460 &bounds,
461 bumpy,
462 &TerrainOptions {
463 max_error: 1.0,
464 compression_level: 0,
465 normals: NormalMode::FaceNormals,
466 ..Default::default()
467 },
468 );
469 let mesh = DecodedMesh::decode(&bytes).expect("decode");
470 let normals = mesh.extensions.normals.expect("normals present");
471 assert_eq!(normals.len(), mesh.vertices.len());
472 for n in &normals {
473 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
474 assert!(
476 (len - 1.0).abs() < 0.05,
477 "normal not ~unit: {n:?} (len {len})"
478 );
479 }
480 }
481
482 #[test]
483 fn buffered_gradient_normals_are_emitted() {
484 let grid_size = 65u32;
485 let buffer = 1u32;
486 let full = (grid_size + 2 * buffer) as usize;
487 let mut buffered = Vec::with_capacity(full * full);
489 for j in 0..full {
490 for i in 0..full {
491 let x = i as i64 - buffer as i64;
492 let y = j as i64 - buffer as i64;
493 buffered.push(bumpy(x.max(0) as u32, y.max(0) as u32));
494 }
495 }
496 let buffered = BufferedElevations::new(buffered, grid_size, buffer);
497
498 let bounds = TileBounds::new(139.0, 35.0, 139.01, 35.01);
499 let bytes = encode_terrain_from_fn(
500 grid_size,
501 &bounds,
502 bumpy,
503 &TerrainOptions {
504 max_error: 1.0,
505 compression_level: 0,
506 normals: NormalMode::BufferedGradient(buffered),
507 ..Default::default()
508 },
509 );
510 let mesh = DecodedMesh::decode(&bytes).expect("decode");
511 let normals = mesh.extensions.normals.expect("normals present");
512 assert_eq!(normals.len(), mesh.vertices.len());
513 }
514
515 #[test]
516 #[should_panic(expected = "elevations length mismatch")]
517 fn slice_length_mismatch_panics() {
518 let bounds = TileBounds::new(139.0, 35.0, 139.01, 35.01);
519 encode_terrain(&[0.0f32; 10], 65, &bounds, &TerrainOptions::default());
520 }
521}