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

Binary file not shown.

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

Binary file not shown.

View File

@ -3,7 +3,7 @@
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://bjd3utm2spgs0"
uid="uid://dduudwpqocdxu"
path="res://.godot/imported/level.glb-045fc5cf8c0e529ecdc2678ee7acde2a.scn"
[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 drag_end: Vector3
var drag_end_y := 0.0
var drag_len := 0.0
var gear := 1
var revs := 0.0
@ -165,7 +166,7 @@ func _ready() -> void:
var sp: Dictionary = junction["spawn"]
var jf := Vector3(sp["dx"], 0, sp["dz"]).normalized()
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
# 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)
@ -174,7 +175,8 @@ func _ready() -> void:
elif mode == "drag" and _drag_markers(level).size() == 2:
var mk := _drag_markers(level)
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()
drag_len = Vector3(from.x, 0, from.z).distance_to(drag_end)
car.transform = Transform3D(Basis.looking_at(dir, Vector3.UP),
@ -189,7 +191,8 @@ func _ready() -> void:
var f := -t.basis.z
f.y = 0.0
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:
car.position = Vector3.UP * 0.6
add_child(car)
@ -244,10 +247,10 @@ func _ready() -> void:
var strip := Path3D.new()
var sc := Curve3D.new()
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
# 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
add_child(strip)
var pool := Registry.cars().values()
@ -296,7 +299,8 @@ func _ready() -> void:
var path := Path3D.new()
var curve := Curve3D.new()
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
add_child(path)
var L := curve.get_baked_length()
@ -447,7 +451,8 @@ func _spawn_smashables(level: Node) -> void:
add_child(b)
var t: Transform3D = (m as Node3D).global_transform
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
n += 1
for prefix in SMASHABLE:
@ -514,7 +519,7 @@ func _spawn_smashables(level: Node) -> void:
var t: Transform3D = (m as Node3D).global_transform
b.global_transform = Transform3D(
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
n += 1

View File

@ -14,7 +14,7 @@ while IFS='|' read -r id json lat lon specs; do
IFS=';' read -ra SPEC <<< "$specs"
python3 tools/osm_junction.py "$C/$json" "levels/$id/junctions.json" "$lat" "$lon" "${SPEC[@]}"
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
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

View File

@ -41,6 +41,50 @@ TEX = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "assets", "
M_LAT = 110574.0
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
AWNING_Z = 3.75 # underside of the street awning
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)))
return r if area > 0 else list(reversed(r))
def poly_bm(pts, z, h=0.0):
"""Filled (optionally extruded) polygon bmesh from closed pt list."""
def poly_bm(pts, z, h=0.0, drape=False):
"""Filled (optionally extruded) polygon bmesh from closed pt list.
drape=True adds terrain height per vertex (parks); flat otherwise (water)."""
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)
if h > 0.0:
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)
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`.
`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):
(x1, y1), (x2, y2) = pts[i], pts[i + 1]
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:
continue
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:
x1, y1 = x1 + nx * offset, y1 + ny * offset
x2, y2 = x2 + nx * offset, y2 + ny * offset
if drop is None:
quad(bm, [(x1 + nx * half, y1 + ny * half, z), (x1 - nx * half, y1 - ny * half, z),
(x2 - nx * half, y2 - ny * half, z), (x2 + nx * half, y2 + ny * half, z)], mat)
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 + h2), (x2 + nx * half, y2 + ny * half, z + h2)], mat)
else:
inner = max(half - PATH_CHAMFER, half * 0.35)
for s0, s1, z0, z1 in ((half, inner, z - drop, z), (inner, -inner, z, z),
(-inner, -half, z, z - drop)):
quad(bm, [(x1 + nx * s0, y1 + ny * s0, z0), (x1 + nx * s1, y1 + ny * s1, z1),
(x2 + nx * s1, y2 + ny * s1, z1), (x2 + nx * s0, y2 + ny * s0, z0)], mat)
for s0, s1, d0, d1 in ((half, inner, drop, 0.0), (inner, -inner, 0.0, 0.0),
(-inner, -half, 0.0, drop)):
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, z - d1 + h2), (x2 + nx * s0, y2 + ny * s0, z - d0 + h2)], mat)
# ------------------------------------------------------------------- props
def add_tree(fol, jac, frond, trunk, x, y, rng):
"""Fig (broad layered canopy), jacaranda (same shape, purple), or palm."""
z0 = H(x, y)
if rng.random() < 0.18:
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):
a = i * math.tau / 7 + rng.uniform(-0.25, 0.25)
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)))
beam(frond, Vector((x, y, h)), tip, 0.7, 0.12)
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, z0 + h)), tip, 0.7, 0.12)
else:
# 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
clear = rng.uniform(3.6, 4.8)
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
for i in range(3):
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)),
subdiv=2)
def add_bench(bm, x, y, rng):
z0 = H(x, y)
a = rng.random() * math.tau
d = Vector((math.cos(a), math.sin(a), 0))
p = Vector((-d.y, d.x, 0))
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)
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(),
(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):
"""Aussie traffic signal: pole, lantern head, mast arm with a second head.
Lenses go in their own bmesh so they can be emissive."""
z0 = H(x, y)
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
d = Vector((math.cos(a), math.sin(a), 0))
p = Vector((-d.y, d.x, 0))
for at, top in ((Vector((x, y, 0)) + p * 0.22, h - 0.15),
(Vector((x, y, 0)) + d * 3.6, h + 0.55)):
for at, top in ((Vector((x, y, z0)) + p * 0.22, z0 + h - 0.15),
(Vector((x, y, z0)) + d * 3.6, z0 + h + 0.55)):
# lantern housing + three stacked lenses
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)):
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
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):
"""Timber power pole with a crossarm; wires sag to the previous pole."""
z0 = H(x, y)
h = 8.6
cone(bm, (x, y, 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)
tops = [Vector((x - 0.8, y, h - 0.38)), Vector((x, y, h - 0.05)), Vector((x + 0.8, y, h - 0.38))]
cone(bm, (x, y, z0 + h / 2), 0.19, 0.13, h, 6)
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, 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:
for a, b in zip(prev, tops):
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
d = Vector((math.cos(a), math.sin(a), 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 sp in (-1.1, 1.1):
f = c + d * sd + p * sp
@ -460,23 +532,27 @@ for e in d["elements"]:
bm = bmesh.new()
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)):
(x1, y1), (x2, y2) = ring[i], ring[(i + 1) % len(ring)]
if gf > 0.0:
quad(bm, [(x1, y1, 0), (x2, y2, 0), (x2, y2, gf), (x1, y1, gf)], 2)
quad(bm, [(x1, y1, gf), (x2, y2, gf), (x2, y2, h), (x1, y1, h)], 0)
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, bz + gf), (x2, y2, bz + gf), (x2, y2, bz + h), (x1, y1, bz + h)], 0)
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:
pass
if not qld:
for i in range(len(ring)): # parapet caps the roof edge
(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
cx = sum(p[0] 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)
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})
@ -492,16 +568,16 @@ for e in d["elements"]:
mx, my = (ex1 + ex2) / 2, (ey1 + ey2) / 2
ux, uy = (ex2 - ex1) / eL, (ey2 - ey1) / eL
half = min(4.0, eL * 0.35)
a = Vector((mx - ux * half, my - uy * half, h + 2.6))
b = 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, bz + h + 2.6))
bb = bill_bm[e["id"] % 3]
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 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:
az = AWNING_Z + rng.uniform(-0.35, 0.5)
az = bz + AWNING_Z + rng.uniform(-0.35, 0.5)
amat = e["id"] % 3
out = AWNING_OUT * rng.uniform(0.85, 1.1)
for i in range(len(ring)):
@ -522,7 +598,7 @@ for e in d["elements"]:
if not closed:
continue
try:
bm = poly_bm(pts, -0.35)
bm = poly_bm(pts, WATER_Z)
except Exception:
continue
# ponytail: water is solid (-col) so cars skim it instead of falling forever
@ -534,11 +610,12 @@ for e in d["elements"]:
continue
ring = ring_ccw(pts)
bm = bmesh.new()
pz = min(H(x, y) for x, y in ring)
for i in range(len(ring)): # raised corten planter box
(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:
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:
bm.free()
continue
@ -551,7 +628,7 @@ for e in d["elements"]:
if not closed:
continue
try:
bm = poly_bm(pts, 0.02)
bm = poly_bm(pts, 0.02, drape=True)
except Exception:
continue
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":
bm = bmesh.new()
for i in range(len(pts) - 1):
beam(bm, Vector((pts[i][0], pts[i][1], 7.0)),
Vector((pts[i + 1][0], pts[i + 1][1], 7.0)), 2.0, 14.0)
beam(bm, Vector((pts[i][0], pts[i][1], H(*pts[i]) + 1.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)
counts["cliff"] += 1
@ -608,7 +685,10 @@ for e in d["elements"]:
wide = ROAD_W.get(hw, 6.0)
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
if hw in ("residential", "unclassified"):
# 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)
for k in range(int(L / 9.0)):
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),
(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
# 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))
counts["tree"] += 1
add_obj("roads", road_bm["road"], [M_ROAD], 8.0)
add_obj("mall_paving", road_bm["mall"], [M_MALL], 4.0)
add_obj("busway", road_bm["bus"], [M_BUSRED], 8.0)
add_obj("roads-col", road_bm["road"], [M_ROAD], 8.0)
add_obj("mall_paving-col", road_bm["mall"], [M_MALL], 4.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_jacaranda", jac_bm, [M_JAC], 0.9, smooth=True)
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
N = 26
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 = []
for i in range(N + 1):
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)) \
+ 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.normalize()
samples.append((p, Vector((-fwd.y, fwd.x, 0)), h))
@ -859,13 +952,52 @@ if level_id == "southbank":
bpy.context.collection.objects.link(obj_a)
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]
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
y0g, y1g = min(ys) - 50, max(ys) + 50
if DEM is not None:
STEP = 9.0
nx = max(int((x1g - x0g) / STEP), 2)
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=(max(xs) - min(xs) + 100, max(ys) - min(ys) + 100, 1.0), verts=res["verts"])
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)
@ -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 i, (mx, my, yaw) in enumerate(spots[:cap]):
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)
bpy.context.collection.objects.link(emp)
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])
for nm, p in (("DragStart", p0), ("DragEnd", p1)):
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)
bpy.context.collection.objects.link(emp)
counts["drag_m"] = int(run)
@ -920,7 +1052,7 @@ if spawn_arg:
slat, slon, sbear = [float(v) for v in spawn_arg.split(",")]
sx, sy = xy(slat, slon)
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))
bpy.context.collection.objects.link(sp)
@ -931,7 +1063,7 @@ if rp_arg:
plat, plon = [float(v) for v in pair.split(",")]
px, py = xy(plat, plon)
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)
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 math
import os
import sys
from itertools import zip_longest
@ -33,6 +34,36 @@ M_LON = 111320.0 * math.cos(math.radians(LAT0))
def xz(lat, lon):
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))
ways = {}
for e in data["elements"]:
@ -88,13 +119,14 @@ def launch_point(pts, jpt):
dx, dz = jpt[0] - end[0], jpt[1] - end[1]
L = math.hypot(dx, dz) or 1.0
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
# 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,
# scoring nothing in 12 of 12 tuning runs. Same transform as offset().
# 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
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 = []
for spec in sys.argv[5:]:
@ -141,9 +173,10 @@ for spec in sys.argv[5:]:
for i in range(len(near) - 1))
if length < MIN_STREAM:
continue
by_street[street].append({"pts": offset(near, LANE), "speed": 12.0, "interval": 1.5})
by_street[street].append({"pts": offset(list(reversed(near)), LANE),
"speed": 11.0, "interval": 1.8})
fwd_pts = [[px, pz, round(hgt(px, pz), 2)] for px, pz in offset(near, LANE)]
rev_pts = [[px, pz, round(hgt(px, pz), 2)] for px, pz in offset(list(reversed(near)), LANE)]
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:
# OSM splits a street into many ways; only the one that actually
# reaches the junction can define the launch, or we start 40 m