From d63865bbcf2cce3fd83cc5a632165cfcbc92fa23 Mon Sep 17 00:00:00 2001 From: Leona Chen <88675442+leonac24@users.noreply.github.com> Date: Sat, 26 Sep 2026 21:09:13 -0400 Subject: [PATCH] web: stop rivers z-fighting with low riverbanks Land near the pool sat within a fraction of a unit of the water plane, below the depth buffer's precision at city zoom, so shorelines flickered as the camera moved. Floor land at 0.8 above the water, drop the riverbed, bias the water plane back in depth, and share one WATER_LEVEL with the bridges. Co-Authored-By: Claude Opus 5.5 --- web/src/three/bridges.ts | 3 ++- web/src/three/engine.ts | 12 ++++++++---- web/src/three/terrain.ts | 12 +++++++++--- 3 files changed, 19 insertions(+), 8 deletions(-) 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',