Fast terrain-RGB tile generator from elevation rasters.
Converts GeoTIFF, VRT, or any GDAL-supported elevation raster into Mapbox or Terrarium terrain-RGB tiles, packaged as PMTiles or MBTiles. Ready to use with MapLibre GL for hillshading and 3D terrain.
Built as a fast Rust replacement for rio-rgbify. Uses all CPU cores via rayon, shows real-time progress, and outputs to modern tile containers — no Python overhead, no guessing when it'll finish.
GDAL must be installed on your system.
| Platform | Command |
|---|---|
| macOS | brew install gdal |
| Ubuntu / Debian | sudo apt install libgdal-dev gdal-bin |
| Fedora / RHEL | sudo dnf install gdal-devel |
| Windows | OSGeo4W or Conda — ensure gdal-config is on your PATH (untested) |
From crates.io
cargo install massifFrom source
git clone https://github.com/mapriot/massif
cd massif
cargo build --release
# Binary is at target/release/massifOn macOS with Homebrew GDAL you may need:
PKG_CONFIG_PATH="/opt/homebrew/lib/pkgconfig" cargo build --releasemassif [OPTIONS] <INPUT> <OUTPUT>
INPUT is any GDAL-supported elevation raster (GeoTIFF, VRT, HGT, etc., any CRS).
OUTPUT is .pmtiles or .mbtiles — the container format is inferred from the extension.
# Fastest — preview / iteration (WebP, no extra compression)
massif input.tif output.pmtiles
# Production — good balance of size and speed
massif --compress 6 input.tif output.pmtiles
# MBTiles output — same flags, different extension
massif --compress 6 input.tif output.mbtiles
# Terrarium encoding
massif --encoding terrarium --compress 6 input.tif output.pmtiles
# PNG tiles
massif --format png --compress 6 input.tif output.pmtiles
# Maximum compression for smallest files (diminishing returns past r=5)
massif --compress 6 -r 5 input.tif output.pmtiles| Flag | Default | Description |
|---|---|---|
--encoding |
mapbox |
RGB encoding: mapbox or terrarium |
--format |
webp |
Tile image format: webp or png |
--compress |
(omitted) | Compression effort 1–9; omit for fastest |
--min-z |
5 |
Minimum zoom level |
--max-z |
12 |
Maximum zoom level |
--nodata |
(from raster) | Override nodata value (e.g. 0, -9999, -32768) |
-j, --workers |
all CPUs | Thread count |
Mapbox encoding only:
| Flag | Default | Description |
|---|---|---|
-b, --base-val |
-10000 |
Base elevation offset |
-i, --interval |
0.1 |
Elevation precision in metres |
-r, --round-digits |
3 |
Zero out lowest N bits of encoded value (reduces entropy) |
GDAL overviews precompute downsampled versions of your raster so massif can read low-zoom tiles cheaply instead of resampling the full-resolution data each time. This reduces processing time by 20–40%.
# Single TIF — writes a sidecar .ovr file, does not modify the input
gdaladdo -ro -r average input.tif 2 4 8 16 32 64 128 256
# VRT — same approach, creates merged.vrt.ovr
gdaladdo -ro -r average merged.vrt 2 4 8 16 32 64 128 256Massif (via GDAL) picks up the .ovr sidecar automatically. The tradeoff is storage: the .ovr file can be as large as the source data itself. If disk space is constrained, skip overviews and run without — massif handles it, just slower.
| Machine | Version | Overviews | Command | Time | Output |
|---|---|---|---|---|---|
| Apple M4 Pro, 14 threads | v0.1.1 | yes | massif |
0:51 | 4,560 MB |
| Apple M4 Pro, 14 threads | v0.1.1 | yes | massif --compress 6 |
4:52 | 2,844 MB |
| Apple M4 Pro, 14 threads | v0.1.1 | no | massif |
2:02 | 4,560 MB |
| Apple M4 Pro, 14 threads | v0.1.1 | no | massif --compress 6 |
6:18 | 2,844 MB |
| Apple M4 Pro, 14 threads | v0.1.0 | no | massif |
2:30 | 4,560 MB |
| Apple M4 Pro, 14 threads | v0.1.0 | yes | massif |
1:28 | 4,560 MB |
| Apple M4 Pro, 14 threads | v0.1.0 | no | massif --compress 6 |
6:29 | 2,844 MB |
| Apple M4 Pro, 14 threads | v0.1.0 | yes | massif --compress 6 |
5:35 | 2,844 MB |
| Xeon Silver 4210, 20 threads | v0.1.0 | no | massif |
7:20 | 4,560 MB |
| Xeon Silver 4210, 20 threads | v0.1.0 | yes | massif |
5:42 | 4,560 MB |
| Xeon Silver 4210, 20 threads | v0.1.0 | no | massif --compress 6 |
16:21 | 2,844 MB |
| Xeon Silver 4210, 20 threads | v0.1.0 | yes | massif --compress 6 |
12:44 | 2,844 MB |
| Xeon Silver 4210, 20 threads | — | no | rio-rgbify |
25:51 | ~2,810 MB |
| Machine | Command | Version | Time | Output |
|---|---|---|---|---|
| Xeon Silver 4210, 20 threads | massif |
v0.1.1 | 1h 36m | 48,062 MB |
| Xeon Silver 4210, 20 threads | massif --compress 6 |
v0.1.1 | 4h 00m | 29,877 MB |
| Xeon Silver 4210, 20 threads | massif |
v0.1.0 | 15h 47m | 48,062 MB |
| Xeon Silver 4210, 20 threads | rio-rgbify |
- | DNF after 48h | — |
rio-rgbify did not finish after 48 hours on the same machine and dataset. All massif tiles are 512×512 lossless WebP images. The Xeon results were measured on a server under normal production load — actual times on an idle machine would be lower.
| Setting | Impact | Notes |
|---|---|---|
| EPSG:4326 input | ~2.5× faster (no compress) | massif skips GDAL transforms entirely; use gdalwarp -t_srs EPSG:4326 |
| GDAL overviews | −20–40% time | Effective for single TIFs; .ovr can match source file size |
| WebP vs PNG | WebP is 2× smaller | Use PNG only if client doesn't support WebP |
--compress 6 |
−38% size vs no compression | Best size/speed tradeoff; gains flatten past 5 |
-r 3 (default) |
−43% size vs r=0 | Biggest lever for file size; invisible for hillshading at most latitudes |
| Terrarium vs Mapbox | Terrarium is 3.1× larger | No round-digits equivalent; use Mapbox when possible |
For full benchmark methodology, all 36 parameter combinations, and recommended settings by use case, see docs/benchmarks.md.
encoded = floor((elevation - base_val) / interval)
R = (encoded >> 16) & 0xFF
G = (encoded >> 8) & 0xFF
B = encoded & 0xFF
MapLibre decodes as:
height = base_val + (R × 65536 + G × 256 + B) × interval
With the defaults (-b -10000 -i 0.1), the encodable range is −10,000 m to +1,677,721.5 m at 0.1 m precision. The -r flag zeroes the lowest N bits of the encoded integer — this reduces entropy for better compression with negligible quality loss for hillshading. Note: -r 3 may produce visible artifacts at high latitudes (e.g. northern Norway, Svalbard, Greenland) where elevation gradients are subtle; use -r 1 or -r 0 for polar regions.
val = elevation + 32768
R = floor(val / 256)
G = floor(val) mod 256
B = floor(frac(val) × 256)
MapLibre decodes as:
height = (R × 256 + G + B / 256) − 32768
Range: −32,768 m to +32,767.996 m at ~0.004 m precision. Used by Mapzen and many open elevation datasets. No configurable parameters — -b, -i, and -r are ignored with a warning.
{
"sources": {
"terrain": {
"type": "raster-dem",
"url": "pmtiles://https://example.com/terrain.pmtiles",
"encoding": "mapbox",
"tileSize": 512
}
},
"terrain": {
"source": "terrain",
"exaggeration": 1.5
},
"layers": [
{
"id": "hillshading",
"type": "hillshade",
"source": "terrain"
}
]
}For Terrarium output, set "encoding": "terrarium" in the source.
Any raster supported by GDAL — GeoTIFF (.tif), Virtual Raster (.vrt), HGT, IMG, and more. Any pixel data type works (Float32, Float64, Int16, UInt16, etc.) — GDAL converts to Float32 internally. The input can be in any CRS; massif reprojects each tile to Web Mercator on the fly.
Common elevation data sources:
- ALOS World 3D
- SRTM
- Copernicus DEM (GLO-30, GLO-90)
MIT — see LICENSE