Real terrain: DEM elevation under every level

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>
This commit is contained in:
m3ultra 2026-07-30 15:08:42 +10:00
parent 30d5cfd846
commit 65da434678
24 changed files with 1469 additions and 792 deletions

View File

@ -2,445 +2,535 @@
{ {
"name": "Alice x George", "name": "Alice x George",
"spawn": { "spawn": {
"dx": 0.766833198312044, "x": -163.58628244865508,
"dz": -0.641846434878719, "z": 162.06599199440433,
"x": -163.586282448655, "y": 14.0,
"z": 162.065991994404 "dx": 0.7668331983120437,
"dz": -0.6418464348787193
}, },
"streams": [ "streams": [
{ {
"interval": 1.5,
"pts": [ "pts": [
[ [
-71.6230075670676, -71.62300756706763,
85.0918821008208 85.0918821008208,
15.68
], ],
[ [
-72.1027981957511, -72.10279819575109,
85.4906175386659 85.49061753866586,
15.69
], ],
[ [
-82.5534519291224, -82.55345192912239,
94.8058802983977 94.80588029839768,
16.5
], ],
[ [
-104.449283255202, -104.44928325520195,
113.364728030556 113.36472803055571,
16.93
], ],
[ [
-114.552953384248, -114.55295338424757,
122.938157884478 122.93815788447793,
16.38
], ],
[ [
-118.635517096346, -118.63551709634598,
126.332146888185 126.33214688818488,
16.11
], ],
[ [
-124.179725942706, -124.17972594270637,
130.600603993254 130.60060399325408,
15.66
], ],
[ [
-132.412657846607, -132.4126578466066,
136.841371982625 136.84137198262476,
15.15
], ],
[ [
-135.426194974764, -135.42619497476394,
139.131158627196 139.13115862719584,
15.05
], ],
[ [
-137.017391502614, -137.0173915026139,
140.329922425279 140.32992242527928,
15.02
], ],
[ [
-142.2269104628, -142.22691046279974,
144.181294358731 144.18129435873087,
14.99
] ]
], ],
"speed": 12.0 "speed": 12.0,
"interval": 1.5
}, },
{ {
"interval": 1.5,
"pts": [ "pts": [
[ [
-75.361941989609, -75.361941989609,
84.8229236525054 84.82292365250544,
16.0
], ],
[ [
-80.9395388682742, -80.93953886827424,
78.6116377012041 78.61163770120406,
16.61
], ],
[ [
-82.5659998940916, -82.56599989409156,
76.7348960854059 76.7348960854059,
16.75
], ],
[ [
-115.405581435619, -115.40558143561937,
38.7415552213739 38.74155522137386,
23.41
] ]
], ],
"speed": 12.0 "speed": 12.0,
"interval": 1.5
}, },
{ {
"interval": 1.8,
"pts": [ "pts": [
[ [
-145.437080580831, -145.4370805808313,
139.83908204088 139.83908204087984,
15.0
], ],
[ [
-140.236730470549, -140.2367304705485,
135.994503574748 135.99450357474848,
15.01
], ],
[ [
-138.687188507682, -138.6871885076824,
134.826983772678 134.82698377267795,
15.02
], ],
[ [
-135.676038606364, -135.67603860636424,
132.539006817179 132.53900681717874,
15.1
], ],
[ [
-127.454661333638, -127.45466133363814,
126.307027606927 126.30702760692736,
15.64
], ],
[ [
-121.996408938187, -121.99640893818653,
122.105516312011 122.1055163120108,
15.97
], ],
[ [
-118.197496828121, -118.19749682812089,
118.953524515431 118.9535245154313,
16.16
], ],
[ [
-108.01476176314, -108.01476176313997,
109.309193169476 109.30919316947609,
16.87
], ],
[ [
-86.0785257896557, -86.07852578965569,
90.7151769019407 90.71517690194068,
16.9
], ],
[ [
-75.6896883110217, -75.68968831102165,
81.454007661433 81.45400766143295,
16.16
], ],
[ [
-75.0830599635874, -75.08305996358739,
80.9460362988959 80.94603629889595,
16.14
] ]
], ],
"speed": 11.0 "speed": 11.0,
"interval": 1.8
}, },
{ {
"interval": 1.8,
"pts": [ "pts": [
[ [
-111.320181741659, -111.32018174165945,
35.2103359785921 35.21033597859207,
22.71
], ],
[ [
-78.4807980853871, -78.48079808538708,
73.2034479147318 73.20344791473178,
16.61
], ],
[ [
-76.9072614072839, -76.9072614072839,
75.0198778988802 75.01987789888024,
16.48
], ],
[ [
-71.344125541046, -71.34412554104603,
81.2149947472113 81.2149947472113,
15.86
] ]
], ],
"speed": 11.0 "speed": 11.0,
"interval": 1.8
}, },
{ {
"interval": 1.5,
"pts": [ "pts": [
[ [
-72.1166888351082, -72.11668883510822,
80.6186568827489 80.61865688274891,
15.94
], ],
[ [
-63.3155168708224, -63.31551687082244,
85.2319664647318 85.23196646473183,
15.12
], ],
[ [
-54.5517817587045, -54.55178175870453,
95.5382290534301 95.53822905343011,
14.31
], ],
[ [
-39.7832019492811, -39.78320194928109,
112.967845683357 112.96784568335697,
16.67
], ],
[ [
-38.2495347293041, -38.24953472930413,
114.641424782394 114.64142478239386,
16.96
], ],
[ [
-37.46679081946, -37.466790819459995,
115.770892092411 115.77089209241056,
17.13
], ],
[ [
-36.907545471025, -36.90754547102499,
116.868308404357 116.86830840435711,
17.26
], ],
[ [
-36.6415428486163, -36.64154284861633,
117.9601284403 117.96012844030032,
17.36
], ],
[ [
-36.5496258677079, -36.54962586770791,
118.928290187952 118.92829018795193,
17.43
], ],
[ [
-36.5838040309518, -36.58380403095183,
120.631630966954 120.63163096695364,
17.52
], ],
[ [
-36.7335166728013, -36.733516672801336,
121.773248002537 121.77324800253669,
17.57
], ],
[ [
-36.9496009730767, -36.949600973076684,
122.555067847412 122.55506784741175,
17.58
], ],
[ [
-37.3111869258717, -37.31118692587169,
123.404225782126 123.40422578212599,
17.59
], ],
[ [
-38.0206496416879, -38.02064964168787,
124.385542652475 124.38554265247478,
17.56
], ],
[ [
-39.4579334455589, -39.45793344555888,
125.801006946898 125.80100694689808,
17.47
], ],
[ [
-41.9601597793989, -41.96015977939887,
127.878176766196 127.8781767661959,
17.33
] ]
], ],
"speed": 12.0 "speed": 12.0,
"interval": 1.5
}, },
{ {
"interval": 1.8,
"pts": [ "pts": [
[ [
-45.4076787424784, -45.40767874247836,
123.721902834029 123.72190283402932,
16.65
], ],
[ [
-43.0308962305509, -43.03089623055088,
121.752064253174 121.75206425317394,
16.81
], ],
[ [
-41.9792929338678, -41.9792929338678,
120.712785747195 120.7127857471952,
16.87
], ],
[ [
-41.9973981173857, -41.99739811738572,
120.721051417958 120.72105141795794,
16.87
], ],
[ [
-42.0626879873161, -42.06268798731611,
120.818306152528 120.81830615252781,
16.87
], ],
[ [
-42.0417354235454, -42.041735423545425,
120.781878397269 120.78187839726885,
16.87
], ],
[ [
-41.9741642668029, -41.974164266802894,
120.309115032842 120.30911503284231,
16.85
], ],
[ [
-41.9490832168424, -41.94908321684239,
119.004843012052 119.00484301205215,
16.78
], ],
[ [
-41.975684667957, -41.975684667956976,
118.800920359363 118.80092035936316,
16.76
], ],
[ [
-41.9467189152085, -41.94671891520851,
118.809115195214 118.80911519521383,
16.77
], ],
[ [
-42.0788310934576, -42.0788310934576,
118.579643507368 118.57964350736775,
16.74
], ],
[ [
-42.3825061541171, -42.38250615411705,
118.116845217633 118.1168452176333,
16.67
], ],
[ [
-43.8908120639013, -43.89081206390128,
116.473204316403 116.47320431640294,
16.4
], ],
[ [
-58.6728283240367, -58.67282832403665,
99.0277813465423 99.02778134654227,
14.55
], ],
[ [
-66.8177230511832, -66.81772305118318,
89.3422647349603 89.3422647349603,
15.08
], ],
[ [
-74.5893786955468, -74.5893786955468,
85.4192615169678 85.41926151696784,
15.91
] ]
], ],
"speed": 11.0 "speed": 11.0,
"interval": 1.8
} }
], ],
"targets": [ "targets": [
26500, 15000,
29000, 35000,
46500 60000
] ]
}, },
{ {
"name": "Margaret x Edward", "name": "Margaret x Edward",
"spawn": { "spawn": {
"dx": -0.742705793125526, "x": 274.9079157711072,
"dz": 0.669617879732749, "z": -373.12117782739404,
"x": 274.907915771107, "y": 13.91,
"z": -373.121177827394 "dx": -0.7427057931255263,
"dz": 0.6696178797327494
}, },
"streams": [ "streams": [
{ {
"interval": 1.5,
"pts": [ "pts": [
[ [
185.833815296718, 185.8338152967176,
-292.812645167915 -292.8126451679154,
14.05
], ],
[ [
193.655933950983, 193.65593395098287,
-299.882511644341 -299.88251164434115,
14.74
], ],
[ [
211.377868675405, 211.37786867540518,
-315.230725704805 -315.2307257048046,
15.6
], ],
[ [
216.02310568687, 216.02310568686988,
-319.254598202675 -319.25459820267537,
16.29
], ],
[ [
237.642996194709, 237.6429961947087,
-338.582293338578 -338.58229333857753,
18.91
], ],
[ [
239.660997181915, 239.66099718191472,
-341.288346333546 -341.2883463335457,
19.31
] ]
], ],
"speed": 12.0 "speed": 12.0,
"interval": 1.5
}, },
{ {
"interval": 1.5,
"pts": [ "pts": [
[ [
179.818121576327, 179.81812157632743,
-304.166235842252 -304.16623584225226,
14.19
], ],
[ [
181.359839275663, 181.35983927566346,
-302.406992562087 -302.4069925620872,
14.27
], ],
[ [
184.213303452985, 184.2133034529848,
-299.144813785439 -299.1448137854387,
14.28
], ],
[ [
189.715557971244, 189.71555797124375,
-292.541650836584 -292.5416508365839,
14.26
], ],
[ [
194.923453137675, 194.9234531376747,
-286.336679339567 -286.3366793395667,
14.2
], ],
[ [
234.056756167219, 234.05675616721896,
-241.131525360219 -241.1315253602194,
16.15
], ],
[ [
244.509931259631, 244.50993125963066,
-229.329340361402 -229.32934036140196,
14.64
] ]
], ],
"speed": 12.0 "speed": 12.0,
"interval": 1.5
}, },
{ {
"interval": 1.8,
"pts": [ "pts": [
[ [
243.953470490227, 243.95347049022743,
-338.011965266358 -338.01196526635846,
16.8
], ],
[ [
241.329499501326, 241.32949950132615,
-334.636448261753 -334.63644826175323,
16.37
], ],
[ [
219.611149514375, 219.61114951437548,
-315.219013797425 -315.21901379742513,
15.39
], ],
[ [
214.913183354807, 214.91318335480702,
-311.14886949499 -311.1488694949895,
15.27
], ],
[ [
197.217859554354, 197.21785955435436,
-295.823855956056 -295.8238559560563,
14.84
], ],
[ [
189.454804209519, 189.45480420951895,
-288.806594832065 -288.8065948320647,
14.03
] ]
], ],
"speed": 11.0 "speed": 11.0,
"interval": 1.8
}, },
{ {
"interval": 1.8,
"pts": [ "pts": [
[ [
240.467498400426, 240.4674984004257,
-225.74901403845 -225.74901403845038,
13.81
], ],
[ [
229.982416914263, 229.982416914263,
-237.587550240129 -237.5875502401286,
14.69
], ],
[ [
190.834294657849, 190.83429465784906,
-282.809813460791 -282.80981346079136,
13.75
], ],
[ [
185.573061534993, 185.5730615349928,
-289.077589163396 -289.07758916339617,
13.81
], ],
[ [
180.092607888122, 180.09260788812207,
-295.654847014577 -295.6548470145767,
13.73
], ],
[ [
177.296693403105, 177.29669340310514,
-298.850189837967 -298.8501898379666,
13.58
], ],
[ [
175.756931830544, 175.7569318305437,
-300.607199758038 -300.6071997580377,
13.51
] ]
], ],
"speed": 11.0 "speed": 11.0,
"interval": 1.8
} }
], ],
"targets": [ "targets": [
22500, 15000,
25000, 35000,
30000 60000
] ]
} }
] ]

