San Diego: A Complete Minimal Example

A soup-to-nuts example showing the complete pipeline from downloading data to rendering a beautiful 3D terrain visualization in under 200 lines of code.

San Diego Terrain

Overview

This is the perfect starting point for terrain-maker. It demonstrates the complete workflow:

  1. Download - Automatically fetch SRTM elevation data from NASA

  2. Load - Read DEM files with one function call

  3. Transform - Reproject, flip, scale, and downsample the terrain

  4. Render - Create a beautiful 3D visualization with lighting and materials

The entire script is ~180 lines including comments and argument parsing.

Quick Start

# First time (downloads data from NASA)
python examples/san_diego_demo.py

# Subsequent runs (skip download)
python examples/san_diego_demo.py --skip-download

Requirements:

  • Blender Python API (uv pip install bpy)

  • NASA Earthdata credentials in .env file (see SRTM Data Guide)

The Complete Pipeline

Step 1: Download DEM Data

from src.terrain.dem_downloader import download_dem_by_bbox

bbox = (32.5, -117.6, 33.5, -116.0)  # San Diego County
download_dem_by_bbox(
    bbox=bbox,
    output_dir="data/san_diego_dem",
    username=os.environ.get("EARTHDATA_USERNAME"),
    password=os.environ.get("EARTHDATA_PASSWORD"),
)

The library automatically:

  • Determines which SRTM tiles cover the bounding box

  • Downloads NASADEM tiles from NASA’s Earthdata servers

  • Saves ZIP files for later use

Step 2: Load DEM Files

from src.terrain.data_loading import load_dem_files

dem, transform = load_dem_files("data/san_diego_dem")

The library handles:

  • Extracting HGT files from ZIPs

  • Loading multiple tiles

  • Merging into a single array

  • Preserving geographic transform

Step 3: Create and Transform Terrain

from src.terrain.core import Terrain

terrain = Terrain(dem, transform, dem_crs="EPSG:4326")

# Add transforms: reproject, flip, scale, downsample
terrain.add_transform(reproject_raster("EPSG:4326", "EPSG:32611", num_threads=4))
terrain.add_transform(flip_raster(axis="horizontal"))
terrain.add_transform(scale_elevation(scale_factor=0.0001))
terrain.configure_for_target_vertices(10_000_000, method="average")
terrain.apply_transforms()

What this does:

  • Reprojects from WGS84 (EPSG:4326) to UTM Zone 11N (EPSG:32611)

  • Flips terrain horizontally for correct orientation

  • Scales elevation from meters to Blender units (0.0001)

  • Downsamples to 10M vertices for manageable rendering

Step 4: Setup Colors and Water

from src.terrain.core import elevation_colormap

terrain.set_color_mapping(
    lambda elev: elevation_colormap(elev, cmap_name="plasma"),
    source_layers=["dem"],
)

water_mask = terrain.detect_water_highres(
    slope_threshold=0.0000000000000001,
    fill_holes=False,
    scale_factor=0.0001,
)

Uses the perceptually uniform plasma colormap (viridis family) and detects water bodies like San Diego Bay.

Step 5: Create Mesh

terrain.compute_colors()
mesh = terrain.create_mesh(
    scale_factor=100,
    height_scale=8.0,
    center_model=True,
    boundary_extension=True,
    water_mask=water_mask,
    base_depth=1.0
)

Creates a high-quality 3D mesh with:

  • Smooth fractional edges (boundary_extension)

  • Centered positioning

  • Water base for San Diego Bay

  • Vertex colors baked in

Step 6: Setup Scene and Render

from src.terrain.scene_setup import position_camera_relative, setup_hdri_lighting, create_background_plane
from src.terrain.materials import apply_colormap_material

# Camera positioned south-southwest looking northeast
camera = position_camera_relative(
    mesh,
    direction="south-southwest",
    camera_type="PERSP",
    focal_length=50,
    distance=1.0,
    elevation=1.0,
)

# Realistic sky lighting
setup_hdri_lighting(
    sun_elevation=15.0,      # Mid-afternoon
    sun_rotation=225.0,      # From southwest
    sun_intensity=0.05,
    air_density=0.05,
    visible_to_camera=False,
    sky_strength=1.75
)

# Background plane for shadows
create_background_plane(
    camera=camera,
    mesh_or_meshes=mesh,
    distance_below=0.0,
    color="#000000",
    roughness=0.20,
    size_multiplier=10,
    receive_shadows=True,
)

# Apply eggshell material
apply_colormap_material(mesh.data.materials[0], terrain_material="eggshell")

# Render
render_scene_to_file("san_diego_demo.jpg", width=720, height=576)

Key Features Demonstrated

NASA Data Integration

Shows how to use terrain-maker’s built-in NASA Earthdata downloader to fetch SRTM elevation data programmatically.

Transform Pipeline

Demonstrates the power of terrain-maker’s transform system:

  • Geographic reprojection

  • Data manipulation (flip, scale)

  • Automatic downsampling

High-Quality Rendering

Uses modern Blender features:

  • HDRI sky lighting for realistic illumination

  • Eggshell material for subtle surface sheen

  • Background plane for drop shadows

  • Perceptually uniform colormap

Command-Line Options

# Download and render (default)
python examples/san_diego_demo.py

# Skip download if you have data
python examples/san_diego_demo.py --skip-download

# Setup scene without rendering (for inspection in Blender)
python examples/san_diego_demo.py --no-render

# Custom output directory
python examples/san_diego_demo.py --output-dir renders/

# Custom DEM directory
python examples/san_diego_demo.py --dem-dir data/my_san_diego_tiles/

Key Functions Used

Function

Purpose

download_dem_by_bbox()

Download SRTM tiles from NASA

load_dem_files()

Load and merge DEM files

Terrain()

Main terrain processing class

reproject_raster()

Convert between coordinate systems

elevation_colormap()

Map elevation to colors

position_camera_relative()

Smart camera positioning

setup_hdri_lighting()

Realistic sky lighting

apply_colormap_material()

Apply terrain materials

What Makes This Minimal?

This example is intentionally minimal to show the essentials:

  • No score layers - Just elevation coloring

  • No road overlays - Focus on terrain

  • Single camera view - Not generating multiple angles

  • Straightforward transforms - Common reprojection workflow

  • ~180 lines total - Including imports, args, and comments

For more advanced features, see:

See Also