tower / files / src / terrain / sea-state.ts
sea-state.ts
| 1 | /** |
| 2 | * The sea state of every lake and sea at a moment. See world-generation.md §18. |
| 3 | * |
| 4 | * Until Phase 4 a body's waves were compiled with the ground, from its fixed wind. Now the |
| 5 | * wind follows Tower's weather plan (world/weather.ts), which changes by the hour, and |
| 6 | * recompiling the world for every gust would be absurd. So the ground is compiled once, |
| 7 | * each water vertex carries only the index of its body, and the waves are looked up here |
| 8 | * and handed to the shader as a small table. |
| 9 | */ |
| 10 | import type { World } from '../world/schema.js' |
| 11 | import type { Wind, WeatherKind } from '../world/schema.js' |
| 12 | import { actualAt, seaWindAt } from '../world/weather.js' |
| 13 | import type { CBasin, CompiledWorld } from './compile.js' |
| 14 | import { fetchOf, type WindSea, windSea } from './water.js' |
| 15 | |
| 16 | /** Most bodies of water the shader can tell apart. More than any world here needs. */ |
| 17 | export const MAX_BODIES = 32 |
| 18 | |
| 19 | export interface SeaState { |
| 20 | id: string |
| 21 | /** The wind blowing now, and the one the waves are still answering. */ |
| 22 | wind: Wind |
| 23 | seaWind: Wind |
| 24 | kind: WeatherKind |
| 25 | /** Why the weather is not as published, if Tower has revised it. */ |
| 26 | revised: string | null |
| 27 | sea: WindSea |
| 28 | /** Amplitude of the shader's main component: the sum of four peaks near Hs / 2. */ |
| 29 | ampM: number |
| 30 | dirRad: number |
| 31 | whitecaps: number |
| 32 | } |
| 33 | |
| 34 | /** |
| 35 | * The share of a sea's surface in whitecaps, from the wind speed at 10 m (Monahan and |
| 36 | * O'Muircheartaigh, 1980): W = 3.84e-6 · U^3.41. About 1% at 10 m/s, 9% at 20, 29% at 28. |
| 37 | */ |
| 38 | export function whitecapFraction(windMS: number): number { |
| 39 | return Math.min(1, 3.84e-6 * Math.max(0, windMS) ** 3.41) |
| 40 | } |
| 41 | |
| 42 | /** A significant wave height is crest to trough; the shader's main component is 0.35 of it. */ |
| 43 | export const MAIN_COMPONENT = 0.35 |
| 44 | |
| 45 | export function seaStateOf(world: World, b: CBasin, hours: number): SeaState { |
| 46 | const f = world.features.find((x) => x.id === b.id) |
| 47 | const baseline = f && (f.kind === 'lake' || f.kind === 'sea') ? f.wind : { directionDeg: 0, speedMS: 0 } |
| 48 | const now = actualAt(world.weather, b.id, baseline, hours) |
| 49 | const seaWind = seaWindAt(world.weather, b.id, baseline, hours) |
| 50 | const dirRad = (seaWind.directionDeg * Math.PI) / 180 |
| 51 | const sea = windSea(seaWind.speedMS, fetchOf(b.shape, dirRad)) |
| 52 | return { |
| 53 | id: b.id, wind: now.wind, seaWind, kind: now.kind, revised: now.revision?.reason ?? null, |
| 54 | sea, ampM: sea.heightM * MAIN_COMPONENT, dirRad, whitecaps: whitecapFraction(now.wind.speedMS), |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | /** Numbers per body in the shader's table. */ |
| 59 | export const BODY_STRIDE = 4 |
| 60 | |
| 61 | /** The shader's table: amplitude, wavelength, direction and whitecaps per body, in basin order. */ |
| 62 | export function seaStateTable(world: World, compiled: CompiledWorld, hours: number, out: Float32Array): SeaState[] { |
| 63 | out.fill(0) |
| 64 | return compiled.basins.slice(0, MAX_BODIES).map((b) => { |
| 65 | const s = seaStateOf(world, b, hours) |
| 66 | const o = b.index * BODY_STRIDE |
| 67 | out[o] = s.ampM |
| 68 | out[o + 1] = s.sea.lengthM |
| 69 | out[o + 2] = s.dirRad |
| 70 | out[o + 3] = s.whitecaps |
| 71 | return s |
| 72 | }) |
| 73 | } |