hmterrain turns a grayscale heightmap into a 3D terrain mesh. It is a fast,
multi-threaded command-line tool and Rust library.
- Adaptive meshes. Flat areas get large triangles and detailed areas get small ones, with a guaranteed maximum error. A typical terrain needs 3% to 30% of the vertices of a full grid.
- Exact full grids when you want one vertex per pixel.
- glTF, OBJ, STL and PLY output, with normals and texture coordinates.
- Solid bases for 3D printing: closed, watertight meshes.
- 16-bit and float input in PNG, TIFF, JPEG, BMP, TGA and PNM. Any aspect ratio.
Real terrain from the samples folder, converted with the default settings at true scale (heights in meters, real pixel size). The renders use 2× vertical exaggeration so the relief is easy to see. Times are for an Apple M4 Pro.
Each row is one command, for example:
hmterrain samples/heightmaps/everest_16bit.png everest.glb --min-height 2169 --max-height 8741 --pixel-size 67.497The height ranges and pixel sizes of all samples are in
samples/heightmaps/metadata.json.
Rebuild this gallery with samples/scripts/build_gallery.py.
--max-error sets the trade-off. The same 512×512 heightmap of Mount St.
Helens at three error limits, in meters:
--max-error 3 |
--max-error 10 |
--max-error 30 |
|---|---|---|
![]() |
![]() |
![]() |
| 51,931 triangles (10.0% of pixels) | 11,603 triangles (2.2% of pixels) | 2,347 triangles (0.5% of pixels) |
--base 300 adds walls and a flat bottom 300 m below the lowest point. Open
assets/st_helens.stl on GitHub to rotate a 20,000
triangle print-ready version in your browser.
Download a binary for Linux, macOS or Windows from the latest release. Every archive has a SHA-256 checksum and a signed build provenance attestation:
gh attestation verify hmterrain-2.0.0-aarch64-apple-darwin.tar.gz --repo shanto462/HeightMapTo3dTerrainOr build from source with Rust 1.88 or newer:
cargo install --locked --git https://github.com/shanto462/HeightMapTo3dTerrainhmterrain heightmap.png terrain.glb --max-height 250Try it with the bundled sample, a 16-bit heightmap of Mount St. Helens at true scale (heights in meters, 13.2 m per pixel), and open the result in any glTF viewer, for example f3d:
hmterrain samples/heightmaps/st_helens_16bit.png st_helens.glb --min-height 942.66 --max-height 2537.14 --pixel-size 13.229
f3d st_helens.glbThe output format comes from the file extension. Some common tasks:
| Task | Command |
|---|---|
| Web viewer or game engine | hmterrain dem.png terrain.glb --max-height 250 |
| 3D print, 0.2 mm per pixel, 3 mm base | hmterrain dem.png print.stl --max-height 40 --pixel-size 0.2 --base 3 |
| Small mesh, error up to 1.5 units | hmterrain dem.png small.obj --max-height 300 --max-error 1.5 |
| Fixed budget of 100k triangles | hmterrain dem.png lod.glb --max-height 300 --max-triangles 100000 |
| One vertex per pixel | hmterrain dem.png full.ply --max-height 300 --mode grid |
| Black is high (depth map) | hmterrain depth.png out.glb --invert |
| Option | Default | Meaning |
|---|---|---|
--min-height Z |
0 |
Height of the lowest pixel. |
--max-height Z |
100 |
Height of the highest pixel. |
--no-normalize |
off | Map black to --min-height and white to --max-height, instead of the darkest and brightest pixel. |
--invert |
off | Bright pixels are low, dark pixels are high. |
--pixel-size SIZE |
1 |
Horizontal distance between two pixels. |
--mode tin|grid |
tin |
Adaptive mesh, or one vertex per pixel. |
-e, --max-error Z |
see below | Largest vertical distance between any pixel and the mesh. |
--max-triangles N |
none | Stop refining before the mesh exceeds N triangles. |
--max-vertices N |
none | Stop refining before the mesh exceeds N vertices. |
--base THICKNESS |
none | Add walls and a flat bottom this far below the lowest point. |
--no-normals, --no-uvs |
off | Leave out vertex normals or texture coordinates. |
--no-center |
off | Put the top-left pixel at the origin instead of centering the mesh. |
-f, --format FMT |
from extension | glb, obj, stl or ply. |
-j, --threads N |
all cores | Worker threads. |
-q, --quiet |
off | Print nothing except errors. |
The default --max-error is 0.1% of the height range. For 8-bit images it is
half a gray level when that is larger, because finer detail would only
reproduce quantization noise.
Y points up. Image columns run along +X and rows along +Z, so the top edge of the image faces −Z. Triangles wind counter-clockwise seen from outside. This matches glTF, Blender (Y-up import), Three.js, Unity and Godot. Texture coordinates map the heightmap image onto the terrain, so the same image (or an aligned satellite photo) can be used as a texture.
| Format | Normals | UVs | Notes |
|---|---|---|---|
.glb |
yes | yes | Binary glTF 2.0. Best for the web and game engines. Up to 4 GiB. |
.obj |
yes | yes | Text, widely supported, largest file. |
.stl |
per face | no | Binary STL for slicers. |
.ply |
yes | yes | Binary little-endian. |
With --base, walls and the bottom have their own vertices so their edges
stay sharp. The mesh is watertight once coincident vertices are merged, which
slicers do on import.
- Load. The image is decoded to luminance at its full precision (8-bit, 16-bit or float) and mapped to the requested height range.
- Triangulate.
tinuses greedy Delaunay insertion (Garland and Heckbert, 1995). It starts with two triangles. Then it keeps inserting the pixel with the largest error until every pixel is within--max-error. After each insertion, edge flips keep the triangulation Delaunay, which avoids thin slivers. Each step only rescans the triangles it changed. All geometric tests use exact integer arithmetic, so regular grids and collinear points never break the mesh.gridsplits every cell into two triangles along the diagonal whose corners have the closer heights, so diagonal ridges stay sharp.
- Build. Area-weighted vertex normals, texture coordinates, and the optional solid base.
- Write. Output is formatted in parallel and written to a temporary file that replaces the target only when complete.
On an Apple M4 Pro, a 2049×2049 16-bit terrain becomes a 0.1%-error mesh with 14% of the pixel count in under a second.
[dependencies]
heightmap-to-3d-terrain = { git = "https://github.com/shanto462/HeightMapTo3dTerrain" }use heightmap_terrain::{export, HeightMapping, Heightmap, Mesh, MeshOptions, TinOptions};
let mapping = HeightMapping { min: 0.0, max: 250.0, ..HeightMapping::default() };
let map = Heightmap::load("dem.png", &mapping)?;
let options = TinOptions { max_error: 0.25, ..TinOptions::default() };
let (triangulation, stats) = heightmap_terrain::triangulate_tin(&map, &options)?;
let mesh = Mesh::build(&triangulation, &map, &MeshOptions::default())?;
export::save(&mesh, export::Format::Glb, "terrain.glb".as_ref())?;Version 2 is a rewrite in Rust that replaces the .NET tool. The v1 command line
hmterrain in.png out.obj -200 200 still works. Version 2 also fixes several
1.x bugs that affected every mesh: wrong height scaling, OBJ face indices off
by one, overlapping triangles, and locale-dependent number formatting. See the
changelog.
Bug reports and pull requests are welcome. See CONTRIBUTING.md for the development setup and checks. Please report security issues privately, as described in SECURITY.md.
The sample heightmaps (except Pasadena) come from the Terrain Tiles open dataset on AWS. See samples/README.md for the sources and attribution.




















