hmm is a heightmap meshing utility.
If you've done any 3D game development, 3D printing, or other such things, you've likely wanted to convert a grayscale heightmap image into a 3D mesh. The naive way is pretty simple but generates huge meshes with millions of triangles. After hacking my way through various solutions over the years, I finally decided I needed to write a good tool for this purpose.
hmm is a modern implementation of a nice algorithm from the 1995 paper
Fast Polygonal Approximation of Terrains and Height Fields
by Garland and Heckbert. The meshes produced by
hmm satisfy the Delaunay
condition and can satisfy a specified maximal error or maximal number of
triangles or vertices. It's also very fast.
- C++11 or higher
brew install glm # on macOS sudo apt-get install libglm-dev # on Ubuntu / Debian git clone https://github.com/fogleman/hmm.git cd hmm make make install
heightmap meshing utility usage: hmm --zscale=float [options] ... infile outfile.stl options: -z, --zscale z scale relative to x & y (float) -x, --zexagg z exaggeration (float [=1]) -e, --error maximum triangulation error (float [=0.001]) -t, --triangles maximum number of triangles (int [=0]) -p, --points maximum number of vertices (int [=0]) -b, --base solid base height (float [=0]) --level auto level input to full grayscale range --invert invert heightmap --blur gaussian blur sigma (int [=0]) --gamma gamma curve exponent (float [=0]) --border-size border size in pixels (int [=0]) --border-height border z height (float [=1]) --normal-map path to write normal map png (string [=]) --shade-path path to write hillshade png (string [=]) --shade-alt hillshade light altitude (float [=45]) --shade-az hillshade light azimuth (float [=0]) -q, --quiet suppress console output -?, --help print this message
hmm supports a variety of file formats like PNG, JPG, etc. for the input
heightmap. The output is always a binary STL file. The only other required
-z, which specifies how much to scale the Z axis in the output
$ hmm input.png output.stl -z ZSCALE
You can also provide a maximal allowed error, number of triangles, or number of vertices. (If multiple are specified, the first one reached is used.)
$ hmm input.png output.stl -z 100 -e 0.001 -t 1000000
Click on the image below to see examples of various command line arguments. You can try these examples yourself with this heightmap: gale.png.
-z parameter defines the distance between a fully black pixel
and a fully white pixel in the vertical Z axis, with units equal to one pixel
width or height. For example, if each pixel in the heightmap represented a 1x1
meter square area, and the vertical range of the heightmap was 100 meters, then
-z 100 should be used.
-x parameter is simply an extra multiplier on top of the provided Z
scale. It is provided as a convenience so you don't have to do multiplication
in your head just to exaggerate by, e.g. 2x, since Z scales are often derived
from real world data and can have strange values like 142.2378.
-e parameter defines the maximum allowed error in the output mesh, as a
percentage of the total mesh height. For example, if
-e 0.01 is used, then no
pixel will have an error of more than 1% of the distance between a fully black
pixel and a fully white pixel. This means that for an 8-bit input image, an
e = 1 / 256 ~= 0.0039 will ensure that no pixel has an error greater
than one full grayscale unit. (It may still be desirable to use a lower value
0.5 / 256.)
-b option is used to create a solid mesh, it defines the height of
the base before the lowest part of the heightmesh appears, as a percentage of
the heightmap's height. For example, if
-z 100 -b 0.5 were used, then the
final mesh would be about 150 units tall (if a fully white pixel exists in the
A border can be added to the mesh with the
--border-height flags. The heightmap will be padded by
before triangulating. The (pre-scaled) Z value of the border can be set with
border-height which defaults to 1.
A Gaussian blur can be applied with the
--blur flag. This is particularly
useful for noisy images.
The heightmap can be inverted with the
--invert flag. This is useful for
The heightmap can be auto-leveled with the
--level flag. This will stretch
the grayscale values to use the entire black => white range.
A gamma curve can be applied to the heightmap with the
--gamma flag. This
x = x ^ gamma to each pixel, where
x is in [0, 1].
A full resolution normal map
can be generated with the
--normal-map argument. This will save a normal map
as an RGB PNG to the specified path. This is useful for rendering higher
resolution bumps and details while using a lower resolution triangle mesh.
A grayscale hillshade image can be generated with the
The altitude and azimuth of the light source can be changed with the
--shade-az arguments, which default to 45 degrees in
altitude and 0 degrees from north (up).
Performance depends a lot on the amount of detail in the heightmap, but here are some figures for an example heightmap of a 40x40 kilometer area centered on Mount Everest. Various heightmap resolutions and permitted max errors are shown. Times computed on a 2018 13" MacBook Pro (2.7 GHz Intel Core i7).
Runtime in Seconds
|Image Size / Error||e=0.01||e=0.001||e=0.0005||e=0.0001|
|9490 x 9490 px (90.0 MP)||6.535||13.102||19.394||58.949|
|4745 x 4745 px (22.5 MP)||1.867||4.903||8.886||33.327|
|2373 x 2373 px (5.6 MP)||0.559||2.353||4.930||14.243|
|1187 x 1187 px (1.4 MP)||0.168||1.021||1.961||3.709|
Number of Triangles Output
|Image Size / Error||e=0.01||e=0.001||e=0.0005||e=0.0001|
|9490 x 9490 px (90.0 MP)||33,869||1,084,972||2,467,831||14,488,022|
|4745 x 4745 px (22.5 MP)||33,148||1,032,263||2,323,772||11,719,491|
|2373 x 2373 px (5.6 MP)||31,724||935,787||1,979,227||6,561,070|
|1187 x 1187 px (1.4 MP)||27,275||629,352||1,160,079||2,347,713|
- reconstruct grayscale image?
- better error handling, especially for file I/O
- better overflow handling - what's the largest supported heightmap?
- mesh validation?