Hillshade (Shaded Relief)
HillshadeRasterTileLayer renders shaded relief from an RGB-encoded elevation data source.
The fork extends it with multiple algorithms, smooth exaggeration, shader contours and a custom
shader base class.
Basic usage
import com.massifmaps.layers.HillshadeRasterTileLayer
import com.massifmaps.layers.HillshadeMethod
import com.massifmaps.datasources.HTTPTileDataSource
val dem = HTTPTileDataSource(0, 12, "https://your.tiles/dem/{z}/{x}/{y}.png").apply {
setMetaDataElement("dem_encoding", Variant("terrarium")) // or "mapbox"
}
val hillshade = HillshadeRasterTileLayer(dem).apply {
setHillshadeMethod(HillshadeMethod.COMBINED)
setExaggeration(1.3f) // smooth per-frame relief factor (shader uniform)
setIlluminationDirection(MapVec(-0.7071, 0.7071, -0.7071)) // azimuth 315°, altitude 45°
}
mapView.layers.add(hillshade)
The illumination is also influenced by the main light source configured on Options.
With the surface API
Every setter below is a property, so the whole layer is one spec:
map.addLayer("hillshade", Spec.of("hillshade")
.set("source", Spec.of("http")
.set("url", "https://your.tiles/dem/{z}/{x}/{y}.png")
.set("maxZoom", 12)
// Attached to every tile the source loads; it is what picks the elevation decoder.
.set("metaData", Spec.object().set("dem_encoding", "terrarium")))
.set("hillshadeMethod", "MULTIDIRECTIONAL")
.set("exaggeration", 1.3)
.set("contrast", 0.6));
// Animatable afterwards — a shader uniform, no re-decode.
map.layer("hillshade").set("exaggeration", 1.8);
Every property and its enum values: hillshade in the layer reference.
Hillshade methods
setHillshadeMethod(...) selects the algorithm (default IGOR):
| Method | Description |
|---|---|
STANDARD | MapLibre's legacy hillshade algorithm. |
COMBINED | Combined algorithm based on GDAL. |
IGOR | Igor's soft hillshade (GDAL). |
MULTIDIRECTIONAL | Four light sources for richer relief; ignores the illumination azimuth, uses only its altitude. |
BASIC | Basic algorithm based on GDAL. |
Appearance controls
| Setter | Purpose |
|---|---|
setExaggeration(float) | Per-frame relief factor (no re-decode). Default 1.0. |
setIlluminationDirection(MapVec) | Light direction vector, (0,0,-1) straight down. Default MapLibre's azimuth 335° at 45° altitude. |
setHeightScale(float) / setExagerateHeightScaleEnabled(bool) | Vertical scale of the elevation. |
setContrast(float) | Shading contrast. |
setShadowColor / setAccentColor / setHighlightColor | Tint the shadow / accent / highlight bands. |
setIlluminationMapRotationEnabled(bool) | Rotate the light with the map. |
Matching MapLibre
The defaults are not MapLibre's. To draw what a MapLibre hillshade layer draws from the same DEM:
| MapLibre paint | HillshadeRasterTileLayer |
|---|---|
hillshade-exaggeration | contrast, same number (not exaggeration) |
hillshade-method (default standard) | hillshadeMethod, STANDARD |
hillshade-shadow-color / -highlight-color / -accent-color | shadowColor / highlightColor / accentColor |
| — (the true slope) | heightScale 1, exaggeration 1 |
heightScale defaults to 0.05, a twentieth of MapLibre's slope; exaggeration multiplies on top of
it. Measured against MapLibre 5.24 at the same camera and DEM zoom, this mapping shades within 0.5%
(internals).
MapLibre up to 5.1 divided the elevation by 4 before the slope and doubled the slope factor, so
against those versions it is exaggeration 0.5.
The facade reads a colour as "#rrggbb", "#rrggbbaa" or an ARGB number; hsl() is not read. A
CartoCSS slot reads any CSS colour.
The DEM source must name its encoding with dem_encoding. Without it the tiles are read as Mapbox
Terrain-RGB, and a Terrarium DEM (Mapterhorn, AWS) then shades 25.6 times too steep. An MBTiles or
PMTiles archive whose own metadata declares "encoding": "terrarium" (or "mapbox") needs nothing:
the source adopts it, and an explicit dem_encoding still wins.
Shader contours
The hillshade layer can also draw contour lines in the fragment shader from the same elevation — see On-the-fly Contours:
hillshade.setContourEnabled(true)
hillshade.setContourInterval(100f)
hillshade.setContourColor(Color(0xFF7A4B28.toInt()))
hillshade.setContourWidth(1.0f)
Custom lighting shader
For full control, enable elevation encoding and provide your own GLSL lighting shader (it can read
getElevation() and getMapZoom()):
hillshade.setElevationEncodingEnabled(true)
hillshade.setNormalMapLightingShader(myGlslSource)
HillshadeRasterTileLayer is a specialization of
CustomRasterTileLayer — the DEM-specific case of the
general custom-shader raster layer.
Sharing the DEM with 3D terrain
The same elevation source can drive both hillshade and 3D terrain.
Wrap it in a MemoryCacheTileDataSource so both features share tiles:
val dem = MemoryCacheTileDataSource(httpDem)
mapView.layers.add(HillshadeRasterTileLayer(dem))
mapView.options.terrainOptions = TerrainOptions(dem).apply { isEnabled = true }
You can also fold hillshade into a single style with the Composite Vector Tile Layer.