Binary file not shown.

View File

@ -3,7 +3,7 @@
importer="scene" importer="scene"
importer_version=1 importer_version=1
type="PackedScene" type="PackedScene"
uid="uid://dm4gmb5igf0n5" uid="uid://b3e8mqi8lqhx7"
path="res://.godot/imported/level.glb-4aa597fe23a02438c7c91aac8329adb8.scn" path="res://.godot/imported/level.glb-4aa597fe23a02438c7c91aac8329adb8.scn"
[deps] [deps]

View File

@ -2,149 +2,174 @@
{ {
"name": "Cliffs Corner", "name": "Cliffs Corner",
"spawn": { "spawn": {
"dx": 0.711628947630267, "x": 21.72600853145121,
"dz": -0.702555507340622, "z": -89.96723823539561,
"x": 21.7260085314512, "y": 6.96,
"z": -89.9672382353956 "dx": 0.7116289476302671,
"dz": -0.7025555073406219
}, },
"streams": [ "streams": [
{ {
"interval": 1.5,
"pts": [ "pts": [
[ [
63.9261938834286, 63.92619388342862,
-109.989375180132 -109.98937518013152,
17.17
], ],
[ [
78.3750416498091, 78.3750416498091,
-148.834021380346 -148.8340213803462,
25.6
] ]
], ],
"speed": 12.0 "speed": 12.0,
"interval": 1.5
}, },
{ {
"interval": 1.5,
"pts": [ "pts": [
[ [
106.136602457004, 106.13660245700423,
-179.682190508149 -179.68219050814878,
30.64
], ],
[ [
105.148984701035, 105.14898470103478,
-213.871671308174 -213.87167130817437,
28.03
] ]
], ],
"speed": 12.0 "speed": 12.0,
"interval": 1.5
}, },
{ {
"interval": 1.8,
"pts": [ "pts": [
[ [
83.4362514359038, 83.43625143590378,
-146.951428620045 -146.95142862004474,
26.11
], ],
[ [
68.9874036695233, 68.98740366952329,
-108.10678241983 -108.10678241983003,
17.79
] ]
], ],
"speed": 11.0 "speed": 11.0,
"interval": 1.8
}, },
{ {
"interval": 1.8,
"pts": [ "pts": [
[ [
110.546733134203, 110.54673313420321,
-214.027593892259 -214.02759389225906,
27.69
], ],
[ [
111.534350890173, 111.53435089017266,
-179.838113092233 -179.83811309223347,
30.29
] ]
], ],
"speed": 11.0 "speed": 11.0,
"interval": 1.8
}, },
{ {
"interval": 1.5,
"pts": [ "pts": [
[ [
110.037936896846, 110.03793689684629,
-168.495835638497 -168.49583563849748,
31.15
], ],
[ [
105.077018158356, 105.07701815835642,
-166.641057207958 -166.64105720795808,
31.55
], ],
[ [
102.766219089942, 102.76621908994164,
-165.688685229298 -165.6886852292976,
31.69
], ],
[ [
100.056232383348, 100.0562323833477,
-163.762330372999 -163.76233037299872,
31.53
], ],
[ [
96.9873892973125, 96.9873892973125,
-160.786566032783 -160.78656603278262,
31.04
], ],
[ [
94.5049985015079, 94.50499850150788,
-158.034131644146 -158.03413164414619,
30.28
], ],
[ [
91.4342291831665, 91.4342291831665,
-153.718055174129 -153.71805517412884,
29.08
], ],
[ [
82.7084835068341, 82.70848350683406,
-145.882804882052 -145.88280488205245,
25.76
] ]
], ],
"speed": 12.0 "speed": 12.0,
"interval": 1.5
}, },
{ {
"interval": 1.8,
"pts": [ "pts": [
[ [
79.1028095788788, 79.10280957887883,
-149.902645118338 -149.90264511833848,
25.94
], ],
[ [
87.5616128524851, 87.56161285248508,
-157.481410426472 -157.4814104264721,
28.74
], ],
[ [
90.2585312272088, 90.25853122720883,
-161.369924756482 -161.369924756482,
29.84
], ],
[ [
93.1092763097095, 93.10927630970947,
-164.544256767181 -164.5442567671812,
30.49
], ],
[ [
96.5784627653289, 96.57846276532892,
-167.893325227024 -167.89332522702423,
31.01
], ],
[ [
100.07071556656, 100.07071556655998,
-170.367815571085 -170.36781557108517,
31.29
], ],
[ [
103.112804730391, 103.11280473039093,
-171.671153192072 -171.67115319207213,
31.36
], ],
[ [
108.146577680852, 108.14657768085227,
-173.553775961996 -173.5537759619962,
30.96
] ]
], ],
"speed": 11.0 "speed": 11.0,
"interval": 1.8
} }
], ],
"targets": [ "targets": [
4000, 15000,
6500, 35000,
10500 60000
] ]
} }
] ]

Binary file not shown.

View File

@ -3,7 +3,7 @@
importer="scene" importer="scene"
importer_version=1 importer_version=1
type="PackedScene" type="PackedScene"
uid="uid://dik381ld32een" uid="uid://cr8nyyans2y6h"
path="res://.godot/imported/level.glb-30461b4810ef38297351fb89cbe9aa18.scn" path="res://.godot/imported/level.glb-30461b4810ef38297351fb89cbe9aa18.scn"
[deps] [deps]

View File

@ -2,397 +2,432 @@
{ {
"name": "Albert x Adelaide", "name": "Albert x Adelaide",
"spawn": { "spawn": {
"dx": -0.663386853147646, "x": 20.69205272588558,
"dz": -0.748276608662107, "z": -24.71065228160878,
"x": 20.6920527258856, "y": 29.84,
"z": -24.7106522816088 "dx": -0.6633868531476461,
"dz": -0.7482766086621067
}, },
"streams": [ "streams": [
{ {
"interval": 1.5,
"pts": [ "pts": [
[ [
-58.8461484663001, -58.84614846630014,
-114.424174209248 -114.42417420924829,
11.97
], ],
[ [
-53.880474905008, -53.880474905008,
-108.391854739427 -108.39185473942744,
12.77
], ],
[ [
-51.2405150349884, -51.24051503498836,
-105.525786544745 -105.52578654474546,
14.25
], ],
[ [
-20.3529842406907, -20.352984240690695,
-70.7696216351455 -70.76962163514554,
23.48
] ]
], ],
"speed": 12.0 "speed": 12.0,
"interval": 1.5
}, },
{ {
"interval": 1.5,
"pts": [ "pts": [
[ [
-86.703252503893, -86.70325250389301,
-94.0877138699484 -94.08771386994844,
22.9
], ],
[ [
-77.607019688744, -77.60701968874399,
-101.920941690901 -101.92094169090129,
18.15
], ],
[ [
-72.0652250322851, -72.06522503228508,
-106.9478629653 -106.94786296530025,
14.98
], ],
[ [
-68.7920906183039, -68.79209061830386,
-109.508381296246 -109.50838129624599,
13.7
], ],
[ [
-62.6926613135619, -62.692661313561906,
-114.753855415977 -114.75385541597699,
12.25
], ],
[ [
-56.4508191491618, -56.45081914916183,
-120.138498306347 -120.13849830634709,
11.56
], ],
[ [
-53.1441813532654, -53.14418135326541,
-122.980904904961 -122.9809049049611,
11.66
], ],
[ [
-38.7548693234, -38.75486932340002,
-135.567757774861 -135.5677577748611,
14.48
], ],
[ [
-8.13888016446398, -8.138880164463979,
-163.054303917178 -163.05430391717795,
16.8
], ],
[ [
7.53742621322278, 7.537426213222782,
-176.886292547868 -176.88629254786764,
19.45
] ]
], ],
"speed": 12.0 "speed": 12.0,
"interval": 1.5
}, },
{ {
"interval": 1.8,
"pts": [ "pts": [
[ [
-24.3893890595013, -24.38938905950134,
-67.1825007647702 -67.18250076477023,
24.51
], ],
[ [
-55.2720133743741, -55.272013374374126,
-101.933152255045 -101.93315225504524,
15.33
], ],
[ [
-57.9853352512913, -57.98533525129133,
-104.883276460764 -104.88327646076375,
14.17
], ],
[ [
-63.0150881333189, -63.01508813331892,
-110.991982190617 -110.99198219061685,
12.81
] ]
], ],
"speed": 11.0 "speed": 11.0,
"interval": 1.8
}, },
{ {
"interval": 1.8,
"pts": [ "pts": [
[ [
11.1101677795621, 11.110167779562063,
-172.837154651951 -172.83715465195058,
20.06
], ],
[ [
-4.54306405533622, -4.543064055336218,
-159.025643282508 -159.02564328250827,
17.14
], ],
[ [
-35.1638772092336, -35.16387720923361,
-131.534796625037 -131.5347966250365,
14.59
], ],
[ [
-49.5953698809185, -49.59536988091847,
-118.910777494948 -118.91077749494814,
12.53
], ],
[ [
-52.9260113021268, -52.92601130212681,
-116.047565693668 -116.04756569366813,
12.11
], ],
[ [
-59.1685752860572, -59.168575286057155,
-110.662300983888 -110.66230098388816,
12.46
], ],
[ [
-65.3362602521517, -65.33626025215173,
-105.359015503696 -105.35901550369637,
14.71
], ],
[ [
-68.5423745958537, -68.5423745958537,
-102.855244634386 -102.85524463438601,
15.97
], ],
[ [
-74.0429581637501, -74.0429581637501,
-97.8641615087792 -97.86416150877923,
18.58
], ],
[ [
-83.1794893924223, -83.17948939242227,
-89.9958813300908 -89.9958813300908,
23.25
] ]
], ],
"speed": 11.0 "speed": 11.0,
"interval": 1.8
}, },
{ {
"interval": 1.5,
"pts": [ "pts": [
[ [
-63.049013950748, -63.049013950748005,
-111.034041203303 -111.03404120330288,
12.8
], ],
[ [
-67.6731119774683, -67.67311197746825,
-116.885568358634 -116.88556835863362,
12.36
], ],
[ [
-69.3364043278144, -69.33640432781439,
-119.016515361795 -119.01651536179543,
12.14
], ],
[ [
-72.7496535402502, -72.74965354025018,
-122.556584617365 -122.55658461736515,
11.98
], ],
[ [
-98.2440886423925, -98.24408864239251,
-142.519976823492 -142.51997682349207,
18.09
], ],
[ [
-106.032628822328, -106.03262882232805,
-149.462816226915 -149.46281622691504,
21.13
], ],
[ [
-127.254841098403, -127.2548410984026,
-167.50831278782 -167.5083127878196,
27.66
] ]
], ],
"speed": 12.0 "speed": 12.0,
"interval": 1.5
}, },
{ {
"interval": 1.8,
"pts": [ "pts": [
[ [
-123.756786956036, -123.7567869560363,
-171.622145212345 -171.62214521234466,
28.43
], ],
[ [
-102.508313753821, -102.50831375382124,
-153.554173373131 -153.55417337313145,
20.4
], ],
[ [
-94.8493767077653, -94.84937670776534,
-146.719492376612 -146.71949237661244,
17.28
], ],
[ [
-69.3394816295263, -69.3394816295263,
-126.743555782696 -126.74355578269574,
11.23
], ],
[ [
-65.3252993333639, -65.32529933336386,
-122.631904237628 -122.63190423762802,
11.48
], ],
[ [
-63.4329063916396, -63.432906391639634,
-120.229317241236 -120.22931724123647,
11.7
], ],
[ [
-58.8122226488711, -58.81222264887106,
-114.382115196562 -114.38211519656227,
11.97
] ]
], ],
"speed": 11.0 "speed": 11.0,
"interval": 1.8
} }
], ],
"targets": [ "targets": [
23000, 15000,
25500, 35000,
29500 60000
] ]
}, },
{ {
"name": "Edward x Elizabeth", "name": "Elizabeth x George",
"spawn": { "spawn": {
"dx": 0.688388502507091, "x": -167.94554211176842,
"dz": 0.725342174160613, "z": 282.4630938016437,
"x": 171.921077302301, "y": 13.4,
"z": -175.651225571771 "dx": 0.7551073297079488,
"dz": -0.6556011902226315
}, },
"streams": [ "streams": [
{ {
"interval": 1.5,
"pts": [ "pts": [
[ [
228.742601685131, -151.6480512074991,
-123.809388411418 261.15863374688365,
20.11
], ],
[ [
250.487851794393, -144.0595617023658,
-101.124186911579 254.3334990786212,
21.13
], ],
[ [
253.191844757795, -143.3213169995492,
-98.1321613960494 253.69278253341383,
21.21
], ],
[ [
258.485283289911, -91.08224191170491,
-92.2834398309675 208.5453042937327,
26.1
],
[
-88.11035783744204,
205.98090738044175,
25.99
],
[
-80.87164084704513,
199.7122732897846,
25.71
] ]
], ],
"speed": 12.0 "speed": 12.0,
"interval": 1.5
}, },
{ {
"interval": 1.5,
"pts": [ "pts": [
[ [
184.819681990446, -118.25454597584726,
-31.5997398360326 162.31711832498388,
27.58
], ],
[ [
200.990172438104, -141.88997628727992,
-45.5669611828909 134.01017432495556,
], 28.01
[
206.298224766768,
-50.115127103271
],
[
208.012771617165,
-51.5958506176373
],
[
243.567491578602,
-82.5547120764429
],
[
246.131368490435,
-84.8446372752634
],
[
254.684705396329,
-92.485245640523
] ]
], ],
"speed": 12.0 "speed": 12.0,
"interval": 1.5
}, },
{ {
"interval": 1.8,
"pts": [ "pts": [
[ [
254.481581996358, -77.33660099284295,
-88.659853768515 203.79436751079447,
25.77
], ],
[ [
249.186979870745, -84.57742467711269,
-94.509861403438 210.06482501974904,
26.02
], ],
[ [
246.577198699258, -87.55143656981038,
-97.4003726882982 212.6310617067039,
26.03
], ],
[ [
224.844319166858, -139.79024774732977,
-120.07262598818 257.7783118670535,
21.09
],
[
-140.45452335644003,
258.3539093219179,
21.03
],
[
-148.03695777611765,
265.1736062536181,
19.8
] ]
], ],
"speed": 11.0 "speed": 11.0,
"interval": 1.8
}, },
{ {
"interval": 1.8,
"pts": [ "pts": [
[ [
258.282159889941, -137.74491839162505,
-88.4580479589594 130.5491780750776,
27.82
], ],
[ [
249.72869976276, -114.10948808019242,
-80.817329524774 158.85612207510593,
], 26.89
[
247.117079187487,
-78.4852615230074
],
[
211.558093240145,
-47.5226829818001
],
[
209.815723834205,
-46.017908496633
],
[
204.515981699117,
-41.4768916168623
],
[
188.349489765203,
-27.5131205641572
] ]
], ],
"speed": 11.0 "speed": 11.0,
"interval": 1.8
}, },
{ {
"interval": 1.5,
"pts": [ "pts": [
[ [
258.480541378725, -80.89109247754493,
-92.2886654287672 199.7292817019625,
25.71
], ],
[ [
262.27371225915, -74.7953733286614,
-88.1195420122156 194.34772839631796,
25.24
], ],
[ [
266.827379303769, -35.46218143888725,
-83.1100839184909 160.46495405823146,
], 21.98
[
270.873333747605,
-78.6578477713867
],
[
298.089289341555,
-47.3042762934113
] ]
], ],
"speed": 12.0 "speed": 12.0,
"interval": 1.5
}, },
{ {
"interval": 1.5,
"pts": [ "pts": [
[ [
254.709252094248, -81.10769187758535,
-92.5069068273425 203.56321084385132,
25.83
], ],
[ [
259.703037116882, -86.41612277054091,
-96.8600914480509 197.68669995304248,
25.87
], ],
[ [
263.665450959975, -97.11136428582687,
-100.3447248693 185.75737187487675,
26.43
], ],
[ [
288.615579063857, -118.18679983484368,
-122.117679372756 162.3951682081065,
27.58
] ]
], ],
"speed": 12.0 "speed": 12.0,
"interval": 1.5
} }
], ],
"targets": [ "targets": [
22000, 15000,
24500, 35000,
28500 60000
] ]
} }
] ]

