Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Real Terrain Assets

srb.assets.scenery.RealTerrain is the entry point for using real Lunar / Martian terrain — actual Digital Elevation Models (DEMs) from NASA/USGS/ESA surveys, baked into simulation-ready meshes — in place of procedurally generated surfaces. Use it when you need geographically accurate, photorealistic scenes. This guide covers the runtime classes; for adding new DEM sources and the offline DEM → mesh → USD bake pipeline, see Adding Terrain Sites.

Basic usage

from srb.assets.scenery import RealTerrain
from srb.assets.scenery.real_terrain_sites import LunarSiteApollo17, MartianSiteJezero

# Generic constructor — body + site name from manifest.
terrain = RealTerrain(body="moon", site="apollo17")

# Or use the per-site shortcut class (auto-generated from manifest).
terrain = LunarSiteApollo17()

# Or an arbitrary lat/lon region — no manifest entry at all (v2 stacks;
# see the terrain-stacks guide for identity/normalization/fallback rules).
terrain = RealTerrain(body="moon", lat_deg=-89.66, lon_deg=0.0, size_m=2000.0)

# Returns an Isaac Lab AssetBaseCfg you can attach to a scene.
env_cfg.scene.terrain = terrain.asset_cfg

Companion assets

Each RealTerrain exposes optional sky / lighting / rocks companions, all configured through the manifest with per-instance overrides:

# Use everything the manifest specifies for this site (recommended).
terrain = LunarSiteApollo17()
terrain.attach_companions(env_cfg)
# → env_cfg.scene now has skydome, lighting, rocks_00, ... attached.

# Override individual companions.
terrain = LunarSiteApollo17(
    sky="apollo17_pan",      # explicit catalog name
    lighting="auto",         # use manifest default (default value)
    rocks=None,              # disable rocks
)

The env path now attaches companions automatically (OI-1, terrain v2 Phase 4): when env.scenery resolves to a RealTerrain with attach_companions_enabled=True (the default), BaseEnvCfg._add_scenery calls attach_companions itself — arbitrated against the env.ephemeris sun/skydome drives, with an all-or-nothing warn-once degrade when companion products cannot be built (the shipped sites’ companion catalog entries are placeholders until operator action O3, so today every curated site degrades and renders as before). Set attach_companions_enabled=False to opt a task out. The precedence table and the rendered-output caveat live in Terrain Stacks → Companions and the ephemeris sun.

Ephemeris-driven lighting

A site whose manifest sets lighting: ephemeris (plus coords + a curated epoch) bakes its Sun / Earthshine lights from real SPICE ephemeris at that epoch rather than a static preset, via srb/terrain/lights/ephemeris_bridge.py. The bridge runs one query(), folds the terrain’s persisted georef.north_convergence_deg into the sun azimuth (az_grid = az_true + γ), and writes an epoch-keyed, content-hashed lights.usd (.../srb_lights/ephemeris/<body>_<site>_<epoch>_<hash8>/) so a changed epoch or grid-north correction yields a fresh file. With the pinned NAIF kernels absent it warns once and falls back to the body-default static preset (air-gapped-safe); srb ephemeris download enables the real path. See Ephemeris → Terrain lighting for the manifest fields, the offline fallback, and the srb asset list provenance nuance (listed azimuth is raw true-north; the bake folds ).

SimForge augmentation

For per-env variation (rocks scattered with a different seed per env), pass augment="simforge":

terrain = RealTerrain(
    body="moon", site="apollo17", augment="simforge",
)
# Baked terrain is the static backbone; SimForge generates rocks on top.
# Augment params come from `augment_preset="lunar_default"` (default).

augment="simforge" requires simforge_foundry to be importable; otherwise raises SimforgeMissingError.

User-supplied DEMs

Skip the manifest entirely and bake a local DEM:

terrain = RealTerrain(
    body="moon",
    dem_path="/data/my_lunar_dem.tif",
    site_name="my_polar_crater",      # cache key
    attribution="Acme Surveying, 2026",
    patch_size_m=300,
    use_ortho=False,
)

attribution is REQUIRED for user-supplied DEMs — license compliance.

site, dem_path, and the region triplet (lat_deg/lon_deg/size_m) are mutually exclusive selection modes — exactly one must be given, and setting two (including site + dem_path together, which used to slip through) is rejected at the spec boundary. Note the per-site shortcut classes bake site in as a class default, so LunarSiteApollo17(dem_path=...) is a both-set error unless you also explicitly clear site=None — for a local DEM, plain RealTerrain(body=..., dem_path=...) is the clearer spelling.

CLI workflow

# Discover available sites + presets.
srb asset list terrain
srb asset list terrain --body moon
srb asset list rocks
srb asset list hdri
srb asset list lights
srb asset info terrain apollo17

