feat(city): add zebra crosswalks and segmented sidewalk rendering
Renders zebra crosswalks with longitudinal bars and updates sidewalk generation to stop at crossing bands instead of spanning across roadways.
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 280 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 254 KiB |
+154
-10
@@ -104,6 +104,31 @@ const sidewalkTex = canvasTexture(256, (ctx, s) => {
|
|||||||
}
|
}
|
||||||
}, 4, 1);
|
}, 4, 1);
|
||||||
|
|
||||||
|
// Zebra crosswalk: white longitudinal bars parallel to vehicle travel
|
||||||
|
// (MUTCD "longitudinal bar" design), transparent gaps so the asphalt
|
||||||
|
// underneath shows through. Six bars (~0.6 unit each with ~0.6 gaps,
|
||||||
|
// NZTA proportions) across the 8-unit road width. `barsAcrossU` draws
|
||||||
|
// bars along V (for planes spanning roadW × band) — otherwise along U.
|
||||||
|
function zebraTexture(barsAcrossU) {
|
||||||
|
return canvasTexture(512, (ctx, s) => {
|
||||||
|
ctx.clearRect(0, 0, s, s);
|
||||||
|
ctx.fillStyle = '#f5f5f5';
|
||||||
|
const bars = 6, bar = 38, pitch = 76;
|
||||||
|
const start = (s - (bars * bar + (bars - 1) * (pitch - bar))) / 2;
|
||||||
|
for (let i = 0; i < bars; i++) {
|
||||||
|
const p = start + i * pitch;
|
||||||
|
if (barsAcrossU) {
|
||||||
|
ctx.fillRect(p, 0, bar, s);
|
||||||
|
} else {
|
||||||
|
ctx.fillRect(0, p, s, bar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const zebraTexOverZRoad = zebraTexture(true);
|
||||||
|
const zebraTexOverXRoad = zebraTexture(false);
|
||||||
|
|
||||||
function facadePair(baseColor = '#c9a84c', floorCount = 6, windowsPerFloor = 4) {
|
function facadePair(baseColor = '#c9a84c', floorCount = 6, windowsPerFloor = 4) {
|
||||||
const w = 512, h = 512;
|
const w = 512, h = 512;
|
||||||
const c = document.createElement('canvas');
|
const c = document.createElement('canvas');
|
||||||
@@ -183,6 +208,8 @@ const crowdMeshes = [];
|
|||||||
const facadeMaterials = [];
|
const facadeMaterials = [];
|
||||||
const lampHeads = [];
|
const lampHeads = [];
|
||||||
let grid = null;
|
let grid = null;
|
||||||
|
let crosswalkCount = 0;
|
||||||
|
let sidewalkSegmentCount = 0;
|
||||||
let trafficSystem = null;
|
let trafficSystem = null;
|
||||||
let vehicleSystem = null;
|
let vehicleSystem = null;
|
||||||
let pedestrianSystem = null;
|
let pedestrianSystem = null;
|
||||||
@@ -463,6 +490,45 @@ function onRoad(x, z, g) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split the interval [a, b] of a road's travel axis into segments that
|
||||||
|
* exclude ±half bands around every perpendicular crossing line. Sidewalks
|
||||||
|
* and curbs use this so they stop at each crosswalk instead of running
|
||||||
|
* across the roadway like a plain walkway.
|
||||||
|
*
|
||||||
|
* @param {number[]} lines grid lines of the perpendicular roads
|
||||||
|
* @return {Array<[number, number]>}
|
||||||
|
*/
|
||||||
|
function stripSegments(lines, a, b, half) {
|
||||||
|
if (a > b) [a, b] = [b, a];
|
||||||
|
const gaps = [];
|
||||||
|
for (const line of lines) {
|
||||||
|
const g0 = line - half;
|
||||||
|
const g1 = line + half;
|
||||||
|
if (g1 > a && g0 < b) gaps.push([Math.max(g0, a), Math.min(g1, b)]);
|
||||||
|
}
|
||||||
|
gaps.sort((p, q) => p[0] - q[0]);
|
||||||
|
|
||||||
|
const merged = [];
|
||||||
|
for (const gap of gaps) {
|
||||||
|
const last = merged[merged.length - 1];
|
||||||
|
if (last && gap[0] <= last[1] + 1e-6) {
|
||||||
|
last[1] = Math.max(last[1], gap[1]);
|
||||||
|
} else {
|
||||||
|
merged.push([gap[0], gap[1]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const segs = [];
|
||||||
|
let cursor = a;
|
||||||
|
for (const [g0, g1] of merged) {
|
||||||
|
if (g0 - cursor > 0.05) segs.push([cursor, g0]);
|
||||||
|
cursor = Math.max(cursor, g1);
|
||||||
|
}
|
||||||
|
if (b - cursor > 0.05) segs.push([cursor, b]);
|
||||||
|
return segs;
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Traffic light system
|
// Traffic light system
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
@@ -2704,6 +2770,8 @@ function buildCity(data) {
|
|||||||
mayorSystem = null;
|
mayorSystem = null;
|
||||||
meMesh = null;
|
meMesh = null;
|
||||||
spawnApplied = false;
|
spawnApplied = false;
|
||||||
|
crosswalkCount = 0;
|
||||||
|
sidewalkSegmentCount = 0;
|
||||||
plazas = [];
|
plazas = [];
|
||||||
gymEquipment = [];
|
gymEquipment = [];
|
||||||
exercise = { active: false, equipmentId: null, repTimer: 0, animPhase: 0, nearId: null };
|
exercise = { active: false, equipmentId: null, repTimer: 0, animPhase: 0, nearId: null };
|
||||||
@@ -2799,45 +2867,119 @@ function buildCity(data) {
|
|||||||
|
|
||||||
const swWidth = 2;
|
const swWidth = 2;
|
||||||
const offset = w / 2 + swWidth / 2;
|
const offset = w / 2 + swWidth / 2;
|
||||||
|
// Sidewalks stop at each perpendicular roadway: the crossing band
|
||||||
|
// itself is painted as a zebra crosswalk below.
|
||||||
|
const crossingLines = r.axis === 'z' ? grid.zs : grid.xs;
|
||||||
|
const travel1 = r.axis === 'z' ? z1 : x1;
|
||||||
|
const travel2 = r.axis === 'z' ? z2 : x2;
|
||||||
|
const spans = stripSegments(crossingLines, travel1, travel2, grid.roadW / 2);
|
||||||
[-1, 1].forEach(side => {
|
[-1, 1].forEach(side => {
|
||||||
|
for (const [a, b] of spans) {
|
||||||
|
const segLen = b - a;
|
||||||
|
const cmid = (a + b) / 2;
|
||||||
const sg = r.axis === 'z'
|
const sg = r.axis === 'z'
|
||||||
? new THREE.PlaneGeometry(swWidth, len)
|
? new THREE.PlaneGeometry(swWidth, segLen)
|
||||||
: new THREE.PlaneGeometry(len, swWidth);
|
: new THREE.PlaneGeometry(segLen, swWidth);
|
||||||
const sm = new THREE.Mesh(sg, walkMat);
|
const sm = new THREE.Mesh(sg, walkMat);
|
||||||
sm.rotation.x = -Math.PI / 2;
|
sm.rotation.x = -Math.PI / 2;
|
||||||
if (r.axis === 'z') {
|
if (r.axis === 'z') {
|
||||||
sm.position.set(mx + side * offset, 0.12, mz);
|
sm.position.set(mx + side * offset, 0.12, cmid);
|
||||||
} else {
|
} else {
|
||||||
sm.position.set(mx, 0.12, mz + side * offset);
|
sm.position.set(cmid, 0.12, mz + side * offset);
|
||||||
}
|
}
|
||||||
sm.receiveShadow = true;
|
sm.receiveShadow = true;
|
||||||
worldGroup.add(sm);
|
worldGroup.add(sm);
|
||||||
|
sidewalkSegmentCount++;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- Curbs ----
|
// ---- Curbs (interrupted at crosswalks — dropped-curb look) ----
|
||||||
layout.roads.forEach(r => {
|
layout.roads.forEach(r => {
|
||||||
if (r === layoutExtras.bridgeRoad) return;
|
if (r === layoutExtras.bridgeRoad) return;
|
||||||
const [x1,, z1] = r.from;
|
const [x1,, z1] = r.from;
|
||||||
const [x2,, z2] = r.to;
|
const [x2,, z2] = r.to;
|
||||||
const w = r.width;
|
const w = r.width;
|
||||||
const len = Math.hypot(x2 - x1, z2 - z1);
|
|
||||||
const mx = (x1 + x2) / 2, mz = (z1 + z2) / 2;
|
const mx = (x1 + x2) / 2, mz = (z1 + z2) / 2;
|
||||||
const offset = w / 2 + 0.1;
|
const offset = w / 2 + 0.1;
|
||||||
|
const crossingLines = r.axis === 'z' ? grid.zs : grid.xs;
|
||||||
|
const travel1 = r.axis === 'z' ? z1 : x1;
|
||||||
|
const travel2 = r.axis === 'z' ? z2 : x2;
|
||||||
|
const spans = stripSegments(crossingLines, travel1, travel2, grid.roadW / 2);
|
||||||
[-1, 1].forEach(side => {
|
[-1, 1].forEach(side => {
|
||||||
|
for (const [a, b] of spans) {
|
||||||
|
const segLen = b - a;
|
||||||
|
const cmid = (a + b) / 2;
|
||||||
const curbGeo = r.axis === 'z'
|
const curbGeo = r.axis === 'z'
|
||||||
? new THREE.BoxGeometry(0.2, 0.14, len)
|
? new THREE.BoxGeometry(0.2, 0.14, segLen)
|
||||||
: new THREE.BoxGeometry(len, 0.14, 0.2);
|
: new THREE.BoxGeometry(segLen, 0.14, 0.2);
|
||||||
const curb = new THREE.Mesh(curbGeo, new THREE.MeshStandardMaterial({ color: 0x888c94, roughness: 0.9 }));
|
const curb = new THREE.Mesh(curbGeo, new THREE.MeshStandardMaterial({ color: 0x888c94, roughness: 0.9 }));
|
||||||
if (r.axis === 'z') {
|
if (r.axis === 'z') {
|
||||||
curb.position.set(mx + side * offset, 0.07, mz);
|
curb.position.set(mx + side * offset, 0.07, cmid);
|
||||||
} else {
|
} else {
|
||||||
curb.position.set(mx, 0.07, mz + side * offset);
|
curb.position.set(cmid, 0.07, mz + side * offset);
|
||||||
}
|
}
|
||||||
worldGroup.add(curb);
|
worldGroup.add(curb);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- Zebra crosswalks (real markings, not sidewalk-coloured bands) ----
|
||||||
|
//
|
||||||
|
// Four approaches per intersection; each band sits exactly where the
|
||||||
|
// perpendicular sidewalk used to cross the roadway (2 units deep =
|
||||||
|
// the sidewalk width), directly over the asphalt. Bars run parallel
|
||||||
|
// to vehicle travel (MUTCD longitudinal-bar zebra design).
|
||||||
|
const zebraMatOverZ = new THREE.MeshStandardMaterial({
|
||||||
|
map: zebraTexOverZRoad, transparent: true, alphaTest: 0.4, roughness: 0.9,
|
||||||
|
});
|
||||||
|
const zebraMatOverX = new THREE.MeshStandardMaterial({
|
||||||
|
map: zebraTexOverXRoad, transparent: true, alphaTest: 0.4, roughness: 0.9,
|
||||||
|
});
|
||||||
|
const crossHalf = grid.roadW / 2;
|
||||||
|
const crossBand = 2;
|
||||||
|
const eps = 0.01;
|
||||||
|
const crossRoadX = layoutExtras.crossRoad ? layoutExtras.crossRoad.from[0] : null;
|
||||||
|
|
||||||
|
for (const vx of grid.xs) {
|
||||||
|
for (const vz of grid.zs) {
|
||||||
|
// Crossings over the vertical (z) road — south & north bands.
|
||||||
|
const zSpans = [
|
||||||
|
{ side: 1, valid: vz + crossHalf + crossBand <= grid.maxZ + eps || vx === crossRoadX },
|
||||||
|
{ side: -1, valid: vz - crossHalf - crossBand >= grid.minZ - eps },
|
||||||
|
];
|
||||||
|
for (const { side, valid } of zSpans) {
|
||||||
|
if (!valid) continue;
|
||||||
|
const zebra = new THREE.Mesh(
|
||||||
|
new THREE.PlaneGeometry(grid.roadW, crossBand), zebraMatOverZ
|
||||||
|
);
|
||||||
|
zebra.rotation.x = -Math.PI / 2;
|
||||||
|
zebra.position.set(vx, 0.02, vz + side * (crossHalf + crossBand / 2));
|
||||||
|
zebra.receiveShadow = true;
|
||||||
|
worldGroup.add(zebra);
|
||||||
|
crosswalkCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crossings over the horizontal (x) road — east & west bands.
|
||||||
|
// At the bridge corner the west approach rides on the deck.
|
||||||
|
const xSpans = [
|
||||||
|
{ side: 1, valid: vx + crossHalf + crossBand <= grid.maxX + eps },
|
||||||
|
{ side: -1, valid: vx - crossHalf - crossBand >= grid.minX - eps },
|
||||||
|
];
|
||||||
|
for (const { side, valid } of xSpans) {
|
||||||
|
if (!valid) continue;
|
||||||
|
const zebra = new THREE.Mesh(
|
||||||
|
new THREE.PlaneGeometry(crossBand, grid.roadW), zebraMatOverX
|
||||||
|
);
|
||||||
|
zebra.rotation.x = -Math.PI / 2;
|
||||||
|
zebra.position.set(vx + side * (crossHalf + crossBand / 2), 0.02, vz);
|
||||||
|
zebra.receiveShadow = true;
|
||||||
|
worldGroup.add(zebra);
|
||||||
|
crosswalkCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Street lamps (on the sidewalk, never in the roadway) ----
|
// ---- Street lamps (on the sidewalk, never in the roadway) ----
|
||||||
const lampPoleMat = new THREE.MeshStandardMaterial({ color: 0x2a2a2e, roughness: 0.7, metalness: 0.5 });
|
const lampPoleMat = new THREE.MeshStandardMaterial({ color: 0x2a2a2e, roughness: 0.7, metalness: 0.5 });
|
||||||
const lampSwOffset = grid.roadW / 2 + 1; // sidewalk centre = 5 from road centre
|
const lampSwOffset = grid.roadW / 2 + 1; // sidewalk centre = 5 from road centre
|
||||||
@@ -4666,6 +4808,8 @@ window.__city10 = {
|
|||||||
})(),
|
})(),
|
||||||
pedestrians: pedestrianSystem ? pedestrianSystem.count() : 0,
|
pedestrians: pedestrianSystem ? pedestrianSystem.count() : 0,
|
||||||
trafficLights: trafficSystem ? trafficSystem.heads.length / 2 : 0,
|
trafficLights: trafficSystem ? trafficSystem.heads.length / 2 : 0,
|
||||||
|
crosswalks: crosswalkCount,
|
||||||
|
sidewalkSegments: sidewalkSegmentCount,
|
||||||
airplanes: airplaneSystem ? airplaneSystem.count() : 0,
|
airplanes: airplaneSystem ? airplaneSystem.count() : 0,
|
||||||
trains: trainSystem ? 1 : 0,
|
trains: trainSystem ? 1 : 0,
|
||||||
river: !!riverSystem,
|
river: !!riverSystem,
|
||||||
|
|||||||
Reference in New Issue
Block a user