tools/fetch_dem.py pulls the open AWS terrarium elevation tiles for each level's bbox (derived from the OSM geometry itself -- the fetch bboxes were never recorded) and writes an f32 heightmap. The builder bilinear-samples it: roads/footpaths/crossings drape (subdivided to ~8 m so long OSM segments stop chording across curved ground), buildings lift rigidly to the lowest terrain under their footprint with a 2 m foundation skirt, trees/props/planters ride their own base heights, and the flat ground slab becomes a real terrain mesh. Bridges get an explicit deck profile (lerp between end heights plus a hump) instead of draping down into the river, the truss follows the deck, and the Bradfield Highway now genuinely flies over Kemp Place -- the game has an underpass. Kangaroo Point's cliffs are an actual 20 m drop, Edward Street climbs to 55 m at Spring Hill. Junction events bake spawn/convoy heights (osm_junction.py grew a pure-python DEM sampler); the game respects marker heights everywhere it used to hardcode ground=0. Bugs the terrain surfaced, all found by measurement: - Long road segments chorded across curved ground: the car visually sank to its windows mid-block. Fixed by subdividing draped ribbons. - The 9 m terrain grid aliases cliff edges and bulged metres ABOVE the finely draped roads: a car spawning there was inside the terrain sheet and got depenetrated through the floor into the void (y = -710). Fixed by giving roads real collision and carving the terrain grid down wherever a draped road sample lands (11k+ carve samples on Kangaroo Point alone). - The junction validator and tuner drove throttle-only, which cross-slope drift turned into 40-86 m misses; they now hold aim like a player would. - Edward x Elizabeth launched down a 17% grade, went airborne at the crest and wedged off-street: unwinnable geometry, not a bug. Swapped for Elizabeth x George on the flat lower CBD. All 9 junctions validate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
102 lines
3.4 KiB
Python
102 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Fetch real elevation for a level's OSM area from the AWS terrain tiles.
|
|
|
|
Usage: fetch_dem.py <area.json> <out_prefix>
|
|
|
|
Reads the area's bounding box straight from its OSM geometry (the original
|
|
fetch bboxes were never recorded -- same trap as the junction specs), grabs
|
|
the covering terrarium tiles at z15 (~4.2 m/px at Brisbane's latitude), and
|
|
writes:
|
|
<out_prefix>_dem.f32 row-major float32 heights in metres (little-endian)
|
|
<out_prefix>_dem.json {"w", "h", "min_lat", "min_lon", "max_lat", "max_lon"}
|
|
|
|
Terrarium encoding: height = R*256 + G + B/256 - 32768.
|
|
Tiles: https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png
|
|
(open data, no key). Data (c) Mapzen/AWS Terrain Tiles contributors.
|
|
"""
|
|
import io
|
|
import json
|
|
import math
|
|
import struct
|
|
import sys
|
|
import urllib.request
|
|
|
|
from PIL import Image
|
|
|
|
Z = 15
|
|
URL = "https://s3.amazonaws.com/elevation-tiles-prod/terrarium/%d/%d/%d.png"
|
|
|
|
|
|
def tile_of(lat, lon, z):
|
|
n = 2 ** z
|
|
xt = (lon + 180.0) / 360.0 * n
|
|
lat_r = math.radians(lat)
|
|
yt = (1.0 - math.asinh(math.tan(lat_r)) / math.pi) / 2.0 * n
|
|
return xt, yt
|
|
|
|
|
|
def tile_bounds(xt, yt, z):
|
|
n = 2 ** z
|
|
lon0 = xt / n * 360.0 - 180.0
|
|
lat0 = math.degrees(math.atan(math.sinh(math.pi * (1 - 2 * yt / n))))
|
|
return lat0, lon0
|
|
|
|
|
|
def main():
|
|
src, prefix = sys.argv[1], sys.argv[2]
|
|
d = json.load(open(src))
|
|
lats, lons = [], []
|
|
for e in d["elements"]:
|
|
for g in e.get("geometry") or []:
|
|
lats.append(g["lat"])
|
|
lons.append(g["lon"])
|
|
if e.get("type") == "node" and "lat" in e:
|
|
lats.append(e["lat"])
|
|
lons.append(e["lon"])
|
|
lo_lat, hi_lat = min(lats), max(lats)
|
|
lo_lon, hi_lon = min(lons), max(lons)
|
|
|
|
x0, y1 = tile_of(lo_lat, lo_lon, Z) # south-west -> larger y tile index
|
|
x1, y0 = tile_of(hi_lat, hi_lon, Z)
|
|
tx0, tx1 = int(x0), int(x1)
|
|
ty0, ty1 = int(y0), int(y1)
|
|
cols = tx1 - tx0 + 1
|
|
rows = ty1 - ty0 + 1
|
|
print("bbox lat %.4f..%.4f lon %.4f..%.4f -> %dx%d tiles @z%d"
|
|
% (lo_lat, hi_lat, lo_lon, hi_lon, cols, rows, Z))
|
|
|
|
mosaic = Image.new("RGB", (cols * 256, rows * 256))
|
|
for ty in range(ty0, ty1 + 1):
|
|
for tx in range(tx0, tx1 + 1):
|
|
req = urllib.request.Request(URL % (Z, tx, ty),
|
|
headers={"User-Agent": "BurnoutShitbox/1.0"})
|
|
png = urllib.request.urlopen(req, timeout=60).read()
|
|
mosaic.paste(Image.open(io.BytesIO(png)), ((tx - tx0) * 256, (ty - ty0) * 256))
|
|
|
|
# mosaic geographic bounds (tile edges, not the request bbox)
|
|
m_hi_lat, m_lo_lon = tile_bounds(tx0, ty0, Z)
|
|
m_lo_lat, m_hi_lon = tile_bounds(tx1 + 1, ty1 + 1, Z)
|
|
|
|
w, h = mosaic.size
|
|
px = mosaic.load()
|
|
out = open(prefix + "_dem.f32", "wb")
|
|
lo, hi = 1e9, -1e9
|
|
for yy in range(h):
|
|
row = bytearray()
|
|
for xx in range(w):
|
|
r, g, b = px[xx, yy]
|
|
hm = r * 256 + g + b / 256.0 - 32768.0
|
|
hm = max(hm, 0.0) # clamp bathymetry: the river reads as 0, not -8
|
|
lo, hi = min(lo, hm), max(hi, hm)
|
|
row += struct.pack("<f", hm)
|
|
out.write(row)
|
|
out.close()
|
|
json.dump({"w": w, "h": h, "min_lat": m_lo_lat, "max_lat": m_hi_lat,
|
|
"min_lon": m_lo_lon, "max_lon": m_hi_lon},
|
|
open(prefix + "_dem.json", "w"))
|
|
print("wrote %s_dem.f32 (%dx%d, %.1f..%.1f m)" % (prefix, w, h, lo, hi))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|