Binary file not shown.

View File

@ -3,7 +3,7 @@
importer="scene" importer="scene"
importer_version=1 importer_version=1
type="PackedScene" type="PackedScene"
uid="uid://djsujf76tbo3k" uid="uid://do65sk46ogjid"
path="res://.godot/imported/level.glb-f093d0a503a60c4e982eefdc88ad879e.scn" path="res://.godot/imported/level.glb-f093d0a503a60c4e982eefdc88ad879e.scn"
[deps] [deps]

View File

@ -2,345 +2,407 @@
{ {
"name": "Melbourne x Merivale", "name": "Melbourne x Merivale",
"spawn": { "spawn": {
"dx": 0.852877329272599,
"dz": -0.522111349438832,
"x": -596.868944790812, "x": -596.868944790812,
"z": -210.352681833747 "z": -210.35268183374677,
"y": 8.98,
"dx": 0.8528773292725991,
"dz": -0.5221113494388324
}, },
"streams": [ "streams": [
{ {
"interval": 1.5,
"pts": [ "pts": [
[ [
-522.243146999435, -522.2431469994345,
-263.291609906853 -263.2916099068527,
7.69
], ],
[ [
-513.298474365917, -513.2984743659174,
-268.564620105823 -268.5646201058227,
7.44
], ],
[ [
-505.766456184546, -505.76645618454586,
-272.847714331466 -272.8477143314658,
6.89
], ],
[ [
-497.908990415611, -497.90899041561073,
-277.262924417905 -277.2629244179048,
6.23
] ]
], ],
"speed": 12.0 "speed": 12.0,
"interval": 1.5
}, },
{ {
"interval": 1.5,
"pts": [ "pts": [
[ [
-518.331960424561, -518.3319604245614,
-316.208166257863 -316.20816625786284,
9.74
], ],
[ [
-502.539989208635, -502.53998920863535,
-289.117722143497 -289.11772214349674,
7.36
], ],
[ [
-501.031072012682, -501.0310720126822,
-286.533998911906 -286.5339989119062,
7.03
], ],
[ [
-499.307492099731, -499.3074920997309,
-283.769724904009 -283.76972490400925,
6.7
], ],
[ [
-498.930545303579, -498.93054530357944,
-283.241377841783 -283.2413778417831,
6.63
], ],
[ [
-494.345014474119, -494.34501447411924,
-276.414559193545 -276.414559193545,
6.05
] ]
], ],
"speed": 12.0 "speed": 12.0,
"interval": 1.5
}, },
{ {
"interval": 1.8,
"pts": [ "pts": [
[ [
-495.263684530077, -495.26368453007734,
-272.555233182069 -272.55523318206934,
6.0
], ],
[ [
-503.109412344477, -503.1094123444767,
-268.146638068316 -268.14663806831595,
6.51
], ],
[ [
-510.589252181608, -510.58925218160783,
-263.893419493984 -263.8934194939837,
6.98
], ],
[ [
-519.500789611495, -519.5007896114952,
-258.639784893289 -258.6397848932893,
7.39
] ]
], ],
"speed": 11.0 "speed": 11.0,
"interval": 1.8
}, },
{ {
"interval": 1.8,
"pts": [ "pts": [
[ [
-498.827660471569, -498.82766047156883,
-273.403598406429 -273.4035984064291,
6.2
], ],
[ [
-503.407264010886, -503.4072640108861,
-280.221611358154 -280.2216113581537,
6.94
], ],
[ [
-503.859919895901, -503.85991989590116,
-280.865348695483 -280.8653486954825,
7.03
], ],
[ [
-505.652275149097, -505.6522751490968,
-283.740348688276 -283.7403486882755,
7.41
], ],
[ [
-507.204986888727, -507.20498688872703,
-286.397833056652 -286.3978330566516,
7.75
], ],
[ [
-522.997174842978, -522.997174842978,
-313.488648942198 -313.4886489421985,
10.11
] ]
], ],
"speed": 11.0 "speed": 11.0,
"interval": 1.8
}, },
{ {
"interval": 1.5,
"pts": [ "pts": [
[ [
-497.709720982148, -497.7097209821479,
-277.364279299137 -277.3642792991366,
6.22
], ],
[ [
-489.265341389724, -489.2653413897242,
-281.217004706688 -281.21700470668776,
6.15
], ],
[ [
-470.163954224667, -470.16395422466655,
-292.974174195101 -292.9741741951012,
5.43
], ],
[ [
-453.397320014964, -453.3973200149641,
-303.36964984342 -303.3696498434199,
6.15
], ],
[ [
-438.717138348101, -438.7171383481014,
-313.023771847006 -313.0237718470059,
8.99
], ],
[ [
-428.175689682796, -428.17568968279613,
-319.695221818248 -319.6952218182481,
11.09
] ]
], ],
"speed": 12.0 "speed": 12.0,
"interval": 1.5
}, },
{ {
"interval": 1.5,
"pts": [ "pts": [
[ [
-490.912703075514, -490.9127030755138,
-271.24169070377 -271.2416907037699,
6.0
], ],
[ [
-487.079481353019, -487.07948135301893,
-265.232181447622 -265.2321814476218,
6.0
], ],
[ [
-460.796114039586, -460.79611403958603,
-221.705738945924 -221.7057389459239,
6.26
], ],
[ [
-449.787661487498, -449.78766148749827,
-203.621656847663 -203.62165684766288,
6.74
] ]
], ],
"speed": 12.0 "speed": 12.0,
"interval": 1.5
} }
], ],
"targets": [ "targets": [
35500, 15000,
38000, 35000,
73000 60000
] ]
}, },
{ {
"name": "Grey x Glenelg", "name": "Grey x Glenelg",
"spawn": { "spawn": {
"dx": 0.5279471843098, "x": -137.3579079842604,
"dz": 0.849277204792024, "z": -96.07581886007603,
"x": -137.35790798426, "y": 13.36,
"z": -96.075818860076 "dx": 0.5279471843098005,
"dz": 0.8492772047920241
}, },
"streams": [ "streams": [
{ {
"interval": 1.5,
"pts": [ "pts": [
[ [
-66.8941396578206, -66.8941396578206,
18.0214549249385 18.021454924938546,
8.55
], ],
[ [
-68.9372122719242, -68.93721227192415,
14.7292502722074 14.729250272207434,
8.67
], ],
[ [
-74.0341764510855, -74.03417645108553,
5.78813512164328 5.788135121643277,
9.23
], ],
[ [
-78.6658442785157, -78.66584427851572,
-1.69267819146961 -1.692678191469608,
9.75
], ],
[ [
-82.8206925318667, -82.82069253186671,
-8.34435236597675 -8.344352365976752,
10.15
] ]
], ],
"speed": 12.0 "speed": 12.0,
"interval": 1.5
}, },
{ {
"interval": 1.5,
"pts": [ "pts": [
[ [
-33.0502081552841, -33.05020815528413,
-14.7586201862311 -14.758620186231095,
8.09
], ],
[ [
-35.0675152831888, -35.06751528318885,
-14.5994094114893 -14.599409411489315,
8.2
], ],
[ [
-37.270016261863, -37.27001626186296,
-13.9961460505448 -13.996146050544787,
8.29
], ],
[ [
-43.3585248193085, -43.35852481930847,
-10.7606682738875 -10.760668273887504,
8.5
], ],
[ [
-53.6949181138706, -53.694918113870564,
-4.20452046261836 -4.204520462618362,
8.68
], ],
[ [
-56.3314625884028, -56.331462588402815,
-2.47024586902805 -2.4702458690280507,
8.73
], ],
[ [
-63.0911519484621, -63.091151948462084,
2.2185465904455 2.2185465904455017,
8.85
] ]
], ],
"speed": 12.0 "speed": 12.0,
"interval": 1.5
}, },
{ {
"interval": 1.8,
"pts": [ "pts": [
[ [
-78.2407420345622, -78.24074203456223,
-11.2051308338488 -11.205130833848836,
10.32
], ],
[ [
-74.079811043889, -74.07981104388904,
-4.54369540834144 -4.543695408341438,
9.85
], ],
[ [
-69.3883249482344, -69.38832494823436,
3.03567007854908 3.0356700785490762,
9.17
], ],
[ [
-64.2732751867703, -64.27327518677029,
12.0075429283282 12.007542928328153,
8.41
], ],
[ [
-62.3066015113759, -62.30660151137585,
15.1728598756354 15.172859875635417,
8.28
] ]
], ],
"speed": 11.0 "speed": 11.0,
"interval": 1.8
}, },
{ {
"interval": 1.8,
"pts": [ "pts": [
[ [
-66.1688465571084, -66.1688465571084,
-2.2185465904455 -2.2185465904455017,
9.29
], ],
[ [
-59.3783588139033, -59.37835881390333,
-6.92854413081255 -6.9285441308125515,
9.26
], ],
[ [
-56.6027334256873, -56.602733425687326,
-8.75475233718959 -8.754752337189586,
9.26
], ],
[ [
-46.1200499391949, -46.12004993919494,
-15.4011401262142 -15.401140126214159,
9.08
], ],
[ [
-39.5077365181293, -39.50773651812935,
-18.9106763490265 -18.910676349026538,
8.74
], ],
[ [
-36.1005431869604, -36.1005431869604,
-19.8996785886188 -19.899678588618844,
8.57
], ],
[ [
-33.5155306840827, -33.51553068408273,
-20.1385342138149 -20.138534213814854,
8.43
] ]
], ],
"speed": 11.0 "speed": 11.0,
"interval": 1.8
}, },
{ {
"interval": 1.5,
"pts": [ "pts": [
[ [
-62.4286691941866, -62.42866919418658,
-1.5633764655738 -1.5633764655737972,
9.03
], ],
[ [
-56.2253414936718, -56.22534149367183,
7.17631569340894 7.176315693408943,
8.15
], ],
[ [
-31.6082945749603, -31.608294574960304,
46.3990126063831 46.399012606383145,
8.71
] ]
], ],
"speed": 12.0 "speed": 12.0,
"interval": 1.5
}, },
{ {
"interval": 1.5,
"pts": [ "pts": [
[ [
-105.444153258371, -105.44415325837058,
17.2933813892185 17.29338138921854,
9.0
], ],
[ [
-124.855497585617, -124.85549758561747,
29.0280377160981 29.02803771609806,
10.01
], ],
[ [
-145.965198973204, -145.9651989732041,
41.6802343084808 41.680234308480806,
12.4
] ]
], ],
"speed": 12.0 "speed": 12.0,
"interval": 1.5
} }
], ],
"targets": [ "targets": [
22500, 15000,
25000, 35000,
29000 60000
] ]
} }
] ]

