Scene Generation

How AODT builds 3D scenes from geospatial data
View as Markdown

This page covers the concepts behind the pipeline that powers DigitalTwinClient.prepare_map(). For a practical quickstart, see GIS Pipeline.

Overview

Scene generation converts geospatial data into AODT-ready maps.

AODT maps support data layers such as buildings, building interiors, terrain, and vegetation. These data may be created procedurally, by importing GIS source data, or some combination of the two.

Available parameters

AODT supports two building import methods: OpenStreetMap (OSM) and CityGML (GML). Additional parameters let you generate further layers (terrain, vegetation, interiors) or modify the scene.

Below are the parameters which may be passed as keyword arguments when constructing OSMTask or GMLTask. Default None defers to the GIS pipeline defaults.

ParameterDescriptionRequiredApplies to
output_folder_keyS3 key prefix under the bucket. The pipeline writes /sim and /viz subfolders here.YesBoth
coordsBounding box as (min_lon, min_lat, max_lon, max_lat) in degrees. Supported to 25 km².YesOSM
input_filesServer-accessible CityGML file paths (must be reachable from the GIS worker container).YesGML
epsg_inInput coordinate reference system as an EPSG code, e.g. "6697". May be geographic (angular units, e.g. lat/lon) or projected (linear units, e.g. meters).YesGML
include_elevationFetch and use terrain elevation. When False, the pipeline emits a synthetic, flat ground plane.YesBoth
epsg_outOutput CRS as an EPSG code. Must be a projected (linear-units) CRS. When omitted, the pipeline auto-derives the Universal Transverse Mercator (UTM) zone for the scene center. Set this explicitly when you need a specific projection — for example, to align with other geospatial data you plan to combine with the scene.NoGML
ground_sourceTerrain source: "terrarium" (default) or "srtm". For GML jobs, the input CityGML’s TINRelief is used as terrain when present, with Terrarium as a fallback.NoBoth
vegetation_sourceVegetation source method. Only "procedural" is currently supported via the client; other values produce no vegetation. Default: "procedural".NoBoth
vegetation_densityTarget trees per hectare for procedural vegetation. Default: 50.NoBoth
vegetation_scale_minMinimum random scale applied to procedural trees. Default: 0.8.NoBoth
vegetation_scale_maxMaximum random scale applied to procedural trees. Default: 1.2.NoBoth
cesium3dtiles_b3dmEmit Batched 3D Model (B3DM) tiles instead of binary glTF (GLB). Defers to GIS pipeline (currently B3DM). Default: None.NoBoth
cesium3dtiles_dracoApply Draco mesh compression to GLB tiles. Requires cesium3dtiles_b3dm=False; auto-enabled when b3dm is off. Defers to GIS pipeline (currently off when b3dm is on). Default: None.NoBoth
cesium3dtiles_gzipGzip-compress tile payloads. Hosting layer must set Content-Encoding: gzip. Defers to GIS pipeline (currently on). Default: None.NoBoth
cesium3dtiles_chunk_sizeSpatial partition size in meters for tile generation. Omit/None to disable chunking.NoBoth
cesium3dtiles_veg_instancedUse GPU instancing for vegetation tiles (smaller, faster). Default: True.NoBoth
disable_interiorsSkip per-building floor-slice generation. Default: False.NoBoth
terrain_clip_marginTerrain clip radius in meters beyond the building extent. Default: 200.NoBoth
roughUse approximate cuts when generating the mobility-domain mesh. Default: True.NoBoth
terraform_configOptional TerraformConfig overrides for the terrain-shaping pass.NoBoth

TerraformConfig fields

TerraformConfig controls the terrain-shaping pass that fits terrain to buildings. It is only available when using terrain (rather than synthetic, flat ground). Every field is optional; None falls back to the GIS pipeline default shown below.

ParameterDescriptionDefault
terraformConform terrain to building base heights.False
pad_radiusBuilding footprint padding radius (meters).8.0
pre_tessellation_lengthTarget edge length for pre-terraform tessellation (meters).4.0
pre_smooth_terrainSmooth terrain before terraforming.True
pre_smooth_itersPre-smooth iterations.2
pre_smooth_lambdaPre-smooth Laplacian lambda.0.5
terraform_smoothSmooth terrain after terraforming.True
terraform_smooth_itersPost-terraform smoothing iterations.8
terraform_smooth_lambdaPost-terraform Laplacian lambda.0.6
terraform_smooth_radiusSmoothing radius in meters (0 = global).0.0
building_base_methodHow to derive each building’s base height: "min", "max", "average", "top10", "bottom10"."max"
base_merge_distanceDistance threshold (m) for merging nearby building bases.175.0
base_influence_radiusRadius of influence (m) for base-height blending.140.0
base_influence_sigmaGaussian sigma (m) for base-height blending.80.0
base_smooth_itersBase-height smoothing iterations.1
adaptive_bandsUse adaptive near/far tessellation bands.False
near_radiusNear-band radius in meters.1.5
near_tessellation_thresholdEdge-length threshold (m) for near-band tessellation.5.0
far_tessellation_thresholdEdge-length threshold (m) for far-band tessellation.100.0