# Pre-bake explicitly (otherwise it bakes lazily on first use).
srb asset download terrain apollo17
srb asset download terrain --all --body moon
# Arbitrary region (v2 stacks; no manifest site — mutually exclusive with a
# positional target/--all and the site-only flags):
srb asset download terrain --lat -89.66 --lon 0.0 --size-m 2000 \
                           --body moon --deshade
srb asset bake terrain --dem /data/my.tif --body moon --site-name my_local \
                       --attribution "Me, 2026"

# Manage the cache.
srb asset cache-stats
srb asset clean terrain apollo17 --baked
srb asset clean terrain --all
srb asset validate-manifest

# CI: warm everything at lod=low.
srb asset prefetch-fixtures

Real-terrain v2: source catalog and stack resolution

A per-body source catalog (srb/terrain/catalog/<body>.yaml, schema in srb/terrain/catalog_schema.py) and a resolver (srb.terrain.catalog.resolve.resolve_stacks) exist alongside the v1 manifest described above. Given a location, the resolver turns the catalog into a CompositionPlan: a prioritized DEM stack, plus sunlit-color, PSR-color, mask, and quality stacks, each layer’s estimated fetch bytes, and which layers would need large-download consent. The v2 resolver and bake path are live. The srb asset stack command is only a read-only preview: it never touches the network or moves a byte.

srb asset stack --body moon --lat -89.46 --lon 137.3 --size 2000
srb asset stack --body moon --lat -89.46 --lon 137.3 --size 2000 --json

The per-body source catalogs now carry real, curated data — but coverage is narrow. srb/terrain/catalog/moon.yaml carries 19 real, sha256-pinned layers; mars.yaml and earth.yaml carry 2 each (one HiRISE DTM+ortho pair for Jezero; one Copernicus polar tile and one 3DEP non-polar tile). For access: cog_remote remains a catalog compatibility label. Production first downloads the complete source through SRB’s redirect-validating, size-bounded provider, verifies the pinned SHA-256, then reads local raster windows. Direct GDAL /vsicurl network access is refused because it cannot enforce SRB’s per-hop destination-host policy. First use therefore needs full-source storage and large-download consent where applicable; content-addressed reuse avoids a second download. Each contributing layer records bytes_verified: true in its meta.json fusion provenance. Curated layer data (a real, downloaded-once sha256 per file, a reviewed footprint, an assigned priority_class) is added by a human, offline (operator action O3; see Adding a v2 source layer) — this is an ongoing, incremental process, not a one-shot completed catalog. A request whose footprint falls outside every curated layer still resolves to an empty plan, and srb asset stack prints the same v1-fallback line RealTerrain itself falls back to: no v2 source coverage for <body> at (<lat>, <lon>) — falling back to the v1 single-source path. See Terrain Stacks → Catalog status per body for the exact curated layer list.

Wired for both request forms. A srb/terrain/manifest.yaml site whose sources declares a dem_stack (and, optionally, a color_stack) under a version: 2 manifest bakes end to end through the exact same RealTerrain(body=..., site=...) / srb asset download terrain <site> / srb asset bake calls a v1 site uses — bake_or_load recognizes it automatically (sources.dem is None on a v2 site) and runs it through the fusion engine described below. See Adding a v2 site with a declared stack for the full walkthrough. An arbitrary regionRealTerrain(body="moon", lat_deg=..., lon_deg=..., size_m=...) with no manifest site/patch at all, or the matching srb asset download terrain --lat --lon --size-m CLI form — resolves its stacks by footprint intersection and bakes through the same path, with a deterministic slug identity, degrade-to-procedural in envs, and an illumination-correction (deshade) knob. That request path has its own operator guide: Terrain Stacks — identity/normalization rules, the body-aware (polar canonical / non-polar local-stereographic) resolution CRS, deshade precedence and cache-keying, and the companion/ephemeris arbitration table all live there.

Consent — read this before trusting the preview. srb asset stack’s “consent required” line is a descriptive preview (CompositionPlan.consented, an OR of --allow-large and SRB_TERRAIN_LARGE_OK) — it is not what actually protects a fetch. The real, non-bypassable gate (check_layer_consent / layer_max_bytes in srb/terrain/sources/acquisition.py, an AND of the catalog layer’s own allow_large_download flag and SRB_TERRAIN_LARGE_OK) lives on the fetch path and re-derives consent independently, per layer — it never reads the preview’s consented field. Seeing consent required: none in srb asset stack’s output is not authorization to fetch anything; nothing in Phase 2 fetches automatically either way (no code path calls input() or prompts — consent is env-var/flag only, never interactive).

Real-terrain v2: the fusion engine

