File size: 11,089 Bytes
a2b07e0 585bfe0 a2b07e0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | // Atmosphere: weather (clear, rain with wet shiny roads, thunderstorms, fog, snow), clouds, a sun lens flare,
// skid marks when you slide or brake hard, and fireworks over Central Park at night.
import * as THREE from "three";
import { Lensflare, LensflareElement } from "three/addons/objects/Lensflare.js";
const clamp = (x, a = 0, b = 1) => Math.min(b, Math.max(a, x));
export const WEATHERS = ["clear", "rain", "storm", "fog", "snow"];
export function createAtmos(W, sound, COARSE) {
const { scene, camera } = W;
const tex = (draw, w = 128, h = 128) => { const c = document.createElement("canvas"); c.width = w; c.height = h; draw(c.getContext("2d"), w, h); const t = new THREE.CanvasTexture(c); t.colorSpace = THREE.SRGBColorSpace; return t; };
const radial = (stops) => tex((g, w) => { const r = g.createRadialGradient(w / 2, w / 2, 0, w / 2, w / 2, w / 2); stops.forEach(([o, c]) => r.addColorStop(o, c)); g.fillStyle = r; g.fillRect(0, 0, w, w); });
// ---- rain and snow: a box of particles that rides with the camera ----
const RN = COARSE ? 1400 : 4000, BOX = 70, H = 40;
const rainPos = new Float32Array(RN * 6), rainGeo = new THREE.BufferGeometry(); rainGeo.setAttribute("position", new THREE.BufferAttribute(rainPos, 3));
const drops = Array.from({ length: RN }, () => [Math.random() * BOX - BOX / 2, Math.random() * H, Math.random() * BOX - BOX / 2, 18 + Math.random() * 8]);
const rain = new THREE.LineSegments(rainGeo, new THREE.LineBasicMaterial({ color: 0xaecbe8, transparent: true, opacity: .38, depthWrite: false })); rain.frustumCulled = false; rain.visible = false; scene.add(rain);
const SN = COARSE ? 1200 : 3000, snowPos = new Float32Array(SN * 3), snowGeo = new THREE.BufferGeometry(); snowGeo.setAttribute("position", new THREE.BufferAttribute(snowPos, 3));
const flakes = Array.from({ length: SN }, () => [Math.random() * BOX - BOX / 2, Math.random() * H, Math.random() * BOX - BOX / 2, Math.random() * 6.28]);
const snow = new THREE.Points(snowGeo, new THREE.PointsMaterial({ color: 0xffffff, size: .18, map: radial([[0, "rgba(255,255,255,1)"], [1, "rgba(255,255,255,0)"]]), transparent: true, depthWrite: false })); snow.frustumCulled = false; snow.visible = false; scene.add(snow);
// ---- clouds: a big soft layer high up, drifting ----
const cloudTex = tex((g, w, h) => {
const img = g.createImageData(w, h), N = (x, y, s) => { const xi = Math.floor(x / s), yi = Math.floor(y / s), f = (a, b) => { const v = Math.sin((a % 64) * 127.1 + (b % 64) * 311.7) * 43758.5; return v - Math.floor(v); }; const fx = x / s - xi, fy = y / s - yi, u = fx * fx * (3 - 2 * fx), v = fy * fy * (3 - 2 * fy); return (f(xi, yi) * (1 - u) + f(xi + 1, yi) * u) * (1 - v) + (f(xi, yi + 1) * (1 - u) + f(xi + 1, yi + 1) * u) * v; };
for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) { let n = 0, a = .5; for (const s of [64, 32, 16, 8]) { n += N(x, y, s) * a; a *= .5; } const k = (y * w + x) * 4, c = clamp((n - .45) * 2.2); img.data[k] = img.data[k + 1] = img.data[k + 2] = 255; img.data[k + 3] = c * 220; }
g.putImageData(img, 0, 0);
}, 256, 256);
cloudTex.wrapS = cloudTex.wrapT = THREE.RepeatWrapping; cloudTex.repeat.set(5, 5);
const clouds = new THREE.Mesh(new THREE.PlaneGeometry(9000, 9000).rotateX(Math.PI / 2), new THREE.MeshBasicMaterial({ map: cloudTex, transparent: true, depthWrite: false, fog: false, opacity: .85, side: THREE.DoubleSide }));
clouds.frustumCulled = false; clouds.renderOrder = -1; scene.add(clouds);
// ---- sun lens flare ----
const flareLight = new THREE.Object3D(); scene.add(flareLight); // not a light: hiding a real light changes the light count and rebuilds every material
const lf = new Lensflare();
lf.addElement(new LensflareElement(radial([[0, "rgba(255,250,235,1)"], [.2, "rgba(255,230,180,.6)"], [1, "rgba(255,200,120,0)"]]), 380, 0));
const ring = radial([[0, "rgba(255,255,255,0)"], [.6, "rgba(255,255,255,0)"], [.7, "rgba(180,220,255,.35)"], [1, "rgba(255,255,255,0)"]]), dot = radial([[0, "rgba(255,220,180,.5)"], [1, "rgba(255,220,180,0)"]]);
for (const [s, d, t] of [[60, .4, dot], [90, .6, ring], [40, .75, dot], [140, .95, ring]]) lf.addElement(new LensflareElement(t, s, d));
flareLight.add(lf);
// ---- skid marks: a ring buffer of dark quads laid where the rear tyres slide ----
const SK = 1600, skPos = new Float32Array(SK * 18), skGeo = new THREE.BufferGeometry(); skGeo.setAttribute("position", new THREE.BufferAttribute(skPos, 3));
const skids = new THREE.Mesh(skGeo, new THREE.MeshBasicMaterial({ color: 0x0a0a0a, transparent: true, opacity: .55, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -4 })); skids.frustumCulled = false; scene.add(skids);
let skI = 0; const lastT = [null, null];
function lay(k, x, z, px, pz) {
const dx = x - px, dz = z - pz, L = Math.hypot(dx, dz); if (L < .05 || L > 3) return;
const nx = -dz / L * .14, nz = dx / L * .14, y = .045, o = (skI++ % SK) * 18;
skPos.set([px + nx, y, pz + nz, px - nx, y, pz - nz, x - nx, y, z - nz, px + nx, y, pz + nz, x - nx, y, z - nz, x + nx, y, z + nz], o);
skGeo.attributes.position.needsUpdate = true; skGeo.setDrawRange(0, Math.min(skI, SK) * 6);
}
// ---- fireworks over the park at night ----
const FW = 600, fwPos = new Float32Array(FW * 3), fwCol = new Float32Array(FW * 3), fwGeo = new THREE.BufferGeometry();
fwGeo.setAttribute("position", new THREE.BufferAttribute(fwPos, 3)); fwGeo.setAttribute("color", new THREE.BufferAttribute(fwCol, 3));
const fw = new THREE.Points(fwGeo, new THREE.PointsMaterial({ size: 2.4, vertexColors: true, map: radial([[0, "rgba(255,255,255,1)"], [1, "rgba(255,255,255,0)"]]), transparent: true, depthWrite: false, blending: THREE.AdditiveBlending, fog: false, toneMapped: false }));
fw.frustumCulled = false; scene.add(fw);
const sparks = Array.from({ length: FW }, () => ({ life: 0, p: new THREE.Vector3(), v: new THREE.Vector3(), c: new THREE.Color() })); let fwNext = 4, fwI = 0;
function burst() {
const L = W.city.lawn, at = new THREE.Vector3(L.x + (Math.random() - .5) * 200, 120 + Math.random() * 80, L.z + (Math.random() - .5) * 200), col = new THREE.Color().setHSL(Math.random(), .9, .6);
for (let k = 0; k < 120; k++) { const s = sparks[fwI++ % FW], th = Math.random() * 6.28, ph = Math.acos(2 * Math.random() - 1), sp = 18 + Math.random() * 10; s.life = 1.6 + Math.random() * .6; s.p.copy(at); s.v.set(Math.sin(ph) * Math.cos(th) * sp, Math.cos(ph) * sp, Math.sin(ph) * Math.sin(th) * sp); s.c.copy(col).multiplyScalar(2.5); }
if (sound.on && sound.ctx) { const d = camera.position.distanceTo(at); if (d < 900) sound.pop(); }
}
let weather = "clear", flash = 0, nextBolt = 6;
const asphalt = W.city.asphaltMat, asphalt0 = asphalt ? { r: asphalt.roughness, c: asphalt.color.clone(), e: asphalt.envMapIntensity } : null;
function setWeather(w) {
weather = w;
const wet = w === "rain" || w === "storm";
rain.visible = wet; snow.visible = w === "snow";
if (asphalt) { asphalt.roughness = wet ? .18 : w === "snow" ? .95 : asphalt0.r; asphalt.color.copy(asphalt0.c).multiplyScalar(wet ? .55 : w === "snow" ? 1.9 : 1); asphalt.envMapIntensity = wet ? 1.6 : asphalt0.e; }
W.weatherFog = w === "fog" ? .3 : w === "storm" ? .55 : wet ? .7 : w === "snow" ? .6 : 1;
W.weatherFog = w === "fog" ? .3 : w === "storm" ? .55 : wet ? .7 : w === "snow" ? .6 : 1;
W.setWeatherLook(w === "storm" ? .55 : wet ? .72 : w === "fog" ? .85 : w === "snow" ? .9 : 1, w !== "clear");
}
return {
get weather() { return weather; }, setWeather,
update(dt, time, S, g) {
const cam = camera.position, tod = W.timeOfDay;
// rain / snow follow the camera
if (rain.visible) {
const wx = 3, wz = 1.5;
for (let i = 0; i < RN; i++) { const d = drops[i]; d[1] -= d[3] * dt; if (d[1] < 0) { d[1] += H; d[0] = Math.random() * BOX - BOX / 2; d[2] = Math.random() * BOX - BOX / 2; } const x = cam.x + d[0], y = cam.y - 10 + d[1], z = cam.z + d[2], o = i * 6; rainPos[o] = x; rainPos[o + 1] = y; rainPos[o + 2] = z; rainPos[o + 3] = x + wx * .05; rainPos[o + 4] = y + .9; rainPos[o + 5] = z + wz * .05; }
rainGeo.attributes.position.needsUpdate = true;
}
if (snow.visible) {
for (let i = 0; i < SN; i++) { const f = flakes[i]; f[1] -= 1.6 * dt; f[3] += dt; if (f[1] < 0) f[1] += H; snowPos[i * 3] = cam.x + f[0] + Math.sin(f[3]) * .6; snowPos[i * 3 + 1] = cam.y - 10 + f[1]; snowPos[i * 3 + 2] = cam.z + f[2] + Math.cos(f[3] * .7) * .6; }
snowGeo.attributes.position.needsUpdate = true;
}
// lightning in a storm
if (weather === "storm" && (nextBolt -= dt) <= 0) { nextBolt = 5 + Math.random() * 10; flash = 1; if (sound.on) setTimeout(() => sound.crash(), 300 + Math.random() * 1200); }
flash = Math.max(0, flash - dt * 3); W.setFlash(flash * (Math.random() < .5 ? 1 : .4));
// clouds: day and sunset (grey and lower in rain)
clouds.visible = tod !== "night" || weather !== "clear";
clouds.position.set(cam.x, weather === "clear" ? 520 : 260, cam.z);
cloudTex.offset.set((time * .002 + cam.x / 9000 * 5) % 1, (time * .001 + cam.z / 9000 * 5) % 1);
clouds.material.color.set(weather !== "clear" ? 0x6a7280 : tod === "sunset" ? 0xffc8a0 : 0xffffff); clouds.material.opacity = weather === "storm" ? 1 : weather !== "clear" ? .95 : tod === "night" ? .25 : .8;
// the lens flare sits on the sun, only on a clear day or sunset
flareLight.visible = tod !== "night" && weather === "clear";
if (flareLight.visible && W.sunDir) flareLight.position.copy(cam).addScaledVector(W.sunDir, 800);
// skid marks: sliding sideways or braking hard on the road
if (g && g.mode === "drive" && (g.veh || "car") === "car" && g.y < .2) {
const fx = Math.cos(g.yaw), fz = Math.sin(g.yaw), u = g.vel.x * fx + g.vel.z * fz, w = -g.vel.x * fz + g.vel.z * fx;
const sliding = Math.abs(w) > 3.5 || (g.lastU != null && g.lastU - u > 14 * dt && u > 6);
g.lastU = u;
for (const k of [0, 1]) {
const side = k ? .8 : -.8, x = g.x - fx * 1.4 - fz * side, z = g.z - fz * 1.4 + fx * side;
if (sliding && lastT[k]) lay(k, x, z, lastT[k][0], lastT[k][1]);
lastT[k] = sliding ? [x, z] : null;
}
} else lastT[0] = lastT[1] = null;
// fireworks at night
if (tod === "night" && weather !== "storm" && (fwNext -= dt) <= 0) { fwNext = 5 + Math.random() * 9; burst(); }
for (let i = 0; i < FW; i++) {
const s = sparks[i];
if (s.life > 0) { s.life -= dt; s.v.y -= 6 * dt; s.v.multiplyScalar(Math.pow(.6, dt)); s.p.addScaledVector(s.v, dt); const f = clamp(s.life); fwPos.set([s.p.x, s.p.y, s.p.z], i * 3); fwCol.set([s.c.r * f, s.c.g * f, s.c.b * f], i * 3); }
else fwPos[i * 3 + 1] = -999;
}
fwGeo.attributes.position.needsUpdate = fwGeo.attributes.color.needsUpdate = true;
},
};
}
|