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:
+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) ------------
|
||||
local wheelParent: Part = hub
|
||||
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({
|
||||
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,
|
||||
CanCollide = false,
|
||||
Parent = model,
|
||||
})
|
||||
knuckle.Transparency = 1
|
||||
local hubSteerAtt = makeAttachment(hub, "SteerAtt", 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
|
||||
|
||||
-- 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({
|
||||
Name = "Wheel_" .. cornerName,
|
||||
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
|
||||
-- means its torso now overlaps the slab, which is why the driver goes in its
|
||||
-- own collision group (see DRIVER_GROUP).
|
||||
seat.CFrame = origin
|
||||
* CFrame.new(0, Chassis.GROUND_OFFSET + Chassis.SEAT_ABOVE_GROUND + seat.Size.Y / 2, 0.2)
|
||||
seat.CFrame = origin * CFrame.new(0, Chassis.GROUND_OFFSET + Chassis.SEAT_ABOVE_GROUND + seat.Size.Y / 2, 0.2)
|
||||
seat.Parent = model
|
||||
local seatWeld = Instance.new("WeldConstraint")
|
||||
seatWeld.Part0 = body
|
||||
@@ -333,12 +348,24 @@ local function buildCarModel(car: Car, tuning: Tuning): Model
|
||||
if p:IsA("BasePart") then
|
||||
found = true
|
||||
local cf, s = p.CFrame, p.Size
|
||||
local hx = 0.5 * (math.abs(cf.RightVector.X * s.X)
|
||||
+ 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.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 hx = 0.5
|
||||
* (
|
||||
math.abs(cf.RightVector.X * s.X)
|
||||
+ 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.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
|
||||
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))
|
||||
@@ -372,8 +399,66 @@ local function buildCarModel(car: Car, tuning: Tuning): Model
|
||||
if lo2 then
|
||||
bottomOffset = lo2.Y - shell:GetPivot().Position.Y
|
||||
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(
|
||||
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
|
||||
-- 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
|
||||
bestVol = vol
|
||||
local cf, sz = p.CFrame, p.Size
|
||||
roofY = p.Position.Y + 0.5 * (math.abs(cf.RightVector.Y * sz.X)
|
||||
+ math.abs(cf.UpVector.Y * sz.Y)
|
||||
+ math.abs(cf.LookVector.Y * sz.Z))
|
||||
roofY = p.Position.Y
|
||||
+ 0.5
|
||||
* (math.abs(cf.RightVector.Y * sz.X) + math.abs(cf.UpVector.Y * sz.Y) + math.abs(
|
||||
cf.LookVector.Y * sz.Z
|
||||
))
|
||||
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
|
||||
-- car quietly loses that much of its intended clearance -- which
|
||||
-- is why hyper_x measured 0.07 instead of the requested 0.30.
|
||||
local wanted = roofLocal - Chassis.HEAD_ABOVE_SEAT
|
||||
- Chassis.ROOF_CLEARANCE - seatPart.Size.Y / 2
|
||||
local wanted = roofLocal - Chassis.HEAD_ABOVE_SEAT - Chassis.ROOF_CLEARANCE - seatPart.Size.Y / 2
|
||||
-- Allow sinking to the cabin floor: a low roof must win over the
|
||||
-- nominal seat height, otherwise the clamp silently discards the
|
||||
-- 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
|
||||
-- -- hyper_x (lowest roof) needs that headroom.
|
||||
local floor = Chassis.GROUND_OFFSET - SHELL_DROP + 0.1
|
||||
local seatLocal = math.clamp(
|
||||
wanted,
|
||||
floor,
|
||||
Chassis.GROUND_OFFSET + Chassis.SEAT_ABOVE_GROUND
|
||||
)
|
||||
seatPart.CFrame = body.CFrame
|
||||
* CFrame.new(0, seatLocal + seatPart.Size.Y / 2, 0.2)
|
||||
local seatLocal = math.clamp(wanted, 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
|
||||
-- was created, so moving the seat afterwards would be snapped
|
||||
-- straight back. Rebuild the weld at the new offset.
|
||||
for _, w in ipairs(body:GetChildren()) do
|
||||
if w:IsA("WeldConstraint")
|
||||
and (w.Part1 == seatPart or w.Part0 == seatPart) then
|
||||
if w:IsA("WeldConstraint") and (w.Part1 == seatPart or w.Part0 == seatPart) then
|
||||
w:Destroy()
|
||||
end
|
||||
end
|
||||
@@ -461,6 +541,93 @@ local function buildCarModel(car: Car, tuning: Tuning): Model
|
||||
shell.Parent = model
|
||||
-- Hide the placeholder blocks the mesh replaces.
|
||||
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")
|
||||
if cabin then
|
||||
cabin:Destroy()
|
||||
|
||||
Reference in New Issue
Block a user