← All work PRJ_03 — Geospatial / Tooling · 2026

TERRA
SKETCH

A Python tool suite that turns real-world open geodata into game-ready 3D terrain — 1-metre elevation, carved roads and rivers, aerial-derived biome textures, buildings and vegetation, all extracted for an actual place and exported straight to Unity.

// pipeline — end to end

TerraSketch runs as a single CLI pass over a WGS84 bounding box. The box is projected into Lambert-93 (EPSG:2154) so every downstream step works in real metres, with a local south-west origin that maps straight onto Unity's X/Z. Elevation is IGN's RGE ALTI 1 m bare-ground model, fetched as a GeoTIFF and resampled into a 16-bit heightmap whose smoothing is fixed in metres, not pixels — so the relief stays stable across output resolutions.

That heightmap is diced into an N×N grid of chunk meshes with multiple LOD levels and an extruded soil skirt, then a relief-normal map is baked from the full-resolution surface so even a low-poly chunk reads as detailed. In parallel, BD ORTHO aerial imagery is pulled from the IGN WMTS at ≈0.45 m/px, aligned to the terrain, LAB-graded into a stylized colour map, and classified into a biome splatmap — slope and altitude overriding a k-means clustering of the aerial colours into grass / rock / forest / water weights.

Vector features come from OpenStreetMap via the Overpass API: the highway hierarchy is Chaikin-smoothed, rasterized into an anti-aliased road mask and carved into the heightmap with graded shoulders, while a chosen waterway is draped onto the surface as a tapered ribbon and cut into a meandering channel whose bed can only descend downstream. IGN LiDAR HD canopy heights plus a Bridson Poisson-disk sampler drive multi-layer tree / rock / grass scatter, BD TOPO footprints extrude into gable-roofed buildings, and everything is written out as OBJ/FBX meshes, PNG textures and a JSON manifest — a self-describing bundle the Unity importer reads in one step.

// a real 2 km valley near Babeau-Bouldoux, reconstructed from open data

↑ One command turns a lat/lon bounding box into this: IGN 1 m elevation, BD ORTHO aerial texture, the OSM road/track network and a carved river — orbiting in an interactive viewer.

TerraSketch output — a real valley reconstructed as a 3D terrain block with draped aerial imagery, extracted roads and tracks, a carved river channel, POI pins and an extruded soil skirt

The exported terrain block — draped BD ORTHO imagery, the extracted road/track network, a carved river channel, semantic POI pins, and the extruded soil skirt under the surface.

Role
Solo dev
Built with
Python · NumPy · SciPy · scikit-learn
Type
Geospatial → 3D pipeline
Year
2026
// overview

TerraSketch is a command-line pipeline that reconstructs a real place — pick a WGS84 bounding box and it returns a textured, game-ready 3D terrain. It pulls France's national open geodata (IGN Géoplateforme) plus OpenStreetMap, reprojects everything into Lambert-93 metres, and runs an ingest → transform → export pipeline that ends in Unity-ready meshes, textures and placement data.

Elevation is the real RGE ALTI 1 m bare-ground model, so the terrain carries true metre-scale relief rather than resampled noise. On top of that surface the tool extracts and bakes real features: the OSM highway hierarchy becomes a road mask and is carved into the heightmap with graded shoulders; a chosen waterway is draped into a meandering, carved river channel; aerial imagery is classified into a biome splatmap; IGN LiDAR canopy heights scatter trees; and BD TOPO footprints rise into gable-roofed buildings.

Every literal lives in one constants.py (no magic numbers), each stage validates its inputs and fails fast, and the offline sub-commands re-run from pristine backups so tuning a shoulder width or a river depth never compounds onto an already-edited bundle.

// what it extracts
  • IGN RGE ALTI 1 m elevationDTM
  • BD ORTHO aerial ≈0.45 m/pxORTHO
  • OSM roads → carved corridorsROADS
  • OSM river → meandering channelRIVER
  • Slope + colour biome splatmapTEXTURE
  • LiDAR HD canopy tree scatterTREES
  • BD TOPO gable buildingsBUILT
  • Chunked LOD + relief-normal bakeMESH
// the pipeline
  • WGS84 bbox → Lambert-93 metresREPROJECT
  • WMS elevation → 16-bit heightmapINGEST
  • Chunked LOD mesh + soil skirtMESH
  • WMTS ortho → stylized colourIMAGERY
  • Overpass features → POIs + pathsOSM
  • Road carve + river carve/paintCARVE
  • Poisson-disk multi-layer scatterSCATTER
  • Manifest + bundle → Unity importEXPORT
// inside the pipeline — Python
splatmap.py — biome textures Python

Terrain textures from the land itself: slope and altitude override an aerial-colour k-means classification (condensed).

# R = grass · G = rock · B = forest · A = water
weights = color_biome_onehot(cluster_labels)   # from k-means over the ortho

# Steep ground is rock, whatever colour the aerial says.
steep = slope_deg > SLOPE_ROCK_MIN_DEGREES     # 35 deg
weights[steep] = 0.0
weights[steep, ROCK] = 1.0

# Gentle, unclaimed ground leans grass; high ground leans rock.
gentle = slope_deg < SLOPE_GRASS_MAX_DEGREES   # 20 deg
weights[gentle & unclaimed, GRASS] += 0.5
weights[altitude_frac > 0.75, ROCK]    += 0.3

splat = normalize(gaussian_blur(weights))      # soft biome seams, Σ = 255
river_carve.py — a river, not a canal Python

A raised-cosine bowl: full depth at the thalweg, meeting terrain tangentially at the bank (no seam). The bed is forced downhill so water never runs uphill.

def bowl_coverage(dist_to_centerline, core_half, bank_reach):
    reach = core_half + bank_reach
    ramp  = np.clip(dist_to_centerline / np.maximum(reach, 1.0), 0.0, 1.0)
    cov   = 0.5 * (1.0 + np.cos(np.pi * ramp))   # 1 at centre → 0 at edge
    cov[~np.isfinite(dist_to_centerline)] = 0.0
    return cov

# Blend the carved channel into the terrain by coverage...
carved = terrain * (1.0 - cov) + bed_floor * cov

# ...and the bed can only ever descend downstream (a dip becomes a pool).
bed_floor = np.minimum.accumulate(sampled_terrain - depth)