Example

A typical OSM job with a partial TerraformConfig override. Any field left as None falls back to the GIS pipeline default:

1from dt_client import DigitalTwinClient, OSMTask, TerraformConfig
2from _config import S3Config
3
4client = DigitalTwinClient("localhost:50051")
5
6task = OSMTask(
7 output_folder_key="demo_gis/tokyo",
8 coords=(139.74, 35.66, 139.75, 35.67),
9 include_elevation=True,
10 terraform_config=TerraformConfig(
11 terraform=True,
12 base_influence_sigma=75.0,
13 terraform_smooth_iters=7,
14 ),
15)
16
17s3 = S3Config(
18 bucket="aerial-data",
19 provider="minio",
20 endpoint_url="http://<worker-host>:9002",
21 access_key="minioadmin",
22 secret_key="minioadmin",
23)
24
25result = client.prepare_map(task, s3)
26print(result["s3_url"])

See client/examples/example_prepare_map_terraform.py for a full CLI version.

Output layout

A successful job writes the following layout. AODT scenes are stored as Universal Scene Description (USD) files alongside JSON metadata sidecars:

s3://aerial-data/demo_gis/tokyo/
sim/
scene_metadata.json # sidecar describing the scene's georeferencing and provenance
master.usd # tiled-USD master stage (paired with master_metadata.json)
vegetation.geojson # vegetation features (when generated)
viz/
tiles/ # Cesium 3D Tiles for the viewer (exterior, interior, vegetation)
terrain/ # quantized-mesh terrain tiles

Inside master.usd, the key prims are:

  • Buildings — meshes with material assignments and per-face GlobalSurfaceHash primvars (USD per-primitive variables). The hash is a stable per-face identifier that lets calibration and result lookups join back to specific building surfaces across runs.
  • Terrain / ground plane — the terrain mesh, conformed to building bases when terraform is enabled. With no elevation source, this is a flat plane.
  • Interiors — floor-slice meshes generated per building (skipped when disable_interiors=True). Buildings with ambiguous geometry are skipped here and are also excluded from indoor user equipment (UE) mobility, so the two stay consistent.
  • Vegetation — tree instances. See Vegetation for how candidates are sampled and filtered if placed procedurally.
  • Georeferencing metadata — recorded as USD attributes (asim:crs, asim:center_lat, asim:center_lon, asim:vertical_datum) on the stage. To inspect without opening the USD, read the mirrored fields from master_metadata.json. Calibration, AODT simulations, and the viewer all read these to anchor the scene to real-world coordinates.

Each top-level USD is accompanied by a sidecar *_metadata.json file. It is a small JSON document recording:

  • Georeferencingcrs, center_lat, center_lon, center_z, vertical_datum, and the projected anchor (anchor_x, anchor_y). These mirror the asim:* attributes on the USD and let downstream tools read the scene’s georeferencing without opening the USD.
  • Provenancedate_created, input_hash and input_hash_recipe (a stable fingerprint of the job parameters and source-file bytes), and code_version (a map of the library versions — aodt, pyproj, proj, gdal, rasterio, usd, python — that influenced the output). The same fields are also written into the USD’s customLayerData under gis:* keys.

Terrain

The pipeline imports terrain when include_elevation=True. When include_elevation=False, the pipeline emits a synthetic, flat ground plane — TerraformConfig settings are then ignored, since there is no terrain to reshape.

Terrain sources

The ground_source field selects which digital elevation model (DEM) provider feeds the terrain mesh:

SourceNotes
"terrarium"AWS Terrarium DEM tiles. Default.
"srtm"NASA SRTM tiles.

For GML jobs, if the input CityGML files contain TINRelief (Triangulated Irregular Network) features, those features are imported as terrain automatically — you do not need to set ground_source.

Conforming buildings to terrain

Imported buildings and terrain rarely line up by default — the two data sources often have different vertical references, and even when they share one, slopes and DEM noise can leave buildings clipping into the ground or floating above it. The pipeline reconciles them in one of two ways.

Default behavior — building grounding. Every map generation includes a step that matches buildings to terrain. For each building, the pipeline samples the terrain elevation under the footprint center and rigid-translates the building in Z so its base sits at that height. No geometry is reshaped; buildings are dropped onto the terrain. This handles the common case of mismatched datums and gently varying terrain.

How well this simple grounding works depends on terrain resolution and shape. On flat or smoothly varying terrain it is usually enough. On noisy DEMs, steep slopes, or coarse-resolution terrain under fine-grained footprints, individual buildings can still clip into or float above the ground.

For greater control — terraform_config.terraform=True. For these cases, the pipeline can additionally reshape the terrain itself so each building sits cleanly:

  1. For each footprint, derive a representative base elevation from the terrain samples underneath it (building_base_method, default "max").
  2. Smooth those base elevations across neighboring buildings so adjacent footprints share consistent heights (base_merge_distance, base_influence_radius, base_influence_sigma, base_smooth_iters).
  3. Re-tessellate and reshape the terrain mesh locally so it is flat at the smoothed base elevation under each footprint, with a pad_radius blend ring outside.
  4. Optionally smooth the terraformed terrain (terraform_smooth, terraform_smooth_iters, terraform_smooth_lambda).
  5. Re-seat buildings on the now-flattened patches.