Binary file not shown.

View File

@ -3,7 +3,7 @@
importer="scene" importer="scene"
importer_version=1 importer_version=1
type="PackedScene" type="PackedScene"
uid="uid://dw52qeb8csi30" uid="uid://dxm6gf8nrgrl1"
path="res://.godot/imported/level.glb-17af9cdeb05fd15a816097b7ae0fb96c.scn" path="res://.godot/imported/level.glb-17af9cdeb05fd15a816097b7ae0fb96c.scn"
[deps] [deps]

View File

@ -2,185 +2,219 @@
{ {
"name": "Bridge Approach", "name": "Bridge Approach",
"spawn": { "spawn": {
"dx": -0.114095858645717, "x": 35.51608905758518,
"dz": -0.993469745407426, "z": -118.5964830886139,
"x": 35.5160890575852, "y": 6.7,
"z": -118.596483088614 "dx": -0.11409585864571734,
"dz": -0.9934697454074263
}, },
"streams": [ "streams": [
{ {
"interval": 1.5,
"pts": [ "pts": [
[ [
21.8244656781075, 21.82446567810751,
-237.813875098783 -237.81387509878263,
37.66
], ],
[ [
23.1457200273832, 23.145720027383163,
-226.455207706528 -226.45520770652828,
37.76
], ],
[ [
24.7882096121438, 24.788209612143817,
-213.848153713549 -213.84815371354867,
35.66
], ],
[ [
26.1455691749522, 26.145569174952232,
-200.297419769933 -200.29741976993301,
31.44
] ]
], ],
"speed": 12.0 "speed": 12.0,
"interval": 1.5
}, },
{ {
"interval": 1.5,
"pts": [ "pts": [
[ [
2.19994158852704, 2.199941588527042,
-240.327171190027 -240.32717119002737,
36.31
], ],
[ [
-2.52481722237557, -2.5248172223755736,
-243.919505367749 -243.91950536774928,
35.81
], ],
[ [
-6.32534953080772, -6.325349530807717,
-247.60965512377 -247.60965512377,
35.17
], ],
[ [
-15.1839832375373, -15.183983237537262,
-255.261358332787 -255.2613583327868,
33.26
], ],
[ [
-19.8227220865701, -19.822722086570074,
-258.67299529667 -258.6729952966697,
32.36
], ],
[ [
-24.2795153329838, -24.279515332983824,
-261.703355786389 -261.70335578638884,
31.56
], ],
[ [
-29.0114379744006, -29.011437974400625,
-263.890761064968 -263.89076106496833,
30.9
] ]
], ],
"speed": 12.0 "speed": 12.0,
"interval": 1.5
}, },
{ {
"interval": 1.8,
"pts": [ "pts": [
[ [
20.7724471517801, 20.772447151780117,
-199.7593122304 -199.75931223039953,
31.4
], ],
[ [
19.4233779811381, 19.423377981138138,
-213.232863886751 -213.23286388675078,
35.3
], ],
[ [
17.786545159723, 17.78654515972297,
-225.792452293243 -225.7924522932427,
37.39
], ],
[ [
16.4606356403737, 16.46063564037367,
-237.18991410136 -237.18991410136036,
37.31
] ]
], ],
"speed": 11.0 "speed": 11.0,
"interval": 1.8
}, },
{ {
"interval": 1.8,
"pts": [ "pts": [
[ [
-26.7370429025148, -26.737042902514784,
-268.788426535065 -268.7884265350654,
30.16
], ],
[ [
-21.6112433779486, -21.61124337794858,
-266.398068213575 -266.3980682135746,
30.89
], ],
[ [
-16.704188313209, -16.704188313209045,
-263.081481102976 -263.0814811029762,
31.77
], ],
[ [
-11.7617743504717, -11.761774350471743,
-259.438496866914 -259.43849686691397,
32.83
], ],
[ [
-2.72241951437748, -2.7224195143774796,
-251.63195487599 -251.63195487598963,
34.86
], ],
[ [
0.983930787446262, 0.9839307874462617,
-248.024220632365 -248.02422063236523,
35.6
], ],
[ [
5.46498065752023, 5.4649806575202335,
-244.628278010132 -244.6282780101317,
36.2
] ]
], ],
"speed": 11.0 "speed": 11.0,
"interval": 1.8
}, },
{ {
"interval": 1.5,
"pts": [ "pts": [
[ [
11.2707008429023, 11.27070084290225,
-197.847843241665 -197.84784324166512,
31.27
], ],
[ [
10.8961758047183, 10.896175804718348,
-204.766417702285 -204.76641770228525,
33.12
], ],
[ [
10.4764778796752, 10.476477879675205,
-211.209757888086 -211.20975788808624,
34.75
], ],
[ [
9.92304078347784, 9.92304078347784,
-216.207006965937 -216.20700696593718,
35.63
], ],
[ [
8.93295830185362, 8.932958301853615,
-221.625317668162 -221.62531766816238,
36.37
], ],
[ [
7.50873869919987, 7.508738699199866,
-226.865371707994 -226.8653717079938,
36.97
] ]
], ],
"speed": 12.0 "speed": 12.0,
"interval": 1.5
}, },
{ {
"interval": 1.5,
"pts": [ "pts": [
[ [
28.875188928697, 28.875188928696993,
-229.492425462448 -229.49242546244793,
38.44
], ],
[ [
28.7254802520939, 28.72548025209394,
-214.053661592775 -214.05366159277472,
35.92
], ],
[ [
28.1508746327618, 28.150874632761848,
-209.459560296273 -209.45956029627345,
34.63
], ],
[ [
27.2791473530598, 27.279147353059784,
-204.781298995536 -204.7812989955361,
32.98
], ],
[ [
26.0948795290309, 26.094879529030933,
-199.443402630656 -199.44340263065618,
31.15
] ]
], ],
"speed": 12.0 "speed": 12.0,
"interval": 1.5
} }
], ],
"targets": [ "targets": [
5000, 15000,
7500, 35000,
11500 60000
] ]
} }
] ]

Binary file not shown.

View File

