emergency: add nearest-city emergency vehicle routes
This commit is contained in:
1 parent
6553d2fd1d
commit
fec1941e12
2 files changed
+233
-1
No files matched your search
Vendored
+210
-1
@@ -3463,6 +3463,20 @@ var carPtsVboB = null, carPtsCountB = 0;
|
||||
var carProg = null, carAttribs = null, carUniforms = null;
|
||||
var carPtsProg = null, carPtsAttribs = null, carPtsUniforms = null;
|
||||
|
||||
var EMERGENCY_CFG = (window.CONFIG && window.CONFIG.emergencyVehicles) || {};
|
||||
var EMERGENCY_ON = EMERGENCY_CFG.enabled !== false;
|
||||
var EMERGENCY_COUNT = (typeof EMERGENCY_CFG.count === 'number') ? Math.max(1, Math.min(12, EMERGENCY_CFG.count)) : 3;
|
||||
var EMERGENCY_SPEED_KMH = (typeof EMERGENCY_CFG.speedKmh === 'number') ? Math.max(10, Math.min(220, EMERGENCY_CFG.speedKmh)) : 85;
|
||||
var EMERGENCY_MIN_KM = (typeof EMERGENCY_CFG.minKm === 'number') ? Math.max(5, EMERGENCY_CFG.minKm) : 40;
|
||||
var EMERGENCY_MAX_KM = (typeof EMERGENCY_CFG.maxKm === 'number') ? Math.max(EMERGENCY_MIN_KM, EMERGENCY_CFG.maxKm) : 80;
|
||||
var EMERGENCY_RANK_MAX = (typeof EMERGENCY_CFG.rankMax === 'number') ? Math.max(0, Math.min(4, EMERGENCY_CFG.rankMax)) : 1;
|
||||
var EMERGENCY_COLOR = Array.isArray(EMERGENCY_CFG.color) ? EMERGENCY_CFG.color : [0.22, 0.55, 1.0];
|
||||
var EMERGENCY_FLASH_HZ = (typeof EMERGENCY_CFG.flashHz === 'number') ? Math.max(0.2, Math.min(8, EMERGENCY_CFG.flashHz)) : 2.2;
|
||||
var EMERGENCY_NIGHT_ONLY = EMERGENCY_CFG.nightOnly !== false;
|
||||
var EMERGENCY_BRIGHT = (typeof EMERGENCY_CFG.bright === 'number') ? Math.max(0.1, Math.min(4, EMERGENCY_CFG.bright)) : 1.35;
|
||||
var emergencyRoutes = [], emergencyVehicles = [], emergencyVbo = null, emergencyPts = null, emergencyCount = 0;
|
||||
var emergencyLastT = null, emergencyStartCity = null;
|
||||
|
||||
function carLonToX(lon) {
|
||||
var n = 1 << GROUND_TILE_Z;
|
||||
return GROUND_BASE_X - ((lon + 180) / 360) * n;
|
||||
@@ -3490,6 +3504,159 @@ function carYAt(x, z) {
|
||||
return m * ELEV_SCALE_UNITS + CAR_Y;
|
||||
}
|
||||
|
||||
function emergencyChainAt(c, idx) {
|
||||
var k = 0, i, j;
|
||||
for (i = 0; i < idx; i++) k += c.cnts[i] * 3;
|
||||
var n = c.cnts[idx], out = [];
|
||||
for (j = 0; j < n; j++) {
|
||||
out.push({ x: c.pts[k], z: c.pts[k + 1], d: c.pts[k + 2] });
|
||||
k += 3;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function nearestEmergencyCity() {
|
||||
var cx = carLonToX(_baseLon), cz = carLatToZ(_baseLat);
|
||||
var best = { x: cx, z: cz, rank: 0, pop: 0, dist: 0 };
|
||||
if (!cityGeo || cityGeo.length < 4) return best;
|
||||
var found = false;
|
||||
for (var i = 0; i + 3 < cityGeo.length; i += 4) {
|
||||
var rank = cityGeo[i + 2] | 0;
|
||||
if (rank > EMERGENCY_RANK_MAX) continue;
|
||||
var dx = cityGeo[i] - cx, dz = cityGeo[i + 1] - cz;
|
||||
var d = Math.hypot(dx, dz);
|
||||
if (!found || d < best.dist) {
|
||||
found = true;
|
||||
best = { x: cityGeo[i], z: cityGeo[i + 1], rank: rank, pop: cityGeo[i + 3], dist: d };
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function selectEmergencyRoutes() {
|
||||
emergencyRoutes = [];
|
||||
emergencyVehicles = [];
|
||||
emergencyCount = 0;
|
||||
emergencyLastT = null;
|
||||
if (!EMERGENCY_ON || !carGeo || !carGeo.secondary || !carGeo.secondary.cnts || !carGeo.secondary.cnts.length) {
|
||||
window.__emergencyDbg = { enabled: EMERGENCY_ON, startCity: null, routes: 0 };
|
||||
return;
|
||||
}
|
||||
emergencyStartCity = nearestEmergencyCity();
|
||||
var sec = carGeo.secondary;
|
||||
var targetKm = (EMERGENCY_MIN_KM + EMERGENCY_MAX_KM) / 2;
|
||||
var candidates = [];
|
||||
var k = 0;
|
||||
for (var ci = 0; ci < sec.cnts.length; ci++) {
|
||||
var n = sec.cnts[ci];
|
||||
if (n < 2) { k += n * 3; continue; }
|
||||
var firstX = sec.pts[k], firstZ = sec.pts[k + 1], firstD = sec.pts[k + 2];
|
||||
var last = k + (n - 1) * 3;
|
||||
var lastX = sec.pts[last], lastZ = sec.pts[last + 1], lastD = sec.pts[last + 2];
|
||||
var len = Math.max(0, lastD - firstD);
|
||||
var lenKm = len * KM_PER_TILE;
|
||||
var dStart = Math.hypot(firstX - emergencyStartCity.x, firstZ - emergencyStartCity.z);
|
||||
var dEnd = Math.hypot(lastX - emergencyStartCity.x, lastZ - emergencyStartCity.z);
|
||||
var near = Math.min(dStart, dEnd);
|
||||
if (near < 1.2 && lenKm >= EMERGENCY_MIN_KM * 0.55 && lenKm <= EMERGENCY_MAX_KM * 1.45) {
|
||||
candidates.push({
|
||||
ci: ci, len: len, lenKm: lenKm, near: near,
|
||||
score: Math.abs(lenKm - targetKm) + near * 18,
|
||||
reverse: dEnd < dStart
|
||||
});
|
||||
}
|
||||
k += n * 3;
|
||||
}
|
||||
candidates.sort(function (a, b) { return a.score - b.score; });
|
||||
if (!candidates.length) {
|
||||
var best = null, k2 = 0;
|
||||
for (var ci2 = 0; ci2 < sec.cnts.length; ci2++) {
|
||||
var n2 = sec.cnts[ci2];
|
||||
if (n2 < 2) { k2 += n2 * 3; continue; }
|
||||
var f2 = k2, l2 = k2 + (n2 - 1) * 3;
|
||||
var len2 = Math.max(0, sec.pts[l2 + 2] - sec.pts[f2 + 2]);
|
||||
var near2 = Math.min(
|
||||
Math.hypot(sec.pts[f2] - emergencyStartCity.x, sec.pts[f2 + 1] - emergencyStartCity.z),
|
||||
Math.hypot(sec.pts[l2] - emergencyStartCity.x, sec.pts[l2 + 1] - emergencyStartCity.z));
|
||||
if (near2 < 2.5 && (!best || len2 > best.len)) best = { ci: ci2, len: len2, lenKm: len2 * KM_PER_TILE, near: near2, reverse: false };
|
||||
k2 += n2 * 3;
|
||||
}
|
||||
if (best) candidates.push(best);
|
||||
}
|
||||
for (var r = 0; r < Math.min(EMERGENCY_COUNT, candidates.length); r++) {
|
||||
var route = emergencyChainAt(sec, candidates[r].ci);
|
||||
if (candidates[r].reverse) route.reverse();
|
||||
var len = route.length ? route[route.length - 1].d - route[0].d : 0;
|
||||
if (len <= 0) continue;
|
||||
emergencyRoutes.push({ pts: route, len: len, lenKm: len * KM_PER_TILE, nearKm: candidates[r].near * KM_PER_TILE });
|
||||
}
|
||||
for (var v = 0; v < emergencyRoutes.length; v++) {
|
||||
emergencyVehicles.push({
|
||||
route: v,
|
||||
d: emergencyRoutes[v].len * (0.08 + 0.72 * ((v * 0.371) % 1)),
|
||||
dir: ((v % 2) === 0) ? 1 : -1,
|
||||
seed: (v * 0.6180339887) % 1
|
||||
});
|
||||
}
|
||||
emergencyCount = emergencyVehicles.length;
|
||||
window.__emergencyDbg = {
|
||||
enabled: EMERGENCY_ON,
|
||||
startCity: emergencyStartCity ? {
|
||||
x: +emergencyStartCity.x.toFixed(3),
|
||||
z: +emergencyStartCity.z.toFixed(3),
|
||||
rank: emergencyStartCity.rank,
|
||||
pop: emergencyStartCity.pop,
|
||||
km: +(emergencyStartCity.dist * KM_PER_TILE).toFixed(1)
|
||||
} : null,
|
||||
routes: emergencyRoutes.map(function (r) { return { km: +r.lenKm.toFixed(1), nearKm: +r.nearKm.toFixed(1) }; })
|
||||
};
|
||||
}
|
||||
|
||||
function emergencyPointAt(route, dist) {
|
||||
var pts = route.pts;
|
||||
if (!pts.length) return null;
|
||||
dist = Math.max(0, Math.min(route.len, dist));
|
||||
var lo = 0, hi = pts.length - 1;
|
||||
while (lo + 1 < hi) {
|
||||
var mid = (lo + hi) >> 1;
|
||||
if (pts[mid].d < dist) lo = mid; else hi = mid;
|
||||
}
|
||||
var a = pts[lo], b = pts[hi];
|
||||
var seg = Math.max(0.0001, b.d - a.d);
|
||||
var f = Math.max(0, Math.min(1, (dist - a.d) / seg));
|
||||
var x = a.x + (b.x - a.x) * f;
|
||||
var z = a.z + (b.z - a.z) * f;
|
||||
var dx = b.x - a.x, dz = b.z - a.z;
|
||||
var L = Math.hypot(dx, dz) || 1;
|
||||
return { x: x, y: carYAt(x, z), z: z, tx: dx / L, tz: dz / L };
|
||||
}
|
||||
|
||||
function updateEmergencyVehicles(now) {
|
||||
if (!EMERGENCY_ON || !emergencyVehicles.length) { emergencyCount = 0; return 0; }
|
||||
if (emergencyLastT === null) emergencyLastT = now;
|
||||
var dt = Math.max(0, Math.min(5, now - emergencyLastT));
|
||||
emergencyLastT = now;
|
||||
var speed = EMERGENCY_SPEED_KMH / 3600.0 / KM_PER_TILE;
|
||||
var max = emergencyVehicles.length * 10;
|
||||
if (!emergencyPts || emergencyPts.length < max) emergencyPts = new Float32Array(max);
|
||||
var pi = 0;
|
||||
for (var i = 0; i < emergencyVehicles.length; i++) {
|
||||
var v = emergencyVehicles[i], route = emergencyRoutes[v.route];
|
||||
if (!route) continue;
|
||||
v.d += v.dir * speed * dt;
|
||||
if (v.d >= route.len) { v.d = route.len; v.dir = -1; }
|
||||
if (v.d <= 0) { v.d = 0; v.dir = 1; }
|
||||
var p = emergencyPointAt(route, v.d);
|
||||
if (!p) continue;
|
||||
emergencyPts[pi++] = p.x; emergencyPts[pi++] = p.y; emergencyPts[pi++] = p.z;
|
||||
emergencyPts[pi++] = p.tx; emergencyPts[pi++] = p.tz;
|
||||
emergencyPts[pi++] = 1; emergencyPts[pi++] = v.seed; emergencyPts[pi++] = 1;
|
||||
emergencyPts[pi++] = v.seed; emergencyPts[pi++] = route.len;
|
||||
}
|
||||
emergencyCount = pi / 10;
|
||||
return emergencyCount;
|
||||
}
|
||||
|
||||
function resamplePoly(pts) {
|
||||
var out = [];
|
||||
if (pts.length < 2) return out;
|
||||
@@ -3754,6 +3921,7 @@ function fetchSettlements() {
|
||||
function publish() {
|
||||
cityGeo = packPlaces(list);
|
||||
kickCityBuild();
|
||||
selectEmergencyRoutes();
|
||||
// settlement weight feeds the car density field, so a fresh place
|
||||
// list must re-shape the traffic too
|
||||
if (carGeo) kickCarBuild(false);
|
||||
@@ -4005,6 +4173,10 @@ var CARP_FRAG_SRC = [
|
||||
'uniform float uFadeB;', // distance where it is fully faded',
|
||||
'uniform float uWet;', // wet-road factor 0..1',
|
||||
'uniform float uWetGain;', // extra halo on lights when wet',
|
||||
'uniform float uEmergency;',
|
||||
'uniform vec3 uEmergencyColor;',
|
||||
'uniform float uEmergencyFlashHz;',
|
||||
'uniform float uEmergencyNightOnly;',
|
||||
'varying float vApproach;',
|
||||
'varying float vDist;',
|
||||
'varying float vSeed;',
|
||||
@@ -4037,6 +4209,10 @@ var CARP_FRAG_SRC = [
|
||||
'float age = mod(uTime + 3607.0 * hash1(vSeed * 0.913 + 7.7), per);',
|
||||
'float on = smoothstep(0.0, 25.0, age) * (1.0 - smoothstep(life - 25.0, life, age));',
|
||||
'float inten = on * vis;',
|
||||
'if (uEmergency > 0.5) {',
|
||||
'inten = 1.0;',
|
||||
'if (uEmergencyNightOnly > 0.5 && uNight < 0.12) discard;',
|
||||
'}',
|
||||
'if (inten <= 0.001) discard;',
|
||||
'inten = pow(inten, 1.25) * uBright;',
|
||||
// tangent/view dot without a lane sign, sign applied here: toward',
|
||||
@@ -4044,6 +4220,11 @@ var CARP_FRAG_SRC = [
|
||||
'float approach = -vApproach * vDir;',
|
||||
'vec3 col = mix(vec3(1.0, 0.12, 0.08), vec3(1.0, 0.96, 0.88),',
|
||||
'smoothstep(-0.1, 0.1, approach));',
|
||||
'if (uEmergency > 0.5) {',
|
||||
'float flash = 0.35 + 0.65 * pow(max(0.0, sin(uTime * uEmergencyFlashHz * 6.2831853 + vSeed * 6.2831853)), 2.0);',
|
||||
'col = uEmergencyColor * (0.55 + 0.45 * flash);',
|
||||
'inten *= 0.75 + 0.55 * flash;',
|
||||
'}',
|
||||
// Switching on/off individually: this vehicle's own threshold sits
|
||||
// somewhere in the twilight window, so the A-net lights up piece by
|
||||
// piece in the evening and goes dark piece by piece in the morning.
|
||||
@@ -4890,6 +5071,7 @@ function applyCarBuild(gl, res) {
|
||||
lightsMotorway: carPtsCountM, lightsSide: carPtsCountB,
|
||||
trafficSide: CAR_TRAFFIC > 0 ? 'right' : 'left',
|
||||
maxChainKm: Math.round(carMaxChainKm * 10) / 10 };
|
||||
selectEmergencyRoutes();
|
||||
// After deleting a VBO, any still-enabled vertex attrib slot points at a
|
||||
// destroyed buffer -> the sky/cloud full-screen draws warn "no buffer is
|
||||
// bound to enabled attribute". Re-anchor every enabled slot to the
|
||||
@@ -6977,7 +7159,11 @@ window.clouds3dRender = function(target) {
|
||||
lightLo: gl.getUniformLocation(cpp, 'uLightLo'),
|
||||
lightSoft: gl.getUniformLocation(cpp, 'uLightSoft'),
|
||||
wet: gl.getUniformLocation(cpp, 'uWet'),
|
||||
wetGain: gl.getUniformLocation(cpp, 'uWetGain')
|
||||
wetGain: gl.getUniformLocation(cpp, 'uWetGain'),
|
||||
emergency: gl.getUniformLocation(cpp, 'uEmergency'),
|
||||
emergencyColor: gl.getUniformLocation(cpp, 'uEmergencyColor'),
|
||||
emergencyFlashHz: gl.getUniformLocation(cpp, 'uEmergencyFlashHz'),
|
||||
emergencyNightOnly: gl.getUniformLocation(cpp, 'uEmergencyNightOnly')
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7010,6 +7196,10 @@ window.clouds3dRender = function(target) {
|
||||
gl.uniform1f(carPtsUniforms.fadeB, CAR_FADE_B);
|
||||
gl.uniform1f(carPtsUniforms.wet, _roadWet);
|
||||
gl.uniform1f(carPtsUniforms.wetGain, WET_LIGHT_GAIN);
|
||||
gl.uniform1f(carPtsUniforms.emergency, 0.0);
|
||||
gl.uniform3f(carPtsUniforms.emergencyColor, EMERGENCY_COLOR[0], EMERGENCY_COLOR[1], EMERGENCY_COLOR[2]);
|
||||
gl.uniform1f(carPtsUniforms.emergencyFlashHz, EMERGENCY_FLASH_HZ);
|
||||
gl.uniform1f(carPtsUniforms.emergencyNightOnly, EMERGENCY_NIGHT_ONLY ? 1.0 : 0.0);
|
||||
var carPtsGroup = function (vbo, count, dash, speed, maxDens) {
|
||||
if (!vbo || !count) return;
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
|
||||
@@ -7030,6 +7220,25 @@ window.clouds3dRender = function(target) {
|
||||
};
|
||||
carPtsGroup(carPtsVboM, carPtsCountM, CAR_M_DASH, CAR_M_SPEED, CAR_M_DENS);
|
||||
carPtsGroup(carPtsVboB, carPtsCountB, CAR_B_DASH, CAR_B_SPEED, CAR_B_DENS);
|
||||
var emergencyDrawCount = updateEmergencyVehicles(now);
|
||||
if (emergencyDrawCount > 0) {
|
||||
if (!emergencyVbo) emergencyVbo = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, emergencyVbo);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, emergencyPts.subarray(0, emergencyDrawCount * 10), gl.DYNAMIC_DRAW);
|
||||
gl.vertexAttribPointer(carPtsAttribs.pos, 3, gl.FLOAT, false, 40, 0);
|
||||
gl.vertexAttribPointer(carPtsAttribs.meta, 4, gl.FLOAT, false, 40, 12);
|
||||
gl.vertexAttribPointer(carPtsAttribs.dens, 1, gl.FLOAT, false, 40, 28);
|
||||
gl.vertexAttribPointer(carPtsAttribs.seed, 1, gl.FLOAT, false, 40, 32);
|
||||
gl.vertexAttribPointer(carPtsAttribs.len, 1, gl.FLOAT, false, 40, 36);
|
||||
gl.uniform1f(carPtsUniforms.dash, 0.0);
|
||||
gl.uniform1f(carPtsUniforms.speed, 0.0);
|
||||
gl.uniform1f(carPtsUniforms.dens, 1.0);
|
||||
gl.uniform1f(carPtsUniforms.bright, EMERGENCY_BRIGHT);
|
||||
gl.uniform1f(carPtsUniforms.emergency, 1.0);
|
||||
gl.drawArrays(gl.POINTS, 0, emergencyDrawCount);
|
||||
gl.uniform1f(carPtsUniforms.emergency, 0.0);
|
||||
gl.uniform1f(carPtsUniforms.bright, CAR_BRIGHT);
|
||||
}
|
||||
}
|
||||
}
|
||||
gl.disable(gl.BLEND);
|
||||
|
||||
Reference in new issue
Block a user