fuzzyworld

tower / files / src / terrain / ice.ts

ice.ts

5.8 KB · history · blame

1/**
2 * A sea's ice, and the dim Tower keeps over it. See design.md §1.9.6, D-090, D-091.
3 *
4 * The far cap's window is a cold wall, and Tower lets the Deepward Sea freeze against it in
5 * bands: open water, then pack ice, then fast ice frozen to the foot of the cap. It keeps
6 * that end of the world dim to match — it aims less and less of its light at the sea toward
7 * the ice, and none at the fast ice, which gets only the rod's glow.
8 *
9 * Both are functions of z alone, because the bands are rings: the sea closes on itself
10 * around the world, and nothing about the ice changes with theta but the floes.
11 */
12import { G0 } from '../canon/constants.js'
13import type { SeaIce } from '../world/schema.js'
14
15/** Which way the ice lies from the edge: +1 if the fast ice is deepward of it, else -1. */
16export const iceSide = (ice: SeaIce): number => Math.sign(ice.fastM - ice.edgeM)
17
18/** How far into the ice a point is, in units of the pack's width: 0 at the edge, 1 at the fast ice. */
19export function intoIce(ice: SeaIce, z: number): number {
20 return (z - ice.edgeM) / (ice.fastM - ice.edgeM)
21}
22
23/** Pack ice at its edge covers this much of the sea, and packs to this at the fast ice. */
24export const PACK_AT_EDGE = 0.3
25export const PACK_AT_FAST = 0.95
26
27/**
28 * How much of the sea's surface is ice at a depth `t` into the ice (`intoIce`): none on
29 * the open water, the pack closing up across its band, and 1 — solid — on the fast ice.
30 */
31export function iceCover(t: number): number {
32 if (t < 0) return 0
33 if (t >= 1) return 1
34 return PACK_AT_EDGE + (PACK_AT_FAST - PACK_AT_EDGE) * t
35}
36
37/**
38 * Tower's aimed light at the far end, as a share of what it aims everywhere else
39 * (D-091). It begins to fall at the open water's shore, is down to `AIMED_AT_EDGE` by the
40 * ice edge and `AIMED_AT_FAST` by the fast ice, and is nothing over the fast ice itself.
41 */
42export const AIMED_AT_EDGE = 0.35
43export const AIMED_AT_FAST = 0.1
44
45/**
46 * What still lights the fast ice by day when Tower aims nothing at it: the rod's own glow,
47 * which cannot be aimed away (§1.3.2). Tens of W/m² against the ~1,000 of a noon under the
48 * sun — about a twentieth, the permanent overcast dusk §1.9.6 describes.
49 */
50export const ROD_GLOW_BY_DAY = 0.05
51
52/**
53 * The share of Tower's aimed light at `z` (sun and moon both), given the ice and where its
54 * open water begins — the far side of the open water from the ice, `openM`.
55 */
56export function aimedShare(ice: SeaIce, openM: number, z: number): number {
57 const t = intoIce(ice, z)
58 if (t >= 1) return 0
59 if (t >= 0) return AIMED_AT_EDGE + (AIMED_AT_FAST - AIMED_AT_EDGE) * t
60 // On the open water, from the shore to the edge.
61 const s = (z - openM) / (ice.edgeM - openM)
62 if (s <= 0) return 1
63 const k = Math.min(1, s)
64 return 1 + (AIMED_AT_EDGE - 1) * k * k * (3 - 2 * k)
65}
66
67/** The sky's light by where it comes from, W/m², as sky.ts `lightMix` splits it. */
68export interface LightMix { sun: number; rod: number; moon: number }
69
70/**
71 * The light on the ground at `z` as a share of what the sky gives everywhere else. The rod
72 * is never aimed and is the same everywhere; the sun and the moon are, and fall with
73 * `aimedShare` — the sun no lower than the rod's own glow by day. The shaders do the same
74 * sum per pixel; this one sets the eye's exposure where the observer stands, so the far end
75 * is dusk to someone standing on it rather than black.
76 */
77export function lightShare(ice: SeaIce, openM: number, z: number, mix: LightMix): number {
78 const total = mix.sun + mix.rod + mix.moon
79 if (total <= 0) return 1
80 const aimed = aimedShare(ice, openM, z)
81 return (mix.sun * Math.max(aimed, ROD_GLOW_BY_DAY) + mix.rod + mix.moon * aimed) / total
82}
83
84/**
85 * The ice mountains at the foot of the cap (D-093). Grounded ice cannot stand much higher
86 * than a few hundred metres over a few kilometres — it spreads under its own weight until
87 * the stress at its bed is what the bed will take — so ice that climbs kilometres up the
88 * window has to be standing on something. Tower raised a range there for it, as it raised the
89 * Uplands, and the snow off the window wind and the meltwater of every pane it clears keep it
90 * buried.
91 *
92 * The rock rises from the sea bed at the range's start to its height at the cap, steepening
93 * toward the window. Ice mantles it from where it leaves the water, thickening with the plastic
94 * profile of an ice sheet,
95 *
96 * H² = H0² + (2 τ / ρ g) · x
97 *
98 * x in from the ice's margin, to `ICE_MANTLE_M` on the heights. τ is 150 kPa, the top of what
99 * ice sheets show: cold ice frozen to its bed. The highest ridges of rock stand out of it.
100 */
101export const ICE_DENSITY = 917
102export const ICE_YIELD_PA = 150_000
103/** How thick the ice lies on the range once it is well up it, m. */
104export const ICE_MANTLE_M = 350
105/** The ice's edge where the rock leaves the water: a front of a few tens of metres. */
106export const ICE_FRONT_M = 45
107/** How the rock steepens toward the cap: its height goes as t^RANGE_RISE across the range. */
108export const RANGE_RISE = 1.6
109
110/** The range's rock at `t` of the way across it (0 at its start, 1 at the cap), smoothed. */
111export function rangeRock(t: number, bedM: number, heightM: number): number {
112 const k = Math.min(1, Math.max(0, t))
113 return bedM + (heightM - bedM) * k ** RANGE_RISE
114}
115
116/** How far across the range the rock stands `belowM` under the water: where the ice begins. */
117export function iceMargin(levelM: number, bedM: number, heightM: number, belowM = 40): number {
118 const f = (levelM - belowM - bedM) / (heightM - bedM)
119 return f <= 0 ? 0 : f >= 1 ? 1 : f ** (1 / RANGE_RISE)
120}
121
122/** The ice's thickness `xM` metres in from its margin. */
123export function iceMantle(xM: number): number {
124 if (xM < 0) return 0
125 const k = (2 * ICE_YIELD_PA) / (ICE_DENSITY * G0)
126 return Math.min(ICE_MANTLE_M, Math.sqrt(ICE_FRONT_M * ICE_FRONT_M + k * xM))
127}