Short answer

gdaldem hillshade turns a DEM into a shaded-relief raster by simulating a light source. The output is only trustworthy if three things are right: the DEM is in a projected CRS with matching horizontal and vertical units (so the slope gradients are real), the z-factor correctly reconciles vertical-to-horizontal units, and you have chosen illumination parameters (azimuth, altitude, multidirectional) that suit the terrain. Get those right and the whole thing is a two-command, fully reproducible pipeline: warp, then shade.

What gdaldem hillshade actually computes

Hillshade is the cosine of the angle between the surface normal at each cell and the vector toward a light source. GDAL estimates the surface normal from a 3×3 window of neighbouring cells using a Horn (default) or Zevenbergen–Thorne slope algorithm, then evaluates:

hillshade = 255 × (cos(zenith)·cos(slope) + sin(zenith)·sin(slope)·cos(azimuth − aspect))

Values run 0–255 (0 = full shadow, 255 = fully lit), with 0 typically reserved for NoData. Because the calculation depends on slope and aspect, anything that distorts the gradient — wrong units, wrong CRS, noisy elevations — distorts the shading. The light itself is controlled by two angles:

  • -az azimuth: compass direction the light comes from. Cartographic default is 315° (upper-left / NW). This convention exists because human perception reads NW-lit relief as raised; lighting from the SE tends to invert the apparent topography (the "relief inversion" illusion).
  • -alt altitude: the light's angle above the horizon, default 45°. Lower altitudes (25–30°) exaggerate subtle relief and lengthen shadows; higher altitudes flatten the image.

The z-factor is where most hillshades go wrong

The z-factor multiplies elevation values so they share units with the planar grid. The logic:

  • DEM in metres, grid in metres (e.g. UTM): z = 1. This is the clean case.
  • DEM in US survey feet, grid in metres: z ≈ 0.3048. Without this, vertical relief is overstated ~3.28x and every slope looks like a cliff.
  • DEM in a geographic CRS (degrees): one degree is not one metre, and the metre-per-degree value changes with latitude. GDAL handles this with -s (scale): for an EPSG:4326 DEM in metres, a common approximation is -s 111120. This is a workaround, not a fix — scale error grows toward the poles. Reproject to a projected CRS instead.

If your shaded relief looks far too harsh or implausibly flat, check the z-factor before touching the light angles.

A reproducible two-step pipeline

Step 1 — Inspect and reproject

Never assume the DEM's CRS or units. Read them:

gdalinfo dem_source.tif

Look for the CRS, pixel size (Pixel Size), data type (Type=Float32 is what you want), and NoData Value. If the DEM is geographic or in a far-off projection, warp it to the local UTM zone. For a site at ~46°N, 7°E that is UTM zone 32N, EPSG:32632:

gdalwarp \
  -t_srs EPSG:32632 \
  -r bilinear \
  -tr 10 10 \
  -dstnodata -9999 \
  -co COMPRESS=DEFLATE -co PREDICTOR=2 -co TILED=YES \
  dem_source.tif dem_utm32.tif
  • -r bilinear is appropriate for continuous elevation (use cubic for smoother results; never near for elevation — it produces the stair-stepping that ruins shading).
  • -tr 10 10 sets a deliberate 10 m output resolution; resampling to a wildly different resolution than the source adds noise or loses detail.
  • -dstnodata makes voids explicit so the shade does not treat them as zero elevation.

Step 2 — Generate the hillshade

gdaldem hillshade \
  dem_utm32.tif hillshade.tif \
  -z 1 -az 315 -alt 45 \
  -compute_edges \
  -co COMPRESS=DEFLATE -co TILED=YES
  • -z 1 because both axes are metres now.
  • -compute_edges shades the outermost row and column instead of leaving a 1-pixel NoData frame — important when you tile or mosaic.
  • Add -alg ZevenbergenThorne for smoother terrain; the default Horn algorithm is more robust to noise on rugged DEMs.

Multidirectional for rugged terrain

A single light leaves slopes facing away from it featureless. Multidirectional shading blends illumination from multiple azimuths:

gdaldem hillshade dem_utm32.tif hillshade_multi.tif -multidirectional -compute_edges

This implements the USGS-style multidirectional method (weighted blend of light from ~225°, 270°, 315°, 360°). It recovers detail in steep, multi-aspect terrain at the cost of the crisp directional look. For canyon country and mountains it is usually the better default; for gentle terrain the classic single-direction 315°/45° reads cleaner.

Composite and styling

For presentation, layer the hillshade under a semi-transparent elevation or geology raster. A common recipe in QGIS: load hillshade.tif, set blending mode to Multiply, place a hypsometric-tinted DEM above it at ~50% opacity. To bake a colour-relief blend on the command line:

gdaldem color-relief dem_utm32.tif ramp.txt color.tif
# then blend color.tif over hillshade.tif in your renderer, or with a hillshade-as-alpha approach

Validation

  • Visual sanity against known landforms. Ridgelines should be lit on their NW flanks; valleys should fall into shadow. If ridges look like valleys, you have relief inversion — check the azimuth (likely lit from the SE) or a flipped y-axis.
  • Histogram check. A healthy hillshade fills most of the 0–255 range. A spike at one value usually means a flat-fill artefact or an integer DEM.
  • NoData edges. Open the output and confirm the void areas are NoData, not black (value 0) bleeding into shading.
  • Reproducibility. Keep the exact commands in a Makefile or shell script alongside the source-DEM filename and download date, so the product can be regenerated byte-for-similar from scratch.

Common pitfalls and why they happen

  • Banded, stepped relief. The DEM is stored as integers, so each metre of elevation is a flat plateau and the shading carves terraces. Convert to Float32 (gdal_translate -ot Float32) or source a float DEM.
  • Stair-stepping after resampling. Nearest-neighbour (-r near) was used on continuous data. Re-warp with bilinear or cubic.
  • Over-harsh shading. z-factor too high — usually a feet DEM on a metre grid without z=0.3048, or a geographic DEM without proper scaling.
  • A 1-pixel dark border on every tile. -compute_edges was omitted; the edge rows have no full 3×3 neighbourhood and default to NoData.
  • Inconsistent shading across a wide N–S DEM. It was hillshaded in a geographic CRS, so the degree-to-metre scale drifts with latitude. Reproject first.

Bathyl perspective

A hillshade is the most-shared and least-scrutinised terrain product in any report. We treat it as a derived measurement: reprojected to a low-distortion grid, z-factor verified against the DEM's real units, and produced by a scripted two-step pipeline that records the source DEM and parameters. That way the relief in the figure can be regenerated and defended, not just admired.

Related reading

Sources