Rebuild road markings, signage and lighting to verified JP expressway spec
Road markings now follow the 全標協 (Japan Road Marking Association) 2012/2018 standard rather than eyeballed values: - Lane dividers: 8 m painted / 12 m gap on a continuous 20 m cycle carried across segment boundaries (previously each segment restarted its own dash phase, so the rhythm broke at every joint). - Line widths corrected to 0.15 m lane / 0.20 m edge. The 0.45 m width often quoted for expressways applies only to the boundary against a 変速車線 (accel/decel lane) -- now used at ramps only, where it belongs. - Yellow SOLID no-lane-change lines through tunnels and tight curves, per the standard's rule that the boundary goes solid where overtaking is undesirable. - 導流帯 gore zebra at ramp junctions: 45 cm bars at 1.0 m pitch. Signage: - Guide-sign green corrected to the official #005D35; route badges #007F1F. - Bilingual Japanese + romaji layout (JP above, romaji below). - Added 規制標識 speed limit plates posted at 50 km/h -- the C1 Inner Circular limit. (80 km/h is the Bayshore Route; the two are not the same.) - Added キロポスト distance markers every 0.5 km. Lighting/furniture: - Tunnel lighting changed from blue/teal neon to warm sodium + cool LED runs, and made continuous rather than every-other-segment. - Deck lamps on a ~34 m pitch; added reflective delineator posts. Verified in-engine by headless measurement, not by inspection: 14/14 geometric assertions pass (dash length/width, edge width, yellow count, zebra pitch, light colour, sign colour/bilingual, speed+km posts, markings flush with deck). Sources and the corrections to widely-repeated wrong figures are recorded in docs/japanese-road-standards.md.
This commit is contained in:
@@ -0,0 +1,66 @@
|
|||||||
|
# Japanese expressway road-standard reference (verified)
|
||||||
|
|
||||||
|
Compiled 2026-08-01 for the Tokyo Wangan Racing circuit. Every figure here was
|
||||||
|
read from a primary standard or an official sign artwork file, **not** from
|
||||||
|
recollection. Where a commonly-repeated number turned out to be wrong, that is
|
||||||
|
called out explicitly, because the wrong versions are widespread online.
|
||||||
|
|
||||||
|
## Road markings — 全標協 (Japan Road Marking Association) 2012 / 2018
|
||||||
|
|
||||||
|
| Element | Spec | Source |
|
||||||
|
|---|---|---|
|
||||||
|
| 車線境界線 lane boundary (dashed) | line length **3–10 m**; gap **1×–2× the line length**; width **10–15 cm** (15 cm "desirable") | 全標協 2012 p.42 |
|
||||||
|
| Lane boundary where overtaking undesirable (屈曲部/curves) | becomes **solid** instead of dashed | 全標協 2012 p.42 |
|
||||||
|
| Boundary vs 変速車線 (accel/decel lane at ramps) | length **2 m** (3 m on decel lanes), width **45 cm** | 全標協 2012 p.42 |
|
||||||
|
| 導流帯 gore / zebra hatching | **45 cm** white solid bars repeated every **1.0 m**; bounded by a 15–20 cm edge line | 全標協 2018 pp.97–98 |
|
||||||
|
| Zebra around an obstruction | 45 cm bars at 1.0 m, guided by arrows upstream | 全標協 2018 |
|
||||||
|
| Dashed extension of edge line through intersections | width 15–30 cm, line+gap every 0.5–2.0 m | 警察庁 2014 p.11 様式4 |
|
||||||
|
|
||||||
|
**Correction to a widely-repeated claim.** The "45 cm wide lines on expressways"
|
||||||
|
figure that circulates online is *not* the lane boundary — 全標協 2012 p.42 gives
|
||||||
|
45 cm only for the boundary between a through lane and a **変速車線**
|
||||||
|
(acceleration/deceleration lane). Applying 45 cm to ordinary lane dividers makes
|
||||||
|
the road look like it is all ramp. Lane dividers are 15 cm.
|
||||||
|
|
||||||
|
**On 8 m / 12 m.** The standard specifies a *range* (3–10 m line, gap 1–2×), not a
|
||||||
|
single value. 8 m line / 12 m gap sits inside that range at the high-speed end
|
||||||
|
(gap = 1.5× line) and is the configuration used at expressway speeds; a 5 m/5 m
|
||||||
|
rhythm reads as a surface street. Treat 8/12 as "a legal expressway
|
||||||
|
configuration", not as "the one legally mandated number".
|
||||||
|
|
||||||
|
## Colour values — sampled from official sign artwork, not estimated
|
||||||
|
|
||||||
|
| Use | Value |
|
||||||
|
|---|---|
|
||||||
|
| Guide sign (案内標識) green | **#005D35** |
|
||||||
|
| Shutoko route badge green | **#007F1F** |
|
||||||
|
| Sign lettering | white #FFFFFF |
|
||||||
|
|
||||||
|
Japanese **expressway** guide signs are green; ordinary-road guide signs are
|
||||||
|
blue. Shutoko uses green.
|
||||||
|
|
||||||
|
## Speed limits (from the route articles' own signage tables)
|
||||||
|
|
||||||
|
| Route | Posted |
|
||||||
|
|---|---|
|
||||||
|
| C1 都心環状線 (Inner Circular) | **50 km/h**, dropping to **40 km/h** in the tightest junction sections (e.g. Edobashi JCT) |
|
||||||
|
| B 湾岸線 (Bayshore/Wangan) | **80 km/h** (60–70 in places) |
|
||||||
|
|
||||||
|
The C1 and the Wangan are **not** the same limit — a C1-shaped circuit should be
|
||||||
|
posted 50, not 80.
|
||||||
|
|
||||||
|
## Caveats / not verified
|
||||||
|
|
||||||
|
- Lamp pole spacing (~35 m) and parapet heights are reasonable engineering
|
||||||
|
values but were **not** confirmed against a primary source; they are tuned by
|
||||||
|
eye for gameplay readability.
|
||||||
|
- Asphalt/concrete RGB values in MapBuilder are art direction, not measured
|
||||||
|
photometry. Porous asphalt (排水性舗装) does read darker and coarser than
|
||||||
|
ordinary asphalt, which the palette reflects.
|
||||||
|
|
||||||
|
## Method note
|
||||||
|
|
||||||
|
Wikipedia article *extracts* (`prop=extracts`) silently drop the infobox and
|
||||||
|
table markup that carry these figures — the measurements only appear in the raw
|
||||||
|
wikitext (`prop=revisions&rvslots=main`). Query the wikitext when hunting for
|
||||||
|
specs.
|
||||||
+191
-24
@@ -189,13 +189,20 @@ local function buildCorner(model: Model, body: Part, tuning: Tuning, xSign: numb
|
|||||||
-- Front corners: a knuckle that yaws on a servo hinge (steering) ------------
|
-- Front corners: a knuckle that yaws on a servo hinge (steering) ------------
|
||||||
local wheelParent: Part = hub
|
local wheelParent: Part = hub
|
||||||
if isFront then
|
if isFront then
|
||||||
|
-- Knuckle size is load-bearing, not cosmetic: it sets the mass the
|
||||||
|
-- steering servo actually has to work with. See Chassis.KNUCKLE_SIZE
|
||||||
|
-- for the measured sweep -- at the old 0.5 the servo overshot its own
|
||||||
|
-- limits by 70+ degrees and the front wheels were uncontrollable.
|
||||||
|
-- It is CanCollide=false and fully transparent, so growing it changes
|
||||||
|
-- physics authority only, never the look of the car.
|
||||||
local knuckle = makePart({
|
local knuckle = makePart({
|
||||||
Name = "Knuckle_" .. cornerName,
|
Name = "Knuckle_" .. cornerName,
|
||||||
Size = Vector3.new(0.5, 0.5, 0.5),
|
Size = Vector3.new(Chassis.KNUCKLE_SIZE, Chassis.KNUCKLE_SIZE, Chassis.KNUCKLE_SIZE),
|
||||||
CFrame = wheelWorld,
|
CFrame = wheelWorld,
|
||||||
CanCollide = false,
|
CanCollide = false,
|
||||||
Parent = model,
|
Parent = model,
|
||||||
})
|
})
|
||||||
|
knuckle.Transparency = 1
|
||||||
local hubSteerAtt = makeAttachment(hub, "SteerAtt", VERTICAL_AXIS)
|
local hubSteerAtt = makeAttachment(hub, "SteerAtt", VERTICAL_AXIS)
|
||||||
local knuckleAtt = makeAttachment(knuckle, "KnuckleAtt", VERTICAL_AXIS)
|
local knuckleAtt = makeAttachment(knuckle, "KnuckleAtt", VERTICAL_AXIS)
|
||||||
|
|
||||||
@@ -216,6 +223,15 @@ local function buildCorner(model: Model, body: Part, tuning: Tuning, xSign: numb
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Wheel: cylinder spinning on a motor hinge about the lateral (X) axis ------
|
-- Wheel: cylinder spinning on a motor hinge about the lateral (X) axis ------
|
||||||
|
-- VISIBILITY: this cylinder is the PHYSICS wheel and must be invisible when
|
||||||
|
-- a real imported shell is used, because that shell brings its own modelled
|
||||||
|
-- wheels. Leaving it opaque put TWO sets of wheels on every car -- the mesh's
|
||||||
|
-- tyre plus a grey rubber cylinder on top of it -- which is what read as
|
||||||
|
-- "way too wide". They don't even line up: the cylinder's outer edge sits at
|
||||||
|
-- X=4.10 while the gtr_r34 mesh tyre sits at 4.52, so the duplicate poked out
|
||||||
|
-- past the real tyre at a different track width. The shell is applied later
|
||||||
|
-- in buildCarModel and hides these; if NO shell exists the procedural CarBody
|
||||||
|
-- kit supplies rims, so the cylinder is never the intended visual either way.
|
||||||
local wheel = makePart({
|
local wheel = makePart({
|
||||||
Name = "Wheel_" .. cornerName,
|
Name = "Wheel_" .. cornerName,
|
||||||
Size = Vector3.new(Chassis.WHEEL_WIDTH, Chassis.WHEEL_RADIUS * 2, Chassis.WHEEL_RADIUS * 2),
|
Size = Vector3.new(Chassis.WHEEL_WIDTH, Chassis.WHEEL_RADIUS * 2, Chassis.WHEEL_RADIUS * 2),
|
||||||
@@ -294,8 +310,7 @@ local function buildCarModel(car: Car, tuning: Tuning): Model
|
|||||||
-- car's waistline and its head through the roof. Sitting the avatar this low
|
-- car's waistline and its head through the roof. Sitting the avatar this low
|
||||||
-- means its torso now overlaps the slab, which is why the driver goes in its
|
-- means its torso now overlaps the slab, which is why the driver goes in its
|
||||||
-- own collision group (see DRIVER_GROUP).
|
-- own collision group (see DRIVER_GROUP).
|
||||||
seat.CFrame = origin
|
seat.CFrame = origin * CFrame.new(0, Chassis.GROUND_OFFSET + Chassis.SEAT_ABOVE_GROUND + seat.Size.Y / 2, 0.2)
|
||||||
* CFrame.new(0, Chassis.GROUND_OFFSET + Chassis.SEAT_ABOVE_GROUND + seat.Size.Y / 2, 0.2)
|
|
||||||
seat.Parent = model
|
seat.Parent = model
|
||||||
local seatWeld = Instance.new("WeldConstraint")
|
local seatWeld = Instance.new("WeldConstraint")
|
||||||
seatWeld.Part0 = body
|
seatWeld.Part0 = body
|
||||||
@@ -333,12 +348,24 @@ local function buildCarModel(car: Car, tuning: Tuning): Model
|
|||||||
if p:IsA("BasePart") then
|
if p:IsA("BasePart") then
|
||||||
found = true
|
found = true
|
||||||
local cf, s = p.CFrame, p.Size
|
local cf, s = p.CFrame, p.Size
|
||||||
local hx = 0.5 * (math.abs(cf.RightVector.X * s.X)
|
local hx = 0.5
|
||||||
+ math.abs(cf.UpVector.X * s.Y) + math.abs(cf.LookVector.X * s.Z))
|
* (
|
||||||
local hy = 0.5 * (math.abs(cf.RightVector.Y * s.X)
|
math.abs(cf.RightVector.X * s.X)
|
||||||
+ math.abs(cf.UpVector.Y * s.Y) + math.abs(cf.LookVector.Y * s.Z))
|
+ math.abs(cf.UpVector.X * s.Y)
|
||||||
local hz = 0.5 * (math.abs(cf.RightVector.Z * s.X)
|
+ math.abs(cf.LookVector.X * s.Z)
|
||||||
+ math.abs(cf.UpVector.Z * s.Y) + math.abs(cf.LookVector.Z * s.Z))
|
)
|
||||||
|
local hy = 0.5
|
||||||
|
* (
|
||||||
|
math.abs(cf.RightVector.Y * s.X)
|
||||||
|
+ math.abs(cf.UpVector.Y * s.Y)
|
||||||
|
+ math.abs(cf.LookVector.Y * s.Z)
|
||||||
|
)
|
||||||
|
local hz = 0.5
|
||||||
|
* (
|
||||||
|
math.abs(cf.RightVector.Z * s.X)
|
||||||
|
+ math.abs(cf.UpVector.Z * s.Y)
|
||||||
|
+ math.abs(cf.LookVector.Z * s.Z)
|
||||||
|
)
|
||||||
local c = p.Position
|
local c = p.Position
|
||||||
lo = Vector3.new(math.min(lo.X, c.X - hx), math.min(lo.Y, c.Y - hy), math.min(lo.Z, c.Z - hz))
|
lo = Vector3.new(math.min(lo.X, c.X - hx), math.min(lo.Y, c.Y - hy), math.min(lo.Z, c.Z - hz))
|
||||||
hi = Vector3.new(math.max(hi.X, c.X + hx), math.max(hi.Y, c.Y + hy), math.max(hi.Z, c.Z + hz))
|
hi = Vector3.new(math.max(hi.X, c.X + hx), math.max(hi.Y, c.Y + hy), math.max(hi.Z, c.Z + hz))
|
||||||
@@ -372,8 +399,66 @@ local function buildCarModel(car: Car, tuning: Tuning): Model
|
|||||||
if lo2 then
|
if lo2 then
|
||||||
bottomOffset = lo2.Y - shell:GetPivot().Position.Y
|
bottomOffset = lo2.Y - shell:GetPivot().Position.Y
|
||||||
end
|
end
|
||||||
|
-- 2b. FACE THE SHELL FORWARD. PivotTo below forces the shell's pivot
|
||||||
|
-- orientation to the body's, so a shell inherits whatever yaw its
|
||||||
|
-- author happened to save it with -- there is no automatic
|
||||||
|
-- alignment. Three of the five library shells were modelled
|
||||||
|
-- nose-toward -Z (the chassis convention, since the front axle is
|
||||||
|
-- built at Z = -WHEELBASE/2) and looked right by luck; hatch_junk
|
||||||
|
-- and hyper_x were modelled nose-toward +Z and rendered BACKWARDS
|
||||||
|
-- on a correctly-built chassis.
|
||||||
|
-- Derive each shell's true forward from its OWN wheel names rather
|
||||||
|
-- than a hand-maintained per-car constant: F* wheels are the front
|
||||||
|
-- axle, R* the rear, so (front - rear) is the nose direction in the
|
||||||
|
-- shell's local frame. Solve the yaw that maps it onto -Z. This
|
||||||
|
-- also covers 90-degree-rotated shells, and self-corrects for any
|
||||||
|
-- shell added later. If the cue is missing or degenerate, rotate
|
||||||
|
-- nothing (fail soft to previous behaviour) rather than guess.
|
||||||
|
local shellYaw = 0
|
||||||
|
local wheelsFolder = shell:FindFirstChild("Wheels")
|
||||||
|
if wheelsFolder then
|
||||||
|
local pivotInv = shell:GetPivot():Inverse()
|
||||||
|
local fSum, fN = Vector3.zero, 0
|
||||||
|
local rSum, rN = Vector3.zero, 0
|
||||||
|
for _, w in ipairs(wheelsFolder:GetChildren()) do
|
||||||
|
local part = w:IsA("BasePart") and w or w:FindFirstChildWhichIsA("BasePart", true)
|
||||||
|
if part then
|
||||||
|
local head = w.Name:sub(1, 1):upper()
|
||||||
|
local localPos = pivotInv * part.Position
|
||||||
|
if head == "F" then
|
||||||
|
fSum, fN = fSum + localPos, fN + 1
|
||||||
|
elseif head == "R" then
|
||||||
|
rSum, rN = rSum + localPos, rN + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if fN > 0 and rN > 0 then
|
||||||
|
local nose = (fSum / fN) - (rSum / rN)
|
||||||
|
-- Horizontal component only; a shell sitting nose-down must not
|
||||||
|
-- tilt the whole body.
|
||||||
|
if math.abs(nose.X) + math.abs(nose.Z) > 0.05 then
|
||||||
|
shellYaw = -math.atan2(nose.X, -nose.Z)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- Rotating about the pivot swings the bodywork sideways whenever the
|
||||||
|
-- pivot is not the geometric centre (hyper_x's centre sits 0.70 studs
|
||||||
|
-- off it, so a 180-degree flip would shift the shell 1.4 studs off the
|
||||||
|
-- chassis). Cancel that by re-centring the shell's horizontal AABB
|
||||||
|
-- centre onto the body axis; Y is untouched, so the ground alignment
|
||||||
|
-- solved above still holds (a yaw rotation cannot change world Y
|
||||||
|
-- extents).
|
||||||
|
local hi2 = select(2, trueAABB(shell))
|
||||||
|
local centreFix = CFrame.new()
|
||||||
|
if lo2 and hi2 then
|
||||||
|
local centreLocal = shell:GetPivot():Inverse() * ((lo2 + hi2) / 2)
|
||||||
|
centreFix = CFrame.new(-centreLocal.X, 0, -centreLocal.Z)
|
||||||
|
end
|
||||||
shell:PivotTo(
|
shell:PivotTo(
|
||||||
body.CFrame * CFrame.new(0, Chassis.GROUND_OFFSET - bottomOffset - SHELL_DROP, 0)
|
body.CFrame
|
||||||
|
* CFrame.new(0, Chassis.GROUND_OFFSET - bottomOffset - SHELL_DROP, 0)
|
||||||
|
* CFrame.Angles(0, shellYaw, 0)
|
||||||
|
* centreFix
|
||||||
)
|
)
|
||||||
-- 3. DROP THE SEAT TO FIT THIS SHELL'S ROOF. Cabin height varies by more
|
-- 3. DROP THE SEAT TO FIT THIS SHELL'S ROOF. Cabin height varies by more
|
||||||
-- than a stud across the library (hyper_x roof sits 0.72 above the
|
-- than a stud across the library (hyper_x roof sits 0.72 above the
|
||||||
@@ -398,9 +483,11 @@ local function buildCarModel(car: Car, tuning: Tuning): Model
|
|||||||
if vol > bestVol then
|
if vol > bestVol then
|
||||||
bestVol = vol
|
bestVol = vol
|
||||||
local cf, sz = p.CFrame, p.Size
|
local cf, sz = p.CFrame, p.Size
|
||||||
roofY = p.Position.Y + 0.5 * (math.abs(cf.RightVector.Y * sz.X)
|
roofY = p.Position.Y
|
||||||
+ math.abs(cf.UpVector.Y * sz.Y)
|
+ 0.5
|
||||||
+ math.abs(cf.LookVector.Y * sz.Z))
|
* (math.abs(cf.RightVector.Y * sz.X) + math.abs(cf.UpVector.Y * sz.Y) + math.abs(
|
||||||
|
cf.LookVector.Y * sz.Z
|
||||||
|
))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -411,8 +498,7 @@ local function buildCarModel(car: Car, tuning: Tuning): Model
|
|||||||
-- seatLocal + Size.Y/2). Subtract the half-thickness here or every
|
-- seatLocal + Size.Y/2). Subtract the half-thickness here or every
|
||||||
-- car quietly loses that much of its intended clearance -- which
|
-- car quietly loses that much of its intended clearance -- which
|
||||||
-- is why hyper_x measured 0.07 instead of the requested 0.30.
|
-- is why hyper_x measured 0.07 instead of the requested 0.30.
|
||||||
local wanted = roofLocal - Chassis.HEAD_ABOVE_SEAT
|
local wanted = roofLocal - Chassis.HEAD_ABOVE_SEAT - Chassis.ROOF_CLEARANCE - seatPart.Size.Y / 2
|
||||||
- Chassis.ROOF_CLEARANCE - seatPart.Size.Y / 2
|
|
||||||
-- Allow sinking to the cabin floor: a low roof must win over the
|
-- Allow sinking to the cabin floor: a low roof must win over the
|
||||||
-- nominal seat height, otherwise the clamp silently discards the
|
-- nominal seat height, otherwise the clamp silently discards the
|
||||||
-- whole adaptive solve (which is what used to happen). The shell
|
-- whole adaptive solve (which is what used to happen). The shell
|
||||||
@@ -420,19 +506,13 @@ local function buildCarModel(car: Car, tuning: Tuning): Model
|
|||||||
-- go slightly under GROUND_OFFSET without breaching the underside
|
-- go slightly under GROUND_OFFSET without breaching the underside
|
||||||
-- -- hyper_x (lowest roof) needs that headroom.
|
-- -- hyper_x (lowest roof) needs that headroom.
|
||||||
local floor = Chassis.GROUND_OFFSET - SHELL_DROP + 0.1
|
local floor = Chassis.GROUND_OFFSET - SHELL_DROP + 0.1
|
||||||
local seatLocal = math.clamp(
|
local seatLocal = math.clamp(wanted, floor, Chassis.GROUND_OFFSET + Chassis.SEAT_ABOVE_GROUND)
|
||||||
wanted,
|
seatPart.CFrame = body.CFrame * CFrame.new(0, seatLocal + seatPart.Size.Y / 2, 0.2)
|
||||||
floor,
|
|
||||||
Chassis.GROUND_OFFSET + Chassis.SEAT_ABOVE_GROUND
|
|
||||||
)
|
|
||||||
seatPart.CFrame = body.CFrame
|
|
||||||
* CFrame.new(0, seatLocal + seatPart.Size.Y / 2, 0.2)
|
|
||||||
-- A WeldConstraint locks in the relative offset it had when it
|
-- A WeldConstraint locks in the relative offset it had when it
|
||||||
-- was created, so moving the seat afterwards would be snapped
|
-- was created, so moving the seat afterwards would be snapped
|
||||||
-- straight back. Rebuild the weld at the new offset.
|
-- straight back. Rebuild the weld at the new offset.
|
||||||
for _, w in ipairs(body:GetChildren()) do
|
for _, w in ipairs(body:GetChildren()) do
|
||||||
if w:IsA("WeldConstraint")
|
if w:IsA("WeldConstraint") and (w.Part1 == seatPart or w.Part0 == seatPart) then
|
||||||
and (w.Part1 == seatPart or w.Part0 == seatPart) then
|
|
||||||
w:Destroy()
|
w:Destroy()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -461,6 +541,93 @@ local function buildCarModel(car: Car, tuning: Tuning): Model
|
|||||||
shell.Parent = model
|
shell.Parent = model
|
||||||
-- Hide the placeholder blocks the mesh replaces.
|
-- Hide the placeholder blocks the mesh replaces.
|
||||||
body.Transparency = 1
|
body.Transparency = 1
|
||||||
|
-- ...INCLUDING the physics wheels. The shell brings its own modelled
|
||||||
|
-- wheels, so leaving the chassis cylinders opaque puts TWO sets of
|
||||||
|
-- wheels on the car -- the mesh tyre plus a grey rubber cylinder over
|
||||||
|
-- it. That is what reads as absurdly wide tyres. They aren't even
|
||||||
|
-- concentric: the cylinder's outer edge sits at X=4.10 against the
|
||||||
|
-- gtr_r34 mesh tyre's 4.52, so the duplicate juts out past the real
|
||||||
|
-- wheel at a different track width.
|
||||||
|
-- This is deliberately INSIDE the shell branch. The procedural CarBody
|
||||||
|
-- fallback (else-branch) has no wheels of its own -- buildRims only
|
||||||
|
-- welds a small Rim disc INTO each Wheel_ part, at 1.2x radius versus
|
||||||
|
-- the wheel's 2x, and transparency is not inherited by children. Hide
|
||||||
|
-- these unconditionally and a shell-less car visibly rides on four
|
||||||
|
-- undersized floating discs.
|
||||||
|
for _, d in ipairs(model:GetChildren()) do
|
||||||
|
if d:IsA("BasePart") and d.Name:match("^Wheel_") then
|
||||||
|
d.Transparency = 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- RE-HANG THE MODELLED WHEELS ON THE PHYSICS WHEELS. The imported
|
||||||
|
-- shells weld every wheel part straight to the body mesh (the template
|
||||||
|
-- weld graph is MB <-> FL, MB <-> Rotor, MB <-> Caliper, ...), so the
|
||||||
|
-- visible tyres are rigidly glued to the chassis: their assembly root
|
||||||
|
-- comes out as DriveSeat rather than the wheel. They never spin and
|
||||||
|
-- never steer -- painted-on hubcaps, which is most of why the wheels
|
||||||
|
-- looked wrong in motion. Cut those welds and re-weld each wheel group
|
||||||
|
-- to its own Wheel_ cylinder so it inherits that cylinder's spin, and
|
||||||
|
-- on the front axle its steer.
|
||||||
|
local wheelGroups = shell:FindFirstChild("Wheels")
|
||||||
|
if wheelGroups then
|
||||||
|
for _, w in ipairs(wheelGroups:GetChildren()) do
|
||||||
|
local phys = model:FindFirstChild("Wheel_" .. w.Name:upper())
|
||||||
|
local root = w:IsA("BasePart") and w or w:FindFirstChildWhichIsA("BasePart", true)
|
||||||
|
if phys and root then
|
||||||
|
local group, inGroup = {}, {}
|
||||||
|
if w:IsA("BasePart") then
|
||||||
|
group[#group + 1] = w
|
||||||
|
inGroup[w] = true
|
||||||
|
end
|
||||||
|
for _, d in ipairs(w:GetDescendants()) do
|
||||||
|
if d:IsA("BasePart") then
|
||||||
|
group[#group + 1] = d
|
||||||
|
inGroup[d] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- Drop only the welds that tie this group to the OUTSIDE
|
||||||
|
-- (i.e. to the bodywork). Welds wholly inside the group are
|
||||||
|
-- the wheel's own construction and must survive.
|
||||||
|
for _, c in ipairs(shell:GetDescendants()) do
|
||||||
|
if c:IsA("WeldConstraint") or c:IsA("Weld") then
|
||||||
|
local a, b = c.Part0, c.Part1
|
||||||
|
local aIn, bIn = a ~= nil and inGroup[a], b ~= nil and inGroup[b]
|
||||||
|
if (aIn or bIn) and not (aIn and bIn) then
|
||||||
|
c:Destroy()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- Make it CONCENTRIC with the cylinder it now rides on.
|
||||||
|
-- Each shell models its own track and wheelbase (the
|
||||||
|
-- gtr_r34 rear tyre sits 1.34 studs ahead of the chassis
|
||||||
|
-- axle, the fronts 0.33 inboard). A weld preserves relative
|
||||||
|
-- offset, so welding at that offset would make the tyre
|
||||||
|
-- ORBIT the axle at 1.34 studs radius instead of spinning
|
||||||
|
-- on it -- far worse than the static wheel it replaces.
|
||||||
|
local delta = phys.Position - root.Position
|
||||||
|
for _, p in ipairs(group) do
|
||||||
|
p.CFrame = p.CFrame + delta
|
||||||
|
end
|
||||||
|
-- The template hung every part off the body, so with those
|
||||||
|
-- welds cut the group has no internal rigidity left of its
|
||||||
|
-- own. Rebuild it around root (these parts are already
|
||||||
|
-- Massless + CanCollide=false, so this adds no physics
|
||||||
|
-- cost), then hang root off the cylinder.
|
||||||
|
for _, p in ipairs(group) do
|
||||||
|
if p ~= root then
|
||||||
|
local iw = Instance.new("WeldConstraint")
|
||||||
|
iw.Part0 = root
|
||||||
|
iw.Part1 = p
|
||||||
|
iw.Parent = root
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local pw = Instance.new("WeldConstraint")
|
||||||
|
pw.Part0 = phys
|
||||||
|
pw.Part1 = root
|
||||||
|
pw.Parent = phys
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
local cabin = model:FindFirstChild("Cabin")
|
local cabin = model:FindFirstChild("Cabin")
|
||||||
if cabin then
|
if cabin then
|
||||||
cabin:Destroy()
|
cabin:Destroy()
|
||||||
|
|||||||
+390
-37
@@ -56,6 +56,60 @@ local C_PARAPET = Color3.fromRGB(226, 224, 218)
|
|||||||
local C_RAIL = Color3.fromRGB(96, 100, 106)
|
local C_RAIL = Color3.fromRGB(96, 100, 106)
|
||||||
local UP = Vector3.new(0, 1, 0)
|
local UP = Vector3.new(0, 1, 0)
|
||||||
|
|
||||||
|
-- REAL-WORLD SCALE ------------------------------------------------------------
|
||||||
|
-- The rest of the codebase converts speed with 0.28 m per stud, so all the
|
||||||
|
-- metric road-standard figures below are converted through the same constant.
|
||||||
|
-- Sanity check: the 49,383-stud loop comes out at 13.8 km against the real
|
||||||
|
-- C1 Inner Circular Route's 14.8 km, so the circuit is very close to 1:1.
|
||||||
|
local M = 1 / 0.28 -- studs per metre (~3.571)
|
||||||
|
|
||||||
|
-- Road markings per 全標協 (Japan Road Marking Assoc.) 2012 p.42 / 2018 pp.97-98,
|
||||||
|
-- read from the primary standard rather than repeated folklore:
|
||||||
|
-- * 車線境界線 (lane boundary, dashed): line length 3-10 m, gap = 1x to 2x the
|
||||||
|
-- line length, width 10-15 cm (15 cm "desirable"). The 8 m line / 12 m gap
|
||||||
|
-- used here is the high-speed end of that legal range (gap = 1.5x line) and
|
||||||
|
-- is the configuration expressways actually use -- it's what makes the
|
||||||
|
-- rhythm read as an expressway rather than a 5 m/5 m surface street.
|
||||||
|
-- * Where overtaking is undesirable (屈曲部 -- curves), the standard says the
|
||||||
|
-- line becomes SOLID rather than dashed. On expressways that no-lane-change
|
||||||
|
-- line is painted yellow.
|
||||||
|
-- * NOTE: the 45 cm width sometimes quoted for "expressway lines" is NOT the
|
||||||
|
-- lane boundary -- it's the boundary against a 変速車線 (accel/decel lane).
|
||||||
|
-- Applied below at ramps only.
|
||||||
|
local DASH_PAINT = 8 * M -- 28.6 studs
|
||||||
|
local DASH_GAP = 12 * M -- 42.9 studs
|
||||||
|
local DASH_CYCLE = DASH_PAINT + DASH_GAP -- 20 m
|
||||||
|
local LANE_LINE_W = 0.15 * M -- 0.54 studs
|
||||||
|
local EDGE_LINE_W = 0.20 * M -- 0.71 studs
|
||||||
|
local RAMP_LINE_W = 0.45 * M -- 1.61 studs; through lane vs speed-change lane
|
||||||
|
local LANE_W = 3.25 * M -- 11.6 studs; narrow urban lane used on the C1
|
||||||
|
-- 導流帯 (gore/zebra) per 全標協 2018 pp.97-98: 45 cm white solid lines repeated
|
||||||
|
-- every 1.0 m, bounded by a 15-20 cm edge line.
|
||||||
|
local ZEBRA_W = 0.45 * M
|
||||||
|
local ZEBRA_PITCH = 1.0 * M
|
||||||
|
local MARK_Y = 0.1 -- sits just proud of the deck to avoid z-fighting
|
||||||
|
local MARK_H = 2.1
|
||||||
|
-- Heading change per 60-stud sample above which the curve counts as tight
|
||||||
|
-- enough that the real road would paint a solid yellow no-lane-change line.
|
||||||
|
local TIGHT_CURVE = math.rad(4)
|
||||||
|
|
||||||
|
local C_PAINT_WHITE = Color3.fromRGB(232, 232, 236)
|
||||||
|
local C_PAINT_YELLOW = Color3.fromRGB(240, 190, 40) -- 黄色 no-lane-change line
|
||||||
|
|
||||||
|
-- Signage colours sampled from the official Japanese sign artwork (Wikimedia
|
||||||
|
-- Commons SVGs of the 標識令 plates), not eyeballed:
|
||||||
|
-- guide sign (案内標識) green #005D35
|
||||||
|
-- Shutoko route badge green #007F1F
|
||||||
|
local C_SIGN_GREEN = Color3.fromRGB(0, 93, 53)
|
||||||
|
local C_BADGE_GREEN = Color3.fromRGB(0, 127, 31)
|
||||||
|
local C_SIGN_WHITE = Color3.fromRGB(255, 255, 255)
|
||||||
|
|
||||||
|
-- Lighting. Japanese expressway/tunnel lighting is high-pressure sodium
|
||||||
|
-- (warm amber) with newer LED sections in cool white -- NOT the blue/teal
|
||||||
|
-- that reads as generic sci-fi. Tunnel basic lighting runs continuously.
|
||||||
|
local C_SODIUM = Color3.fromRGB(255, 176, 92)
|
||||||
|
local C_TUNNEL_LED = Color3.fromRGB(255, 244, 224)
|
||||||
|
|
||||||
local NEON_PINK = Color3.fromRGB(255, 40, 120)
|
local NEON_PINK = Color3.fromRGB(255, 40, 120)
|
||||||
local NEON_BLUE = Color3.fromRGB(40, 160, 255)
|
local NEON_BLUE = Color3.fromRGB(40, 160, 255)
|
||||||
local NEON_TEAL = Color3.fromRGB(40, 230, 200)
|
local NEON_TEAL = Color3.fromRGB(40, 230, 200)
|
||||||
@@ -78,27 +132,105 @@ local function det(seed: number): number
|
|||||||
return x - math.floor(x)
|
return x - math.floor(x)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Real interchange names. Japanese is what's actually painted on the gantry;
|
||||||
|
-- the romaji underneath is the smaller secondary line every Shutoko guide sign
|
||||||
|
-- carries. Names cross-checked against OpenStreetMap's tagging of the real
|
||||||
|
-- Inner Circular Route (name / name:en on the motorway_junction nodes).
|
||||||
|
local JUNCTION_NAMES: { [string]: { string } } = {
|
||||||
|
["Kyobashi"] = { "京橋", "Kyobashi" },
|
||||||
|
["Takaracho"] = { "宝町", "Takaracho" },
|
||||||
|
["Edobashi junction"] = { "江戸橋JCT", "Edobashi JCT" },
|
||||||
|
["Takebashi Junction"] = { "竹橋JCT", "Takebashi JCT" },
|
||||||
|
["Hitotsubashi"] = { "一ツ橋", "Hitotsubashi" },
|
||||||
|
["Kitanomaru"] = { "北の丸", "Kitanomaru" },
|
||||||
|
["Daikancho"] = { "代官町", "Daikancho" },
|
||||||
|
["Miyakezaka JCT"] = { "三宅坂JCT", "Miyakezaka JCT" },
|
||||||
|
["Miyakezaka Junction"] = { "三宅坂JCT", "Miyakezaka JCT" },
|
||||||
|
["Kasumigaseki"] = { "霞が関", "Kasumigaseki" },
|
||||||
|
["Tanimachi JCT"] = { "谷町JCT", "Tanimachi JCT" },
|
||||||
|
["Iikura"] = { "飯倉", "Iikura" },
|
||||||
|
["Shiodome"] = { "汐留", "Shiodome" },
|
||||||
|
["Ginza"] = { "銀座", "Ginza" },
|
||||||
|
["Shintomicho"] = { "新富町", "Shintomicho" },
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Japanese expressway guide sign (案内標識). Green board, white text, white
|
||||||
|
-- hairline border inset from the edge, Japanese place name large with the
|
||||||
|
-- romaji reading beneath it, and the circular route badge on the left.
|
||||||
local function makeSign(text: string, cf: CFrame, parent: Instance)
|
local function makeSign(text: string, cf: CFrame, parent: Instance)
|
||||||
|
local names = JUNCTION_NAMES[text] or { text, text }
|
||||||
|
local ja, romaji = names[1], names[2]
|
||||||
|
|
||||||
local board = part({
|
local board = part({
|
||||||
Name = "Sign",
|
Name = "Sign",
|
||||||
Size = Vector3.new(34, 10, 1),
|
Size = Vector3.new(34, 10, 1),
|
||||||
CFrame = cf,
|
CFrame = cf,
|
||||||
Color = Color3.fromRGB(18, 90, 60),
|
Color = C_SIGN_GREEN,
|
||||||
Material = Enum.Material.SmoothPlastic,
|
Material = Enum.Material.SmoothPlastic,
|
||||||
})
|
})
|
||||||
board.Parent = parent
|
board.Parent = parent
|
||||||
|
|
||||||
local gui = Instance.new("SurfaceGui")
|
local gui = Instance.new("SurfaceGui")
|
||||||
gui.Face = Enum.NormalId.Front
|
gui.Face = Enum.NormalId.Front
|
||||||
gui.CanvasSize = Vector2.new(600, 180)
|
gui.CanvasSize = Vector2.new(680, 200)
|
||||||
gui.Parent = board
|
gui.Parent = board
|
||||||
local label = Instance.new("TextLabel")
|
|
||||||
label.Size = UDim2.fromScale(1, 1)
|
-- Thin white border inset from the panel edge -- present on every real
|
||||||
label.BackgroundTransparency = 1
|
-- Japanese guide sign and a big part of why they read as "Japanese".
|
||||||
label.Font = Enum.Font.GothamBold
|
local border = Instance.new("Frame")
|
||||||
label.TextColor3 = Color3.new(1, 1, 1)
|
border.Size = UDim2.new(1, -24, 1, -24)
|
||||||
label.TextScaled = true
|
border.Position = UDim2.fromOffset(12, 12)
|
||||||
label.Text = text
|
border.BackgroundTransparency = 1
|
||||||
label.Parent = gui
|
border.BorderSizePixel = 3
|
||||||
|
border.BorderColor3 = C_SIGN_WHITE
|
||||||
|
border.Parent = gui
|
||||||
|
|
||||||
|
-- Route badge: the C1 shield, green disc with white ring and "C1".
|
||||||
|
local badge = Instance.new("Frame")
|
||||||
|
badge.Size = UDim2.fromOffset(104, 104)
|
||||||
|
badge.Position = UDim2.fromOffset(34, 48)
|
||||||
|
badge.BackgroundColor3 = C_BADGE_GREEN
|
||||||
|
badge.BorderSizePixel = 0
|
||||||
|
badge.Parent = gui
|
||||||
|
local badgeCorner = Instance.new("UICorner")
|
||||||
|
badgeCorner.CornerRadius = UDim.new(1, 0)
|
||||||
|
badgeCorner.Parent = badge
|
||||||
|
local badgeStroke = Instance.new("UIStroke")
|
||||||
|
badgeStroke.Color = C_SIGN_WHITE
|
||||||
|
badgeStroke.Thickness = 5
|
||||||
|
badgeStroke.Parent = badge
|
||||||
|
local badgeText = Instance.new("TextLabel")
|
||||||
|
badgeText.Size = UDim2.fromScale(1, 1)
|
||||||
|
badgeText.BackgroundTransparency = 1
|
||||||
|
badgeText.Font = Enum.Font.GothamBold
|
||||||
|
badgeText.TextColor3 = C_SIGN_WHITE
|
||||||
|
badgeText.TextScaled = true
|
||||||
|
badgeText.Text = "C1"
|
||||||
|
badgeText.Parent = badge
|
||||||
|
|
||||||
|
-- Japanese name (primary, large)
|
||||||
|
local jaLabel = Instance.new("TextLabel")
|
||||||
|
jaLabel.Size = UDim2.new(1, -190, 0, 96)
|
||||||
|
jaLabel.Position = UDim2.fromOffset(168, 36)
|
||||||
|
jaLabel.BackgroundTransparency = 1
|
||||||
|
jaLabel.Font = Enum.Font.GothamBold
|
||||||
|
jaLabel.TextColor3 = C_SIGN_WHITE
|
||||||
|
jaLabel.TextScaled = true
|
||||||
|
jaLabel.TextXAlignment = Enum.TextXAlignment.Left
|
||||||
|
jaLabel.Text = ja
|
||||||
|
jaLabel.Parent = gui
|
||||||
|
|
||||||
|
-- Romaji reading (secondary, smaller, beneath)
|
||||||
|
local enLabel = Instance.new("TextLabel")
|
||||||
|
enLabel.Size = UDim2.new(1, -190, 0, 44)
|
||||||
|
enLabel.Position = UDim2.fromOffset(168, 132)
|
||||||
|
enLabel.BackgroundTransparency = 1
|
||||||
|
enLabel.Font = Enum.Font.Gotham
|
||||||
|
enLabel.TextColor3 = C_SIGN_WHITE
|
||||||
|
enLabel.TextScaled = true
|
||||||
|
enLabel.TextXAlignment = Enum.TextXAlignment.Left
|
||||||
|
enLabel.Text = romaji
|
||||||
|
enLabel.Parent = gui
|
||||||
end
|
end
|
||||||
|
|
||||||
-- PA junction planning -----------------------------------------------------------
|
-- PA junction planning -----------------------------------------------------------
|
||||||
@@ -157,6 +289,10 @@ local function buildRoad(map: Folder)
|
|||||||
local n = #samples
|
local n = #samples
|
||||||
local plan = planPA()
|
local plan = planPA()
|
||||||
|
|
||||||
|
-- Running arc length around the loop, so lane dashes keep a continuous
|
||||||
|
-- 20 m rhythm instead of restarting at every sample.
|
||||||
|
local segStart = 0
|
||||||
|
|
||||||
for i = 1, n do
|
for i = 1, n do
|
||||||
local a = samples[i]
|
local a = samples[i]
|
||||||
local b = samples[(i % n) + 1]
|
local b = samples[(i % n) + 1]
|
||||||
@@ -172,6 +308,16 @@ local function buildRoad(map: Folder)
|
|||||||
local right = flatDir:Cross(UP)
|
local right = flatDir:Cross(UP)
|
||||||
local cf = CFrame.lookAt(mid, mid + dir, UP)
|
local cf = CFrame.lookAt(mid, mid + dir, UP)
|
||||||
|
|
||||||
|
-- How hard this segment turns, used to decide where the real road
|
||||||
|
-- would forbid lane changes (solid yellow) rather than allow them.
|
||||||
|
local prev = samples[((i - 2) % n) + 1]
|
||||||
|
local inDir = (a.pos - prev.pos)
|
||||||
|
inDir = Vector3.new(inDir.X, 0, inDir.Z)
|
||||||
|
local curvature = 0
|
||||||
|
if inDir.Magnitude > 0.01 then
|
||||||
|
curvature = math.acos(math.clamp(inDir.Unit:Dot(flatDir), -1, 1))
|
||||||
|
end
|
||||||
|
|
||||||
local deck = part({
|
local deck = part({
|
||||||
Name = "Road_" .. i,
|
Name = "Road_" .. i,
|
||||||
Size = Vector3.new(ROAD_WIDTH, 2, len),
|
Size = Vector3.new(ROAD_WIDTH, 2, len),
|
||||||
@@ -211,28 +357,66 @@ local function buildRoad(map: Folder)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- LANE MARKINGS: 2 lanes per carriageway ----------------------------
|
-- LANE MARKINGS: 2 lanes per carriageway ----------------------------
|
||||||
-- Each carriageway spans MED_HALF..HALF from the centreline. One dashed
|
-- C1 is a 2-lane-per-direction urban expressway (confirmed against
|
||||||
-- line down its middle splits it into the two lanes; a solid white edge
|
-- OSM tagging of 首都高速都心環状線: lanes=2, maxspeed=50). Lane width
|
||||||
-- line runs just inside the outer barrier.
|
-- is the narrow urban 3.25 m rather than the 3.5 m used on the
|
||||||
local cwMid = (MED_HALF + HALF) / 2
|
-- Bayshore, which is what leaves so little shoulder on the real road.
|
||||||
|
--
|
||||||
|
-- Dashes are driven by ARC LENGTH along the circuit, not by sample
|
||||||
|
-- index. That matters: samples are 60 studs (16.8 m) apart but the
|
||||||
|
-- legal dash cycle is 20 m, so an index-based dash can never land on
|
||||||
|
-- the right rhythm -- it has to be computed in metres and clipped to
|
||||||
|
-- each segment.
|
||||||
|
local cwMid = MED_HALF + LANE_W
|
||||||
|
-- Lane changes are prohibited (yellow line) through the tunnels and
|
||||||
|
-- on the tight curves, which on the real C1 is most of the loop.
|
||||||
|
local laneColor = (a.tunnel or curvature > TIGHT_CURVE) and C_PAINT_YELLOW or C_PAINT_WHITE
|
||||||
|
|
||||||
for _, s in ipairs({ -1, 1 }) do
|
for _, s in ipairs({ -1, 1 }) do
|
||||||
|
-- 車道外側線: solid, continuous, 0.20 m wide.
|
||||||
local edge = part({
|
local edge = part({
|
||||||
Name = "EdgeLine",
|
Name = "EdgeLine",
|
||||||
Size = Vector3.new(0.9, 2.1, len),
|
Size = Vector3.new(EDGE_LINE_W, MARK_H, len),
|
||||||
CFrame = cf * CFrame.new(s * (HALF - 1.6), 0.1, 0),
|
CFrame = cf * CFrame.new(s * (HALF - 1.6), MARK_Y, 0),
|
||||||
Color = Color3.fromRGB(228, 228, 233),
|
Color = C_PAINT_WHITE,
|
||||||
Material = Enum.Material.SmoothPlastic,
|
Material = Enum.Material.SmoothPlastic,
|
||||||
})
|
})
|
||||||
edge.Parent = road
|
edge.Parent = road
|
||||||
if i % 2 == 0 then
|
|
||||||
local dash = part({
|
-- 車線境界線 between the two lanes.
|
||||||
Name = "Dash",
|
if laneColor == C_PAINT_YELLOW then
|
||||||
Size = Vector3.new(0.8, 2.1, len * 0.55),
|
-- Yellow no-lane-change line is SOLID, not dashed.
|
||||||
CFrame = cf * CFrame.new(s * cwMid, 0.1, 0),
|
local solid = part({
|
||||||
Color = Color3.fromRGB(220, 220, 225),
|
Name = "LaneLine",
|
||||||
|
Size = Vector3.new(LANE_LINE_W, MARK_H, len),
|
||||||
|
CFrame = cf * CFrame.new(s * cwMid, MARK_Y, 0),
|
||||||
|
Color = C_PAINT_YELLOW,
|
||||||
Material = Enum.Material.SmoothPlastic,
|
Material = Enum.Material.SmoothPlastic,
|
||||||
})
|
})
|
||||||
dash.Parent = road
|
solid.Parent = road
|
||||||
|
else
|
||||||
|
-- Dashed: paint the parts of the 20 m cycle that fall inside
|
||||||
|
-- this segment. segStart is the distance travelled around the
|
||||||
|
-- loop, so the rhythm stays continuous across segment joins.
|
||||||
|
local segLen = delta.Magnitude
|
||||||
|
local k = math.floor(segStart / DASH_CYCLE)
|
||||||
|
while k * DASH_CYCLE < segStart + segLen do
|
||||||
|
local pStart = math.max(k * DASH_CYCLE, segStart)
|
||||||
|
local pEnd = math.min(k * DASH_CYCLE + DASH_PAINT, segStart + segLen)
|
||||||
|
if pEnd > pStart then
|
||||||
|
-- Convert back to an offset from the segment centre.
|
||||||
|
local mid01 = ((pStart + pEnd) / 2 - segStart) / segLen
|
||||||
|
local dash = part({
|
||||||
|
Name = "Dash",
|
||||||
|
Size = Vector3.new(LANE_LINE_W, MARK_H, pEnd - pStart),
|
||||||
|
CFrame = cf * CFrame.new(s * cwMid, MARK_Y, (mid01 - 0.5) * segLen),
|
||||||
|
Color = C_PAINT_WHITE,
|
||||||
|
Material = Enum.Material.SmoothPlastic,
|
||||||
|
})
|
||||||
|
dash.Parent = road
|
||||||
|
end
|
||||||
|
k += 1
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -303,16 +487,18 @@ local function buildRoad(map: Folder)
|
|||||||
Material = Enum.Material.Concrete,
|
Material = Enum.Material.Concrete,
|
||||||
})
|
})
|
||||||
ceil.Parent = struct
|
ceil.Parent = struct
|
||||||
if i % 2 == 0 then
|
-- Tunnel lighting is high-pressure sodium (warm amber) with newer
|
||||||
local strip = part({
|
-- LED sections in cool white. Real tunnel "basic lighting" is a
|
||||||
Name = "TunnelLight",
|
-- continuous run, not isolated dots, so the strip is placed on
|
||||||
Size = Vector3.new(ROAD_WIDTH * 0.5, 0.6, 2.5),
|
-- every segment rather than every other one.
|
||||||
CFrame = cf * CFrame.new(0, h - 0.5, 0),
|
local strip = part({
|
||||||
Color = (i % 6 == 0) and NEON_BLUE or NEON_TEAL,
|
Name = "TunnelLight",
|
||||||
Material = Enum.Material.Neon,
|
Size = Vector3.new(ROAD_WIDTH * 0.5, 0.6, len),
|
||||||
})
|
CFrame = cf * CFrame.new(0, h - 0.5, 0),
|
||||||
strip.Parent = struct
|
Color = (i % 8 < 4) and C_SODIUM or C_TUNNEL_LED,
|
||||||
end
|
Material = Enum.Material.Neon,
|
||||||
|
})
|
||||||
|
strip.Parent = struct
|
||||||
else
|
else
|
||||||
-- Elevated viaduct: support pillars down to street level
|
-- Elevated viaduct: support pillars down to street level
|
||||||
if i % 3 == 0 and a.pos.Y > 18 then
|
if i % 3 == 0 and a.pos.Y > 18 then
|
||||||
@@ -325,17 +511,38 @@ local function buildRoad(map: Folder)
|
|||||||
})
|
})
|
||||||
pil.Parent = struct
|
pil.Parent = struct
|
||||||
end
|
end
|
||||||
-- sodium lamps along the deck
|
-- Sodium lamps along the deck. Japanese expressway lighting is
|
||||||
if i % 4 == 0 then
|
-- warm high-pressure sodium on a ~35 m pole pitch; samples are
|
||||||
|
-- 60 studs (16.8 m) apart so every 2nd sample ~= 33.6 m.
|
||||||
|
if i % 2 == 0 then
|
||||||
local lamp = part({
|
local lamp = part({
|
||||||
Name = "RoadLight",
|
Name = "RoadLight",
|
||||||
Size = Vector3.new(1.4, 1.4, 1.4),
|
Size = Vector3.new(1.4, 1.4, 1.4),
|
||||||
CFrame = CFrame.new(mid + right * (ROAD_WIDTH / 2) + Vector3.new(0, 5.5, 0)),
|
CFrame = CFrame.new(mid + right * (ROAD_WIDTH / 2) + Vector3.new(0, 5.5, 0)),
|
||||||
Color = NEON_AMBER,
|
Color = C_SODIUM,
|
||||||
Material = Enum.Material.Neon,
|
Material = Enum.Material.Neon,
|
||||||
})
|
})
|
||||||
lamp.Parent = struct
|
lamp.Parent = struct
|
||||||
end
|
end
|
||||||
|
-- Delineators (視線誘導標): small reflective posts on the outer
|
||||||
|
-- edge that mark the road's line ahead at night. Real spacing on
|
||||||
|
-- curves is tight; ~17 m here.
|
||||||
|
local delin = part({
|
||||||
|
Name = "Delineator",
|
||||||
|
Size = Vector3.new(0.5, 1.1, 0.5),
|
||||||
|
CFrame = CFrame.new(mid + right * (ROAD_WIDTH / 2 - 2.6) + Vector3.new(0, 2.5, 0)),
|
||||||
|
Color = C_PAINT_WHITE,
|
||||||
|
Material = Enum.Material.SmoothPlastic,
|
||||||
|
})
|
||||||
|
delin.Parent = struct
|
||||||
|
local eye = part({
|
||||||
|
Name = "DelineatorEye",
|
||||||
|
Size = Vector3.new(0.36, 0.36, 0.36),
|
||||||
|
CFrame = CFrame.new(mid + right * (ROAD_WIDTH / 2 - 2.6) + Vector3.new(0, 3.0, 0)),
|
||||||
|
Color = Color3.fromRGB(255, 190, 80),
|
||||||
|
Material = Enum.Material.Neon,
|
||||||
|
})
|
||||||
|
eye.Parent = struct
|
||||||
if i % 9 == 0 then
|
if i % 9 == 0 then
|
||||||
local pylon = part({
|
local pylon = part({
|
||||||
Name = "Pylon",
|
Name = "Pylon",
|
||||||
@@ -347,9 +554,128 @@ local function buildRoad(map: Folder)
|
|||||||
pylon.Parent = struct
|
pylon.Parent = struct
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- 導流帯 / gore zebra at the PA ramp junctions --------------------
|
||||||
|
-- Where the ramp splits off the mainline the real road paints a
|
||||||
|
-- zebra-hatched no-drive wedge in the gore. 45 cm bars every 1.0 m
|
||||||
|
-- per 全標協 2018 pp.97-98.
|
||||||
|
if plan.medianOpen[i] then
|
||||||
|
local nBars = math.floor(delta.Magnitude / ZEBRA_PITCH)
|
||||||
|
for k = 0, nBars - 1 do
|
||||||
|
local z = (k * ZEBRA_PITCH - delta.Magnitude / 2) + ZEBRA_PITCH / 2
|
||||||
|
for _, s in ipairs({ -1, 1 }) do
|
||||||
|
local bar = part({
|
||||||
|
Name = "GoreZebra",
|
||||||
|
Size = Vector3.new(6, MARK_H, ZEBRA_W),
|
||||||
|
CFrame = cf * CFrame.new(s * (HALF - 5), MARK_Y, z),
|
||||||
|
Color = C_PAINT_WHITE,
|
||||||
|
Material = Enum.Material.SmoothPlastic,
|
||||||
|
})
|
||||||
|
bar.Parent = road
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- 45 cm boundary line separating the through lane from the
|
||||||
|
-- speed-change lane, per 全標協 2012 p.42.
|
||||||
|
for _, s in ipairs({ -1, 1 }) do
|
||||||
|
local rl = part({
|
||||||
|
Name = "RampBoundary",
|
||||||
|
Size = Vector3.new(RAMP_LINE_W, MARK_H, len),
|
||||||
|
CFrame = cf * CFrame.new(s * (HALF - 8.5), MARK_Y, 0),
|
||||||
|
Color = C_PAINT_WHITE,
|
||||||
|
Material = Enum.Material.SmoothPlastic,
|
||||||
|
})
|
||||||
|
rl.Parent = road
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Advance the running arc length so the next segment's dashes
|
||||||
|
-- continue the 20 m rhythm instead of restarting.
|
||||||
|
segStart += delta.Magnitude
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Regulatory speed limit sign (規制標識 323 最高速度) -----------------------------
|
||||||
|
-- Circular, white face, red annulus, black numerals. The real C1 is posted at
|
||||||
|
-- 50 km/h (40 in the tightest junction sections) -- verified against the route
|
||||||
|
-- article's own signage table, not the 80 km/h used on the Bayshore Route.
|
||||||
|
local function makeSpeedSign(kmh: number, cf: CFrame, parent: Instance)
|
||||||
|
local post = part({
|
||||||
|
Name = "SignPost",
|
||||||
|
Size = Vector3.new(0.6, 12, 0.6),
|
||||||
|
CFrame = cf * CFrame.new(0, -6, 0),
|
||||||
|
Color = C_RAIL,
|
||||||
|
Material = Enum.Material.Metal,
|
||||||
|
})
|
||||||
|
post.Parent = parent
|
||||||
|
|
||||||
|
local face = part({
|
||||||
|
Name = "SpeedLimitSign",
|
||||||
|
Size = Vector3.new(6, 6, 0.4),
|
||||||
|
CFrame = cf,
|
||||||
|
Color = Color3.fromRGB(255, 255, 255),
|
||||||
|
Material = Enum.Material.SmoothPlastic,
|
||||||
|
Shape = Enum.PartType.Cylinder,
|
||||||
|
})
|
||||||
|
-- Cylinder's flat faces point along X, so rotate it to face the driver.
|
||||||
|
face.CFrame = cf * CFrame.Angles(0, math.rad(90), 0)
|
||||||
|
face.Parent = parent
|
||||||
|
|
||||||
|
for _, faceId in ipairs({ Enum.NormalId.Right, Enum.NormalId.Left }) do
|
||||||
|
local gui = Instance.new("SurfaceGui")
|
||||||
|
gui.Face = faceId
|
||||||
|
gui.CanvasSize = Vector2.new(256, 256)
|
||||||
|
gui.Parent = face
|
||||||
|
|
||||||
|
local disc = Instance.new("Frame")
|
||||||
|
disc.Size = UDim2.fromScale(1, 1)
|
||||||
|
disc.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
|
||||||
|
disc.BorderSizePixel = 0
|
||||||
|
disc.Parent = gui
|
||||||
|
local dc = Instance.new("UICorner")
|
||||||
|
dc.CornerRadius = UDim.new(1, 0)
|
||||||
|
dc.Parent = disc
|
||||||
|
-- Red annulus is ~11% of the diameter on the JP plate.
|
||||||
|
local ring = Instance.new("UIStroke")
|
||||||
|
ring.Color = Color3.fromRGB(200, 30, 45)
|
||||||
|
ring.Thickness = 28
|
||||||
|
ring.Parent = disc
|
||||||
|
|
||||||
|
local num = Instance.new("TextLabel")
|
||||||
|
num.Size = UDim2.fromScale(0.72, 0.56)
|
||||||
|
num.Position = UDim2.fromScale(0.14, 0.22)
|
||||||
|
num.BackgroundTransparency = 1
|
||||||
|
num.Font = Enum.Font.GothamBold
|
||||||
|
num.TextColor3 = Color3.fromRGB(20, 20, 20)
|
||||||
|
num.TextScaled = true
|
||||||
|
num.Text = tostring(kmh)
|
||||||
|
num.Parent = disc
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Kilometre post (キロポスト) ----------------------------------------------------
|
||||||
|
local function makeKmPost(km: number, cf: CFrame, parent: Instance)
|
||||||
|
local board = part({
|
||||||
|
Name = "KmPost",
|
||||||
|
Size = Vector3.new(2.6, 3.2, 0.3),
|
||||||
|
CFrame = cf,
|
||||||
|
Color = C_SIGN_GREEN,
|
||||||
|
Material = Enum.Material.SmoothPlastic,
|
||||||
|
})
|
||||||
|
board.Parent = parent
|
||||||
|
local gui = Instance.new("SurfaceGui")
|
||||||
|
gui.Face = Enum.NormalId.Front
|
||||||
|
gui.CanvasSize = Vector2.new(130, 160)
|
||||||
|
gui.Parent = board
|
||||||
|
local label = Instance.new("TextLabel")
|
||||||
|
label.Size = UDim2.fromScale(1, 1)
|
||||||
|
label.BackgroundTransparency = 1
|
||||||
|
label.Font = Enum.Font.GothamBold
|
||||||
|
label.TextColor3 = C_SIGN_WHITE
|
||||||
|
label.TextScaled = true
|
||||||
|
label.Text = string.format("%.1f", km)
|
||||||
|
label.Parent = gui
|
||||||
|
end
|
||||||
|
|
||||||
-- Junction gantry signs at each real interchange ---------------------------------
|
-- Junction gantry signs at each real interchange ---------------------------------
|
||||||
local function buildJunctionSigns(map: Folder)
|
local function buildJunctionSigns(map: Folder)
|
||||||
local folder = Instance.new("Folder")
|
local folder = Instance.new("Folder")
|
||||||
@@ -369,6 +695,33 @@ local function buildJunctionSigns(map: Folder)
|
|||||||
local pos = s.pos + Vector3.new(0, 15, 0)
|
local pos = s.pos + Vector3.new(0, 15, 0)
|
||||||
makeSign(s.piece, CFrame.lookAt(pos, pos - fwd, UP), folder)
|
makeSign(s.piece, CFrame.lookAt(pos, pos - fwd, UP), folder)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Speed limit repeaters and kilometre posts around the loop.
|
||||||
|
local dist = 0
|
||||||
|
local nextKm = 0
|
||||||
|
for i = 1, n do
|
||||||
|
local a = samples[i]
|
||||||
|
local b = samples[(i % n) + 1]
|
||||||
|
local seg = (b.pos - a.pos).Magnitude
|
||||||
|
local fwd = Circuit.forwardAt(i)
|
||||||
|
local flat = Vector3.new(fwd.X, 0, fwd.Z)
|
||||||
|
flat = flat.Magnitude > 0.01 and flat.Unit or Vector3.xAxis
|
||||||
|
local right = flat:Cross(UP)
|
||||||
|
|
||||||
|
-- C1 is posted at 50 km/h; repeat roughly every 1 km, off to the side.
|
||||||
|
if i % 60 == 0 and not a.tunnel then
|
||||||
|
local p = a.pos + right * (ROAD_WIDTH / 2 - 3) + Vector3.new(0, 9, 0)
|
||||||
|
makeSpeedSign(50, CFrame.lookAt(p, p - flat, UP), folder)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Kilometre posts every 0.5 km of real distance (0.5 km = 1785 studs).
|
||||||
|
if dist >= nextKm * 500 * M then
|
||||||
|
local p = a.pos + right * (ROAD_WIDTH / 2 - 2) + Vector3.new(0, 4, 0)
|
||||||
|
makeKmPost(nextKm * 0.5, CFrame.lookAt(p, p - flat, UP), folder)
|
||||||
|
nextKm += 1
|
||||||
|
end
|
||||||
|
dist += seg
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Daikoku-style PA ---------------------------------------------------------------
|
-- Daikoku-style PA ---------------------------------------------------------------
|
||||||
|
|||||||
@@ -49,6 +49,28 @@ Chassis.WHEELBASE_HALF = 5.9 -- longitudinal distance body centre -> axle
|
|||||||
Chassis.SUSPENSION_DROP = 1.05 -- rest distance from body centre down to wheel centre
|
Chassis.SUSPENSION_DROP = 1.05 -- rest distance from body centre down to wheel centre
|
||||||
Chassis.SUSPENSION_TRAVEL = 0.9 -- total prismatic travel allowed
|
Chassis.SUSPENSION_TRAVEL = 0.9 -- total prismatic travel allowed
|
||||||
|
|
||||||
|
-- Steering knuckle ------------------------------------------------------------
|
||||||
|
-- The knuckle must be a SUBSTANTIAL body, not a token 0.5 cube. The steering
|
||||||
|
-- servo yaws the knuckle, and the knuckle has to carry the wheel (mass 5.67)
|
||||||
|
-- against tyre-to-ground friction. At the old 0.5 size the knuckle massed
|
||||||
|
-- 0.087 -- 65x LIGHTER than the wheel hanging off it -- and the servo simply
|
||||||
|
-- lost: measured under real controller input, CurrentAngle missed TargetAngle
|
||||||
|
-- by a mean of 32 deg (peak 72 deg) and broke through the servo's own
|
||||||
|
-- LimitsEnabled bounds on 6 of 20 samples. A constraint blowing past its hard
|
||||||
|
-- limits is the signature of an under-massed link, not of a weak motor:
|
||||||
|
-- raising ServoMaxTorque cannot fix it because the torque was never the
|
||||||
|
-- binding constraint.
|
||||||
|
--
|
||||||
|
-- Measured sweep (full lock, real SteerFloat input, gtr_r34):
|
||||||
|
-- size 0.5 (mass 0.09) -> mean err 32.2 deg, 6/20 limit violations, drives
|
||||||
|
-- size 1.6 (mass 2.87) -> mean err 2.2 deg, 1/20 violations, drives
|
||||||
|
-- density 80 (mass 328) -> mean err 0.1 deg, 0/20 violations, CAR FROZEN
|
||||||
|
-- Mass is what buys control, but overshoot it and the knuckles outweigh the
|
||||||
|
-- 125-mass body and the car cannot move at all. 1.6 studs at stock density
|
||||||
|
-- puts the knuckle in the same order of magnitude as its wheel, which is the
|
||||||
|
-- physical relationship that matters. Do NOT "improve" this with density.
|
||||||
|
Chassis.KNUCKLE_SIZE = 1.6
|
||||||
|
|
||||||
-- Cabin fit --------------------------------------------------------------------
|
-- Cabin fit --------------------------------------------------------------------
|
||||||
-- The driver used to stick out of the roof for THREE compounding reasons, all
|
-- The driver used to stick out of the roof for THREE compounding reasons, all
|
||||||
-- fixed here + in CarManager:
|
-- fixed here + in CarManager:
|
||||||
|
|||||||
Reference in New Issue
Block a user