See TerraformConfig fields for the full set of tuning knobs.

Vegetation

The pipeline places trees procedurally across the scene’s plantable area. The pipeline writes output_folder_key/sim/vegetation.geojson, describing every placed tree for downstream consumers.

Placement algorithm

Placement runs in three phases.

1. Build the permissible area. The scene bounding box minus a union of exclusion polygons derived from the input data. Excluded categories include buildings, roads, railways, water bodies (inland and supplemental marine sources), bare-rock terrain (rock, scree, sand, dunes, glaciers, beaches), harbors and piers, sports facilities, playgrounds, parking, runways and taxiways, and power infrastructure. For GML jobs, GML building footprints are added to the exclusion set on top of any OSM data.

2. Place mapped trees first. OSM natural=tree and natural=tree_row features are imported at their exact coordinates and count toward the density target. When an OSM tree carries a height tag, the tag is used to derive its scale; otherwise a uniform random scale is drawn.

3. Procedural fill. The remaining target count is sampled inside the permissible area using a Thomas cluster process: parent points are drawn uniformly across the permissible area, and each parent emits a small cluster of child points with Gaussian scatter. This produces natural-looking groves and clearings rather than a uniform grid. A minimum-spacing pass then thins out positions that came out too close to each other.

After placement, an overlap-resolution pass keeps canopies clean:

  • For each pair of overlapping canopies, the larger of the two trees is shrunk so the canopies just touch.
  • Trees adjacent to buildings are shrunk so their canopies do not enter the footprint.
  • Trees that would have to shrink below vegetation_scale_min are removed.

Vegetation model and sizing

Every tree is an instance of a single shared prototype with a canopy diameter of about 6 m and a total height of about 10 m at scale 1.0. Per-instance size is randomized:

  • A uniform random scale is drawn from [vegetation_scale_min, vegetation_scale_max] (default 0.81.2) and applied isotropically to the whole tree (canopy and trunk together).
  • Mapped OSM trees with a height tag have their scale derived from the tag (height / 10 m), clipped to the same [vegetation_scale_min, vegetation_scale_max] range.

Density

vegetation_density is a target in trees per hectare across the permissible area. The pipeline computes a target count density × permissible_area_ha, subtracts the number of mapped OSM trees, and asks the procedural sampler for the remainder.

The achieved count is usually lower than the target. Each of the steps above can drop trees: positions outside the permissible area after Gaussian scatter are filtered, the minimum-spacing pass thins clusters, and the overlap-resolution pass shrinks or removes trees that crowd buildings or each other. As a result:

  • Sparse scenes typically meet the target density.
  • Dense urban scenes saturate well below the target. Increasing vegetation_density past the saturation point yields diminishing returns; lower it if you want a measured outcome to match the configured value.
  • Fully excluded scenes — for example a bounding box that lies entirely inside buildings or water — emit zero trees and log No permissible area for vegetation.

Setting vegetation_density=0 is a special case: procedural fill is disabled entirely, and only mapped OSM trees are placed. Use this when you want to keep real-world tree locations without adding any synthetic ones.

Indoor

The pipeline generates per-building interior geometry. Buildings shorter than one floor, or with self-intersecting or otherwise unusable geometry, are skipped.

To skip indoor generation entirely, set disable_interiors=True. This is the right choice when:

  • You only need outdoor links.
  • You’re iterating on map geometry and want shorter pipeline runs.
  • Your scene has no buildings (terrain-only export).

Common issues

coords area is too large

In practice, the pipeline supports map areas up to 25 km². A hard cap is enforced at 100 km², after which client.prepare_map() returns success=False with this message:

Map generation input error: 'coords' area is too large (>100 km²). Use a smaller bounding box.

Even within the technical cap, processing time grows quickly past 25 km². Use a smaller box, or split your region across multiple prepare_map calls.

Missing or malformed S3 endpoint

When S3Config(provider="minio", endpoint_url=...) is wrong, the workflow fails with an upload error. Unlike the bounding-box case above, these errors raise from client.prepare_map() as a RuntimeError (or ValueError for malformed URLs) — they do not return success=False.

Common forms:

RuntimeError: S3 connection/credential error, aborting upload: Could not connect to the endpoint URL: "http://wrong-host:9002/..."
RuntimeError: S3 configuration error (NoSuchBucket), aborting upload: ...
ValueError: endpoint_url must be an HTTP(S) URL, got: <url>

Check that:

  • endpoint_url starts with http:// or https:// and is reachable from the worker host.
  • The bucket exists and the access/secret keys are correct.
  • For the default worker stack, MinIO uses minioadmin / minioadmin.

Cesium tiles render fails

Gzip-compressed tiles (the GIS pipeline’s current default) require the hosting layer to set Content-Encoding: gzip on each object. If your host does not, either configure it to do so or pass cesium3dtiles_gzip=False to prepare_map.

Reference