use std::io::{self, Write};
use martini::Martini;
use quantized_mesh::{
EdgeIndices, EncodeOptions, QUANTIZED_MAX, QuantizedMeshEncoder, QuantizedMeshHeader,
QuantizedVertices, TileBounds, TileMetadata, WaterMask,
};
use crate::normals::{BufferedElevations, buffered_gradient_normals, face_normals};
#[derive(Debug, Clone, Default)]
pub enum NormalMode {
#[default]
None,
FaceNormals,
BufferedGradient(BufferedElevations),
}
#[derive(Debug, Clone)]
pub struct TerrainOptions {
pub max_error: f64,
pub compression_level: u32,
pub normals: NormalMode,
pub water_mask: Option<WaterMask>,
pub metadata: Option<TileMetadata>,
}
impl Default for TerrainOptions {
fn default() -> Self {
Self {
max_error: 1.0,
compression_level: 6,
normals: NormalMode::None,
water_mask: None,
metadata: None,
}
}
}
pub fn encode_terrain_from_fn<F>(
grid_size: u32,
bounds: &TileBounds,
get_height: F,
options: &TerrainOptions,
) -> Vec<u8>
where
F: Fn(u32, u32) -> f64,
{
let (encoder, encode_opts) = build(grid_size, bounds, get_height, options);
encoder.encode_with_options(&encode_opts)
}
pub fn encode_terrain_from_fn_to<F, W>(
grid_size: u32,
bounds: &TileBounds,
get_height: F,
options: &TerrainOptions,
writer: W,
) -> io::Result<()>
where
F: Fn(u32, u32) -> f64,
W: Write,
{
let (encoder, encode_opts) = build(grid_size, bounds, get_height, options);
encoder.encode_to_with_options(writer, &encode_opts)
}
pub fn encode_terrain(
elevations: &[f32],
grid_size: u32,
bounds: &TileBounds,
options: &TerrainOptions,
) -> Vec<u8> {
assert_grid_len(elevations.len(), grid_size);
let gs = grid_size as usize;
encode_terrain_from_fn(
grid_size,
bounds,
|x, y| elevations[y as usize * gs + x as usize] as f64,
options,
)
}
pub fn encode_terrain_to<W: Write>(
elevations: &[f32],
grid_size: u32,
bounds: &TileBounds,
options: &TerrainOptions,
writer: W,
) -> io::Result<()> {
assert_grid_len(elevations.len(), grid_size);
let gs = grid_size as usize;
encode_terrain_from_fn_to(
grid_size,
bounds,
|x, y| elevations[y as usize * gs + x as usize] as f64,
options,
writer,
)
}
fn assert_grid_len(len: usize, grid_size: u32) {
let expected = (grid_size as usize) * (grid_size as usize);
assert_eq!(
len, expected,
"elevations length mismatch: expected {expected} ({grid_size}×{grid_size}), got {len}"
);
}
pub fn curvature_bulge(x: u32, y: u32, grid_size: u32, bounds: &TileBounds) -> f64 {
const EARTH_RADIUS_M: f64 = 6_371_008.8;
let n = (grid_size.saturating_sub(1)).max(1) as f64;
let u = x as f64 / n;
let v = y as f64 / n;
let dlon = (bounds.east - bounds.west).to_radians();
let dlat = (bounds.north - bounds.south).to_radians();
let mid_lat = ((bounds.south + bounds.north) * 0.5).to_radians();
let dlon_eff = dlon * mid_lat.cos();
0.5 * EARTH_RADIUS_M * (dlat * dlat * v * (1.0 - v) + dlon_eff * dlon_eff * u * (1.0 - u))
}
fn build<F>(
grid_size: u32,
bounds: &TileBounds,
get_height: F,
options: &TerrainOptions,
) -> (QuantizedMeshEncoder, EncodeOptions)
where
F: Fn(u32, u32) -> f64,
{
if let NormalMode::BufferedGradient(buf) = &options.normals {
assert_eq!(
buf.tile_grid_size, grid_size,
"BufferedGradient tile_grid_size ({}) must equal encode grid_size ({grid_size})",
buf.tile_grid_size
);
}
let mut martini = Martini::new(grid_size);
let max = (grid_size - 1) as f64;
let tile = martini.create_terrain(|x, y| {
get_height(x as u32, y as u32) + curvature_bulge(x as u32, y as u32, grid_size, bounds)
});
let (positions, indices, _uvs) =
tile.construct_mesh(&mut martini, options.max_error, &mut |(u, v)| {
let gx = (u * max).round();
let gy = ((1.0 - v) * max).round();
(u, v, get_height(gx as u32, gy as u32))
});
let vertex_count = positions.len() / 3;
let mut min_h = f64::INFINITY;
let mut max_h = f64::NEG_INFINITY;
for i in 0..vertex_count {
let h = positions[i * 3 + 2] as f64;
min_h = min_h.min(h);
max_h = max_h.max(h);
}
if vertex_count == 0 {
min_h = 0.0;
max_h = 0.0;
}
let height_span = max_h - min_h;
let quant_max = QUANTIZED_MAX as f64;
let mut vertices = QuantizedVertices::with_capacity(vertex_count);
for i in 0..vertex_count {
let u = positions[i * 3] as f64;
let v = positions[i * 3 + 1] as f64;
let h = positions[i * 3 + 2] as f64;
let uq = (u * quant_max).round().clamp(0.0, quant_max) as u16;
let vq = (v * quant_max).round().clamp(0.0, quant_max) as u16;
let hq = if height_span > 0.0 {
(((h - min_h) / height_span) * quant_max)
.round()
.clamp(0.0, quant_max) as u16
} else {
0
};
vertices.push(uq, vq, hq);
}
let edge_indices = EdgeIndices::from_vertices(&vertices);
let lon_span = bounds.east - bounds.west;
let lat_span = bounds.north - bounds.south;
let geodetic = (0..vertex_count).map(|i| {
let u = positions[i * 3] as f64;
let v = positions[i * 3 + 1] as f64;
let h = positions[i * 3 + 2] as f64;
[bounds.west + u * lon_span, bounds.south + v * lat_span, h]
});
let header = QuantizedMeshHeader::from_bounds_with_vertices_iter(
bounds,
min_h as f32,
max_h as f32,
geodetic,
);
let normals = match &options.normals {
NormalMode::None => None,
NormalMode::FaceNormals => Some(face_normals(&vertices, &indices, bounds, min_h, max_h)),
NormalMode::BufferedGradient(buf) => {
Some(buffered_gradient_normals(&vertices, bounds, buf))
}
};
let encode_opts = EncodeOptions {
include_normals: normals.is_some(),
normals,
include_water_mask: options.water_mask.is_some(),
water_mask: options.water_mask.clone(),
include_metadata: options.metadata.is_some(),
metadata: options.metadata.clone(),
compression_level: options.compression_level,
};
let encoder = QuantizedMeshEncoder::new(header, vertices, indices, edge_indices);
(encoder, encode_opts)
}
#[cfg(test)]
mod tests {
use super::*;
use quantized_mesh::DecodedMesh;
fn bumpy(x: u32, y: u32) -> f64 {
((x as f64) / 8.0).sin() * 50.0 + ((y as f64) / 8.0).cos() * 30.0
}
#[test]
fn flat_high_zoom_tile_collapses_to_two_triangles() {
let bounds = TileBounds::new(139.0, 35.0, 139.01, 35.01);
let bytes = encode_terrain_from_fn(
65,
&bounds,
|_, _| 0.0,
&TerrainOptions {
max_error: 1.0,
compression_level: 0,
..Default::default()
},
);
let mesh = DecodedMesh::decode(&bytes).expect("decode");
assert_eq!(mesh.indices.len(), 6);
assert_eq!(mesh.header.min_height, 0.0);
assert_eq!(mesh.header.max_height, 0.0);
assert!(mesh.vertices.height.iter().all(|&h| h == 0));
}
#[test]
fn flat_low_zoom_tile_subdivides_for_curvature_keeping_heights_flat() {
let bounds = TileBounds::new(0.0, 0.0, 90.0, 45.0);
let mesh = DecodedMesh::decode(&encode_terrain_from_fn(
65,
&bounds,
|_, _| 0.0,
&TerrainOptions {
max_error: 1.0,
compression_level: 0,
..Default::default()
},
))
.expect("decode");
assert!(
mesh.indices.len() > 6,
"curvature must subdivide a wide flat tile, got {} indices",
mesh.indices.len()
);
assert_eq!(mesh.header.min_height, 0.0);
assert_eq!(mesh.header.max_height, 0.0);
assert!(mesh.vertices.height.iter().all(|&h| h == 0));
}
#[test]
fn curvature_bulge_zero_on_corners_and_positive_at_centre() {
let b = TileBounds::new(0.0, 0.0, 90.0, 45.0);
assert_eq!(curvature_bulge(0, 0, 65, &b), 0.0);
assert_eq!(curvature_bulge(64, 0, 65, &b), 0.0);
assert_eq!(curvature_bulge(0, 64, 65, &b), 0.0);
assert_eq!(curvature_bulge(64, 64, 65, &b), 0.0);
assert!(curvature_bulge(32, 32, 65, &b) > 0.0);
}
#[test]
fn default_options_gzip_compress() {
let bounds = TileBounds::new(139.0, 35.0, 139.01, 35.01);
let bytes = encode_terrain_from_fn(65, &bounds, bumpy, &TerrainOptions::default());
assert_eq!(&bytes[0..2], &[0x1f, 0x8b]); }
#[test]
fn height_range_matches_decoded_extremes() {
let bounds = TileBounds::new(139.0, 35.0, 139.01, 35.01);
let bytes = encode_terrain_from_fn(
129,
&bounds,
bumpy,
&TerrainOptions {
max_error: 0.5,
compression_level: 0,
..Default::default()
},
);
let mesh = DecodedMesh::decode(&bytes).expect("decode");
assert_eq!(*mesh.vertices.height.iter().min().unwrap(), 0);
assert_eq!(*mesh.vertices.height.iter().max().unwrap(), QUANTIZED_MAX);
assert!(mesh.header.max_height > mesh.header.min_height);
}
#[test]
fn slice_and_closure_agree() {
let grid_size = 65u32;
let gs = grid_size as usize;
let elevations: Vec<f32> = (0..gs * gs)
.map(|i| bumpy((i % gs) as u32, (i / gs) as u32) as f32)
.collect();
let bounds = TileBounds::new(139.0, 35.0, 139.01, 35.01);
let opts = TerrainOptions {
max_error: 1.0,
compression_level: 0,
..Default::default()
};
let from_slice = encode_terrain(&elevations, grid_size, &bounds, &opts);
let from_fn = encode_terrain_from_fn(
grid_size,
&bounds,
|x, y| elevations[y as usize * gs + x as usize] as f64,
&opts,
);
assert_eq!(from_slice, from_fn);
}
#[test]
fn writer_form_matches_vec_form() {
let bounds = TileBounds::new(139.0, 35.0, 139.01, 35.01);
let opts = TerrainOptions {
max_error: 1.0,
compression_level: 6,
..Default::default()
};
let vec_form = encode_terrain_from_fn(129, &bounds, bumpy, &opts);
let mut writer_form = Vec::new();
encode_terrain_from_fn_to(129, &bounds, bumpy, &opts, &mut writer_form).unwrap();
assert_eq!(vec_form, writer_form);
}
#[test]
fn face_normals_are_emitted_and_unit_length() {
let bounds = TileBounds::new(139.0, 35.0, 139.01, 35.01);
let bytes = encode_terrain_from_fn(
65,
&bounds,
bumpy,
&TerrainOptions {
max_error: 1.0,
compression_level: 0,
normals: NormalMode::FaceNormals,
..Default::default()
},
);
let mesh = DecodedMesh::decode(&bytes).expect("decode");
let normals = mesh.extensions.normals.expect("normals present");
assert_eq!(normals.len(), mesh.vertices.len());
for n in &normals {
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
assert!(
(len - 1.0).abs() < 0.05,
"normal not ~unit: {n:?} (len {len})"
);
}
}
#[test]
fn buffered_gradient_normals_are_emitted() {
let grid_size = 65u32;
let buffer = 1u32;
let full = (grid_size + 2 * buffer) as usize;
let mut buffered = Vec::with_capacity(full * full);
for j in 0..full {
for i in 0..full {
let x = i as i64 - buffer as i64;
let y = j as i64 - buffer as i64;
buffered.push(bumpy(x.max(0) as u32, y.max(0) as u32));
}
}
let buffered = BufferedElevations::new(buffered, grid_size, buffer);
let bounds = TileBounds::new(139.0, 35.0, 139.01, 35.01);
let bytes = encode_terrain_from_fn(
grid_size,
&bounds,
bumpy,
&TerrainOptions {
max_error: 1.0,
compression_level: 0,
normals: NormalMode::BufferedGradient(buffered),
..Default::default()
},
);
let mesh = DecodedMesh::decode(&bytes).expect("decode");
let normals = mesh.extensions.normals.expect("normals present");
assert_eq!(normals.len(), mesh.vertices.len());
}
#[test]
#[should_panic(expected = "elevations length mismatch")]
fn slice_length_mismatch_panics() {
let bounds = TileBounds::new(139.0, 35.0, 139.01, 35.01);
encode_terrain(&[0.0f32; 10], 65, &bounds, &TerrainOptions::default());
}
}