terrain-codec
Terrain processing utilities for 3D tile generation in Rust.
Ties together martini-rs (RTIN
mesh generation) and quantized-mesh
(Cesium quantized-mesh-1.0 encode/decode), and adds higher-level utilities
that don't fit cleanly in either — most notably seamless vertex normals
computed from a buffer-extended DEM grid, which keeps shading continuous
across tile boundaries.
Installation
[]
= "0.1"
# Optional: enable one or more image-container encoders for
# heightmap::container (each pulls in the `image` crate + its codec).
= { = "0.1", = ["png", "webp", "avif"] }
Feature flags:
| Feature | Adds |
|---|---|
png |
heightmap::container::rgb_to_png + PNG decoding in decode_image |
webp |
heightmap::container::rgb_to_webp (lossless) + WebP decoding |
avif |
heightmap::container::rgb_to_avif (encode-only via ravif) |
What's inside
Re-exports
use ;
Both crates are re-exported as modules, so downstream code can use them through a single dependency.
normals — vertex-normal computation
Two strategies, both returning unit-length ECEF normals (Cesium's convention for oct-encoded normals):
face_normals— accumulate triangle face normals onto vertices. Simple, but produces a visible seam at tile boundaries because each tile only sees its own triangles.buffered_gradient_normals— sample a buffer-extended DEM grid that covers cells beyond the tile on every side. Adjacent tiles read the same samples at any shared physical position, so seam vertices get identical normals on both sides and lighting is continuous.
use ;
let buffered = new;
let normals = buffered_gradient_normals;
heightmap — RGB tile codecs
Symmetric encode/decode pairs for the three common RGB elevation
tile formats:
heightmap::terrarium— Mapzen / Tilezen / Stadia Terrarium.heightmap::mapbox— Mapbox Terrain-RGB.heightmap::gsi— GSI 地理院標高タイル (signed 24-bit, with NaN no-data sentinel).
All operate on raw (R, G, B) byte triplets, so they're agnostic to
the container. Enable one or more of the png, webp, avif cargo
features to wrap them via heightmap::container.
A runtime-dispatched rgb_to_container(ContainerFormat, …) is also
provided for when the format is picked dynamically.
Each format exposes per-pixel and bulk APIs:
use ;
// Per-pixel (for streaming, hot loops, tests)
let rgb_px: = encode_pixel;
let h: f32 = decode_pixel; // 0.0
// Bulk (row-major width × height buffer)
let rgb = encode;
let decoded = decode;
Why buffered normals?
Face-normal accumulation only sees triangles inside the current tile, so the same physical edge is shaded inconsistently from adjacent tiles. Gradient normals computed from a buffer-extended DEM grid use the same samples both tiles can see, so edge vertices get identical normals on both sides. This is the same trick raster pipelines use under names like "padding" or "buffer cells".
The crate ships regression tests that:
- Verify that a perfectly tilted plane produces the analytical ENU normal everywhere (within float tolerance).
- Verify that two adjacent tiles sharing an east/west edge produce bit-identical normals at the seam vertices when both use the same DEM field.
License
MIT OR Apache-2.0