Skip to main content

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.

Hillshade, running live on the web SDK. The code →

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):

MethodDescription
STANDARDMapLibre's legacy hillshade algorithm.
COMBINEDCombined algorithm based on GDAL.
IGORIgor's soft hillshade (GDAL).
MULTIDIRECTIONALFour light sources for richer relief; ignores the illumination azimuth, uses only its altitude.
BASICBasic algorithm based on GDAL.

Appearance controls​

SetterPurpose
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 / setHighlightColorTint 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 paintHillshadeRasterTileLayer
hillshade-exaggerationcontrast, same number (not exaggeration)
hillshade-method (default standard)hillshadeMethod, STANDARD
hillshade-shadow-color / -highlight-color / -accent-colorshadowColor / 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.