map: signature crossings + fix skyline setback clipping the road
buildBridges() adds the two landmark structures the generated road was
missing, driven off the existing per-sample tunnel/bridge flags in
Circuit.luau (200 tunnel / 474 bridge samples, recorded at trace time):
* NihonbashiViaduct - box girder carried on piers over the historic
stone bridge, the C1's most recognisable stretch.
* Shiodome cable-stayed span - twin H-towers with fanned stays.
Also fixes buildSkyline() placing geometry inside the racing surface:
* the setback test compared the building CENTRE against 220 studs, so a
220-wide tower still reached 110 studs inward - through the tunnel
bore walls. Now tests the outer face (near - w/2) against
ROAD_WIDTH/2 + 60.
* it strided samples by 3, skipping the closest approach on tight
corners. Now tests every sample.
* NeonSign hangs off the +X face and was never clearance-checked at
all; it now gets its own test.
Verified in Studio against a full rebuild (823 samples -> 2468 road
parts): 400 tunnel walls / 200 ceilings for the 200 flagged samples,
621 buildings placed, and raycasts over every road part report 0
headroom and 0 lateral obstructions. Minimum building clearance 47.8
studs.
This commit is contained in:
+304
-13
@@ -562,21 +562,28 @@ local function buildSkyline(map: Folder)
|
||||
local x = math.sin(a) * r - 477
|
||||
local z = math.cos(a) * r - 473
|
||||
|
||||
-- keep buildings off the racing surface
|
||||
local h = 120 + det(i * 5.1) ^ 2.2 * 900
|
||||
local w = 90 + det(i * 8.3) * 130
|
||||
|
||||
-- keep buildings off the racing surface.
|
||||
-- The test must use the building's OUTER FACE, not its centre: a 220-stud
|
||||
-- centre setback still lets a 220-wide tower reach 110 studs inward, which
|
||||
-- put it through the tunnel bore (walls sit at ROAD_WIDTH/2 + 1.5 + 3).
|
||||
-- Sample every point too -- striding by 3 skipped the closest approach on
|
||||
-- tight corners.
|
||||
local CLEAR = ROAD_WIDTH / 2 + 60
|
||||
local near = math.huge
|
||||
for k = 1, n, 3 do
|
||||
for k = 1, n do
|
||||
local s = samples[k].pos
|
||||
local d = (Vector3.new(s.X, 0, s.Z) - Vector3.new(x, 0, z)).Magnitude
|
||||
if d < near then
|
||||
near = d
|
||||
end
|
||||
end
|
||||
if near < 220 then
|
||||
if near - w / 2 < CLEAR then
|
||||
continue
|
||||
end
|
||||
|
||||
local h = 120 + det(i * 5.1) ^ 2.2 * 900
|
||||
local w = 90 + det(i * 8.3) * 130
|
||||
local b = part({
|
||||
Name = "Building",
|
||||
Size = Vector3.new(w, h, w),
|
||||
@@ -588,15 +595,27 @@ local function buildSkyline(map: Folder)
|
||||
b.Parent = folder
|
||||
placed += 1
|
||||
|
||||
-- signs hang off the +X face, so they need their own clearance check
|
||||
if det(i * 13.7) > 0.72 then
|
||||
local sign = part({
|
||||
Name = "NeonSign",
|
||||
Size = Vector3.new(12, h * 0.42, 5),
|
||||
CFrame = CFrame.new(x + w / 2, h * 0.62, z),
|
||||
Color = ({ NEON_PINK, NEON_BLUE, NEON_TEAL, NEON_AMBER })[(i % 4) + 1],
|
||||
Material = Enum.Material.Neon,
|
||||
})
|
||||
sign.Parent = folder
|
||||
local sx = x + w / 2 + 6
|
||||
local snear = math.huge
|
||||
for k = 1, n do
|
||||
local s = samples[k].pos
|
||||
local d = (Vector3.new(s.X, 0, s.Z) - Vector3.new(sx, 0, z)).Magnitude
|
||||
if d < snear then
|
||||
snear = d
|
||||
end
|
||||
end
|
||||
if snear > CLEAR then
|
||||
local sign = part({
|
||||
Name = "NeonSign",
|
||||
Size = Vector3.new(12, h * 0.42, 5),
|
||||
CFrame = CFrame.new(sx, h * 0.62, z),
|
||||
Color = ({ NEON_PINK, NEON_BLUE, NEON_TEAL, NEON_AMBER })[(i % 4) + 1],
|
||||
Material = Enum.Material.Neon,
|
||||
})
|
||||
sign.Parent = folder
|
||||
end
|
||||
end
|
||||
if placed > 620 then
|
||||
break
|
||||
@@ -614,6 +633,277 @@ local function buildSkyline(map: Folder)
|
||||
ground.Parent = folder
|
||||
end
|
||||
|
||||
--[[
|
||||
Signature crossings.
|
||||
|
||||
Two hand-placed structures on top of the generated road:
|
||||
|
||||
* NihonbashiViaduct -- the C1's most famous stretch. The expressway runs
|
||||
on a box girder directly OVER the historic Nihonbashi stone bridge and
|
||||
the Nihonbashi-gawa. The stone bridge underneath is the actual landmark,
|
||||
so it is modelled too (deck, balustrades, ornamental posts, arches).
|
||||
|
||||
* CableStayed -- a red twin-tower cable-stayed span on the straightest
|
||||
bay-side run, with fanned stay cables and aviation warning lights.
|
||||
|
||||
Both index into Circuit.samples, so they follow the road wherever it moves.
|
||||
Span indices were chosen from the georeferenced OSM trace: Nihonbashi maps
|
||||
to the Edobashi-junction samples, the cable-stayed span to the Shiodome
|
||||
straight (lowest cumulative heading change of any elevated, non-tunnel run).
|
||||
]]
|
||||
local function buildBridges(map: Folder)
|
||||
local folder = Instance.new("Folder")
|
||||
folder.Name = "Bridges"
|
||||
folder.Parent = map
|
||||
|
||||
local samples = Circuit.samples
|
||||
local n = Circuit.SAMPLE_COUNT
|
||||
|
||||
local function mid(i: number): Vector3
|
||||
local a = samples[i]
|
||||
local b = samples[(i % n) + 1]
|
||||
return (a.pos + b.pos) / 2
|
||||
end
|
||||
|
||||
----------------------------------------------------------------- Nihonbashi
|
||||
do
|
||||
local NB = Instance.new("Folder")
|
||||
NB.Name = "NihonbashiViaduct"
|
||||
NB.Parent = folder
|
||||
|
||||
local A, B = 46, 62
|
||||
local pa, pb = samples[A].pos, samples[B].pos
|
||||
local centre = (pa + pb) / 2
|
||||
local axis = pb - pa
|
||||
local flat = Vector3.new(axis.X, 0, axis.Z).Unit
|
||||
local right = flat:Cross(UP)
|
||||
local span = axis.Magnitude
|
||||
|
||||
-- Nihonbashi-gawa flowing under the viaduct
|
||||
local waterCF = CFrame.lookAt(Vector3.new(centre.X, 2, centre.Z), Vector3.new(centre.X, 2, centre.Z) + flat, UP)
|
||||
local water = part({
|
||||
Name = "RiverWater",
|
||||
Size = Vector3.new(150, 3, span + 300),
|
||||
CFrame = waterCF,
|
||||
Color = Color3.fromRGB(18, 32, 42),
|
||||
Material = Enum.Material.Glass,
|
||||
})
|
||||
water.Reflectance = 0.35
|
||||
water.Parent = NB
|
||||
|
||||
for _, side in ipairs({ -1, 1 }) do
|
||||
local q = part({
|
||||
Name = "RiverWall",
|
||||
Size = Vector3.new(10, 26, span + 300),
|
||||
CFrame = waterCF * CFrame.new(side * 80, 11, 0),
|
||||
Color = Color3.fromRGB(58, 56, 52),
|
||||
Material = Enum.Material.Concrete,
|
||||
})
|
||||
q.Parent = NB
|
||||
end
|
||||
|
||||
-- the historic stone bridge, crossing UNDER the expressway
|
||||
local bpos = Vector3.new(centre.X, 16, centre.Z)
|
||||
local bcf = CFrame.lookAt(bpos, bpos + right, UP)
|
||||
local deck = part({
|
||||
Name = "NihonbashiStoneDeck",
|
||||
Size = Vector3.new(46, 4, 180),
|
||||
CFrame = bcf,
|
||||
Color = Color3.fromRGB(150, 142, 128),
|
||||
Material = Enum.Material.Slate,
|
||||
})
|
||||
deck.Parent = NB
|
||||
|
||||
for _, s in ipairs({ -1, 1 }) do
|
||||
local bal = part({
|
||||
Name = "Balustrade",
|
||||
Size = Vector3.new(2.5, 5, 180),
|
||||
CFrame = bcf * CFrame.new(s * 22, 4, 0),
|
||||
Color = Color3.fromRGB(168, 160, 146),
|
||||
Material = Enum.Material.Slate,
|
||||
})
|
||||
bal.Parent = NB
|
||||
for k = -2, 2 do
|
||||
local post = part({
|
||||
Name = "BridgePost",
|
||||
Size = Vector3.new(6, 14, 6),
|
||||
CFrame = bcf * CFrame.new(s * 22, 8, k * 44),
|
||||
Color = Color3.fromRGB(120, 112, 100),
|
||||
Material = Enum.Material.Slate,
|
||||
})
|
||||
post.Parent = NB
|
||||
if math.abs(k) == 2 then
|
||||
local lamp = part({
|
||||
Name = "BridgeLamp",
|
||||
Size = Vector3.new(4, 6, 4),
|
||||
CFrame = bcf * CFrame.new(s * 22, 17, k * 44),
|
||||
Color = Color3.fromRGB(255, 214, 150),
|
||||
Material = Enum.Material.Neon,
|
||||
})
|
||||
local pl = Instance.new("PointLight")
|
||||
pl.Brightness = 2
|
||||
pl.Range = 60
|
||||
pl.Color = Color3.fromRGB(255, 214, 150)
|
||||
pl.Parent = lamp
|
||||
lamp.Parent = NB
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, s in ipairs({ -1, 1 }) do
|
||||
local arch = part({
|
||||
Name = "StoneArch",
|
||||
Size = Vector3.new(40, 22, 70),
|
||||
CFrame = bcf * CFrame.new(0, -10, s * 46),
|
||||
Color = Color3.fromRGB(138, 130, 118),
|
||||
Material = Enum.Material.Slate,
|
||||
})
|
||||
arch.Parent = NB
|
||||
end
|
||||
|
||||
-- box girder + pier bents carrying the expressway deck
|
||||
for i = A, B do
|
||||
local m = mid(i)
|
||||
local fwd = Circuit.forwardAt(i)
|
||||
local cf = CFrame.lookAt(m, m + fwd, UP)
|
||||
local g = part({
|
||||
Name = "BoxGirder",
|
||||
Size = Vector3.new(ROAD_WIDTH + 10, 9, 64),
|
||||
CFrame = cf * CFrame.new(0, -6, 0),
|
||||
Color = Color3.fromRGB(64, 64, 72),
|
||||
Material = Enum.Material.Metal,
|
||||
})
|
||||
g.Parent = NB
|
||||
|
||||
if i % 3 == 0 then
|
||||
local y = samples[i].pos.Y
|
||||
for _, side in ipairs({ -1, 1 }) do
|
||||
local leg = part({
|
||||
Name = "ViaductPier",
|
||||
Size = Vector3.new(9, math.max(y - 14, 4), 9),
|
||||
CFrame = CFrame.new(Vector3.new(m.X, math.max(y - 14, 4) / 2 + 14, m.Z) + right * (side * 34)),
|
||||
Color = Color3.fromRGB(72, 70, 76),
|
||||
Material = Enum.Material.Concrete,
|
||||
})
|
||||
leg.Parent = NB
|
||||
end
|
||||
local cap = part({
|
||||
Name = "PierCap",
|
||||
Size = Vector3.new(88, 6, 12),
|
||||
CFrame = cf * CFrame.new(0, -13, 0),
|
||||
Color = Color3.fromRGB(72, 70, 76),
|
||||
Material = Enum.Material.Concrete,
|
||||
})
|
||||
cap.Parent = NB
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------- cable-stayed
|
||||
do
|
||||
local CS = Instance.new("Folder")
|
||||
CS.Name = "CableStayed"
|
||||
CS.Parent = folder
|
||||
|
||||
local A, B = 646, 659
|
||||
local pa, pb = samples[A].pos, samples[B].pos
|
||||
local axis = pb - pa
|
||||
local flat = Vector3.new(axis.X, 0, axis.Z).Unit
|
||||
local right = flat:Cross(UP)
|
||||
local deckY = pa.Y
|
||||
local TOWER_H = 210
|
||||
local RED = Color3.fromRGB(196, 72, 72)
|
||||
|
||||
local tops = {}
|
||||
for _, idx in ipairs({ A + 3, B - 3 }) do
|
||||
local c = mid(idx)
|
||||
for _, side in ipairs({ -1, 1 }) do
|
||||
local top = deckY + TOWER_H
|
||||
local leg = part({
|
||||
Name = "BridgeTower",
|
||||
Size = Vector3.new(12, top, 12),
|
||||
CFrame = CFrame.new(Vector3.new(c.X, top / 2, c.Z) + right * (side * 56)),
|
||||
Color = RED,
|
||||
Material = Enum.Material.Metal,
|
||||
})
|
||||
leg.Parent = CS
|
||||
table.insert(tops, { pos = Vector3.new(c.X, top, c.Z) + right * (side * 56), idx = idx, side = side })
|
||||
end
|
||||
|
||||
for _, h in ipairs({ 0.55, 0.85 }) do
|
||||
local y = deckY + TOWER_H * h
|
||||
local beam = part({
|
||||
Name = "TowerCrossbeam",
|
||||
Size = Vector3.new(124, 8, 8),
|
||||
CFrame = CFrame.lookAt(Vector3.new(c.X, y, c.Z), Vector3.new(c.X, y, c.Z) + flat, UP)
|
||||
* CFrame.Angles(0, math.pi / 2, 0),
|
||||
Color = RED,
|
||||
Material = Enum.Material.Metal,
|
||||
})
|
||||
beam.Parent = CS
|
||||
end
|
||||
|
||||
local al = part({
|
||||
Name = "AviationLight",
|
||||
Size = Vector3.new(6, 3, 6),
|
||||
CFrame = CFrame.new(c.X, deckY + TOWER_H + 3, c.Z),
|
||||
Color = Color3.fromRGB(255, 60, 60),
|
||||
Material = Enum.Material.Neon,
|
||||
})
|
||||
local pl = Instance.new("PointLight")
|
||||
pl.Brightness = 3
|
||||
pl.Range = 140
|
||||
pl.Color = Color3.fromRGB(255, 60, 60)
|
||||
pl.Parent = al
|
||||
al.Parent = CS
|
||||
end
|
||||
|
||||
-- fanned stay cables
|
||||
for _, t in ipairs(tops) do
|
||||
for k = 1, 6 do
|
||||
local target = math.clamp(t.idx + (k * 2 - 7), A, B)
|
||||
local anchor = mid(target) + right * (t.side * 46) + Vector3.new(0, 2, 0)
|
||||
local delta = anchor - t.pos
|
||||
local dist = delta.Magnitude
|
||||
if dist > 1 then
|
||||
local cable = part({
|
||||
Name = "Cable",
|
||||
Size = Vector3.new(0.9, 0.9, dist),
|
||||
CFrame = CFrame.lookAt(t.pos + delta / 2, anchor, UP),
|
||||
Color = Color3.fromRGB(225, 225, 232),
|
||||
Material = Enum.Material.Metal,
|
||||
})
|
||||
cable.Parent = CS
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for i = A, B do
|
||||
local m = mid(i)
|
||||
local fwd = Circuit.forwardAt(i)
|
||||
local g = part({
|
||||
Name = "BridgeGirder",
|
||||
Size = Vector3.new(ROAD_WIDTH + 14, 7, 64),
|
||||
CFrame = CFrame.lookAt(m, m + fwd, UP) * CFrame.new(0, -5, 0),
|
||||
Color = RED,
|
||||
Material = Enum.Material.Metal,
|
||||
})
|
||||
g.Parent = CS
|
||||
end
|
||||
|
||||
local centre = (pa + pb) / 2
|
||||
local bay = part({
|
||||
Name = "BayWater",
|
||||
Size = Vector3.new(900, 3, axis.Magnitude + 500),
|
||||
CFrame = CFrame.lookAt(Vector3.new(centre.X, 1, centre.Z), Vector3.new(centre.X, 1, centre.Z) + flat, UP),
|
||||
Color = Color3.fromRGB(14, 26, 38),
|
||||
Material = Enum.Material.Glass,
|
||||
})
|
||||
bay.Reflectance = 0.4
|
||||
bay.Parent = CS
|
||||
end
|
||||
end
|
||||
|
||||
function MapBuilder.build(): Folder
|
||||
local existing = Workspace:FindFirstChild("Map")
|
||||
if existing then
|
||||
@@ -628,6 +918,7 @@ function MapBuilder.build(): Folder
|
||||
buildPA(map)
|
||||
buildLandmarks(map)
|
||||
buildSkyline(map)
|
||||
buildBridges(map)
|
||||
|
||||
-- Start/finish across the road at sample 1
|
||||
local s1 = Circuit.samples[1]
|
||||
|
||||
Reference in New Issue
Block a user