fuzzyworld

tower / files / src / terrain / sea-state.ts

sea-state.ts

Blame: the commit that last changed each line · file · history

a088e5d5 2026-09-17 Pavel Safronov1/**
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 */
10import type { World } from '../world/schema.js'
11import type { Wind, WeatherKind } from '../world/schema.js'
12import { actualAt, seaWindAt } from '../world/weather.js'
13import type { CBasin, CompiledWorld } from './compile.js'
14import { fetchOf, type WindSea, windSea } from './water.js'
15
16/** Most bodies of water the shader can tell apart. More than any world here needs. */
17export const MAX_BODIES = 32
18
19export 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 */
38export 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. */
43export const MAIN_COMPONENT = 0.35
44
45export 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. */
59export const BODY_STRIDE = 4
60
61/** The shader's table: amplitude, wavelength, direction and whitecaps per body, in basin order. */
62export 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}