extends Node3D ## Queensland Raceway's National Circuit -- "the Paperclip" -- built in code. ## The centreline is digitised from the official track map (pixel coords) and ## scaled so the lap is exactly 3126 m, the real National Circuit length. ## Three tilted parallel straights, two outer hairpins, the inner loop and the ## T6 fishhook. Corners get kerbs, sand traps and tyre stacks automatically ## from curvature; straights get billboard hoardings. ## ## Race mode picks up the RacePath (same curve) and grids the field on it. const TEX := "res://assets/textures/%s.jpg" const LAP_M := 3126.0 const ROAD_W := 13.0 const STEP := 4.0 # road-mesh sampling, metres ## National Circuit red line off the track map, in image pixels (x, y-down). ## Drive order: SF heading west -> T1 -> west straight -> T2 hairpin -> top ## straight east -> east 180 -> middle straight west -> inner loop left -> ## third straight east -> T6 fishhook -> front straight -> SF. const CENTRE_PX := [ [680, 795], [300, 800], [150, 782], [85, 720], [70, 620], [68, 400], [85, 290], [130, 235], [220, 215], [700, 262], [1290, 335], [1345, 372], [1352, 420], [1300, 452], [900, 470], [460, 485], [400, 505], [375, 545], [385, 590], [430, 622], [520, 640], [800, 680], [1080, 718], [1150, 720], [1200, 748], [1195, 782], [1140, 795], ] var curve: Curve3D func _ready() -> void: var rng := RandomNumberGenerator.new() rng.seed = 3126 curve = _build_curve() var samples := _samples() # grass world sized off the track's bounding box var lo := Vector3(INF, 0, INF) var hi := Vector3(-INF, 0, -INF) for s in samples: lo.x = minf(lo.x, s["pos"].x); lo.z = minf(lo.z, s["pos"].z) hi.x = maxf(hi.x, s["pos"].x); hi.z = maxf(hi.z, s["pos"].z) var mid := (lo + hi) * 0.5 var span := hi - lo _slab(Vector3(mid.x, -0.5, mid.z), Vector3(span.x + 260, 1, span.z + 260), _mat("grass", 18.0)) _road(samples) var corners := _corners(samples) _kerbs(samples) for c in corners: _corner_dressing(c, samples) _hoardings(samples, corners) _start_finish(samples) _pits(samples) # the pond, as seen from every aerial photo of the place var pond := Vector3(lo.x + span.x * 0.25, 0.03, lo.z + span.z * 0.37) _slab(pond, Vector3(55, 0.03, 38), _flat(Color(0.13, 0.25, 0.30), 0.15), false) _gums(rng, samples, lo, hi) var path := Path3D.new() path.name = "RacePath" path.curve = curve add_child(path) var spawn := Marker3D.new() spawn.name = "Spawn" var t0: Dictionary = samples[3] spawn.transform = Transform3D(Basis.looking_at(t0["tan"], Vector3.UP), t0["pos"] + Vector3.UP * 0.5) add_child(spawn) func _build_curve() -> Curve3D: # raw pixel loop -> catmull tangents -> measure -> scale to LAP_M exactly var raw: Array[Vector3] = [] for p in CENTRE_PX: raw.append(Vector3(p[0], 0, p[1])) var c := Curve3D.new() var n := raw.size() for i in n + 1: var p := raw[i % n] var t: Vector3 = (raw[(i + 1) % n] - raw[(i - 1 + n) % n]) * 0.2 c.add_point(p, -t, t) var scale := LAP_M / c.get_baked_length() var sc := Curve3D.new() for i in c.point_count: sc.add_point(c.get_point_position(i) * scale, c.get_point_in(i) * scale, c.get_point_out(i) * scale) return sc func _samples() -> Array: ## [{pos, tan, side, dist}] every STEP metres. side points to the LEFT of ## the drive direction. var out: Array = [] var L := curve.get_baked_length() var d := 0.0 while d < L: var p := curve.sample_baked(d) var q := curve.sample_baked(fmod(d + 2.0, L)) var tn := (q - p) tn.y = 0.0 tn = tn.normalized() if tn.length_squared() > 0.0001 else Vector3.FORWARD out.append({"pos": p, "tan": tn, "side": Vector3.UP.cross(tn).normalized(), "dist": d}) d += STEP return out func _road(samples: Array) -> void: ## One long quad strip with real collision -- cars live on this surface. var st := SurfaceTool.new() st.begin(Mesh.PRIMITIVE_TRIANGLES) var h := Vector3.UP * 0.05 var n := samples.size() for i in n: var a: Dictionary = samples[i] var b: Dictionary = samples[(i + 1) % n] var al: Vector3 = a["pos"] + a["side"] * ROAD_W * 0.5 + h var ar: Vector3 = a["pos"] - a["side"] * ROAD_W * 0.5 + h var bl: Vector3 = b["pos"] + b["side"] * ROAD_W * 0.5 + h var br: Vector3 = b["pos"] - b["side"] * ROAD_W * 0.5 + h var va: float = a["dist"] * 0.14 var vb: float = (a["dist"] + STEP) * 0.14 # Godot front faces wind CLOCKWISE seen from the normal side: the first # cut of this strip wound CCW and the entire road was culled from above for v in [[al, Vector2(0, va)], [br, Vector2(1.8, vb)], [ar, Vector2(1.8, va)], [al, Vector2(0, va)], [bl, Vector2(0, vb)], [br, Vector2(1.8, vb)]]: st.set_normal(Vector3.UP) st.set_uv(v[1]) st.add_vertex(v[0]) var mesh := st.commit() var m := StandardMaterial3D.new() m.albedo_texture = load(TEX % "asphalt") m.roughness = 0.92 mesh.surface_set_material(0, m) var body := StaticBody3D.new() var mi := MeshInstance3D.new() mi.mesh = mesh body.add_child(mi) var cs := CollisionShape3D.new() cs.shape = mesh.create_trimesh_shape() body.add_child(cs) add_child(body) func _curvature(samples: Array, i: int) -> float: ## signed heading change per metre; positive = turning left var n := samples.size() var a: Vector3 = samples[i]["tan"] var b: Vector3 = samples[(i + 1) % n]["tan"] return a.signed_angle_to(b, Vector3.UP) / STEP func _corners(samples: Array) -> Array: ## contiguous runs of |curvature| > threshold, min 6 samples (24 m) ## -> [{from, to, sign}] as sample indices var out: Array = [] var n := samples.size() var run_start := -1 var run_sign := 0.0 for i in n: var k := _curvature(samples, i) if absf(k) > 0.010: if run_start < 0: run_start = i run_sign = signf(k) else: if run_start >= 0 and i - run_start >= 6: out.append({"from": run_start, "to": i - 1, "sign": run_sign}) run_start = -1 if run_start >= 0 and n - run_start >= 6: out.append({"from": run_start, "to": n - 1, "sign": run_sign}) return out func _kerbs(samples: Array) -> void: ## red/white kerb blocks along both edges wherever the road actually turns var red := _flat(Color(0.78, 0.12, 0.10), 0.7) var white := _flat(Color(0.90, 0.90, 0.86), 0.7) var n := samples.size() for i in n: if absf(_curvature(samples, i)) < 0.008 or i % 2 != 0: continue var s: Dictionary = samples[i] for e in [-1.0, 1.0]: var at: Vector3 = s["pos"] + s["side"] * e * (ROAD_W * 0.5 + 0.7) + Vector3.UP * 0.045 var mi := MeshInstance3D.new() var bm := BoxMesh.new() bm.size = Vector3(1.4, 0.07, STEP * 2.0) bm.material = red if (i / 2) % 2 == 0 else white mi.mesh = bm mi.position = at mi.basis = Basis.looking_at(s["tan"], Vector3.UP) add_child(mi) func _corner_dressing(c: Dictionary, samples: Array) -> void: ## outside of every real corner: a sand trap; inside: a tyre stack var midi := int((int(c["from"]) + int(c["to"])) / 2.0) var s: Dictionary = samples[midi] # turning left (+sign) means the outside is the RIGHT edge (-side) var out_dir: Vector3 = s["side"] * (-1.0 if float(c["sign"]) > 0.0 else 1.0) var span := (int(c["to"]) - int(c["from"])) * STEP if span > 40.0: # only proper corners earn a gravel trap var at: Vector3 = s["pos"] + out_dir * (ROAD_W * 0.5 + 13.0) var mi := MeshInstance3D.new() var bm := BoxMesh.new() bm.size = Vector3(minf(span * 0.9, 60.0), 0.04, 26.0) bm.material = _mat("sand", 5.0) mi.mesh = bm mi.position = Vector3(at.x, 0.035, at.z) mi.basis = Basis.looking_at(s["tan"], Vector3.UP) add_child(mi) var rubber := _flat(Color(0.07, 0.07, 0.08), 0.85) var inn: Vector3 = s["pos"] - out_dir * (ROAD_W * 0.5 + 3.5) for j in 5: var mi := MeshInstance3D.new() var cm := CylinderMesh.new() cm.top_radius = 0.58 cm.bottom_radius = 0.58 cm.height = 1.05 cm.radial_segments = 9 cm.material = rubber mi.mesh = cm mi.position = inn + s["tan"] * (j - 2.0) * 1.3 + Vector3.UP * 0.53 add_child(mi) func _hoardings(samples: Array, corners: Array) -> void: ## sponsor boards down the straights, FLUX gibberish and all var ads := ["billboard_ad", "billboard_ad2", "billboard_ad3"] var n := samples.size() var since := 0.0 var adi := 0 for i in n: since += STEP if since < 130.0 or absf(_curvature(samples, i)) > 0.004: continue since = 0.0 var s: Dictionary = samples[i] var at: Vector3 = s["pos"] + s["side"] * (ROAD_W * 0.5 + 9.0) var post := _flat(Color(0.3, 0.31, 0.33), 0.5) for j in [-3.2, 3.2]: _boxat(at + s["tan"] * j + Vector3.UP * 0.9, Vector3(0.18, 1.8, 0.18), post, s["tan"]) _boxat(at + Vector3.UP * 1.9, Vector3(8.0, 1.9, 0.12), _mat(ads[adi % 3], 0.24, 0.4), s["tan"]) adi += 1 func _start_finish(samples: Array) -> void: var s: Dictionary = samples[0] for row in 2: for i in 9: var c := Color(0.05, 0.05, 0.06) if (i + row) % 2 == 0 else Color(0.92, 0.92, 0.9) var at: Vector3 = s["pos"] + s["side"] * (-6.0 + i * 1.5) + s["tan"] * float(row) * 1.5 _boxat(Vector3(at.x, 0.06, at.z), Vector3(1.5, 0.02, 1.5), _flat(c, 0.7), s["tan"]) var steel := _flat(Color(0.25, 0.26, 0.28), 0.5) for e in [-1.0, 1.0]: _boxat(s["pos"] + s["side"] * e * (ROAD_W * 0.5 + 1.2) + Vector3.UP * 3.5, Vector3(0.35, 7.0, 0.35), steel, s["tan"]) _boxat(s["pos"] + Vector3.UP * 6.6, Vector3(ROAD_W + 4.5, 1.6, 0.3), _mat("billboard_ad2", 0.22, 0.4), s["tan"]) func _pits(samples: Array) -> void: ## the red pit complex on the outside of the front straight: garage row, ## pit wall, paddock awnings, a couple of transporters var s: Dictionary = samples[mini(20, samples.size() - 1)] # ~80 m up-track of SF var out_dir: Vector3 = -s["side"] # outside of the circuit on the front straight var red := _flat(Color(0.72, 0.10, 0.08), 0.55) var base: Vector3 = s["pos"] + out_dir * (ROAD_W * 0.5 + 22.0) _boxat(base + Vector3.UP * 4.0, Vector3(14, 8.0, 95), red, s["tan"], true) _boxat(base + Vector3.UP * 8.2, Vector3(15, 0.4, 98), _flat(Color(0.16, 0.17, 0.19), 0.6), s["tan"]) _boxat(base - out_dir * 7.1 + Vector3.UP * 5.9, Vector3(0.2, 2.2, 88), _mat("facade_glass", 0.2, 0.25), s["tan"]) _boxat(base - out_dir * 7.1 + Vector3.UP * 2.0, Vector3(0.25, 3.4, 88), _mat("shopfront", 0.2, 0.4), s["tan"]) # pit wall between track and pit lane _boxat(s["pos"] + out_dir * (ROAD_W * 0.5 + 2.5) + Vector3.UP * 0.45, Vector3(0.4, 0.9, 130), _mat("concrete_ground", 3.0), s["tan"], true) # paddock: white awnings + transporters behind the garages var wt := _mat("panel_white", 0.5, 0.5) for j in 4: _boxat(base + out_dir * 16.0 + s["tan"] * (-33.0 + j * 22.0) + Vector3.UP * 2.9, Vector3(9, 0.2, 7), wt, s["tan"]) for j in 2: _boxat(base + out_dir * 27.0 + s["tan"] * (-12.0 + j * 26.0) + Vector3.UP * 1.9, Vector3(2.5, 3.8, 13.5), _flat(Color(0.82, 0.83, 0.86), 0.4), s["tan"], true) func _gums(rng: RandomNumberGenerator, samples: Array, lo: Vector3, hi: Vector3) -> void: ## eucalypt scatter anywhere that's 25 m clear of the racing line var pts: Array[Vector3] = [] for i in range(0, samples.size(), 3): pts.append(samples[i]["pos"]) var placed := 0 var tries := 0 while placed < 40 and tries < 400: tries += 1 var at := Vector3(rng.randf_range(lo.x - 90, hi.x + 90), 0, rng.randf_range(lo.z - 90, hi.z + 90)) var ok := true for p in pts: if Vector2(at.x - p.x, at.z - p.z).length_squared() < 625.0: ok = false break if not ok: continue placed += 1 var trunk := MeshInstance3D.new() var cm := CylinderMesh.new() cm.top_radius = 0.14 cm.bottom_radius = 0.24 cm.height = rng.randf_range(5.0, 8.5) cm.radial_segments = 7 cm.material = _mat("bark", 1.4) trunk.mesh = cm trunk.position = at + Vector3(0, cm.height * 0.5, 0) add_child(trunk) var crown := MeshInstance3D.new() var sm := SphereMesh.new() sm.radius = rng.randf_range(2.4, 3.6) sm.height = sm.radius * 1.6 sm.radial_segments = 10 sm.rings = 6 sm.material = _mat("foliage", 0.55) crown.mesh = sm crown.position = at + Vector3(0, cm.height + sm.radius * 0.4, 0) add_child(crown) func _boxat(pos: Vector3, size: Vector3, mat: Material, along: Vector3, collide := false) -> void: var mi := MeshInstance3D.new() var bm := BoxMesh.new() bm.size = size bm.material = mat mi.mesh = bm if collide: var body := StaticBody3D.new() var cs := CollisionShape3D.new() var bs := BoxShape3D.new() bs.size = size cs.shape = bs body.add_child(cs) body.add_child(mi) body.position = pos body.basis = Basis.looking_at(along, Vector3.UP) add_child(body) else: mi.position = pos mi.basis = Basis.looking_at(along, Vector3.UP) add_child(mi) func _mat(tex: String, scale: float, rough := 0.9) -> StandardMaterial3D: var m := StandardMaterial3D.new() m.albedo_texture = load(TEX % tex) m.uv1_scale = Vector3(scale, scale, scale) m.uv1_triplanar = true m.roughness = rough m.metallic = 0.0 return m func _flat(color: Color, rough := 0.85) -> StandardMaterial3D: var m := StandardMaterial3D.new() m.albedo_color = color m.roughness = rough m.metallic = 0.0 return m func _slab(pos: Vector3, size: Vector3, mat: Material, collide := true) -> void: var body := StaticBody3D.new() var mi := MeshInstance3D.new() var bm := BoxMesh.new() bm.size = size bm.material = mat mi.mesh = bm body.add_child(mi) if collide: var shape := CollisionShape3D.new() var bs := BoxShape3D.new() bs.size = size shape.shape = bs body.add_child(shape) body.position = pos add_child(body)