@ -3,7 +3,7 @@
importer="scene" importer="scene"
importer_version=1 importer_version=1
type="PackedScene" type="PackedScene"
uid="uid://bjd3utm2spgs0" uid="uid://dduudwpqocdxu"
path="res://.godot/imported/level.glb-045fc5cf8c0e529ecdc2678ee7acde2a.scn" path="res://.godot/imported/level.glb-045fc5cf8c0e529ecdc2678ee7acde2a.scn"
[deps] [deps]

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cfq88o84umvip"
path="res://.godot/imported/shot_bridge_dem.png-75be2353aa1d96cb1fa690daade691e3.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://shot_bridge_dem.png"
dest_files=["res://.godot/imported/shot_bridge_dem.png-75be2353aa1d96cb1fa690daade691e3.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

40
shot_kp_dem.png.import Normal file
View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://boesb5lcxhlfv"
path="res://.godot/imported/shot_kp_dem.png-c9ce5dfc952a5ba8544840830a36b4c0.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://shot_kp_dem.png"
dest_files=["res://.godot/imported/shot_kp_dem.png-c9ce5dfc952a5ba8544840830a36b4c0.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

40
shot_kp_top.png.import Normal file
View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dv0r0ctfsvyse"
path="res://.godot/imported/shot_kp_top.png-9b910b7280f9d3c8de3410ea2430621a.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://shot_kp_top.png"
dest_files=["res://.godot/imported/shot_kp_top.png-9b910b7280f9d3c8de3410ea2430621a.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

40
shot_trolleys.png.import Normal file
View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://w384sbljyqjk"
path="res://.godot/imported/shot_trolleys.png-4bc10206031793a54eda76f31a101806.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://shot_trolleys.png"
dest_files=["res://.godot/imported/shot_trolleys.png-4bc10206031793a54eda76f31a101806.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@ -81,6 +81,7 @@ var busts := 0
var escapes := 0 var escapes := 0
var drag_end: Vector3 var drag_end: Vector3
var drag_end_y := 0.0
var drag_len := 0.0 var drag_len := 0.0
var gear := 1 var gear := 1
var revs := 0.0 var revs := 0.0
@ -165,7 +166,7 @@ func _ready() -> void:
var sp: Dictionary = junction["spawn"] var sp: Dictionary = junction["spawn"]
var jf := Vector3(sp["dx"], 0, sp["dz"]).normalized() var jf := Vector3(sp["dx"], 0, sp["dz"]).normalized()
car.transform = Transform3D(Basis.looking_at(jf, Vector3.UP), car.transform = Transform3D(Basis.looking_at(jf, Vector3.UP),
_clear_spawn(Vector3(sp["x"], 0.7, sp["z"]), jf)) _clear_spawn(Vector3(sp["x"], float(sp.get("y", 0.0)) + 0.7, sp["z"]), jf))
car.hold_wreck = true car.hold_wreck = true
# The marker test lives in the branch condition, not inside it: as an `elif` # The marker test lives in the branch condition, not inside it: as an `elif`
# with the test inside, a level without a drag strip (the code-built carpark) # with the test inside, a level without a drag strip (the code-built carpark)
@ -174,7 +175,8 @@ func _ready() -> void:
elif mode == "drag" and _drag_markers(level).size() == 2: elif mode == "drag" and _drag_markers(level).size() == 2:
var mk := _drag_markers(level) var mk := _drag_markers(level)
drag_end = Vector3(mk[1].global_position.x, 0.0, mk[1].global_position.z) drag_end = Vector3(mk[1].global_position.x, 0.0, mk[1].global_position.z)
var from := Vector3(mk[0].global_position.x, 0.7, mk[0].global_position.z) drag_end_y = mk[1].global_position.y
var from := Vector3(mk[0].global_position.x, mk[0].global_position.y + 0.7, mk[0].global_position.z)
var dir := (drag_end - Vector3(from.x, 0, from.z)).normalized() var dir := (drag_end - Vector3(from.x, 0, from.z)).normalized()
drag_len = Vector3(from.x, 0, from.z).distance_to(drag_end) drag_len = Vector3(from.x, 0, from.z).distance_to(drag_end)
car.transform = Transform3D(Basis.looking_at(dir, Vector3.UP), car.transform = Transform3D(Basis.looking_at(dir, Vector3.UP),
@ -189,7 +191,8 @@ func _ready() -> void:
var f := -t.basis.z var f := -t.basis.z
f.y = 0.0 f.y = 0.0
f = f.normalized() if f.length_squared() > 0.001 else Vector3.FORWARD f = f.normalized() if f.length_squared() > 0.001 else Vector3.FORWARD
car.transform = Transform3D(Basis.looking_at(f, Vector3.UP), Vector3(t.origin.x, 0.7, t.origin.z)) car.transform = Transform3D(Basis.looking_at(f, Vector3.UP),
Vector3(t.origin.x, t.origin.y + 0.7, t.origin.z))
else: else:
car.position = Vector3.UP * 0.6 car.position = Vector3.UP * 0.6
add_child(car) add_child(car)
@ -244,10 +247,10 @@ func _ready() -> void:
var strip := Path3D.new() var strip := Path3D.new()
var sc := Curve3D.new() var sc := Curve3D.new()
sc.add_point(car.global_position + lane) sc.add_point(car.global_position + lane)
sc.add_point(drag_end + Vector3.UP * 0.7 + lane) sc.add_point(drag_end + Vector3.UP * (drag_end_y + 0.7) + lane)
# a 2-point curve makes rival.gd's fmod() lookahead wrap to the start once # a 2-point curve makes rival.gd's fmod() lookahead wrap to the start once
# it nears the end, spinning the car around -- extend past the line instead # it nears the end, spinning the car around -- extend past the line instead
sc.add_point(drag_end + Vector3.UP * 0.7 + lane + fwd * 120.0) sc.add_point(drag_end + Vector3.UP * (drag_end_y + 0.7) + lane + fwd * 120.0)
strip.curve = sc strip.curve = sc
add_child(strip) add_child(strip)
var pool := Registry.cars().values() var pool := Registry.cars().values()
@ -296,7 +299,8 @@ func _ready() -> void:
var path := Path3D.new() var path := Path3D.new()
var curve := Curve3D.new() var curve := Curve3D.new()
for p in stream["pts"]: for p in stream["pts"]:
curve.add_point(Vector3(p[0], 0, p[1])) # third component (when baked) is terrain height
curve.add_point(Vector3(p[0], float(p[2]) if p.size() > 2 else 0.0, p[1]))
path.curve = curve path.curve = curve
add_child(path) add_child(path)
var L := curve.get_baked_length() var L := curve.get_baked_length()
@ -447,7 +451,8 @@ func _spawn_smashables(level: Node) -> void:
add_child(b) add_child(b)
var t: Transform3D = (m as Node3D).global_transform var t: Transform3D = (m as Node3D).global_transform
b.global_transform = Transform3D( b.global_transform = Transform3D(
Basis(Vector3.UP, t.basis.get_euler().y), Vector3(t.origin.x, 0.75, t.origin.z)) Basis(Vector3.UP, t.basis.get_euler().y),
Vector3(t.origin.x, t.origin.y + 0.72, t.origin.z))
b.sleeping = true b.sleeping = true
n += 1 n += 1
for prefix in SMASHABLE: for prefix in SMASHABLE:
@ -514,7 +519,7 @@ func _spawn_smashables(level: Node) -> void:
var t: Transform3D = (m as Node3D).global_transform var t: Transform3D = (m as Node3D).global_transform
b.global_transform = Transform3D( b.global_transform = Transform3D(
Basis(Vector3.UP, t.basis.get_euler().y), Basis(Vector3.UP, t.basis.get_euler().y),
Vector3(t.origin.x, size.y * 0.5 + 0.02, t.origin.z)) Vector3(t.origin.x, t.origin.y + size.y * 0.5 + 0.02, t.origin.z))
b.sleeping = true b.sleeping = true
n += 1 n += 1

View File

@ -14,7 +14,7 @@ while IFS='|' read -r id json lat lon specs; do
IFS=';' read -ra SPEC <<< "$specs" IFS=';' read -ra SPEC <<< "$specs"
python3 tools/osm_junction.py "$C/$json" "levels/$id/junctions.json" "$lat" "$lon" "${SPEC[@]}" python3 tools/osm_junction.py "$C/$json" "levels/$id/junctions.json" "$lat" "$lon" "${SPEC[@]}"
done <<'EOF' done <<'EOF'
queen_st_mall|brisbane_cbd.json|-27.4699|153.0251|Albert x Adelaide|Albert Street|Adelaide Street;Edward x Elizabeth|Edward Street|Elizabeth Street queen_st_mall|brisbane_cbd.json|-27.4699|153.0251|Albert x Adelaide|Albert Street|Adelaide Street;Elizabeth x George|Elizabeth Street|George Street
southbank|southbank.json|-27.4775|153.0215|Melbourne x Merivale|Melbourne Street|Merivale Street;Grey x Glenelg|Grey Street|Glenelg Street southbank|southbank.json|-27.4775|153.0215|Melbourne x Merivale|Melbourne Street|Merivale Street;Grey x Glenelg|Grey Street|Glenelg Street
albert_parklands|albert_parklands.json|-27.4740|153.0280|Alice x George|Alice Street|George Street;Margaret x Edward|Margaret Street|Edward Street albert_parklands|albert_parklands.json|-27.4740|153.0280|Alice x George|Alice Street|George Street;Margaret x Edward|Margaret Street|Edward Street
story_bridge|story_bridge.json|-27.4635|153.0355|Bridge Approach|Bradfield Highway|Kemp Place story_bridge|story_bridge.json|-27.4635|153.0355|Bridge Approach|Bradfield Highway|Kemp Place

View File

@ -41,6 +41,50 @@ TEX = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "assets", "
M_LAT = 110574.0 M_LAT = 110574.0
M_LON = 111320.0 * math.cos(math.radians(LAT0)) M_LON = 111320.0 * math.cos(math.radians(LAT0))
# ---- real terrain (tools/fetch_dem.py output next to the area json) --------
import numpy as _np
DEM = None
DEM_META = None
_dem_base = src[:-5] if src.endswith(".json") else src
if os.path.exists(_dem_base + "_dem.f32"):
DEM_META = json.load(open(_dem_base + "_dem.json"))
DEM = _np.fromfile(_dem_base + "_dem.f32", dtype="<f4").reshape(
DEM_META["h"], DEM_META["w"])
print("DEM loaded: %dx%d, %.1f..%.1f m" % (DEM_META["w"], DEM_META["h"],
float(DEM.min()), float(DEM.max())))
def H(x, y):
"""Terrain height at level-local metres, bilinear. 0 when no DEM."""
if DEM is None:
return 0.0
lon = LON0 + x / M_LON
lat = LAT0 + y / M_LAT
u = (lon - DEM_META["min_lon"]) / (DEM_META["max_lon"] - DEM_META["min_lon"]) * (DEM_META["w"] - 1)
v = (DEM_META["max_lat"] - lat) / (DEM_META["max_lat"] - DEM_META["min_lat"]) * (DEM_META["h"] - 1)
u = min(max(u, 0.0), DEM_META["w"] - 1.001)
v = min(max(v, 0.0), DEM_META["h"] - 1.001)
x0, y0 = int(u), int(v)
fx, fy = u - x0, v - y0
a = DEM[y0, x0] * (1 - fx) + DEM[y0, x0 + 1] * fx
b = DEM[y0 + 1, x0] * (1 - fx) + DEM[y0 + 1, x0 + 1] * fx
return float(a * (1 - fy) + b * fy)
WATER_Z = 0.3 # river surface: above the riverbed's DEM zero, below banks
CARVE = [] # (x, y, h) of every draped road/path sample: terrain may not rise above these
def bridge_profile(pts, named=False):
"""Deck heights for a bridge way: lerp between end terrain plus a hump, so
the deck spans the river instead of draping down into it."""
cum = [0.0]
for i in range(1, len(pts)):
cum.append(cum[-1] + math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]))
T = max(cum[-1], 0.001)
hA, hB = H(*pts[0]), H(*pts[-1])
hump = 16.0 if named else 3.5
return [max(H(*pts[i]),
hA + (hB - hA) * (cum[i] / T) + hump * math.sin(math.pi * cum[i] / T))
for i in range(len(pts))]
GROUND_FLOOR = 4.2 # height of the glazed shopfront band GROUND_FLOOR = 4.2 # height of the glazed shopfront band
AWNING_Z = 3.75 # underside of the street awning AWNING_Z = 3.75 # underside of the street awning
AWNING_OUT = 2.3 # how far it cantilevers over the footpath AWNING_OUT = 2.3 # how far it cantilevers over the footpath
@ -228,10 +272,11 @@ def ring_ccw(pts):
for i in range(len(r))) for i in range(len(r)))
return r if area > 0 else list(reversed(r)) return r if area > 0 else list(reversed(r))
def poly_bm(pts, z, h=0.0): def poly_bm(pts, z, h=0.0, drape=False):
"""Filled (optionally extruded) polygon bmesh from closed pt list.""" """Filled (optionally extruded) polygon bmesh from closed pt list.
drape=True adds terrain height per vertex (parks); flat otherwise (water)."""
bm = bmesh.new() bm = bmesh.new()
verts = [bm.verts.new((x, y, z)) for x, y in pts[:-1]] verts = [bm.verts.new((x, y, z + (H(x, y) if drape else 0.0))) for x, y in pts[:-1]]
face = bm.faces.new(verts) face = bm.faces.new(verts)
if h > 0.0: if h > 0.0:
res = bmesh.ops.extrude_face_region(bm, geom=[face]) res = bmesh.ops.extrude_face_region(bm, geom=[face])
@ -241,9 +286,27 @@ def poly_bm(pts, z, h=0.0):
bmesh.ops.triangulate(bm, faces=bm.faces) bmesh.ops.triangulate(bm, faces=bm.faces)
return bm return bm
def ribbon(bm, pts, half, z, mat=0, drop=None, offset=0.0): def _subdivide(pts, step=8.0):
"""Split long segments so draped ribbons follow the terrain instead of
chording across it -- an OSM segment can run 100+ m, and a straight strip
over curved ground diverges from the collision mesh by metres. That read
in-game as the car sinking to its windows mid-block."""
out = [pts[0]]
for i in range(1, len(pts)):
(x1, y1), (x2, y2) = out[-1], pts[i]
L = math.hypot(x2 - x1, y2 - y1)
n = max(int(L / step), 1)
for k in range(1, n + 1):
out.append((x1 + (x2 - x1) * k / n, y1 + (y2 - y1) * k / n))
return out
def ribbon(bm, pts, half, z, mat=0, drop=None, offset=0.0, zs=None):
"""Strip of width 2*half along a polyline, shifted sideways by `offset`. """Strip of width 2*half along a polyline, shifted sideways by `offset`.
`drop` chamfers the edges down to z-PATH_H.""" `drop` chamfers the edges down to z-PATH_H. Heights come from the terrain
(per segment END, not per corner, so roads don't twist across their width);
`zs` overrides with explicit per-point heights (bridge decks)."""
if zs is None and DEM is not None:
pts = _subdivide(pts)
for i in range(len(pts) - 1): for i in range(len(pts) - 1):
(x1, y1), (x2, y2) = pts[i], pts[i + 1] (x1, y1), (x2, y2) = pts[i], pts[i + 1]
dx, dy = x2 - x1, y2 - y1 dx, dy = x2 - x1, y2 - y1
@ -251,79 +314,88 @@ def ribbon(bm, pts, half, z, mat=0, drop=None, offset=0.0):
if L < 0.1: if L < 0.1:
continue continue
nx, ny = -dy / L, dx / L nx, ny = -dy / L, dx / L
h1 = zs[i] if zs else H(x1, y1)
h2 = zs[i + 1] if zs else H(x2, y2)
if zs is None:
CARVE.append((x1, y1, h1))
CARVE.append((x2, y2, h2))
if offset: if offset:
x1, y1 = x1 + nx * offset, y1 + ny * offset x1, y1 = x1 + nx * offset, y1 + ny * offset
x2, y2 = x2 + nx * offset, y2 + ny * offset x2, y2 = x2 + nx * offset, y2 + ny * offset
if drop is None: if drop is None:
quad(bm, [(x1 + nx * half, y1 + ny * half, z), (x1 - nx * half, y1 - ny * half, z), quad(bm, [(x1 + nx * half, y1 + ny * half, z + h1), (x1 - nx * half, y1 - ny * half, z + h1),
(x2 - nx * half, y2 - ny * half, z), (x2 + nx * half, y2 + ny * half, z)], mat) (x2 - nx * half, y2 - ny * half, z + h2), (x2 + nx * half, y2 + ny * half, z + h2)], mat)
else: else:
inner = max(half - PATH_CHAMFER, half * 0.35) inner = max(half - PATH_CHAMFER, half * 0.35)
for s0, s1, z0, z1 in ((half, inner, z - drop, z), (inner, -inner, z, z), for s0, s1, d0, d1 in ((half, inner, drop, 0.0), (inner, -inner, 0.0, 0.0),
(-inner, -half, z, z - drop)): (-inner, -half, 0.0, drop)):
quad(bm, [(x1 + nx * s0, y1 + ny * s0, z0), (x1 + nx * s1, y1 + ny * s1, z1), quad(bm, [(x1 + nx * s0, y1 + ny * s0, z - d0 + h1), (x1 + nx * s1, y1 + ny * s1, z - d1 + h1),
(x2 + nx * s1, y2 + ny * s1, z1), (x2 + nx * s0, y2 + ny * s0, z0)], mat) (x2 + nx * s1, y2 + ny * s1, z - d1 + h2), (x2 + nx * s0, y2 + ny * s0, z - d0 + h2)], mat)
# ------------------------------------------------------------------- props # ------------------------------------------------------------------- props
def add_tree(fol, jac, frond, trunk, x, y, rng): def add_tree(fol, jac, frond, trunk, x, y, rng):
"""Fig (broad layered canopy), jacaranda (same shape, purple), or palm.""" """Fig (broad layered canopy), jacaranda (same shape, purple), or palm."""
z0 = H(x, y)
if rng.random() < 0.18: if rng.random() < 0.18:
h = rng.uniform(6.5, 9.5) h = rng.uniform(6.5, 9.5)
cone(trunk, (x, y, h / 2), 0.22, 0.14, h, 7) cone(trunk, (x, y, z0 + h / 2), 0.22, 0.14, h, 7)
for i in range(7): for i in range(7):
a = i * math.tau / 7 + rng.uniform(-0.25, 0.25) a = i * math.tau / 7 + rng.uniform(-0.25, 0.25)
reach = rng.uniform(1.8, 2.5) reach = rng.uniform(1.8, 2.5)
tip = Vector((x + math.cos(a) * reach, y + math.sin(a) * reach, h - rng.uniform(0.8, 1.6))) tip = Vector((x + math.cos(a) * reach, y + math.sin(a) * reach, z0 + h - rng.uniform(0.8, 1.6)))
beam(frond, Vector((x, y, h)), tip, 0.7, 0.12) beam(frond, Vector((x, y, z0 + h)), tip, 0.7, 0.12)
else: else:
# canopy well clear of the roof line of a car, and built from three # canopy well clear of the roof line of a car, and built from three
# offset spheres so the silhouette breaks up instead of reading as a ball # offset spheres so the silhouette breaks up instead of reading as a ball
clear = rng.uniform(3.6, 4.8) clear = rng.uniform(3.6, 4.8)
r = rng.uniform(1.7, 2.5) r = rng.uniform(1.7, 2.5)
cone(trunk, (x, y, (clear + 1.0) / 2), 0.20, 0.13, clear + 1.0, 7) cone(trunk, (x, y, z0 + (clear + 1.0) / 2), 0.20, 0.13, clear + 1.0, 7)
target = jac if rng.random() < 0.22 else fol # jacaranda season is eternal here target = jac if rng.random() < 0.22 else fol # jacaranda season is eternal here
for i in range(3): for i in range(3):
off = Vector((rng.uniform(-1, 1), rng.uniform(-1, 1), 0)) * r * 0.42 off = Vector((rng.uniform(-1, 1), rng.uniform(-1, 1), 0)) * r * 0.42
blob(target, (x + off.x, y + off.y, clear + r * (0.45 + 0.34 * i)), blob(target, (x + off.x, y + off.y, z0 + clear + r * (0.45 + 0.34 * i)),
(r * rng.uniform(0.75, 1.05), r * rng.uniform(0.75, 1.05), r * rng.uniform(0.55, 0.75)), (r * rng.uniform(0.75, 1.05), r * rng.uniform(0.75, 1.05), r * rng.uniform(0.55, 0.75)),
subdiv=2) subdiv=2)
def add_bench(bm, x, y, rng): def add_bench(bm, x, y, rng):
z0 = H(x, y)
a = rng.random() * math.tau a = rng.random() * math.tau
d = Vector((math.cos(a), math.sin(a), 0)) d = Vector((math.cos(a), math.sin(a), 0))
p = Vector((-d.y, d.x, 0)) p = Vector((-d.y, d.x, 0))
for s in (-0.75, 0.75): for s in (-0.75, 0.75):
c = Vector((x, y, 0)) + d * s c = Vector((x, y, z0)) + d * s
beam(bm, c + Vector((0, 0, 0.02)), c + Vector((0, 0, 0.45)), 0.36, 0.07) beam(bm, c + Vector((0, 0, 0.02)), c + Vector((0, 0, 0.45)), 0.36, 0.07)
a0 = Vector((x, y, 0.47)) - d * 0.95 a0 = Vector((x, y, z0 + 0.47)) - d * 0.95
slab(bm, [(a0 + p * 0.28).to_tuple(), (a0 - p * 0.28).to_tuple(), slab(bm, [(a0 + p * 0.28).to_tuple(), (a0 - p * 0.28).to_tuple(),
(a0 - p * 0.28 + d * 1.9).to_tuple(), (a0 + p * 0.28 + d * 1.9).to_tuple()], 0.08) (a0 - p * 0.28 + d * 1.9).to_tuple(), (a0 + p * 0.28 + d * 1.9).to_tuple()], 0.08)
def add_signal(bm, lens, x, y, rng): def add_signal(bm, lens, x, y, rng):
"""Aussie traffic signal: pole, lantern head, mast arm with a second head. """Aussie traffic signal: pole, lantern head, mast arm with a second head.
Lenses go in their own bmesh so they can be emissive.""" Lenses go in their own bmesh so they can be emissive."""
z0 = H(x, y)
h = 3.9 h = 3.9
cone(bm, (x, y, h / 2), 0.10, 0.08, h, 6) cone(bm, (x, y, z0 + h / 2), 0.10, 0.08, h, 6)
a = rng.random() * math.tau a = rng.random() * math.tau
d = Vector((math.cos(a), math.sin(a), 0)) d = Vector((math.cos(a), math.sin(a), 0))
p = Vector((-d.y, d.x, 0)) p = Vector((-d.y, d.x, 0))
for at, top in ((Vector((x, y, 0)) + p * 0.22, h - 0.15), for at, top in ((Vector((x, y, z0)) + p * 0.22, z0 + h - 0.15),
(Vector((x, y, 0)) + d * 3.6, h + 0.55)): (Vector((x, y, z0)) + d * 3.6, z0 + h + 0.55)):
# lantern housing + three stacked lenses # lantern housing + three stacked lenses
beam(bm, at + Vector((0, 0, top - 1.0)), at + Vector((0, 0, top)), 0.34, 0.34) beam(bm, at + Vector((0, 0, top - 1.0)), at + Vector((0, 0, top)), 0.34, 0.34)
for i, m in enumerate((0, 1, 2)): for i, m in enumerate((0, 1, 2)):
c = at + Vector((0, 0, top - 0.22 - i * 0.32)) c = at + Vector((0, 0, top - 0.22 - i * 0.32))
# must clear beam()'s 0.05 m minimum length or the lens silently vanishes # must clear beam()'s 0.05 m minimum length or the lens silently vanishes
beam(lens, c + d * 0.15, c + d * 0.28, 0.20, 0.20, m) beam(lens, c + d * 0.15, c + d * 0.28, 0.20, 0.20, m)
beam(bm, Vector((x, y, h + 0.4)), Vector((x, y, h + 0.4)) + d * 3.6, 0.13, 0.13) # mast arm beam(bm, Vector((x, y, z0 + h + 0.4)), Vector((x, y, z0 + h + 0.4)) + d * 3.6, 0.13, 0.13) # mast arm
def add_power_pole(bm, wire_bm, x, y, prev, rng): def add_power_pole(bm, wire_bm, x, y, prev, rng):
"""Timber power pole with a crossarm; wires sag to the previous pole.""" """Timber power pole with a crossarm; wires sag to the previous pole."""
z0 = H(x, y)
h = 8.6 h = 8.6
cone(bm, (x, y, h / 2), 0.19, 0.13, h, 6) cone(bm, (x, y, z0 + h / 2), 0.19, 0.13, h, 6)
beam(bm, (x - 0.95, y, h - 0.5), (x + 0.95, y, h - 0.5), 0.11, 0.11) beam(bm, (x - 0.95, y, z0 + h - 0.5), (x + 0.95, y, z0 + h - 0.5), 0.11, 0.11)
tops = [Vector((x - 0.8, y, h - 0.38)), Vector((x, y, h - 0.05)), Vector((x + 0.8, y, h - 0.38))] tops = [Vector((x - 0.8, y, z0 + h - 0.38)), Vector((x, y, z0 + h - 0.05)), Vector((x + 0.8, y, z0 + h - 0.38))]
if prev is not None: if prev is not None:
for a, b in zip(prev, tops): for a, b in zip(prev, tops):
mid = (a + b) / 2 - Vector((0, 0, (a - b).length * 0.045)) # catenary sag mid = (a + b) / 2 - Vector((0, 0, (a - b).length * 0.045)) # catenary sag
@ -335,7 +407,7 @@ def add_shelter(bm, glassbm, x, y, rng):
a = rng.random() * math.tau a = rng.random() * math.tau
d = Vector((math.cos(a), math.sin(a), 0)) d = Vector((math.cos(a), math.sin(a), 0))
p = Vector((-d.y, d.x, 0)) p = Vector((-d.y, d.x, 0))
c = Vector((x, y, 0)) c = Vector((x, y, H(x, y)))
for sd in (-1.8, 1.8): for sd in (-1.8, 1.8):
for sp in (-1.1, 1.1): for sp in (-1.1, 1.1):
f = c + d * sd + p * sp f = c + d * sd + p * sp
@ -460,23 +532,27 @@ for e in d["elements"]:
bm = bmesh.new() bm = bmesh.new()
gf = 0.0 if qld else min(GROUND_FLOOR, h * 0.45) gf = 0.0 if qld else min(GROUND_FLOOR, h * 0.45)
# rigid terrain lift: base at the LOWEST terrain under the footprint so
# the uphill side cuts into the slope; walls run 2 m below base so no
# daylight shows under the downhill edge
bz = min(H(x, y) for x, y in ring)
for i in range(len(ring)): for i in range(len(ring)):
(x1, y1), (x2, y2) = ring[i], ring[(i + 1) % len(ring)] (x1, y1), (x2, y2) = ring[i], ring[(i + 1) % len(ring)]
if gf > 0.0: if gf > 0.0:
quad(bm, [(x1, y1, 0), (x2, y2, 0), (x2, y2, gf), (x1, y1, gf)], 2) quad(bm, [(x1, y1, bz - 2.0), (x2, y2, bz - 2.0), (x2, y2, bz + gf), (x1, y1, bz + gf)], 2)
quad(bm, [(x1, y1, gf), (x2, y2, gf), (x2, y2, h), (x1, y1, h)], 0) quad(bm, [(x1, y1, bz + gf), (x2, y2, bz + gf), (x2, y2, bz + h), (x1, y1, bz + h)], 0)
try: try:
bm.faces.new([bm.verts.new((x, y, h)) for x, y in ring]).material_index = 1 bm.faces.new([bm.verts.new((x, y, bz + h)) for x, y in ring]).material_index = 1
except ValueError: except ValueError:
pass pass
if not qld: if not qld:
for i in range(len(ring)): # parapet caps the roof edge for i in range(len(ring)): # parapet caps the roof edge
(x1, y1), (x2, y2) = ring[i], ring[(i + 1) % len(ring)] (x1, y1), (x2, y2) = ring[i], ring[(i + 1) % len(ring)]
beam(bm, (x1, y1, h + 0.45), (x2, y2, h + 0.45), 0.35, 0.9, 0) beam(bm, (x1, y1, bz + h + 0.45), (x2, y2, bz + h + 0.45), 0.35, 0.9, 0)
if h > 26: # rooftop plant room if h > 26: # rooftop plant room
cx = sum(p[0] for p in ring) / len(ring) cx = sum(p[0] for p in ring) / len(ring)
cy = sum(p[1] for p in ring) / len(ring) cy = sum(p[1] for p in ring) / len(ring)
beam(bm, (cx - 2.5, cy, h + 1.4), (cx + 2.5, cy, h + 1.4), 4.0, 2.8, 1) beam(bm, (cx - 2.5, cy, bz + h + 1.4), (cx + 2.5, cy, bz + h + 1.4), 4.0, 2.8, 1)
bmesh.ops.triangulate(bm, faces=bm.faces) bmesh.ops.triangulate(bm, faces=bm.faces)
add_obj("bld_%d-col" % e["id"], bm, [fac, M_CORRO if qld else M_ROOF, M_SHOP], add_obj("bld_%d-col" % e["id"], bm, [fac, M_CORRO if qld else M_ROOF, M_SHOP],
{0: 13.0 if not qld else 6.5, 1: 9.0 if not qld else 3.0, 2: 4.6}) {0: 13.0 if not qld else 6.5, 1: 9.0 if not qld else 3.0, 2: 4.6})
@ -492,16 +568,16 @@ for e in d["elements"]:
mx, my = (ex1 + ex2) / 2, (ey1 + ey2) / 2 mx, my = (ex1 + ex2) / 2, (ey1 + ey2) / 2
ux, uy = (ex2 - ex1) / eL, (ey2 - ey1) / eL ux, uy = (ex2 - ex1) / eL, (ey2 - ey1) / eL
half = min(4.0, eL * 0.35) half = min(4.0, eL * 0.35)
a = Vector((mx - ux * half, my - uy * half, h + 2.6)) a = Vector((mx - ux * half, my - uy * half, bz + h + 2.6))
b = Vector((mx + ux * half, my + uy * half, h + 2.6)) b = Vector((mx + ux * half, my + uy * half, bz + h + 2.6))
bb = bill_bm[e["id"] % 3] bb = bill_bm[e["id"] % 3]
beam(bb, a, b, 0.25, 3.0) beam(bb, a, b, 0.25, 3.0)
beam(bb, Vector((mx, my, h)), Vector((mx, my, h + 1.4)), 0.3, 0.3) beam(bb, Vector((mx, my, bz + h)), Vector((mx, my, bz + h + 1.4)), 0.3, 0.3)
# street awning over the footpath -- not every shop has one, and a whole # street awning over the footpath -- not every shop has one, and a whole
# street at one height in one colour reads as a painted stripe # street at one height in one colour reads as a painted stripe
if not qld and 6.0 < h < 45.0 and rng.random() < 0.7: if not qld and 6.0 < h < 45.0 and rng.random() < 0.7:
az = AWNING_Z + rng.uniform(-0.35, 0.5) az = bz + AWNING_Z + rng.uniform(-0.35, 0.5)
amat = e["id"] % 3 amat = e["id"] % 3
out = AWNING_OUT * rng.uniform(0.85, 1.1) out = AWNING_OUT * rng.uniform(0.85, 1.1)
for i in range(len(ring)): for i in range(len(ring)):
@ -522,7 +598,7 @@ for e in d["elements"]:
if not closed: if not closed:
continue continue
try: try:
bm = poly_bm(pts, -0.35) bm = poly_bm(pts, WATER_Z)
except Exception: except Exception:
continue continue
# ponytail: water is solid (-col) so cars skim it instead of falling forever # ponytail: water is solid (-col) so cars skim it instead of falling forever
@ -534,11 +610,12 @@ for e in d["elements"]:
continue continue
ring = ring_ccw(pts) ring = ring_ccw(pts)
bm = bmesh.new() bm = bmesh.new()
pz = min(H(x, y) for x, y in ring)
for i in range(len(ring)): # raised corten planter box for i in range(len(ring)): # raised corten planter box
(x1, y1), (x2, y2) = ring[i], ring[(i + 1) % len(ring)] (x1, y1), (x2, y2) = ring[i], ring[(i + 1) % len(ring)]
quad(bm, [(x1, y1, 0), (x2, y2, 0), (x2, y2, 0.5), (x1, y1, 0.5)], 0) quad(bm, [(x1, y1, pz), (x2, y2, pz), (x2, y2, pz + 0.5), (x1, y1, pz + 0.5)], 0)
try: try:
bm.faces.new([bm.verts.new((x, y, 0.5)) for x, y in ring]).material_index = 1 bm.faces.new([bm.verts.new((x, y, pz + 0.5)) for x, y in ring]).material_index = 1
except ValueError: except ValueError:
bm.free() bm.free()
continue continue
@ -551,7 +628,7 @@ for e in d["elements"]:
if not closed: if not closed:
continue continue
try: try:
bm = poly_bm(pts, 0.02) bm = poly_bm(pts, 0.02, drape=True)
except Exception: except Exception:
continue continue
add_obj("park_%d" % e["id"], bm, [M_GRASS], 14.0) add_obj("park_%d" % e["id"], bm, [M_GRASS], 14.0)
@ -579,8 +656,8 @@ for e in d["elements"]:
elif tags.get("natural") == "cliff": elif tags.get("natural") == "cliff":
bm = bmesh.new() bm = bmesh.new()
for i in range(len(pts) - 1): for i in range(len(pts) - 1):
beam(bm, Vector((pts[i][0], pts[i][1], 7.0)), beam(bm, Vector((pts[i][0], pts[i][1], H(*pts[i]) + 1.0)),
Vector((pts[i + 1][0], pts[i + 1][1], 7.0)), 2.0, 14.0) Vector((pts[i + 1][0], pts[i + 1][1], H(*pts[i + 1]) + 1.0)), 2.0, 14.0)
add_obj("cliff_%d-col" % e["id"], bm, [M_ROCK], 12.0) add_obj("cliff_%d-col" % e["id"], bm, [M_ROCK], 12.0)
counts["cliff"] += 1 counts["cliff"] += 1
@ -608,7 +685,10 @@ for e in d["elements"]:
wide = ROAD_W.get(hw, 6.0) wide = ROAD_W.get(hw, 6.0)
rkey = "mall" if hw == "pedestrian" else ("bus" if hw == "busway" else "road") rkey = "mall" if hw == "pedestrian" else ("bus" if hw == "busway" else "road")
ribbon(road_bm[rkey], pts, wide / 2, 0.03) deck = None
if tags.get("bridge") in ("yes", "viaduct") or (bridge_name and tags.get("name") == bridge_name):
deck = bridge_profile(pts, named=(tags.get("name") == bridge_name))
ribbon(road_bm[rkey], pts, wide / 2, 0.03, zs=deck)
counts["road"] += 1 counts["road"] += 1
if hw in ("residential", "unclassified"): if hw in ("residential", "unclassified"):
# timber power poles down one side with sagging wires -- the single # timber power poles down one side with sagging wires -- the single
@ -661,8 +741,12 @@ for e in d["elements"]:
L = math.hypot(x2 - x1, y2 - y1) L = math.hypot(x2 - x1, y2 - y1)
for k in range(int(L / 9.0)): for k in range(int(L / 9.0)):
t0, t1 = (k * 9.0 + 1.5) / L, (k * 9.0 + 5.0) / L t0, t1 = (k * 9.0 + 1.5) / L, (k * 9.0 + 5.0) / L
dz = None
if deck is not None:
dz = [deck[i] + (deck[i + 1] - deck[i]) * t0,
deck[i] + (deck[i + 1] - deck[i]) * t1]
ribbon(line_bm, [(x1 + (x2 - x1) * t0, y1 + (y2 - y1) * t0), ribbon(line_bm, [(x1 + (x2 - x1) * t0, y1 + (y2 - y1) * t0),
(x1 + (x2 - x1) * t1, y1 + (y2 - y1) * t1)], 0.09, 0.045) (x1 + (x2 - x1) * t1, y1 + (y2 - y1) * t1)], 0.09, 0.045, zs=dz)
# street trees: OSM maps a fraction of what Brisbane actually has, so line the # street trees: OSM maps a fraction of what Brisbane actually has, so line the
# kerbs of named streets and the mall edges with our own, deterministically # kerbs of named streets and the mall edges with our own, deterministically
@ -696,9 +780,9 @@ for tx, ty, seed in tree_sites:
add_tree(fol_bm, jac_bm, frond_bm, trunk_bm, tx, ty, random.Random(seed)) add_tree(fol_bm, jac_bm, frond_bm, trunk_bm, tx, ty, random.Random(seed))
counts["tree"] += 1 counts["tree"] += 1
add_obj("roads", road_bm["road"], [M_ROAD], 8.0) add_obj("roads-col", road_bm["road"], [M_ROAD], 8.0)
add_obj("mall_paving", road_bm["mall"], [M_MALL], 4.0) add_obj("mall_paving-col", road_bm["mall"], [M_MALL], 4.0)
add_obj("busway", road_bm["bus"], [M_BUSRED], 8.0) add_obj("busway-col", road_bm["bus"], [M_BUSRED], 8.0)
add_obj("trees_foliage", fol_bm, [M_FOLIAGE], 0.9, smooth=True) add_obj("trees_foliage", fol_bm, [M_FOLIAGE], 0.9, smooth=True)
add_obj("trees_jacaranda", jac_bm, [M_JAC], 0.9, smooth=True) add_obj("trees_jacaranda", jac_bm, [M_JAC], 0.9, smooth=True)
add_obj("trees_fronds", frond_bm, [M_FOLIAGE], 0.9) add_obj("trees_fronds", frond_bm, [M_FOLIAGE], 0.9)
@ -736,13 +820,22 @@ if bridge_name:
s0, s1 = 0.175 * T, 0.825 * T s0, s1 = 0.175 * T, 0.825 * T
N = 26 N = 26
bm = bmesh.new() bm = bmesh.new()
deck_zs = bridge_profile([(pp.x, pp.y) for pp in pts], named=True)
def deck_at(sd):
for i2 in range(1, len(dists)):
if dists[i2] >= sd:
f2 = (sd - dists[i2 - 1]) / max(dists[i2] - dists[i2 - 1], 0.001)
return deck_zs[i2 - 1] + (deck_zs[i2] - deck_zs[i2 - 1]) * f2
return deck_zs[-1]
samples = [] samples = []
for i in range(N + 1): for i in range(N + 1):
t = i / N t = i / N
p = at(s0 + (s1 - s0) * t) sd = s0 + (s1 - s0) * t
p = at(sd)
p = Vector((p.x, p.y, deck_at(sd))) # truss base rides the deck
h = 4.0 + 16.0 * (math.exp(-((t - 0.32) / 0.10) ** 2) + math.exp(-((t - 0.68) / 0.10) ** 2)) \ h = 4.0 + 16.0 * (math.exp(-((t - 0.32) / 0.10) ** 2) + math.exp(-((t - 0.68) / 0.10) ** 2)) \
+ 6.0 * math.sin(math.pi * t) + 6.0 * math.sin(math.pi * t)
fwd = (at(min(s0 + (s1 - s0) * t + 4, T)) - p) fwd = (at(min(sd + 4, T)) - p)
fwd.z = 0 fwd.z = 0
fwd.normalize() fwd.normalize()
samples.append((p, Vector((-fwd.y, fwd.x, 0)), h)) samples.append((p, Vector((-fwd.y, fwd.x, 0)), h))
@ -859,15 +952,54 @@ if level_id == "southbank":
bpy.context.collection.objects.link(obj_a) bpy.context.collection.objects.link(obj_a)
print("grand arbour tendrils: %d" % n_t) print("grand arbour tendrils: %d" % n_t)
# ground slab sized to content (convex collider: huge trimesh tris break raycasts) # ground: a terrain grid sampled from the DEM (or a flat slab when there is
# none). Small triangles are deliberate -- one huge trimesh tri breaks raycasts.
xs = [v.co.x for o in bpy.data.objects if o.type == "MESH" for v in o.data.vertices] xs = [v.co.x for o in bpy.data.objects if o.type == "MESH" for v in o.data.vertices]
ys = [v.co.y for o in bpy.data.objects if o.type == "MESH" for v in o.data.vertices] ys = [v.co.y for o in bpy.data.objects if o.type == "MESH" for v in o.data.vertices]
gx, gy = (min(xs) + max(xs)) / 2, (min(ys) + max(ys)) / 2 x0g, x1g = min(xs) - 50, max(xs) + 50
bm = bmesh.new() y0g, y1g = min(ys) - 50, max(ys) + 50
res = bmesh.ops.create_cube(bm, size=1.0) if DEM is not None:
bmesh.ops.scale(bm, vec=(max(xs) - min(xs) + 100, max(ys) - min(ys) + 100, 1.0), verts=res["verts"]) STEP = 9.0
bmesh.ops.translate(bm, vec=(gx, gy, -0.5), verts=res["verts"]) nx = max(int((x1g - x0g) / STEP), 2)
add_obj("ground-convcol", bm, [M_GROUND], 18.0) ny = max(int((y1g - y0g) / STEP), 2)
sx = (x1g - x0g) / nx
sy = (y1g - y0g) / ny
carve = _np.full((ny + 1, nx + 1), _np.inf, dtype=_np.float64)
for cx, cy, ch in CARVE:
gi = int(round((cx - x0g) / sx))
gj = int(round((cy - y0g) / sy))
if 0 <= gi <= nx and 0 <= gj <= ny:
carve[gj, gi] = min(carve[gj, gi], ch)
# dilate one cell so the clamp covers the whole face a road crosses
dil = carve.copy()
for dj in (-1, 0, 1):
for di in (-1, 0, 1):
dil = _np.minimum(dil, _np.roll(_np.roll(carve, dj, axis=0), di, axis=1))
bm = bmesh.new()
grid = []
for j in range(ny + 1):
row = []
for i in range(nx + 1):
gx2 = x0g + i * sx
gy2 = y0g + j * sy
gz = H(gx2, gy2) - 0.04
if dil[j, i] != _np.inf:
gz = min(gz, dil[j, i] - 0.22)
row.append(bm.verts.new((gx2, gy2, gz)))
grid.append(row)
for j in range(ny):
for i in range(nx):
bm.faces.new((grid[j][i], grid[j][i + 1], grid[j + 1][i + 1], grid[j + 1][i]))
bmesh.ops.triangulate(bm, faces=bm.faces)
add_obj("terrain-col", bm, [M_GROUND], 18.0)
print("terrain grid %dx%d (%d carve samples)" % (nx, ny, len(CARVE)))
else:
gx, gy = (x0g + x1g) / 2, (y0g + y1g) / 2
bm = bmesh.new()
res = bmesh.ops.create_cube(bm, size=1.0)
bmesh.ops.scale(bm, vec=(x1g - x0g, y1g - y0g, 1.0), verts=res["verts"])
bmesh.ops.translate(bm, vec=(gx, gy, -0.5), verts=res["verts"])
add_obj("ground-convcol", bm, [M_GROUND], 18.0)
# parked-car / wheelie-bin markers -> empties; game.gd fills them with # parked-car / wheelie-bin markers -> empties; game.gd fills them with
# shuntable RigidBodies at load (a static GLB can't carry physics bodies) # shuntable RigidBodies at load (a static GLB can't carry physics bodies)
@ -877,7 +1009,7 @@ batches = [("PP", pp_spots, PP_CAP)] + [(k, v, SMASH_CAP[k]) for k, v in smash_s
for prefix, spots, cap in batches: for prefix, spots, cap in batches:
for i, (mx, my, yaw) in enumerate(spots[:cap]): for i, (mx, my, yaw) in enumerate(spots[:cap]):
emp = bpy.data.objects.new("%s_%03d" % (prefix, i), None) emp = bpy.data.objects.new("%s_%03d" % (prefix, i), None)
emp.location = (mx, my, 0.05) emp.location = (mx, my, H(mx, my) + 0.05)
emp.rotation_euler = (0, 0, yaw) emp.rotation_euler = (0, 0, yaw)
bpy.context.collection.objects.link(emp) bpy.context.collection.objects.link(emp)
counts[prefix.lower()] = min(len(spots), cap) counts[prefix.lower()] = min(len(spots), cap)
@ -911,7 +1043,7 @@ if best_run and best_run[0] >= 150.0:
yaw = math.atan2(p1[1] - p0[1], p1[0] - p0[0]) yaw = math.atan2(p1[1] - p0[1], p1[0] - p0[0])
for nm, p in (("DragStart", p0), ("DragEnd", p1)): for nm, p in (("DragStart", p0), ("DragEnd", p1)):
emp = bpy.data.objects.new(nm, None) emp = bpy.data.objects.new(nm, None)
emp.location = (p[0], p[1], 0.1) emp.location = (p[0], p[1], H(p[0], p[1]) + 0.1)
emp.rotation_euler = (0, 0, yaw) emp.rotation_euler = (0, 0, yaw)
bpy.context.collection.objects.link(emp) bpy.context.collection.objects.link(emp)
counts["drag_m"] = int(run) counts["drag_m"] = int(run)
@ -920,7 +1052,7 @@ if spawn_arg:
slat, slon, sbear = [float(v) for v in spawn_arg.split(",")] slat, slon, sbear = [float(v) for v in spawn_arg.split(",")]
sx, sy = xy(slat, slon) sx, sy = xy(slat, slon)
sp = bpy.data.objects.new("Spawn", None) sp = bpy.data.objects.new("Spawn", None)
sp.location = (sx, sy, 0.1) sp.location = (sx, sy, H(sx, sy) + 0.1)
sp.rotation_euler = (0, 0, math.radians(-sbear)) sp.rotation_euler = (0, 0, math.radians(-sbear))
bpy.context.collection.objects.link(sp) bpy.context.collection.objects.link(sp)
@ -931,7 +1063,7 @@ if rp_arg:
plat, plon = [float(v) for v in pair.split(",")] plat, plon = [float(v) for v in pair.split(",")]
px, py = xy(plat, plon) px, py = xy(plat, plon)
emp = bpy.data.objects.new("RP_%02d" % i, None) emp = bpy.data.objects.new("RP_%02d" % i, None)
emp.location = (px, py, 0.1) emp.location = (px, py, H(px, py) + 0.1)
bpy.context.collection.objects.link(emp) bpy.context.collection.objects.link(emp)
emp.parent = rp emp.parent = rp

101
tools/fetch_dem.py Normal file
View File

@ -0,0 +1,101 @@
#!/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()

View File

@ -16,6 +16,7 @@ are actually crossing when you get there.
""" """
import json import json
import math import math
import os
import sys import sys
from itertools import zip_longest from itertools import zip_longest
@ -33,6 +34,36 @@ M_LON = 111320.0 * math.cos(math.radians(LAT0))
def xz(lat, lon): def xz(lat, lon):
return ((lon - LON0) * M_LON, -(lat - LAT0) * M_LAT) return ((lon - LON0) * M_LON, -(lat - LAT0) * M_LAT)
# terrain heights (tools/fetch_dem.py output), pure-python bilinear -- events
# bake spawn/convoy heights so the game never has to guess ground level
from array import array
DEM = None
DEM_META = None
_base = src[:-5] if src.endswith(".json") else src
if os.path.exists(_base + "_dem.f32"):
DEM_META = json.load(open(_base + "_dem.json"))
DEM = array("f")
with open(_base + "_dem.f32", "rb") as f:
DEM.fromfile(f, DEM_META["w"] * DEM_META["h"])
if sys.byteorder == "big":
DEM.byteswap()
def hgt(x, z):
if DEM is None:
return 0.0
lat = LAT0 - z / M_LAT
lon = LON0 + x / M_LON
w, h = DEM_META["w"], DEM_META["h"]
u = (lon - DEM_META["min_lon"]) / (DEM_META["max_lon"] - DEM_META["min_lon"]) * (w - 1)
v = (DEM_META["max_lat"] - lat) / (DEM_META["max_lat"] - DEM_META["min_lat"]) * (h - 1)
u = min(max(u, 0.0), w - 1.001)
v = min(max(v, 0.0), h - 1.001)
x0, y0 = int(u), int(v)
fx, fy = u - x0, v - y0
a = DEM[y0 * w + x0] * (1 - fx) + DEM[y0 * w + x0 + 1] * fx
b = DEM[(y0 + 1) * w + x0] * (1 - fx) + DEM[(y0 + 1) * w + x0 + 1] * fx
return a * (1 - fy) + b * fy
data = json.load(open(src)) data = json.load(open(src))
ways = {} ways = {}
for e in data["elements"]: for e in data["elements"]:
@ -88,13 +119,14 @@ def launch_point(pts, jpt):
dx, dz = jpt[0] - end[0], jpt[1] - end[1] dx, dz = jpt[0] - end[0], jpt[1] - end[1]
L = math.hypot(dx, dz) or 1.0 L = math.hypot(dx, dz) or 1.0
dx, dz = dx / L, dz / L dx, dz = dx / L, dz / L
ey = round(hgt(end[0] - dz * LANE, end[1] + dx * LANE), 2)
# Sit in a lane, not on the centreline. Convoys are offset +/-LANE either # Sit in a lane, not on the centreline. Convoys are offset +/-LANE either
# side, so a centreline launch threads straight between them -- Grey x # side, so a centreline launch threads straight between them -- Grey x
# Glenelg passed within 2.9 m of a convoy car and never touched one, # Glenelg passed within 2.9 m of a convoy car and never touched one,
# scoring nothing in 12 of 12 tuning runs. Same transform as offset(). # scoring nothing in 12 of 12 tuning runs. Same transform as offset().
# sign matters: offsetting the other way moved the launch AWAY from the # sign matters: offsetting the other way moved the launch AWAY from the
# convoy lane and widened Grey x Glenelg's closest pass from 2.9 m to 5.5 m # convoy lane and widened Grey x Glenelg's closest pass from 2.9 m to 5.5 m
return {"x": end[0] - dz * LANE, "z": end[1] + dx * LANE, "dx": dx, "dz": dz} return {"x": end[0] - dz * LANE, "z": end[1] + dx * LANE, "y": ey, "dx": dx, "dz": dz}
events = [] events = []
for spec in sys.argv[5:]: for spec in sys.argv[5:]:
@ -141,9 +173,10 @@ for spec in sys.argv[5:]:
for i in range(len(near) - 1)) for i in range(len(near) - 1))
if length < MIN_STREAM: if length < MIN_STREAM:
continue continue
by_street[street].append({"pts": offset(near, LANE), "speed": 12.0, "interval": 1.5}) fwd_pts = [[px, pz, round(hgt(px, pz), 2)] for px, pz in offset(near, LANE)]
by_street[street].append({"pts": offset(list(reversed(near)), LANE), rev_pts = [[px, pz, round(hgt(px, pz), 2)] for px, pz in offset(list(reversed(near)), LANE)]
"speed": 11.0, "interval": 1.8}) by_street[street].append({"pts": fwd_pts, "speed": 12.0, "interval": 1.5})
by_street[street].append({"pts": rev_pts, "speed": 11.0, "interval": 1.8})
if street == a: if street == a:
# OSM splits a street into many ways; only the one that actually # OSM splits a street into many ways; only the one that actually
# reaches the junction can define the launch, or we start 40 m # reaches the junction can define the launch, or we start 40 m