California gets roads, traffic, and a car to follow
This commit is contained in:
@@ -12,7 +12,7 @@ import type { Bridge, LatLng } from "./types.ts";
|
||||
import type { World } from "./world.ts";
|
||||
|
||||
/** Resample a lat/lng path into scene-space points that ride the ground. */
|
||||
function drapePath(world: World, path: LatLng[], samplesPerLeg = 14, lift = 0.05): THREE.Vector3[] {
|
||||
function drapePath(world: World, path: LatLng[], samplesPerLeg = 14, lift = 0.14): THREE.Vector3[] {
|
||||
const out: THREE.Vector3[] = [];
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
const from = path[i];
|
||||
@@ -40,12 +40,64 @@ function ribbon(points: THREE.Vector3[], width: number, color: number): THREE.Me
|
||||
return mesh;
|
||||
}
|
||||
|
||||
/** A draped, flat road deck. A tube turns a freeway into a raised pipeline. */
|
||||
function roadRibbon(
|
||||
points: readonly THREE.Vector3[],
|
||||
width: number,
|
||||
color: number,
|
||||
lift = 0,
|
||||
): THREE.Mesh {
|
||||
const positions: number[] = [];
|
||||
const normals: number[] = [];
|
||||
const indices: number[] = [];
|
||||
const half = width / 2;
|
||||
|
||||
for (let index = 0; index < points.length; index += 1) {
|
||||
const point = points[index];
|
||||
const previous = points[Math.max(0, index - 1)];
|
||||
const next = points[Math.min(points.length - 1, index + 1)];
|
||||
if (!point || !previous || !next) continue;
|
||||
const dx = next.x - previous.x;
|
||||
const dz = next.z - previous.z;
|
||||
const length = Math.hypot(dx, dz) || 1;
|
||||
const nx = -dz / length;
|
||||
const nz = dx / length;
|
||||
positions.push(
|
||||
point.x + nx * half, point.y + lift, point.z + nz * half,
|
||||
point.x - nx * half, point.y + lift, point.z - nz * half,
|
||||
);
|
||||
normals.push(0, 1, 0, 0, 1, 0);
|
||||
if (index < points.length - 1) {
|
||||
const a = index * 2;
|
||||
indices.push(a, a + 2, a + 1, a + 1, a + 2, a + 3);
|
||||
}
|
||||
}
|
||||
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
|
||||
geometry.setAttribute("normal", new THREE.Float32BufferAttribute(normals, 3));
|
||||
geometry.setIndex(indices);
|
||||
geometry.computeBoundingSphere();
|
||||
const mesh = new THREE.Mesh(
|
||||
geometry,
|
||||
new THREE.MeshLambertMaterial({ color, side: THREE.DoubleSide }),
|
||||
);
|
||||
mesh.receiveShadow = true;
|
||||
return mesh;
|
||||
}
|
||||
|
||||
export function createRoads(world: World): THREE.Group {
|
||||
const group = new THREE.Group();
|
||||
group.name = "roads";
|
||||
for (const road of world.city.roads) {
|
||||
const color = road.kind === "freeway" ? 0x7d7166 : 0x8b8578;
|
||||
group.add(ribbon(drapePath(world, road.path), road.width, color));
|
||||
const path = drapePath(world, road.path);
|
||||
group.add(roadRibbon(path, road.width, color));
|
||||
if (road.kind === "freeway") {
|
||||
// One warm median stroke is enough at corridor scale to read as divided
|
||||
// highway without spending a textured asset or a draw call per lane.
|
||||
group.add(roadRibbon(path, Math.max(0.025, road.width * 0.035), 0xd7c27c, 0.012));
|
||||
}
|
||||
}
|
||||
return group;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user