terrain-codec
Terrain processing utilities for 3D tile generation in Rust.
Ties together martini (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 (and …_to_writer) + PNG decoding in decode_image |
webp |
heightmap::container::rgb_to_webp (and …_to_writer, lossless) + WebP decoding |
avif |
heightmap::container::rgb_to_avif (and …_to_writer, 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 ships in four allocation profiles so you can pick the right
one — per-pixel for hot loops, caller-owned buffers for tight memory
reuse, a writer-streaming variant that pairs with the container encoders
for a zero-intermediate-allocation DEM → PNG/WebP/AVIF pipeline, and a
Vec<u8> wrapper for casual use:
use ;
// 1. Per-pixel (for streaming, hot loops, tests)
let rgb_px: = encode_pixel;
let h: f32 = decode_pixel; // 0.0
// 2. Caller-owned buffer (no allocation)
let mut rgb = vec!;
encode_into;
let mut elev_back = vec!;
decode_into;
// 3. Streaming to a writer (4 KiB stack buffer, no intermediate Vec)
let mut file = create?;
// Encode DEM → PNG directly, no intermediate Vec:
let mut rgb = vec!;
encode_into;
rgb_to_png_to_writer?;
// 4. Bulk Vec (convenience wrapper, allocates one Vec)
let rgb = encode;
let decoded = decode;
// Zero-copy view: borrow encoded bytes, decode lazily
let view = new;
let h_at = view.get;
for elev in view.iter
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