CLI to automate shaded relief map creation in Blender (Huffman method)
Project description
relievo
relievo is a CLI that drives Daniel Huffman's shaded relief workflow without touching Blender's GUI. Give it a geographic bounding box and a .blend template and it downloads the elevation data, prepares the DEM, runs Blender headlessly and delivers a render. Add --color-relief for a hypsometric colour tint, --clip-mask polygon.geojson to cut the result to your exact polygon and --color-relief-mode separate to get the composite and the raw colour layer - all scriptable, all reproducible.
relievo requires Blender installed locally and available in your PATH (or passed via --blender).
Think of it as the Blender shaded-relief workflow you already know, but without opening Blender, wiring nodes by hand, or clicking through half a dozen panels before your first render. Same idea, same cartographic spirit, just with a friendlier CLI and fewer opportunities to get lost in the interface.
Inspired by Daniel Huffman's Blender method, Nick Underwood's blenderize.sh and Kyaw Naing Win's OpenTopography DEM Downloader QGIS plugin - which pioneered bringing the OpenTopography API directly into a geospatial workflow.
| Shaded relief | Color layer | Combined |
|---|---|---|
And this is the Blender setup it automates behind the scenes, so you can keep the power and skip the interface gymnastics:
Contents
- Features
- How it works
- Installation
- Quick start
- All options
- Workflows and examples
- Bounding box format
- Creating your own Blender template
- Colour ramp format
- Available DEM datasets
- OpenTopography API key
- Cartographic best practices
- Tips and caveats
- Notes on authorship and tooling
Features
- Zero GUI - Blender runs headlessly; the whole pipeline is a single shell command.
- Flexible DEM source - download automatically from OpenTopography (SRTM 30 m / 90 m, NASADEM, Copernicus 30 m / 90 m, ALOS 30 m…) or bring your own GeoTIFF. An API key is only needed for the download step.
- Hypsometric tint - composites a colour-by-elevation layer over the render using multiply blending, with three output modes:
overlay,separate,both. - Polygon clipping - cuts the output to any GeoJSON polygon with full alpha transparency.
- Configurable lighting - override sun azimuth and altitude without touching Blender.
- Vertical exaggeration - dial in the drama.
- Resolution control -
--max-sizeand--scalefor everything from quick 10-second previews to print-quality renders. - TOML config profiles - keep per-project defaults in a file; no more long commands.
- Dry run - preview the download size and pixel count before committing.
- CRS reprojection - reproject the DEM to any metric CRS before rendering.
- Elevation filtering - keep only selected elevation ranges (
--filter-values MIN:MAX) for bathymetry-only or land-only workflows.
How it works
GeoJSON bbox
│
▼
Download DEM ← OpenTopography API (optional - only needed without --dem)
(or load local) ← any GeoTIFF with --dem; no API key required
│
▼
Process DEM ← reproject (optional), rescale to UInt16 (Blender-compatible)
│
▼
Blender (headless) ← loads .blend template, applies DEM as displacement map
│
▼
Post-processing ← hypsometric tint (gdaldem), polygon clip (Pillow)
│
▼
output.png
The Blender step follows the Daniel Huffman shaded relief method: the DEM drives a displacement map on a flat plane lit by a sun lamp; an orthographic camera renders the scene from above.
Installation
Install Blender first: blender.org.
Fastest setup from source (recommended for now)
Use micromamba (recommended).
git clone https://github.com/luissevillano/relievo.git
cd relievo
curl -Ls https://micro.mamba.pm/install.sh | bash
# open a new terminal (or restart your shell)
micromamba env create -f environment.yml
micromamba activate relievo
pip install -e .
Install directly from GitHub
If your system already has GDAL and Blender available, you can install the CLI from the repository without cloning it first:
pip install "relievo @ git+https://github.com/LuisSevillano/relievo.git@v0.0.11"
GDAL is intentionally not installed from PyPI because reliable geospatial installs usually depend on system or Conda GDAL libraries. Use the Conda/micromamba setup above if you do not already have GDAL working in your Python environment.
PyPI package
The project metadata is prepared for PyPI publishing. Once a release is published there, installation will be:
pip install relievo
If you already use Conda
conda env create -f environment.yml
conda activate relievo
pip install -e .
Verify
relievo --help
relievo --list-demtypes
Optional: set an OpenTopography API key only if you want automatic DEM downloads (not needed when using --dem with a local file):
export OPENTOPO_API_KEY=your_key_here
Quick start below shows both modes: with automatic download (--api-key / OPENTOPO_API_KEY) and with a local DEM (--dem).
Quick start
export OPENTOPO_API_KEY=your_key_here # only needed if downloading the DEM automatically
relievo \
--bbox examples/bboxes/tenerife_bbox.geojson \
--template template.blend \
--output tenerife.png
No API key? Pass your own DEM - no account needed:
relievo \
--bbox examples/bboxes/tenerife_bbox.geojson \
--template template.blend \
--dem /data/my_dem.tif \
--output tenerife.png
All options
Usage: relievo [OPTIONS]
Options:
--config FILE TOML config file with default option values.
--list-demtypes List all available DEM datasets and exit.
--bbox FILE GeoJSON bounding box polygon in WGS84. [required]
--template FILE Path to the .blend template file. [required]
--output FILE Output image path (.png, .jpg, .jpeg). [default: output.png]
--buffer FLOAT Buffer added to bbox before downloading (e.g. 0.05 = 5%).
--dem FILE Local DEM GeoTIFF - skips the download step entirely.
--save-dem FILE Save the raw downloaded DEM GeoTIFF for reuse with --dem.
--save-processed-dem FILE Save cropped/reprojected DEM in real metres (before UInt16 conversion).
--crs TEXT Reproject DEM to this CRS before rendering (e.g. EPSG:32628).
--demtype TEXT OpenTopography dataset key. [default: COP90]
--api-key TEXT OpenTopography API key (or OPENTOPO_API_KEY env var).
--exaggeration FLOAT Vertical exaggeration factor.
--samples INT Blender Cycles render samples.
--max-size INT Maximum pixels on the longest side of the output.
--scale INT Render resolution percentage (1–100). [default: 100]
--light-azimuth FLOAT Sun azimuth in degrees (0 = North, clockwise).
--light-altitude FLOAT Sun altitude in degrees (0 = horizon, 90 = overhead).
--smooth FLOAT DEM smoothing factor (>1). Blurs terrain before rendering.
--filter-values TEXT Keep only DEM values within MIN:MAX; outside values become NoData.
--color-relief FILE gdaldem colour ramp file for hypsometric tint.
--color-relief-mode TEXT overlay | separate | both. [default: overlay]
--color-relief-blend TEXT multiply | linearburn. [default: multiply]
--clip-mask FILE GeoJSON polygon used to clip the output. PNG keeps transparency; JPEG uses white.
--worldfile Write georeferencing sidecars next to the output image (PGW/JGW/WLD + PRJ).
--dry-run Print estimated download/render info and exit.
--no-render Download and process DEM only; skip Blender.
--blender PATH Path to the Blender executable.
--verbose Detailed progress log.
--keep-workdir Keep the temporary working directory after render.
--help Show this message and exit.
Gallery
All examples below use examples/bboxes/tenerife_bbox.geojson and dem.tif (pre-downloaded) as input. No API key required.
Shaded relief only
relievo \
--bbox examples/bboxes/tenerife_bbox.geojson \
--template template.blend \
--dem dem.tif \
--output relieve.png
+ Hypsometric tint (--color-relief-mode overlay)
relievo \
--bbox examples/bboxes/tenerife_bbox.geojson \
--template template.blend \
--dem dem.tif \
--output relieve.png \
--color-relief examples/ramp_terrain.txt
Alternative style using a QGIS-inspired elevation ramp:
relievo \
--bbox examples/bboxes/tenerife_bbox.geojson \
--template template.blend \
--dem dem.tif \
--output relieve_qgis.png \
--color-relief examples/ramp_elevation_qgis.txt
Alternative style with a subtler atlas look:
relievo \
--bbox examples/bboxes/tenerife_bbox.geojson \
--template template.blend \
--dem dem.tif \
--output relieve_subtle.png \
--color-relief examples/ramp_subtle_atlas.txt
Light from the NW vs south
# NW light - cartographic convention
relievo ... --light-azimuth 315 --light-altitude 35
# South light - dramatic, reveals north-facing slopes
relievo ... --light-azimuth 180 --light-altitude 18
| NW (315°, 35°) | South (180°, 18°) |
|---|---|
--color-relief-mode both - composite + raw colour layer
relievo \
--bbox examples/bboxes/tenerife_bbox.geojson \
--template template.blend \
--dem dem.tif \
--output relieve.png \
--color-relief examples/ramp_terrain.txt \
--color-relief-mode both
# → relieve.png shaded relief + tint
# → relieve_color.png raw colour layer for further compositing
| Composite | Raw colour layer |
|---|---|
Workflows and examples
1. Minimal - download + render
The simplest invocation. The OpenTopography API key is only used here.
relievo \
--bbox examples/bboxes/tenerife_bbox.geojson \
--template template.blend \
--output tenerife.png
2. Using your own DEM
Skip the download entirely. Any GeoTIFF works - local surveys, IGN, Copernicus Land Monitor, USGS, whatever you have. No API key required.
relievo \
--bbox examples/bboxes/tenerife_bbox.geojson \
--template template.blend \
--dem /data/mdt05-canarias.tif \
--output tenerife.png
3. Large-area strategy with mixed land/sea DEMs (--filter-values)
Some high-resolution datasets (30 m / 90 m) cannot cover very large extents in a single OpenTopography request. A practical workaround is to fetch a broader-coverage DEM (for example GEBCO/SRTM15Plus) and filter elevations so you keep only what you need.
Using examples/bboxes/arabigo_bbox.geojson:
# Bathymetry only (<= 0 m)
relievo \
--bbox examples/bboxes/arabigo_bbox.geojson \
--template template.blend \
--demtype GEBCOIceTopo \
--filter-values "-12000:0" \
--output arabigo_bathymetry.jpg
# Altimetry only (>= 0 m)
relievo \
--bbox examples/bboxes/arabigo_bbox.geojson \
--template template.blend \
--demtype GEBCOIceTopo \
--filter-values "0:" \
--output arabigo_altimetry.jpg
This is especially useful when you want to combine broad-coverage bathymetry with a separate, higher-resolution land DEM in post-processing.
--bboxis still used to crop the DEM to the area of interest.
3. Hypsometric colour tint
Composite a colour-by-elevation layer over the render. Requires gdaldem (ships with any GDAL install).
relievo \
--bbox examples/bboxes/tenerife_bbox.geojson \
--template template.blend \
--output tenerife.png \
--color-relief examples/ramp_terrain.txt
The included examples/ramp_terrain.txt covers −500 m (deep water) to 5 000 m (permanent snow). Edit elevation breakpoints and colours freely.
For brighter, less muddy composites (especially when preparing editable layers for PSD/AI), try the Linear Burn + Screen blend method shown by Daniel Huffman in his Photoshop tutorial (which he credits to techniques learned from Tom Patterson and Tanya Buckingham):
relievo \
--bbox examples/bboxes/rectangle_iran.geojson \
--api-key $OPENTOPO_API_KEY \
--demtype SRTM15Plus \
--template template.blend \
--output hormuz_linearburn.jpg \
--color-relief examples/ramp_hormuz.txt \
--color-relief-mode both \
--color-relief-blend linearburn
Quick visual comparison using the same Amazon test scene (--bbox examples/bboxes/south_america.geojson):
| Multiply (default) | Linear Burn + Screen |
|---|---|
Both runs can also export the raw colour layer (--color-relief-mode both) so you can finish compositing in PSD/AI:
docs/images/amazonas_multiply_color.jpgdocs/images/amazonas_linearburn_color.jpg
4. Separate colour layer for compositing
Get the shaded render and the colour layer as independent files - perfect for further compositing in Photoshop, Affinity Photo or GIMP.
# Both composite and raw colour layer
relievo \
--bbox examples/bboxes/tenerife_bbox.geojson \
--template template.blend \
--output tenerife.png \
--color-relief examples/ramp_terrain.txt \
--color-relief-mode both
# → tenerife.png shaded relief + tint composited
# → tenerife_color.png raw colour layer, no shading
# Only the colour layer - render left untouched
relievo \
--bbox examples/bboxes/tenerife_bbox.geojson \
--template template.blend \
--output tenerife.png \
--color-relief examples/ramp_terrain.txt \
--color-relief-mode separate
# → tenerife.png shaded relief, untouched
# → tenerife_color.png raw colour layer
5. Clip to an irregular polygon
Cut the output to any shape - coastlines, administrative boundaries, watersheds - with full alpha transparency.
relievo \
--bbox examples/bboxes/tenerife_bbox.geojson \
--template template.blend \
--dem dem.tif \
--color-relief examples/ramp_terrain.txt \
--color-relief-mode overlay \
--output tenerife_clipped.png \
--clip-mask examples/bboxes/tenerife.geojson
The render extent is taken from --bbox; the clip shape is taken from --clip-mask. PNG output keeps pixels outside the polygon transparent. JPEG output fills those pixels with white.
If you want to place the output directly in GIS software, add --worldfile:
relievo \
--bbox examples/bboxes/tenerife_bbox.geojson \
--template template.blend \
--dem dem.tif \
--output tenerife.jpg \
--worldfile
# → tenerife.jgw
# → tenerife.prj
Worldfile sidecars are written with standard extensions: .pgw for PNG, .jgw for JPEG, plus a .prj file with CRS information.
6. Custom sun position
Override the light direction without opening Blender.
# Classic NW light (standard cartographic convention)
relievo \
--bbox examples/bboxes/tenerife_bbox.geojson \
--template template.blend \
--output tenerife_nw.png \
--light-azimuth 315 \
--light-altitude 35
# Low, dramatic south light
relievo \
--bbox examples/bboxes/tenerife_bbox.geojson \
--template template.blend \
--output tenerife_south.png \
--light-azimuth 180 \
--light-altitude 15
7. Vertical exaggeration
Amplify or flatten terrain drama.
# Subtle - good for high-relief areas like the Alps or Tenerife
relievo --bbox ... --template ... --output out.png --exaggeration 0.6
# Strong - good for flat areas like deltas or coastal plains
relievo --bbox ... --template ... --output out.png --exaggeration 3.0
8. Smooth the DEM to reduce visual noise
For regional or national-scale maps, the raw DEM contains micro-terrain detail (sensor noise, interpolation artifacts, minor ridges) that at small scale reads as visual clutter rather than useful information. --smooth applies a low-pass filter - downsamples the DEM by the given factor (average resampling) then upsamples back (bilinear) - to merge that noise into broader, cleaner landform structures before rendering.
# No smoothing - every terrain artifact rendered at full fidelity
relievo \
--bbox examples/bboxes/tenerife_bbox.geojson \
--template template.blend \
--dem dem.tif \
--output tenerife_sharp.png
# Smooth factor 8 - major landform structures read more clearly
relievo \
--bbox examples/bboxes/tenerife_bbox.geojson \
--template template.blend \
--dem dem.tif \
--output tenerife_smooth.png \
--smooth 8
| No smoothing | --smooth 8 |
|---|---|
Values to try: 2–4 for subtle softening; 6–10 for regional maps where fine detail is not needed. Values above 10 tend to flatten ridges too aggressively.
9. Fast preview - quick iteration
Iterate quickly without waiting for a full render.
# Quarter resolution, 32 samples - done in seconds
relievo \
--bbox examples/bboxes/tenerife_bbox.geojson \
--template template.blend \
--output preview.png \
--scale 25 \
--samples 32
# Cap longest side to 1 000 px
relievo \
--bbox examples/bboxes/tenerife_bbox.geojson \
--template template.blend \
--output preview.png \
--max-size 1000 \
--samples 64
10. Save the DEM and reuse it
Download once, render many times with different templates, sun positions or exaggerations - without hitting the API again.
# Step 1 - download and process DEM; skip rendering
relievo \
--bbox examples/bboxes/tenerife_bbox.geojson \
--template template.blend \
--no-render \
--save-dem dem_tenerife.tif \
--save-processed-dem dem_tenerife_metres.tif # optional: metres copy for GIS inspection
# Step 2 - render from saved DEM, no internet needed
relievo \
--bbox examples/bboxes/tenerife_bbox.geojson \
--template template.blend \
--dem dem_tenerife.tif \
--output tenerife_v1.png \
--light-azimuth 315 --light-altitude 35
relievo \
--bbox examples/bboxes/tenerife_bbox.geojson \
--template template.blend \
--dem dem_tenerife.tif \
--output tenerife_v2.png \
--light-azimuth 135 --light-altitude 25 \
--exaggeration 1.5
--save-demsaves the raw DEM downloaded from OpenTopography (before crop/reproject/UInt16 conversion), so you can rerun with--demwithout another API call.--save-processed-demsaves the cropped/reprojected DEM in real metres (before UInt16 conversion) - handy for GIS inspection and statistics. Blender never reads this file directly.
11. Reproject to a metric CRS
Reprojecting to a projected CRS reduces distortion, especially at high latitudes or for large areas.
--crs accepts any string that GDAL understands: EPSG codes, PROJ strings, WKT, etc.
# Tenerife → UTM zone 28N
relievo \
--bbox examples/bboxes/tenerife_bbox.geojson \
--template template.blend \
--output tenerife_utm.png \
--crs EPSG:32628
# Norway → UTM zone 33N
relievo \
--bbox norway.geojson \
--template template.blend \
--output norway.png \
--crs EPSG:32633
# Pyrenees → ETRS89 / UTM zone 31N (official projection for Spain & France in that band)
relievo \
--bbox pirineos.geojson \
--dem pirineos.tif \
--template template.blend \
--output pirineos.png \
--crs EPSG:25831
Custom PROJ string - Albers Equal Area for the Gulf of Oman
For regions not covered by a standard EPSG code, or when you need a tailor-made
equal-area projection, pass a full PROJ string.
The example below uses Albers Equal Area centred on the Gulf of Oman / Iran,
paired with a custom hypsometric ramp (ramp_hormuz.txt) that blends
bathymetric blues at sea level into desert ochres at altitude:
relievo \
--bbox examples/bboxes/rectangle_iran.geojson \
--api-key $OPENTOPO_API_KEY \
--demtype SRTMGL3 \
--template template.blend \
--output hormuz_relief.png \
--color-relief examples/ramp_hormuz.txt \
--color-relief-mode both \
--crs "+proj=aea +lat_1=27 +lat_2=37 +lat_0=32 +lon_0=54 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs"
Color ramp examples/ramp_hormuz.txt (excerpt):
nv 0 0 0 0 # nodata → transparent
0 14 60 120 # sea level - deep blue
10 80 155 180 # coastal transition
100 215 195 150 # low desert plains
600 180 155 108 # mid plateau
1500 148 118 78 # mountain flanks
3000 225 215 200 # high peaks
| Shaded relief | Color layer | Combined |
|---|---|---|
12. Configuration file
Store per-project defaults in a TOML file and keep commands short. Any CLI option can go in the config file.
profiles/tenerife.toml
template = "template.blend"
demtype = "COP30"
crs = "EPSG:32628"
exaggeration = 1.2
light_azimuth = 315
light_altitude = 35
samples = 256
color_ramp = "examples/ramp_terrain.txt"
color_relief_mode = "both"
# Only specify what changes per run
relievo \
--config profiles/tenerife.toml \
--bbox examples/bboxes/tenerife_bbox.geojson \
--output renders/tenerife_final.png
Command-line options always override the config file.
13. Dry run - estimate without downloading
Preview the bounding box, pixel count and plane dimensions before committing.
relievo \
--bbox examples/bboxes/tenerife_bbox.geojson \
--template template.blend \
--output tenerife.png \
--color-relief examples/ramp_terrain.txt \
--dry-run
Dry run - nothing will be downloaded or rendered.
BBox (original): W=-17.0400 S=27.9400 E=-15.9000 N=28.6200
BBox (buffered): W=-17.0970 S=27.9060 E=-15.8430 N=28.6540 (+5%)
DEM type: COP90 (Copernicus DSM 90m, 3" / ~90m)
Estimated pixels: 4177 × 2620
Blender plane: 4.177 × 2.620 units
CRS: (none - no reprojection)
Render resolution: (from template) @ 100%
Color relief: examples/ramp_terrain.txt (mode: both)
Output → tenerife.png
If you do not pass --buffer, relievo already adds a 5% buffer by default. In most cases this avoids hard edge cuts near the map frame.
If you still see odd border cuts in shaded relief (for example because of reprojection/crop edge effects), increase the buffer explicitly.
# No extra margin (strict bbox)
relievo \
--bbox examples/bboxes/rectangle_iran.geojson \
--demtype SRTM15Plus \
--template template.blend \
--output docs/images/hormuz_shaded_no_buffer.jpg \
--crs "+proj=aea +lat_1=27 +lat_2=37 +lat_0=32 +lon_0=54 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs" \
--buffer 0
# Extra margin around bbox
relievo \
--bbox examples/bboxes/rectangle_iran.geojson \
--demtype SRTM15Plus \
--template template.blend \
--output docs/images/hormuz_shaded_with_buffer.jpg \
--crs "+proj=aea +lat_1=27 +lat_2=37 +lat_0=32 +lon_0=54 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs" \
--buffer 1
--buffer 0 |
--buffer 1 |
|---|---|
14. Batch processing
# Sequential
for region in alps pyrenees carpathians; do
relievo \
--config profiles/${region}.toml \
--bbox bboxes/${region}.geojson \
--output renders/${region}.png
done
# Parallel with GNU parallel
parallel relievo \
--config profiles/{}.toml \
--bbox bboxes/{}.geojson \
--output renders/{}.png \
::: alps pyrenees carpathians andes rockies
Bounding box format
--bbox expects a GeoJSON file with a Polygon or MultiPolygon in WGS84 (EPSG:4326).
examples/bboxes/tenerife_bbox.geojson
{
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[
[-17.04, 27.94],
[-15.90, 27.94],
[-15.90, 28.62],
[-17.04, 28.62],
[-17.04, 27.94]
]]
}
}]
}
For --clip-mask FILE, use a non-rectangular polygon that follows a coastline or boundary for a much cleaner result. --bbox controls the DEM download/crop extent; --clip-mask controls only the final output cut.
Creating your own Blender template
The .blend file must follow the Daniel Huffman shaded relief setup. The script looks for:
| Element | Requirement |
|---|---|
| Object | Named Plane |
| Material | Use Nodes enabled |
| Texture node | An Image Texture node whose Color output is connected to displacement |
| Camera | Orthographic, pointing straight down (−Z) |
| Light | A Sun lamp (required for --light-azimuth / --light-altitude) |
The included template.blend is a ready-to-use starting point. Open it in Blender, adjust materials, atmosphere, displacement strength, render settings - then save and use it as your --template.
relievo only overrides:
- Which DEM texture is loaded
- Plane dimensions and camera ortho scale (derived from DEM pixel size)
- Render resolution (
--max-size,--scale) - Sun rotation (
--light-azimuth,--light-altitude) - Displacement scale (
--exaggeration) - Render samples (
--samples)
Everything else - look, shading, post-processing nodes, output depth - stays exactly as you set it in the GUI.
Colour ramp format
--color-relief expects a plain-text file in gdaldem color-relief format. Elevations in metres, colours in RGB 0–255.
# elevation_m R G B
-500 50 100 200 # deep water
0 70 130 180 # sea level
1 194 178 128 # coast / sand
500 140 180 100 # hills / forest
1500 160 130 90 # sub-alpine
3000 230 220 210 # snow line
5000 255 255 255 # permanent snow
nv 0 0 0 # nodata
A ready-to-use ramp is included at examples/ramp_terrain.txt.
An additional QGIS-inspired option is available at examples/ramp_elevation_qgis.txt (based on the QGIS Hub "Elevation Ramp" style family).
A soft atlas-style option with subtle blues and warm highlights is available at examples/ramp_subtle_atlas.txt.
Available DEM datasets
relievo --list-demtypes
| Key | Dataset | Resolution | Coverage |
|---|---|---|---|
SRTMGL1 |
SRTM 30 m | ~30 m | 56°S – 60°N |
SRTMGL3 |
SRTM 90 m | ~90 m | 56°S – 60°N |
SRTMGL1_E |
SRTM 30 m Ellipsoidal | ~30 m | 56°S – 60°N |
AW3D30 |
ALOS World 3D | ~30 m | Global |
NASADEM |
NASADEM | ~30 m | 56°S – 60°N |
COP30 |
Copernicus DEM 30 m | ~30 m | Global |
COP90 |
Copernicus DEM 90 m | ~90 m | Global |
SRTM15Plus |
SRTM15+ (bathymetric) | ~500 m | Global (ocean + land) |
Use
COP30for areas outside SRTM coverage (Scandinavia, Alaska, high Arctic…). UseSRTM15Plusfor ocean bathymetry - it's the only dataset with negative elevations below sea level.
Choosing the right resolution
Higher resolution is not always better - it means larger downloads, longer processing times and often no visible improvement at the scale you're mapping. Use the minimum resolution that matches your output scale:
| Map area / scale | Recommended resolution | Suggested dataset |
|---|---|---|
| World map, continent | 500 m – 1 km | SRTM15Plus, COP90 |
| Country, large region (e.g. Iberian Peninsula) | ~90 m | SRTMGL3, COP90 |
| Region, autonomous community | ~30 m | SRTMGL1, COP30, NASADEM |
| Province, metropolitan area | 10–30 m | COP30, AW3D30 |
| Local detail, single city | < 5 m | Your own data (LiDAR, IGN, USGS 1 m…) |
Example: Rendering the entire Iberian Peninsula with
SRTMGL1(30 m) downloads ~500 MB and produces ~50 000 × 40 000 pixels - yet the visible difference vs.SRTMGL3(90 m) at a printed A2 map is zero. Use 90 m and save the bandwidth.
OpenTopography API key
An API key is only required when relievo downloads the DEM automatically (i.e., --dem is not used). If you always supply your own data, no account is needed.
- Register for free at opentopography.org
- Use your key in any of these ways:
# Environment variable - set once in your shell profile
export OPENTOPO_API_KEY=your_key_here
# TOML config file
api_key = "your_key_here"
# Per-command
relievo --api-key your_key_here ...
Responsible API use
OpenTopography is a free academic service. Please be a considerate user:
Download once, render many times. Save the DEM on the first run and reuse it for all subsequent iterations - different sun angles, exaggeration values or colour ramps won't need a new download.
# Step 1 - download and save; skip rendering
relievo \
--bbox my_region.geojson \
--template template.blend \
--no-render \
--save-dem my_region.tif
# Step 2 - render from local file, no API call
relievo \
--bbox my_region.geojson \
--template template.blend \
--dem my_region.tif \
--output relief.png
Choose the minimum useful resolution. See the resolution guide above - downloading 30 m data for a continent-scale map wastes bandwidth and produces no visible benefit.
Respect the OpenTopography terms of use. The API is free but rate-limited. Avoid scripting bulk or repeated downloads of the same area.
Cartographic best practices
This section draws heavily from Daniel Huffman's essay "Towards Less Blender-y Relief" (2022). If you make shaded relief maps, read it.
Blender produces beautiful, photorealistic shading - sometimes too beautiful. The "Blender look" is now instantly recognisable: deep shadows, high contrast, every micro-terrain feature rendered in sharp detail. That aesthetic works for some contexts, but it can overwhelm the rest of the map and make terrain harder to read, not easier.
Avoid over-dramatisation
Tone down the vertical exaggeration. It is tempting to push --exaggeration up to make terrain look dramatic, but high exaggeration makes all relief look volcanic. As a starting point:
| Terrain type | Suggested --exaggeration |
|---|---|
| High mountains (Alps, Tenerife, Andes) | 0.5 – 1.0 |
| Medium relief (Pyrenees, Appalachians) | 1.0 – 1.5 |
| Low hills, coastal plains | 2.0 – 3.5 |
| Flat terrain (deltas, steppes) | 3.5 – 6.0 |
Adjust from there based on your visual judgment - these are departure points, not rules.
Keep the light angle subtle. The cartographic convention of NW light (azimuth ~315°, altitude 35–45°) exists for a reason: it reads neutrally. South-facing or very low-angle light creates drama but can make north-facing slopes unreadably dark.
Smooth the DEM before rendering
Blender faithfully renders every noise artifact, sensor stripe and interpolation glitch in the source data. For regional or national-scale maps, pre-smoothing the DEM eliminates visual clutter and makes large landform structures read more clearly - exactly as traditional hand-drawn relief simplifies and generalises terrain.
Use --smooth to apply a low-pass filter built into the pipeline:
# Factor 4 - subtle softening, good for island or province scale
relievo --dem dem.tif --bbox region.geojson --smooth 4 ...
# Factor 8 - stronger smoothing, suitable for country or region scale
relievo --dem dem.tif --bbox region.geojson --smooth 8 ...
See Smooth the DEM to reduce visual noise for a side-by-side comparison.
Relief as background, not foreground
Shaded relief should support the map - labels, roads, boundaries - not compete with it. If your relief is the only element, push the contrast. If it's a basemap layer, pull it back: use --exaggeration on the lower end, consider a gentle hypsometric tint (--color-relief) to add elevation context without adding shadow drama, and keep the sun angle conventional.
"Terrain relief doesn't always need to be dramatic." - Daniel Huffman
Tips and caveats
Render speed - Cycles is GPU-accelerated. Configure Blender under Preferences → System → Cycles Render Devices. A 4 K render at 256 samples takes ~60 s on an RTX 3080; ~15 min CPU-only.
Aspect ratio - The render resolution automatically adapts to the DEM's natural proportions. Use --max-size to control the longest side in pixels without distortion:
relievo --bbox region.geojson --template relief.blend \
--output out.png --max-size 4000
Large areas - OpenTopography rate-limits large requests. For areas wider than ~10°, split into tiles or use a local DEM.
Blender not on PATH - Pass the full path:
# macOS
--blender /Applications/Blender.app/Contents/MacOS/Blender
# Linux
--blender /opt/blender/blender
Debugging - --verbose shows the full GDAL pipeline and Blender log. --keep-workdir preserves the temporary directory for inspecting intermediate files.
Notes on authorship and tooling
Most of my background is in frontend work, especially design, visualisation, and graphics. I also work on backend tasks, where I mainly use R for data analysis and automation. I had prior Python experience in geospatial workflows and satellite imagery analysis, but not enough to comfortably build a CLI of this complexity on my own.
This project was developed with strong support from AI-assisted tooling. I used Codex (GPT-5.3) and Claude (Sonnet 4.6) to accelerate implementation and iteration.
To be clear:
- The product direction, workflow design, and key decisions are mine.
- A significant part of the code was reviewed, adapted, and iterated manually.
- Parts of the codebase were heavily assisted by these models.
Development workflow
Install development tooling:
pip install -e ".[dev]"
pre-commit install
Run quality checks manually:
ruff check relievo/ tests/
pytest tests/ -q
Build and validate package artifacts:
make package
Publish a release:
- Update
pyproject.toml,relievo/__init__.py, andCHANGELOG.mdfor the new version. - Commit the release changes and tag the commit, for example
git tag v0.0.11. - Push
mainand the tag. - Create a GitHub Release from the tag; the
Publishworkflow builds and uploads the package to PyPI using Trusted Publishing.
The repository keeps only curated binary assets under docs/images/ plus template.blend. Generated renders and temporary rasters should remain local.
Versioning policy
relievo follows semantic versioning from the 0.0.1 baseline:
0.0.xfor patch fixes and documentation corrections.0.x.0for backward-compatible feature additions before stable1.0.0.1.x.yafter CLI behavior and interfaces are considered stable.
Every release must have a matching changelog entry in CHANGELOG.md.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file relievo-0.0.11.tar.gz.
File metadata
- Download URL: relievo-0.0.11.tar.gz
- Upload date:
- Size: 83.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1d707c10cf8c15733e9699a472f08283639cac25544608cb78f4a38ceacc3ce6
|
|
| MD5 |
e3936600f18c2c0f9914a674377d2f80
|
|
| BLAKE2b-256 |
9cb3f989b97d36b2a015d5f6557eef8555bc9f2cc650ed41226d9ee9527eccac
|
Provenance
The following attestation bundles were made for relievo-0.0.11.tar.gz:
Publisher:
publish.yml on LuisSevillano/relievo
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
relievo-0.0.11.tar.gz -
Subject digest:
1d707c10cf8c15733e9699a472f08283639cac25544608cb78f4a38ceacc3ce6 - Sigstore transparency entry: 1654264654
- Sigstore integration time:
-
Permalink:
LuisSevillano/relievo@01c17176b46c3fce42ca4c463466227c00de5cc0 -
Branch / Tag:
refs/tags/v0.0.11 - Owner: https://github.com/LuisSevillano
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@01c17176b46c3fce42ca4c463466227c00de5cc0 -
Trigger Event:
release
-
Statement type:
File details
Details for the file relievo-0.0.11-py3-none-any.whl.
File metadata
- Download URL: relievo-0.0.11-py3-none-any.whl
- Upload date:
- Size: 50.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2b06a97ae7541101f7fcc758770ae03ce4b8ed3f14c16492849823160cd38615
|
|
| MD5 |
35ea2616546281c992187f7bf6011c76
|
|
| BLAKE2b-256 |
1859c71c873a23b50206818e29cd3cf0a5a1f842c7779e52b0c55ff7a9dfe922
|
Provenance
The following attestation bundles were made for relievo-0.0.11-py3-none-any.whl:
Publisher:
publish.yml on LuisSevillano/relievo
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
relievo-0.0.11-py3-none-any.whl -
Subject digest:
2b06a97ae7541101f7fcc758770ae03ce4b8ed3f14c16492849823160cd38615 - Sigstore transparency entry: 1654264793
- Sigstore integration time:
-
Permalink:
LuisSevillano/relievo@01c17176b46c3fce42ca4c463466227c00de5cc0 -
Branch / Tag:
refs/tags/v0.0.11 - Owner: https://github.com/LuisSevillano
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@01c17176b46c3fce42ca4c463466227c00de5cc0 -
Trigger Event:
release
-
Statement type: