chore: establish formatting baseline and verification scripts
This commit is contained in:
+196
-129
@@ -2,146 +2,213 @@ import * as THREE from 'three';
|
||||
|
||||
// Low-poly saloon facing local +X, sized like a Peugeot/Samand (4.4 m long).
|
||||
export function createCarModel(color = '#d9d9d4') {
|
||||
const group = new THREE.Group();
|
||||
const mat = (c, extra = {}) => new THREE.MeshStandardMaterial({color: c, roughness: .45, ...extra});
|
||||
const paint = mat(color, {metalness: .55, roughness: .35}), glass = mat('#1e2c33', {metalness: .6, roughness: .15});
|
||||
const dark = mat('#141618'), chrome = mat('#c8ccce', {metalness: .9, roughness: .2});
|
||||
const box = (w, h, d, m, x, y, z) => {
|
||||
const mesh = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), m);
|
||||
mesh.position.set(x, y, z);
|
||||
mesh.castShadow = mesh.receiveShadow = true;
|
||||
group.add(mesh);
|
||||
return mesh;
|
||||
};
|
||||
box(4.4, .62, 1.78, paint, 0, .68, 0);
|
||||
box(4.42, .16, 1.8, dark, 0, .38, 0);
|
||||
const cabin = box(2.3, .62, 1.6, glass, -.25, 1.28, 0);
|
||||
cabin.scale.set(1, 1, .98);
|
||||
box(2.1, .08, 1.62, paint, -.25, 1.62, 0);
|
||||
box(.9, .06, 1.7, paint, 1.45, 1.0, 0).rotation.z = -.12;
|
||||
for (const z of [-.62, .62]) {
|
||||
box(.06, .18, .34, mat('#fff4cc', {emissive: '#fff1b0', emissiveIntensity: .6}), 2.21, .78, z);
|
||||
box(.06, .16, .34, mat('#b3261e', {emissive: '#7a0f0b', emissiveIntensity: .5}), -2.21, .8, z);
|
||||
box(.18, .08, .1, chrome, .6, 1.02, z * 1.47);
|
||||
const group = new THREE.Group();
|
||||
const mat = (c, extra = {}) =>
|
||||
new THREE.MeshStandardMaterial({ color: c, roughness: 0.45, ...extra });
|
||||
const paint = mat(color, { metalness: 0.55, roughness: 0.35 }),
|
||||
glass = mat('#1e2c33', { metalness: 0.6, roughness: 0.15 });
|
||||
const dark = mat('#141618'),
|
||||
chrome = mat('#c8ccce', { metalness: 0.9, roughness: 0.2 });
|
||||
const box = (w, h, d, m, x, y, z) => {
|
||||
const mesh = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), m);
|
||||
mesh.position.set(x, y, z);
|
||||
mesh.castShadow = mesh.receiveShadow = true;
|
||||
group.add(mesh);
|
||||
return mesh;
|
||||
};
|
||||
box(4.4, 0.62, 1.78, paint, 0, 0.68, 0);
|
||||
box(4.42, 0.16, 1.8, dark, 0, 0.38, 0);
|
||||
const cabin = box(2.3, 0.62, 1.6, glass, -0.25, 1.28, 0);
|
||||
cabin.scale.set(1, 1, 0.98);
|
||||
box(2.1, 0.08, 1.62, paint, -0.25, 1.62, 0);
|
||||
box(0.9, 0.06, 1.7, paint, 1.45, 1.0, 0).rotation.z = -0.12;
|
||||
for (const z of [-0.62, 0.62]) {
|
||||
box(
|
||||
0.06,
|
||||
0.18,
|
||||
0.34,
|
||||
mat('#fff4cc', { emissive: '#fff1b0', emissiveIntensity: 0.6 }),
|
||||
2.21,
|
||||
0.78,
|
||||
z,
|
||||
);
|
||||
box(
|
||||
0.06,
|
||||
0.16,
|
||||
0.34,
|
||||
mat('#b3261e', { emissive: '#7a0f0b', emissiveIntensity: 0.5 }),
|
||||
-2.21,
|
||||
0.8,
|
||||
z,
|
||||
);
|
||||
box(0.18, 0.08, 0.1, chrome, 0.6, 1.02, z * 1.47);
|
||||
}
|
||||
box(0.04, 0.12, 1.0, chrome, 2.22, 0.6, 0);
|
||||
const wheels = [];
|
||||
for (const x of [-1.38, 1.38])
|
||||
for (const z of [-0.86, 0.86]) {
|
||||
const wheel = new THREE.Group();
|
||||
wheel.position.set(x, 0.36, z);
|
||||
const tyre = new THREE.Mesh(new THREE.CylinderGeometry(0.36, 0.36, 0.26, 18), dark);
|
||||
tyre.rotation.x = Math.PI / 2;
|
||||
tyre.castShadow = true;
|
||||
const hub = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.2, 0.27, 10), chrome);
|
||||
hub.rotation.x = Math.PI / 2;
|
||||
wheel.add(tyre, hub);
|
||||
group.add(wheel);
|
||||
wheels.push(wheel);
|
||||
}
|
||||
box(.04, .12, 1.0, chrome, 2.22, .6, 0);
|
||||
const wheels = [];
|
||||
for (const x of [-1.38, 1.38]) for (const z of [-.86, .86]) {
|
||||
const wheel = new THREE.Group();
|
||||
wheel.position.set(x, .36, z);
|
||||
const tyre = new THREE.Mesh(new THREE.CylinderGeometry(.36, .36, .26, 18), dark);
|
||||
tyre.rotation.x = Math.PI / 2;
|
||||
tyre.castShadow = true;
|
||||
const hub = new THREE.Mesh(new THREE.CylinderGeometry(.2, .2, .27, 10), chrome);
|
||||
hub.rotation.x = Math.PI / 2;
|
||||
wheel.add(tyre, hub);
|
||||
group.add(wheel);
|
||||
wheels.push(wheel);
|
||||
}
|
||||
return {group, wheels};
|
||||
return { group, wheels };
|
||||
}
|
||||
|
||||
const COLORS = ['#e9e9e4', '#1d1f22', '#9aa3a8', '#d6c9a5', '#7e1f1d', '#2f4a6b', '#f2c94c', '#efefea'];
|
||||
const COLORS = [
|
||||
'#e9e9e4',
|
||||
'#1d1f22',
|
||||
'#9aa3a8',
|
||||
'#d6c9a5',
|
||||
'#7e1f1d',
|
||||
'#2f4a6b',
|
||||
'#f2c94c',
|
||||
'#efefea',
|
||||
];
|
||||
|
||||
// Ambient cars on the main roads. They stay within a few hundred metres of the player: a car that
|
||||
// falls too far behind is moved to a road ahead or around them. Cars stop for anything in their lane.
|
||||
const WIDTH = [11, 10, 9, 8]; // trunk … tertiary, matching the road strips in city.js
|
||||
|
||||
export function createTraffic(scene, data, count = 28) {
|
||||
const oneway = new Set(data.oneway ?? []);
|
||||
const roads = data.roads.flatMap((r, index) => {
|
||||
if (r[0] > 3) return [];
|
||||
const pts = [];
|
||||
for (let i = 2; i < r.length; i += 2) pts.push(new THREE.Vector2(r[i], r[i + 1]));
|
||||
const lengths = [0];
|
||||
for (let i = 1; i < pts.length; i++) lengths.push(lengths[i - 1] + pts[i].distanceTo(pts[i - 1]));
|
||||
const xs = pts.map(p => p.x), zs = pts.map(p => p.y);
|
||||
return [{pts, lengths, total: lengths.at(-1), width: WIDTH[r[0]], oneway: oneway.has(index),
|
||||
box: {x0: Math.min(...xs), x1: Math.max(...xs), z0: Math.min(...zs), z1: Math.max(...zs)}}];
|
||||
}).filter(r => r.total > 80);
|
||||
const sample = (road, d) => {
|
||||
let i = 1;
|
||||
while (i < road.lengths.length - 1 && road.lengths[i] < d) i++;
|
||||
const a = road.pts[i - 1], b = road.pts[i], t = (d - road.lengths[i - 1]) / (road.lengths[i] - road.lengths[i - 1] || 1);
|
||||
return {x: a.x + (b.x - a.x) * t, z: a.y + (b.y - a.y) * t, dx: b.x - a.x, dz: b.y - a.y};
|
||||
};
|
||||
let seed = 7;
|
||||
const random = () => (seed = (seed * 1664525 + 1013904223) >>> 0) / 4294967296;
|
||||
// Roads whose bounding box comes within 600 m of the player, refreshed as they travel.
|
||||
let nearby = [], nearbyAt = null;
|
||||
const roadsNear = (x, z) => {
|
||||
if (!nearbyAt || Math.hypot(x - nearbyAt.x, z - nearbyAt.z) > 150) {
|
||||
nearbyAt = {x, z};
|
||||
nearby = roads.filter(({box}) => x > box.x0 - 600 && x < box.x1 + 600 && z > box.z0 - 600 && z < box.z1 + 600);
|
||||
}
|
||||
return nearby;
|
||||
};
|
||||
// Places a car on a random main-road point between 120 and 600 m from (x, z), if one exists.
|
||||
function respawn(car, x, z) {
|
||||
const candidates = roadsNear(x, z);
|
||||
for (let attempt = 0; attempt < 30 && candidates.length; attempt++) {
|
||||
const road = candidates[Math.floor(random() * candidates.length)], d = random() * road.total, p = sample(road, d);
|
||||
const dist = Math.hypot(p.x - x, p.z - z);
|
||||
if (dist < 120 || dist > 600) continue;
|
||||
// One-way carriageways: with the flow, in either of two lanes. Two-way roads: right-hand half.
|
||||
const lane = road.oneway ? (random() < .5 ? -1 : 1) * road.width / 4 : road.width / 4;
|
||||
Object.assign(car, {road, d, dir: road.oneway || random() < .5 ? 1 : -1, lane, speed: 9 + random() * 7});
|
||||
car.model.group.visible = true;
|
||||
return true;
|
||||
}
|
||||
car.model.group.visible = false;
|
||||
return false;
|
||||
const oneway = new Set(data.oneway ?? []);
|
||||
const roads = data.roads
|
||||
.flatMap((r, index) => {
|
||||
if (r[0] > 3) return [];
|
||||
const pts = [];
|
||||
for (let i = 2; i < r.length; i += 2) pts.push(new THREE.Vector2(r[i], r[i + 1]));
|
||||
const lengths = [0];
|
||||
for (let i = 1; i < pts.length; i++)
|
||||
lengths.push(lengths[i - 1] + pts[i].distanceTo(pts[i - 1]));
|
||||
const xs = pts.map((p) => p.x),
|
||||
zs = pts.map((p) => p.y);
|
||||
return [
|
||||
{
|
||||
pts,
|
||||
lengths,
|
||||
total: lengths.at(-1),
|
||||
width: WIDTH[r[0]],
|
||||
oneway: oneway.has(index),
|
||||
box: {
|
||||
x0: Math.min(...xs),
|
||||
x1: Math.max(...xs),
|
||||
z0: Math.min(...zs),
|
||||
z1: Math.max(...zs),
|
||||
},
|
||||
},
|
||||
];
|
||||
})
|
||||
.filter((r) => r.total > 80);
|
||||
const sample = (road, d) => {
|
||||
let i = 1;
|
||||
while (i < road.lengths.length - 1 && road.lengths[i] < d) i++;
|
||||
const a = road.pts[i - 1],
|
||||
b = road.pts[i],
|
||||
t = (d - road.lengths[i - 1]) / (road.lengths[i] - road.lengths[i - 1] || 1);
|
||||
return { x: a.x + (b.x - a.x) * t, z: a.y + (b.y - a.y) * t, dx: b.x - a.x, dz: b.y - a.y };
|
||||
};
|
||||
let seed = 7;
|
||||
const random = () => (seed = (seed * 1664525 + 1013904223) >>> 0) / 4294967296;
|
||||
// Roads whose bounding box comes within 600 m of the player, refreshed as they travel.
|
||||
let nearby = [],
|
||||
nearbyAt = null;
|
||||
const roadsNear = (x, z) => {
|
||||
if (!nearbyAt || Math.hypot(x - nearbyAt.x, z - nearbyAt.z) > 150) {
|
||||
nearbyAt = { x, z };
|
||||
nearby = roads.filter(
|
||||
({ box }) => x > box.x0 - 600 && x < box.x1 + 600 && z > box.z0 - 600 && z < box.z1 + 600,
|
||||
);
|
||||
}
|
||||
// All traffic sits in one group so it can be hidden as a whole (camera AR view).
|
||||
const group = new THREE.Group(), cars = [];
|
||||
scene.add(group);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const model = createCarModel(COLORS[i % COLORS.length]);
|
||||
group.add(model.group);
|
||||
model.group.visible = false;
|
||||
cars.push({model, road: null, d: 0, dir: 1, lane: 2.2, speed: 10, x: 1e9, z: 1e9});
|
||||
}
|
||||
// Takes a car off the road; it respawns out of sight on a later frame.
|
||||
function remove(car) {
|
||||
car.road = null;
|
||||
car.x = car.z = 1e9;
|
||||
car.model.group.visible = false;
|
||||
return nearby;
|
||||
};
|
||||
// Places a car on a random main-road point between 120 and 600 m from (x, z), if one exists.
|
||||
function respawn(car, x, z) {
|
||||
const candidates = roadsNear(x, z);
|
||||
for (let attempt = 0; attempt < 30 && candidates.length; attempt++) {
|
||||
const road = candidates[Math.floor(random() * candidates.length)],
|
||||
d = random() * road.total,
|
||||
p = sample(road, d);
|
||||
const dist = Math.hypot(p.x - x, p.z - z);
|
||||
if (dist < 120 || dist > 600) continue;
|
||||
// One-way carriageways: with the flow, in either of two lanes. Two-way roads: right-hand half.
|
||||
const lane = road.oneway ? ((random() < 0.5 ? -1 : 1) * road.width) / 4 : road.width / 4;
|
||||
Object.assign(car, {
|
||||
road,
|
||||
d,
|
||||
dir: road.oneway || random() < 0.5 ? 1 : -1,
|
||||
lane,
|
||||
speed: 9 + random() * 7,
|
||||
});
|
||||
car.model.group.visible = true;
|
||||
return true;
|
||||
}
|
||||
car.model.group.visible = false;
|
||||
return false;
|
||||
}
|
||||
// All traffic sits in one group so it can be hidden as a whole (camera AR view).
|
||||
const group = new THREE.Group(),
|
||||
cars = [];
|
||||
scene.add(group);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const model = createCarModel(COLORS[i % COLORS.length]);
|
||||
group.add(model.group);
|
||||
model.group.visible = false;
|
||||
cars.push({ model, road: null, d: 0, dir: 1, lane: 2.2, speed: 10, x: 1e9, z: 1e9 });
|
||||
}
|
||||
// Takes a car off the road; it respawns out of sight on a later frame.
|
||||
function remove(car) {
|
||||
car.road = null;
|
||||
car.x = car.z = 1e9;
|
||||
car.model.group.visible = false;
|
||||
}
|
||||
|
||||
let cursor = 0;
|
||||
function update(dt, obstacles, around) {
|
||||
// Re-home a couple of distant cars per frame, which keeps the cost per frame small.
|
||||
for (let n = 0; n < 2; n++) {
|
||||
const car = cars[cursor = (cursor + 1) % cars.length];
|
||||
if (!car.road || Math.hypot(car.x - around.x, car.z - around.z) > 800) respawn(car, around.x, around.z);
|
||||
}
|
||||
for (const car of cars) {
|
||||
if (!car.road || !car.model.group.visible) continue;
|
||||
const here = sample(car.road, car.d), len = Math.hypot(here.dx, here.dz) || 1;
|
||||
const fx = here.dx / len * car.dir, fz = here.dz / len * car.dir;
|
||||
const ahead = obstacles.concat(cars.filter(c => c !== car && c.model.group.visible)).some(o => {
|
||||
const ox = o.x - car.x, oz = o.z - car.z, along = ox * fx + oz * fz;
|
||||
return along > 0 && along < 9 && Math.abs(ox * fz - oz * fx) < 2.2;
|
||||
});
|
||||
const speed = ahead ? 0 : car.speed;
|
||||
car.d += speed * car.dir * dt;
|
||||
if (car.d > car.road.total || car.d < 0) {
|
||||
// At the end of a one-way road the car leaves; on a two-way road it turns back.
|
||||
if (car.road.oneway) {
|
||||
remove(car);
|
||||
continue;
|
||||
}
|
||||
car.dir *= -1;
|
||||
car.d = Math.max(0, Math.min(car.road.total, car.d));
|
||||
}
|
||||
// Keep to the right-hand lane relative to the direction of travel.
|
||||
const p = sample(car.road, car.d);
|
||||
car.x = p.x - fz * car.lane;
|
||||
car.z = p.z + fx * car.lane;
|
||||
car.model.group.position.set(car.x, 0, car.z);
|
||||
car.model.group.rotation.y = Math.atan2(-fz, fx);
|
||||
for (const wheel of car.model.wheels) wheel.rotation.z -= speed * dt / .36;
|
||||
}
|
||||
let cursor = 0;
|
||||
function update(dt, obstacles, around) {
|
||||
// Re-home a couple of distant cars per frame, which keeps the cost per frame small.
|
||||
for (let n = 0; n < 2; n++) {
|
||||
const car = cars[(cursor = (cursor + 1) % cars.length)];
|
||||
if (!car.road || Math.hypot(car.x - around.x, car.z - around.z) > 800)
|
||||
respawn(car, around.x, around.z);
|
||||
}
|
||||
return {group, cars, update, remove};
|
||||
for (const car of cars) {
|
||||
if (!car.road || !car.model.group.visible) continue;
|
||||
const here = sample(car.road, car.d),
|
||||
len = Math.hypot(here.dx, here.dz) || 1;
|
||||
const fx = (here.dx / len) * car.dir,
|
||||
fz = (here.dz / len) * car.dir;
|
||||
const ahead = obstacles
|
||||
.concat(cars.filter((c) => c !== car && c.model.group.visible))
|
||||
.some((o) => {
|
||||
const ox = o.x - car.x,
|
||||
oz = o.z - car.z,
|
||||
along = ox * fx + oz * fz;
|
||||
return along > 0 && along < 9 && Math.abs(ox * fz - oz * fx) < 2.2;
|
||||
});
|
||||
const speed = ahead ? 0 : car.speed;
|
||||
car.d += speed * car.dir * dt;
|
||||
if (car.d > car.road.total || car.d < 0) {
|
||||
// At the end of a one-way road the car leaves; on a two-way road it turns back.
|
||||
if (car.road.oneway) {
|
||||
remove(car);
|
||||
continue;
|
||||
}
|
||||
car.dir *= -1;
|
||||
car.d = Math.max(0, Math.min(car.road.total, car.d));
|
||||
}
|
||||
// Keep to the right-hand lane relative to the direction of travel.
|
||||
const p = sample(car.road, car.d);
|
||||
car.x = p.x - fz * car.lane;
|
||||
car.z = p.z + fx * car.lane;
|
||||
car.model.group.position.set(car.x, 0, car.z);
|
||||
car.model.group.rotation.y = Math.atan2(-fz, fx);
|
||||
for (const wheel of car.model.wheels) wheel.rotation.z -= (speed * dt) / 0.36;
|
||||
}
|
||||
}
|
||||
return { group, cars, update, remove };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user