A v2 site’s dem_stack names one role: base layer (gap-free, covers the whole patch) plus zero or more role: inset layers (finer, partial coverage); color_stack is the same shape for colour. srb/terrain/fusion/ composes them into one DEM and one colour raster:

  1. Every contributing layer is warped onto the patch’s own grid (fusion.grid.warp_to_grid).
  2. Each DEM inset is co-registered against the base (published catalog shift, or an estimated one when the catalog records none) and demoted — dropped, with a warning — if the QC residual is too large.
  3. A feathered priority blend composites the DEM stack: the finest/ highest-priority inset wins its own interior outright, feathering to the next layer only within a narrow band (blend_len_m, clamped to 50–200 m) of its own true footprint boundary — never a global weighted average, and never the layer’s declared footprint, only where it actually has data.
  4. Colour is composited the same way (first-finite-wins over the sorted stack) and radiometrically normalized against the stack’s anchor layer.

What a fused bake adds to the artifact, alongside the same terrain.usd / textures a v1 bake produces:

  • provenance.tif — a single-band uint8 GeoTIFF the same size as the fused DEM: 0 means no layer contributed there, 1..N is the 1-based index of the layer that won that pixel (matching meta["fusion"]’s own per-layer index).
  • meta.json["fusion"] — a new top-level block (v1 bakes never carry it; meta["georef"] is unrelated and unchanged in shape either way): fusion_version, grid (crs, center_xy_m, size_m, gsd_m, pixels), dem_layers / color_layers (each entry: layer_id, role, kind, index, area_fraction, native_gsd_m, upsampled, radiometry, alignment, license, attribution), contributors (loaded mask inputs that affected colour selection but own no raster index), provenance_tif / uncertainty_tif (filenames or null), and warnings.
  • meta.json["bake_identity"] — the versioned cache identity for a fused generation: cache key, canonical DEM and colour/auxiliary stack hashes, fusion version, target GSD, and effective deshade state. A fused generation without this record is not ready.
  • ATTRIBUTION.txt — one layer_id / license / attribution block per contributing layer (base, every surviving inset, and loaded mask contributor), in blend order — the v1 form (a single attribution + "\n" string) only ever applies to a v1 (single-source) bake.

Budgets — v2 only; v1 is unchanged forever. A v1 bake keeps its stride-based LOD mesh and full-resolution, uncapped textures exactly as before. A v2 (fused) bake additionally quadric-decimates the mesh toward a per-LOD face budget and caps texture/AO resolution (srb/terrain/fusion/budgets.py):

LODMax mesh faces (grid)Max texture side (px)
high4,000,0008192
medium1,000,0004096
low250,0002048

Ambient occlusion is computed at ≤2048 px regardless of LOD, then upsampled. The face budget applies to the decimated grid; the (never decimated) skirt and solid-shell geometry appended for collision adds a small, perimeter- scaling number of faces on top — the shipped mesh can land a little over the nominal number for that reason, never because the budget failed to bite.

This path is proven against real data for region requests; no declared- stack manifest site uses it yet. The fusion engine, the v2 request plumbing, and the budgets are all real, tested, and reachable both offline (a hand-built manifest + fixture catalog exercises the whole path; see tests/terrain/integration/test_fusion_end_to_end.py) and, since the SPICE/Terrain remaining-work plan, against real network products: four real region bakes (moon polar, mars Jezero, earth polar, earth non-polar) complete end to end through this exact fusion path, area_fraction=1.0 on every base layer (see Terrain Stacks → Catalog status per body and docs/research/real_terrain_v2/). What remains not done: manifest.yaml is still version: 1 — no site declares a dem_stack/color_stack against the real catalog (see Adding a v2 site with a declared stack — its connecting_ridge example is still illustrative, using layer ids that don’t exist in the shipped catalog), so a declared-stack v2 site remains untried; only the arbitrary-region request form has been exercised against real data.

Caching and reproducibility

Bakes are content-addressed by (body, site, patch, lod, seed, materials_version, dem_sha256, ...). The same inputs always produce the same output bytes; changing any input (e.g., bumping materials_version) invalidates the bake.

Cache root: assets/srb_assets/scenery/terrain/<body>/<site>/<patch_key>/. Override via SRB_TERRAIN_CACHE_ROOT=/custom/path.

Disabling auto-fetch

By default, missing bakes are produced on first use. To require an explicit pre-bake step (e.g., air-gapped CI):

export SRB_TERRAIN_AUTO_FETCH=0

RealTerrain(...) will then raise TerrainNotBakedError with the exact srb asset download command to run.

Troubleshooting

SymptomLikely causeRemediation
ManifestSiteUnknownErrortypo in site=srb asset list terrain
LicenseMissingErroruser DEM without attributionpass attribution="..."
ChecksumMismatchErrorupstream DEM changedsrb asset validate-manifest --check-shas
SimforgeMissingErroraugment="simforge" w/o packagepip install simforge_foundry
TerrainNotBakedErrorauto_fetch=False + cache missrun the srb asset download shown