diff --git a/web/src/three/bridges.ts b/web/src/three/bridges.ts index 83f6e44..b70d4d5 100644 --- a/web/src/three/bridges.ts +++ b/web/src/three/bridges.ts @@ -3,6 +3,7 @@ import * as THREE from 'three' import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js' import { mat } from './typologyMeshes' +import { WATER_LEVEL } from './terrain' const K = 3.4 // cartoon scale-up const STEEL = '#ffc21a' // Pittsburgh bridge yellow @@ -30,7 +31,7 @@ export function makeBridge(form: string, lengthUnits: number, deckY: number): TH boxAt(Math.hypot(dx, dy), t, t, (x1 + x2) / 2, (y1 + y2) / 2, z, c, Math.atan2(dy, dx)) } const S = [-0.9, 0.9] - const WY = 0.3 / K + const WY = WATER_LEVEL / K boxAt(L, 0.45, 1.8, 0, y, 0, DECK) for (const s of S) boxAt(L, 0.3, 0.12, 0, y + 0.3, s, STEEL) const piers = (xs: number[]) => xs.forEach((x) => boxAt(0.8, y + 0.6, 1.4, x, (y - 0.6) / 2, 0, PIER)) diff --git a/web/src/three/engine.ts b/web/src/three/engine.ts index 6a742a8..c7c15ec 100644 --- a/web/src/three/engine.ts +++ b/web/src/three/engine.ts @@ -4,7 +4,7 @@ import * as THREE from 'three' import { OrbitControls } from 'three/addons/controls/OrbitControls.js' import { makeBridge } from './bridges' -import { loadTerrain, type Terrain } from './terrain' +import { loadTerrain, WATER_LEVEL, type Terrain } from './terrain' import { box, house, makeBuilding, mat, mesh, type BuildingForm } from './typologyMeshes' export interface SceneConfig { @@ -315,9 +315,13 @@ export function createEngine(container: HTMLElement, opts: EngineOptions): Engin function buildCity(t: Terrain): void { terrain = t - // Water plane at the normal pool; land sits above it. - const water = new THREE.Mesh(new THREE.PlaneGeometry(6000, 6000).rotateX(-Math.PI / 2), new THREE.MeshStandardMaterial({ color: WATER, roughness: 0.28, metalness: 0.1 })) - water.position.y = 0.3 + // Water plane at the normal pool; land sits above it. Depth offset pushes the + // water back so the shoreline never z-fights with the banks. + const water = new THREE.Mesh( + new THREE.PlaneGeometry(6000, 6000).rotateX(-Math.PI / 2), + new THREE.MeshStandardMaterial({ color: WATER, roughness: 0.28, metalness: 0.1, polygonOffset: true, polygonOffsetFactor: 2, polygonOffsetUnits: 4 }), + ) + water.position.y = WATER_LEVEL water.receiveShadow = true city.add(water) // Ground: vivid inside city limits, pale countryside outside. diff --git a/web/src/three/terrain.ts b/web/src/three/terrain.ts index 46d220f..101df00 100644 --- a/web/src/three/terrain.ts +++ b/web/src/three/terrain.ts @@ -17,13 +17,19 @@ interface Meta { fine: { extent: number; step: number; n: number } } -const WATER_H = -0.6 +/** Height of the flat water plane. */ +export const WATER_LEVEL = 0.3 +// Riverbed and lowest land sit well clear of the water plane: at city zoom the +// depth buffer can't separate surfaces a fraction of a unit apart, and flat banks +// near the pool z-fight with the water (rivers flicker as the camera moves). +const WATER_H = WATER_LEVEL - 2.5 +const LAND_MIN = WATER_LEVEL + 0.8 function sampler(data: Int16Array, offset: number, extent: number, step: number, n: number, scale: number, water: number) { const stride = n + 1 const at = (i: number, j: number) => { const v = data[offset + j * stride + i] - return v === water ? WATER_H : v / scale + return v === water ? WATER_H : Math.max(LAND_MIN, v / scale) } const wet = (i: number, j: number) => data[offset + j * stride + i] === water const idx = (x: number, z: number) => { @@ -101,7 +107,7 @@ function ruleTerrain(riverPts: [number, number][][], half: number): Terrain { const d = dist(x, z) if (d < 15) return WATER_H const n = 0.5 + 0.5 * Math.sin(x * 0.011 + 1.3) * Math.cos(z * 0.013 - 0.7) + 0.22 * Math.sin(x * 0.029 + z * 0.023) - return 0.8 + sst(40, 105, d) * sst(80, 190, Math.hypot(x + 15, z)) * (15 + 14 * n) + return LAND_MIN + sst(40, 105, d) * sst(80, 190, Math.hypot(x + 15, z)) * (15 + 14 * n) } return { source: 'rules',