# How Hillshade Elevation Rendering Works in Prettymaps and Its Dependencies

> Learn how prettymaps renders realistic hillshaded elevation maps using SRTM data and Matplotlib's LightSource for stunning visual overlays.

- Repository: [Marcelo de Oliveira Rosa Prates/prettymaps](https://github.com/marceloprates/prettymaps)
- Tags: deep-dive
- Published: 2026-08-20

---

**Prettymaps creates realistic hillshaded maps by converting raw SRTM elevation data into grayscale illumination using Matplotlib's `LightSource` class, then blending the result as an RGBA overlay with variable transparency.**

The **hillshade elevation rendering** pipeline in [marceloprates/prettymaps](https://github.com/marceloprates/prettymaps) transforms topographic data into shaded relief visualizations. This article explains the complete technical flow—from tile acquisition to final compositing—based on the actual source implementation.

---

## Overview of the Hillshade Pipeline

The hillshade feature follows a four-stage pipeline:

1. **Download** SRTM-1 elevation tiles via the internal fetch module
2. **Read** raster data into NumPy arrays using Rasterio
3. **Compute** illumination values with `matplotlib.colors.LightSource.hillshade()`
4. **Blend** the result as an RGBA layer with inverted alpha transparency

Each stage involves specific dependencies that handle geospatial I/O, numerical computation, and visualization.

---

## Stage 1: Fetching Elevation Data (SRTM-1 Tiles)

Prettymaps retrieves elevation data from the USGS Shuttle Radar Topography Mission (SRTM) dataset. The [`fetch.py`](https://github.com/marceloprates/prettymaps/blob/main/fetch.py) module handles HTTP requests and local caching.

```python

# Conceptual flow in prettymaps/fetch.py

import requests
from pathlib import Path

def fetch_srtm_tile(lat, lon, cache_dir="./SRTM1"):
    """Download SRTM-1 tile for given latitude/longitude."""
    tile_name = f"N{int(lat):02d}W{abs(int(lon)):03d}.hgt"
    url = f"https://e4ftl01.cr.usgs.gov/MEASURES/SRTMGL1.003/2000.02.11/{tile_name}.zip"
    # Download, extract, and return local path

    ...

```

Tiles are stored temporarily in `./SRTM1` and cleaned after rendering.

---

## Stage 2: Reading Raster Data with Rasterio

The `draw_hillshade` function in [`prettymaps/draw.py`](https://github.com/marceloprates/prettymaps/blob/main/prettymaps/draw.py) uses **Rasterio** to open GeoTIFF files and extract elevation arrays.

```python

# From prettymaps/draw.py

import rasterio
import numpy as np

with rasterio.open(elevation_file) as src:
    elevation_data = src.read(1)  # First band contains elevation

    transform = src.transform       # Affine geotransform

    crs = src.crs                   # Coordinate reference system

```

The **geotransform** provides spatial metadata. From this, horizontal (`dx`) and vertical (`dy`) resolutions are computed as the absolute pixel dimensions in meters.

---

## Stage 3: Computing Hillshade Illumination

The core calculation happens via **Matplotlib's `LightSource` class**. An instance `ls` is configured with sun position parameters, then invoked with terrain data.

```python

# Core hillshade calculation in prettymaps/draw.py

from matplotlib.colors import LightSource

ls = LightSource(azimuth=315, altitude=45)  # Default sun position

hillshade = ls.hillshade(
    elevation_data,
    vert_exag=1.0,  # Vertical exaggeration factor

    dx=dx,          # X resolution (meters per pixel)

    dy=dy           # Y resolution (meters per pixel)

)

```

### Parameter Reference

| Parameter | Description | Typical Value |
|-----------|-------------|---------------|
| `azimuth` | Sun direction clockwise from north | 315° (NW) |
| `altitude` | Sun angle above horizon | 45° |
| `vert_exag` | Vertical scale multiplier | 0.5–3.0 |
| `dx`, `dy` | Raster ground resolution | ~30m for SRTM-1 |

The `hillshade()` method returns a **float array in range [0, 1]** where 1.0 represents fully illuminated slopes and 0.0 represents shadowed areas.

---

## Stage 4: RGBA Conversion and Overlay

Prettymaps converts the grayscale hillshade into an RGBA image with strategic transparency. This allows underlying map layers to show through in valleys while preserving terrain definition on ridges.

```python

# RGBA conversion in prettymaps/draw.py

import numpy as np

def hillshade_to_rgba(hillshade):
    """Convert float hillshade to RGBA with inverted alpha."""
    h, w = hillshade.shape
    rgba = np.zeros((h, w, 4), dtype=np.uint8)
    
    # RGB channels: grayscale illumination

    rgba[..., :3] = (hillshade[..., np.newaxis] * 255).astype(np.uint8)
    
    # Alpha channel: inverse shade (dark = more transparent)

    rgba[..., 3] = ((1.0 - hillshade) * 255).astype(np.uint8)
    
    return rgba

hillshade_rgba = hillshade_to_rgba(hillshade)

```

The resulting image is overlaid using Matplotlib's `imshow()` with geographic extent mapping:

```python
ax.imshow(
    hillshade_rgba,
    extent=[west, east, south, north],  # Geographic bounds

    interpolation='bilinear',
    zorder=1  # Layer ordering

)

```

---

## Key Dependencies and Their Roles

| Dependency | Version | Function in Pipeline |
|------------|---------|----------------------|
| **rasterio** | ≥1.3 | Reads SRTM GeoTIFF tiles into NumPy arrays |
| **numpy** | ≥1.21 | Array storage, vectorized math operations |
| **matplotlib** | ≥3.5 | `LightSource` hillshade computation; figure rendering |
| **requests** | ≥2.28 | HTTP client for SRTM tile download |
| **scikit-learn** | optional | `MinMaxScaler` for elevation normalization (commented in source) |

The **matplotlib.colors.LightSource** implementation is the critical dependency. It performs:

- Slope calculation from elevation gradients
- Aspect computation (downhill direction)
- Illumination modeling based on Lambertian reflection

---

## Practical Usage Examples

### Basic Hillshade Activation

```python
import prettymaps as pm

# Enable hillshade with default parameters

pm.plot(
    center=(46.5197, 6.6323),  # Lausanne, Switzerland

    style={"hillshade": {}},
    size=(800, 600)
)

```

### Custom Sun Position and Exaggeration

```python
import prettymaps as pm

pm.plot(
    center=(36.0544, -112.1401),  # Grand Canyon

    style={
        "hillshade": {
            "vert_exag": 2.5,
            "azimuth": 270,   # Sun from west

            "altitude": 30    # Lower sun angle = longer shadows

        }
    },
    size=(1200, 800)
)

```

### Programmatic Access to draw_hillshade

```python
from prettymaps.draw import draw_hillshade
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(10, 10))

# Direct usage with prepared elevation path

draw_hillshade(
    ax=ax,
    elevation_path="./SRTM1/N36W113.hgt",
    extent=[-113, -112, 35, 36],
    vert_exag=1.5,
    azimuth=315,
    altitude=45
)

```

---

## Source File Reference

| File | Lines | Purpose |
|------|-------|---------|
| [`prettymaps/draw.py`](https://github.com/marceloprates/prettymaps/blob/main/prettymaps/draw.py) | 515–560 | `draw_hillshade()` function; RGBA conversion and overlay |
| [`prettymaps/fetch.py`](https://github.com/marceloprates/prettymaps/blob/main/prettymaps/fetch.py) | Full | SRTM tile download and caching logic |
| [`prettymaps/utils.py`](https://github.com/marceloprates/prettymaps/blob/main/prettymaps/utils.py) | Various | Raster metadata extraction (`dx`, `dy` calculation) |

---

## Summary

- **Elevation data** originates from SRTM-1 tiles fetched via HTTP and parsed with **Rasterio**
- **Illumination calculation** delegates to `matplotlib.colors.LightSource.hillshade()` with configurable sun geometry
- **RGBA conversion** inverts the hillshade for alpha transparency, creating blended relief effects
- **Cleanup** removes temporary `./SRTM1` directory after rendering
- The entire pipeline requires only standard scientific Python libraries: **NumPy**, **Rasterio**, and **Matplotlib**

---

## Frequently Asked Questions

### What coordinate system does prettymaps use for hillshade data?

Prettymaps uses **WGS84 (EPSG:4326)** for all geographic operations. SRTM tiles are natively in this CRS, so no reprojection is required during the hillshade pipeline. The `extent` parameter passed to `ax.imshow()` uses `[west, east, south, north]` in decimal degrees.

### Can I use custom elevation data instead of SRTM?

Yes. The `draw_hillshade` function accepts any file path readable by Rasterio. Provide a GeoTIFF with a single elevation band and appropriate geotransform. Set `dx` and `dy` explicitly if your raster resolution differs from SRTM's ~30m.

### Why does low `vert_exag` make terrain appear flat?

Vertical exaggeration scales the Z-axis relative to XY. With `vert_exag=1.0`, slopes appear at true geometric angles. Natural terrain often has gentle gradients; reducing `vert_exag` below 1.0 compresses relief further, while values of 2.0–3.0 enhance visual distinction of subtle features.

### How does the alpha inversion create the blended effect?

By setting `alpha = (1 - hillshade) * 255`, shadowed areas (low `hillshade` values) become more transparent. This lets underlying map layers—roads, buildings, water—remain visible in valleys while ridges appear solid and well-defined.