tower / files / src / views / interior-model.ts
interior-model.ts
| 1 | /** |
| 2 | * Tower's inside, as meshes both 3D views draw: the displaced ground, the two glazed |
| 3 | * caps, the bore behind the far one and the beam — with the integrated haze in every |
| 4 | * one of their shaders. See design.md §1.8, §2.4. |
| 5 | * |
| 6 | * The interior view stands in it. The exterior view looks in at it through the |
| 7 | * windows (D-060), and that is why every shader here works in the **tower's frame** |
| 8 | * rather than the world's: the exterior turns the whole model with the hull, and |
| 9 | * haze has to be integrated where the air is, not where the scene graph put it. |
| 10 | * `uFrame` carries world into tower coordinates and `uCam` is the eye already in |
| 11 | * them. Inside, nothing turns, so both are the identity and the eye as it stands. |
| 12 | * |
| 13 | * Scene units are kilometres, so the numbers in the code are the numbers in canon. |
| 14 | */ |
| 15 | import type { Object3D, Texture } from 'three' |
| 16 | import { |
| 17 | AdditiveBlending, BackSide, BufferAttribute, BufferGeometry, CircleGeometry, Color, CylinderGeometry, DataTexture, |
| 18 | DoubleSide, FrontSide, Group, InstancedMesh, LinearFilter, Matrix3, Matrix4, Mesh, NearestFilter, RGBAFormat, |
| 19 | RepeatWrapping, RingGeometry, |
| 20 | ShaderMaterial, SphereGeometry, |
| 21 | Vector3, Vector4, |
| 22 | } from 'three' |
| 23 | import { |
| 24 | BEAM_RADIUS_M, BRIGHTNESS_NIGHT_W_M2, BRIGHTNESS_TEMPERATE_W_M2, BRIGHTNESS_TWILIGHT_W_M2, |
| 25 | COLLECTOR_DIAMETER_M, COLLECTOR_RIM_M, COLLECTOR_STANDOFF_M, |
| 26 | HULL_DEPTH_M, LENGTH_M, OMEGA_RAD_S, RADIUS_M, RAYLEIGH_FLOOR_PER_M, |
| 27 | MOON_RADIUS_M, SPECIFIC_GAS_CONSTANT, SUN_RADIUS_M, TEMPERATURE_K, |
| 28 | } from '../canon/constants.js' |
| 29 | import { DIALS, type Sky, lightMix } from '../world/sky.js' |
| 30 | import { PANE_KM, frostAt, panes } from '../world/frost.js' |
| 31 | import { ICE, PALETTE_GLSL } from '../terrain/palette.js' |
| 32 | import { FARM_GLSL } from '../terrain/farms.js' |
| 33 | import { LOWEST_GROUND_M } from '../terrain/tile.js' |
| 34 | import { AIMED_AT_EDGE, AIMED_AT_FAST, ROD_GLOW_BY_DAY, lightShare as lightShareAt } from '../terrain/ice.js' |
| 35 | import { fallSheet } from '../terrain/falls.js' |
| 36 | import { ForestView } from './forest-view.js' |
| 37 | import { BODY_STRIDE, MAX_BODIES, type SeaState, seaStateTable } from '../terrain/sea-state.js' |
| 38 | import { WAVE_AMP, WAVE_LEN, WAVE_PHASE, WAVE_TURN, boomAngle, shipsAt, waveAt } from '../terrain/ships.js' |
| 39 | import { terrain } from '../terrain/service.js' |
| 40 | import type { RailLine } from '../terrain/rails.js' |
| 41 | import { MIRROR_UV_RADIUS, capGlazingTexture, sunCapGlazingTexture } from './cap-pattern.js' |
| 42 | import { TerrainLOD, type TerrainLODOptions } from './terrain-lod.js' |
| 43 | import { skyMatrix, starTexture } from './stars.js' |
| 44 | import { STAR_FACE_CELLS, STAR_FACE_SIZE } from '../world/celestial.js' |
| 45 | |
| 46 | const KM = 1000 |
| 47 | const R = RADIUS_M / KM |
| 48 | /** The sun's radius, and how far out its glow is drawn, km. */ |
| 49 | const SUN_R = SUN_RADIUS_M / KM |
| 50 | const SUN_GLOW_R = 24 |
| 51 | /** The moon's radius, and how far out its glow is drawn, km (D-088). */ |
| 52 | const MOON_R = MOON_RADIUS_M / KM |
| 53 | const MOON_GLOW_R = 12 |
| 54 | const L = LENGTH_M / KM |
| 55 | const BEAM_R = BEAM_RADIUS_M / KM |
| 56 | /** Wall depth in km. The far cap's window is set this far into its own frame. */ |
| 57 | const HULL_D = HULL_DEPTH_M / KM |
| 58 | /** The collector, in km: 122 km of radius flying 320 km ahead of the sun cap. */ |
| 59 | const COLLECTOR_R = COLLECTOR_DIAMETER_M / KM / 2 |
| 60 | const COLLECTOR_Z = -COLLECTOR_STANDOFF_M / KM |
| 61 | /** Width of the sunlit edge structure, in km (D-054). */ |
| 62 | const COLLECTOR_RIM = COLLECTOR_RIM_M / KM |
| 63 | |
| 64 | /** |
| 65 | * Rayleigh extinction per kilometre at floor density, per channel. |
| 66 | * |
| 67 | * Canon quotes 550 nm (green). Scattering goes as lambda^-4, so red survives about |
| 68 | * twice as far as green and blue about half as far — which is the whole reason |
| 69 | * distant land in Tower goes blue rather than merely grey, and it costs nothing to |
| 70 | * carry three numbers instead of one. |
| 71 | */ |
| 72 | export const K_GREEN = RAYLEIGH_FLOOR_PER_M * KM |
| 73 | const K_RGB: [number, number, number] = [ |
| 74 | K_GREEN * (550 / 650) ** 4, K_GREEN, K_GREEN * (550 / 440) ** 4, |
| 75 | ] |
| 76 | /** Density falls as exp(-a(R^2 - s^2)) — the rotational profile, in 1/km^2. */ |
| 77 | const DENSITY_A = ((OMEGA_RAD_S ** 2 / (2 * SPECIFIC_GAS_CONSTANT * TEMPERATURE_K)) * KM ** 2) |
| 78 | |
| 79 | /** |
| 80 | * Airlight: the radiance the haze itself adds. It is deliberately NEUTRAL. |
| 81 | * |
| 82 | * The obvious move is to make it sky blue, and it is wrong: blue already scatters |
| 83 | * 2.4x harder than green in K_RGB, so a blue source colours the haze twice and |
| 84 | * every distant thing turns mauve regardless of what it is made of. With a neutral |
| 85 | * source the blue falls out of the physics, and the far side of the world stays |
| 86 | * recognisably forest and desert at the 78% canon says it should be. |
| 87 | */ |
| 88 | export const HAZE_DAY = 0x9aa3ad |
| 89 | export const HAZE_DUSK = 0x8d8792 |
| 90 | |
| 91 | |
| 92 | // ---------------------------------------------------------------------- shaders |
| 93 | |
| 94 | /** |
| 95 | * Transmittance from `a` to `b` through Tower's air, per channel. |
| 96 | * |
| 97 | * The integral has no closed form worth having — the exponent is a(s^2 - R^2) with |
| 98 | * s^2 quadratic along the ray — so it is sampled. The integrand is smooth and |
| 99 | * monotonic on each side of the ray's closest approach to the axis, so 24 midpoint |
| 100 | * samples land within a percent of the 512-sample version the instruments use, and |
| 101 | * the two agree on the canon table in design.md §1.8. |
| 102 | * |
| 103 | * **The segment is clipped to the air first.** From inside that changes nothing — |
| 104 | * the eye and everything it sees are both in the air already. From outside it is |
| 105 | * the whole difference between a view in through a window and a view through a |
| 106 | * thousand kilometres of make-believe atmosphere: only the part of the sightline |
| 107 | * that is actually inside the tube, between the sun cap and the far cap's glass, |
| 108 | * gets extinguished. The radius allows for the lake floors cut below the datum, and |
| 109 | * the density is held at floor value there rather than climbing past it. |
| 110 | */ |
| 111 | const HAZE_GLSL = /* glsl */` |
| 112 | uniform vec3 uCam; |
| 113 | uniform vec3 uK; |
| 114 | uniform vec3 uHaze; |
| 115 | uniform float uExposure; |
| 116 | const float A = ${DENSITY_A.toExponential(8)}; |
| 117 | const float RR = ${(R * R).toFixed(4)}; |
| 118 | // The air reaches down past the deepest ground anything generates (tile.ts). |
| 119 | const float RR_AIR = ${((R - LOWEST_GROUND_M / KM + 0.05) ** 2).toFixed(4)}; |
| 120 | const float Z_AIR0 = 0.0; |
| 121 | const float Z_AIR1 = ${(L + HULL_D).toFixed(4)}; |
| 122 | |
| 123 | vec3 towerTransmittance(vec3 from, vec3 to) { |
| 124 | vec3 d = to - from; |
| 125 | float t0 = 0.0; |
| 126 | float t1 = 1.0; |
| 127 | float a = dot(d.xy, d.xy); |
| 128 | float b = dot(from.xy, d.xy); |
| 129 | float c = dot(from.xy, from.xy) - RR_AIR; |
| 130 | if (a > 1e-8) { |
| 131 | float disc = b * b - a * c; |
| 132 | if (disc <= 0.0) return vec3(1.0); |
| 133 | float s = sqrt(disc); |
| 134 | t0 = max(t0, (-b - s) / a); |
| 135 | t1 = min(t1, (-b + s) / a); |
| 136 | } else if (c > 0.0) { |
| 137 | return vec3(1.0); |
| 138 | } |
| 139 | if (abs(d.z) > 1e-8) { |
| 140 | float za = (Z_AIR0 - from.z) / d.z; |
| 141 | float zb = (Z_AIR1 - from.z) / d.z; |
| 142 | t0 = max(t0, min(za, zb)); |
| 143 | t1 = min(t1, max(za, zb)); |
| 144 | } else if (from.z < Z_AIR0 || from.z > Z_AIR1) { |
| 145 | return vec3(1.0); |
| 146 | } |
| 147 | if (t1 <= t0) return vec3(1.0); |
| 148 | |
| 149 | vec3 start = from + d * t0; |
| 150 | vec3 span = d * (t1 - t0); |
| 151 | float len = length(span); |
| 152 | const int N = 24; |
| 153 | float tau = 0.0; |
| 154 | for (int i = 0; i < N; i++) { |
| 155 | float t = (float(i) + 0.5) / float(N); |
| 156 | vec2 p = start.xy + span.xy * t; |
| 157 | tau += exp(-A * max(RR - dot(p, p), 0.0)); |
| 158 | } |
| 159 | return exp(-uK * (tau * len / float(N))); |
| 160 | } |
| 161 | |
| 162 | /** |
| 163 | * How a window is written out. Inside, \`face\` is what fills a pane — sky — and the |
| 164 | * glass is opaque to the renderer because nothing behind it is drawn. Outside, |
| 165 | * \`uSeeThrough\` is set and a pane is clear: it keeps only the haze that lies in |
| 166 | * front of it, and lets the scene behind show through by the rest. The straight |
| 167 | * alpha is taken on green, where canon quotes its extinction. |
| 168 | */ |
| 169 | uniform float uSeeThrough; |
| 170 | vec4 windowColour(vec3 face, vec3 solid, float cover) { |
| 171 | vec3 T = towerTransmittance(uCam, vWorld); |
| 172 | vec3 air = uHaze * uExposure * (1.0 - T); |
| 173 | if (uSeeThrough < 0.5) return vec4(face * T + air, 1.0); |
| 174 | float haze = 1.0 - T.g; |
| 175 | float alpha = cover + (1.0 - cover) * haze; |
| 176 | vec3 col = cover * (solid * T + air) + (1.0 - cover) * air; |
| 177 | return vec4(col / max(alpha, 1e-4), alpha); |
| 178 | } |
| 179 | ` |
| 180 | |
| 181 | /** |
| 182 | * Tower's light (design.md §1.3.4, D-074). By day it comes from the sun: a point on the |
| 183 | * axis `uSun.x` km from the sun cap, lit `uSun.y` of the way up. Tower aims the swarm |
| 184 | * so the sun is as bright seen from anywhere as from under it, so a surface is lit by how |
| 185 | * squarely it faces the sun — as under Earth's — and each place has its noon when the sun |
| 186 | * passes over it. By night the sun is out and the beam is banked, and what light there is |
| 187 | * comes from the whole rod overhead, `uSun.z` of it — and from the moon (D-088), a point on |
| 188 | * the axis `uMoon.x` km from the sun cap throwing `uMoon.y` of the rod's light again. |
| 189 | * |
| 190 | * Every lit surface in both views goes through `direct`, which is what keeps the sun seen |
| 191 | * from the floor and the sun seen in through a window the same sun. |
| 192 | */ |
| 193 | const SUN_GLSL = /* glsl */` |
| 194 | uniform vec3 uSun; |
| 195 | /** The moon: km from the sun cap, its light, how lit it is as it gathers, and its phase — |
| 196 | * the lit share of its disc, positive waxing and negative waning (D-088). */ |
| 197 | uniform vec4 uMoon; |
| 198 | /** |
| 199 | * How much of the beam runs on past the sun by day, to the far cap (D-079). The shoal takes |
| 200 | * the rest; this is what keeps the rod whole from cap to cap, and the far cap's mirror lit. |
| 201 | */ |
| 202 | const float BEAM_PAST_SUN = 0.7; |
| 203 | vec3 sunPos() { return vec3(0.0, 0.0, uSun.x); } |
| 204 | vec3 sunDir(vec3 p) { return normalize(sunPos() - p); } |
| 205 | vec3 moonPos() { return vec3(0.0, 0.0, uMoon.x); } |
| 206 | vec3 moonDir(vec3 p) { return normalize(moonPos() - p); } |
| 207 | vec3 upAt(vec3 p) { return -normalize(vec3(p.xy, 0.0)); } |
| 208 | float direct(vec3 p, vec3 n) { |
| 209 | return uSun.y * max(dot(n, sunDir(p)), 0.0) + uSun.z * max(dot(n, upAt(p)), 0.0) |
| 210 | + uMoon.y * max(dot(n, moonDir(p)), 0.0); |
| 211 | } |
| 212 | |
| 213 | /** |
| 214 | * The far end's dim (D-090, D-091). Tower aims less of its light at the sea toward the ice |
| 215 | * and none at the fast ice, which the rod's glow alone lights by day. \`uDim\` is where the |
| 216 | * open water begins, the ice edge and the fast ice's edge, km along the axis, and which way |
| 217 | * the ice lies from its edge (0: no iced sea). \`uLightMix\` is the sky's light split into |
| 218 | * sun, rod and moon, summing to 1 (sky.ts \`lightMix\`). The sums are ice.ts's, which sets |
| 219 | * the eye by them where the observer stands. |
| 220 | */ |
| 221 | uniform vec4 uDim; |
| 222 | uniform vec3 uLightMix; |
| 223 | float aimedShare(float zKm) { |
| 224 | if (uDim.w == 0.0) return 1.0; |
| 225 | float t = (zKm - uDim.y) / (uDim.z - uDim.y); |
| 226 | if (t >= 1.0) return 0.0; |
| 227 | if (t >= 0.0) return mix(${AIMED_AT_EDGE.toFixed(4)}, ${AIMED_AT_FAST.toFixed(4)}, t); |
| 228 | float s = clamp((zKm - uDim.x) / (uDim.y - uDim.x), 0.0, 1.0); |
| 229 | return mix(1.0, ${AIMED_AT_EDGE.toFixed(4)}, s * s * (3.0 - 2.0 * s)); |
| 230 | } |
| 231 | /** |
| 232 | * How lit a place looks against the rest of the world, 0..1: multiplies everything lit |
| 233 | * there. The watts go through the same 0.35 power the exposure puts the dial's watts |
| 234 | * through (\`setSky\`), because an eye in the dim has adapted to it: the fast ice at a |
| 235 | * twentieth of the light looks like dusk, a third as bright, not like a hole. |
| 236 | */ |
| 237 | float lightShare(vec3 p) { |
| 238 | float a = aimedShare(p.z); |
| 239 | return pow(uLightMix.x * max(a, ${ROD_GLOW_BY_DAY.toFixed(4)}) + uLightMix.y + uLightMix.z * a, 0.35); |
| 240 | } |
| 241 | ` |
| 242 | |
| 243 | /** |
| 244 | * A terrain tile (world-generation.md §6). Vertices arrive already on the cylinder, |
| 245 | * relative to the tile's centre, with normals in the tower's frame and material |
| 246 | * weights beside them — so this does nothing but place them and pass them on. |
| 247 | */ |
| 248 | const GROUND_VERT = /* glsl */` |
| 249 | #include <common> |
| 250 | #include <logdepthbuf_pars_vertex> |
| 251 | uniform mat4 uFrame; |
| 252 | attribute vec4 aMatA; |
| 253 | attribute vec4 aMatB; |
| 254 | attribute vec3 aExtra; |
| 255 | varying vec3 vWorld; |
| 256 | varying vec3 vNormal2; |
| 257 | varying vec4 vMatA; |
| 258 | varying vec4 vMatB; |
| 259 | varying vec3 vExtra; |
| 260 | |
| 261 | void main() { |
| 262 | vWorld = (uFrame * modelMatrix * vec4(position, 1.0)).xyz; |
| 263 | vNormal2 = normal; |
| 264 | vMatA = aMatA; |
| 265 | vMatB = aMatB; |
| 266 | vExtra = aExtra; |
| 267 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); |
| 268 | #include <logdepthbuf_vertex> |
| 269 | } |
| 270 | ` |
| 271 | |
| 272 | const GROUND_FRAG = /* glsl */` |
| 273 | #include <common> |
| 274 | #include <logdepthbuf_pars_fragment> |
| 275 | varying vec3 vWorld; |
| 276 | varying vec3 vNormal2; |
| 277 | varying vec4 vMatA; |
| 278 | varying vec4 vMatB; |
| 279 | varying vec3 vExtra; |
| 280 | ${HAZE_GLSL} |
| 281 | ${SUN_GLSL} |
| 282 | ${PALETTE_GLSL} |
| 283 | ${FARM_GLSL} |
| 284 | /** |
| 285 | * The farms' light (D-095): x hours since sunrise, y the sun's day, z how much brighter |
| 286 | * Tower's noon is than the sky now (through the exposure's 0.35 power), w 0 if the beam is |
| 287 | * shut down and nothing can be lit. |
| 288 | */ |
| 289 | uniform vec4 uFarm; |
| 290 | |
| 291 | /** Cheap value noise, to keep the ground from being flat colour. */ |
| 292 | float hash(vec2 p) { |
| 293 | return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); |
| 294 | } |
| 295 | float vnoise(vec2 p) { |
| 296 | vec2 i = floor(p), f = fract(p); |
| 297 | f = f * f * (3.0 - 2.0 * f); |
| 298 | return mix(mix(hash(i), hash(i + vec2(1, 0)), f.x), |
| 299 | mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x), f.y); |
| 300 | } |
| 301 | |
| 302 | void main() { |
| 303 | #include <logdepthbuf_fragment> |
| 304 | // Elevation from the radius, since the tile knows where it is and nothing else does. |
| 305 | float elevM = (${R.toFixed(1)} - length(vWorld.xy)) * 1000.0; |
| 306 | // The palette is sRGB; every uniform colour here is linear. Three inserts that |
| 307 | // decode into its own materials and never into a custom one. |
| 308 | vec3 base = pow(groundColour(vMatA, vMatB, vExtra.xy, elevM), vec3(2.2)); |
| 309 | /** |
| 310 | * Strata. Rock is laid in beds, and a bed is a level in Tower — a constant radius — so |
| 311 | * the bands line up across separate mesas however far apart they stand, which is what |
| 312 | * makes a field of them read as one formation eroded out of one plateau (§4.5). |
| 313 | */ |
| 314 | float bed = sin(elevM * 0.11) * 0.5 + 0.5; |
| 315 | float thin = sin(elevM * 0.53) * 0.5 + 0.5; |
| 316 | vec3 warm = vec3(1.05, 0.99, 0.93); |
| 317 | float rocky = vMatA.w + vExtra.y; |
| 318 | base *= mix(vec3(1.0), mix(vec3(0.93), warm, bed * 0.7 + thin * 0.3), rocky * 0.6); |
| 319 | // Under drawn water the ground is bed: sediment, darker, whatever the land around it. |
| 320 | base = mix(base, vec3(0.16, 0.14, 0.11), vMatB.w * 0.8); |
| 321 | |
| 322 | // Unrolled surface coordinates, km. World xyz shears a pattern into radial streaks |
| 323 | // exactly where the ground is most grazing, which is most of the view. |
| 324 | vec2 surf = vec2(vWorld.z, atan(vWorld.y, vWorld.x) * ${R.toFixed(3)}); |
| 325 | /** |
| 326 | * Mottling, as the map has it (D-052): a broad layer that dims and brightens, and a |
| 327 | * fine one that pushes the channels apart so ground reads as patchy cover over |
| 328 | * soil rather than one colour turned up and down. Water is left alone. |
| 329 | */ |
| 330 | float land = 1.0 - vMatB.y; |
| 331 | float broad = vnoise(surf / 5.2); |
| 332 | float fine = vnoise(surf / 1.15 + 17.0); |
| 333 | float k = 0.9 + 0.2 * broad; |
| 334 | float mo = (fine - 0.5) * 0.13; |
| 335 | base *= mix(vec3(1.0), vec3(k - mo * 0.55, k + mo, k - mo * 0.25), land); |
| 336 | |
| 337 | /** |
| 338 | * Grain near the observer. Tiles near the eye have metre spacing, but a metre is |
| 339 | * still flat colour at your feet. This is texture, not terrain: it breaks up the |
| 340 | * block without claiming there is anything there, and fades out past 4 km. |
| 341 | */ |
| 342 | float near = 1.0 - smoothstep(0.4, 4.0, distance(uCam, vWorld)); |
| 343 | // Rotated and off-harmonic octaves. Axis-aligned value noise at one scale reads as |
| 344 | // a woven diamond lattice, which is worse than the flat colour it replaced. |
| 345 | vec2 rot = vec2(surf.x * 0.86 - surf.y * 0.51, surf.x * 0.51 + surf.y * 0.86); |
| 346 | float grain = vnoise(surf * 173.0) * 0.55 + vnoise(rot * 611.0) * 0.45; |
| 347 | base *= 1.0 + (grain - 0.5) * 0.12 * near * land; |
| 348 | |
| 349 | /** |
| 350 | * Farmland (farms.ts, D-095): the field this is in, its crop in rows over bare soil, the |
| 351 | * flowers or fruit among the leaves, and tracks between the fields. Close in a row is a |
| 352 | * row; past a few pixels a row, the crop is the mix it averages to. Coordinates are taken |
| 353 | * in the field's block, from its centre, which keeps the noise small enough for float32. |
| 354 | * A farm's edge follows its fields' (D-097), and is sharpened here so the tile's vertices |
| 355 | * do not smear it across a neighbour. |
| 356 | */ |
| 357 | float farmW = smoothstep(0.3, 0.7, vExtra.z); |
| 358 | int crop = 0; |
| 359 | if (farmW > 0.01) { |
| 360 | float zM = vWorld.z * 1000.0; |
| 361 | float aM = atan(vWorld.y, vWorld.x) * ${(R * 1000).toFixed(1)}; |
| 362 | if (aM < 0.0) aM += ${(2 * Math.PI * R * 1000).toFixed(1)}; |
| 363 | vec4 fld = farmField(zM, aM); |
| 364 | crop = int(fld.x + 0.5); |
| 365 | // Along the rows and across them, metres from the block's centre: small, so float32 holds. |
| 366 | vec2 local = fld.wz; |
| 367 | float foot = max(length(fwidth(vec2(zM, aM))), 0.01); |
| 368 | float rowM = CROP_ROW[crop]; |
| 369 | // Close in, a crop is plants: clumps and gaps at a hand's breadth, fading out by the |
| 370 | // time a pixel covers half a metre. |
| 371 | float closeIn = 1.0 - smoothstep(0.05, 0.5, foot); |
| 372 | float clump = vnoise(local / 0.12) * 0.5 + vnoise(local / 0.45 + 7.7) * 0.5; |
| 373 | float plant = 1.0 - 0.3 * smoothstep(0.62, 0.78, 1.0 - clump) * closeIn; |
| 374 | if (rowM > 0.0) { |
| 375 | float st = fract(local.y / rowM); |
| 376 | float band = smoothstep(0.12, 0.22, st) * (1.0 - smoothstep(0.78, 0.88, st)); |
| 377 | plant = mix(band * plant, 0.66, smoothstep(rowM * 0.25, rowM * 0.8, foot)); |
| 378 | } |
| 379 | float cover = CROP_COVER[crop]; |
| 380 | float blossom = vnoise(local / 0.35) * 0.6 + vnoise(local / 1.7 + 3.1) * 0.4; |
| 381 | float bloom = smoothstep(1.0 - cover - 0.08, 1.0 - cover + 0.08, blossom); |
| 382 | bloom = mix(bloom, cover, smoothstep(0.1, 0.6, foot)); |
| 383 | vec3 crops = mix(CROP_LEAF[crop], CROP_BLOOM[crop], bloom) * (1.0 + (clump - 0.5) * 0.5 * closeIn); |
| 384 | vec3 field = mix(FARM_SOIL, crops, plant); |
| 385 | field = mix(field, FARM_TRACK, 1.0 - smoothstep(-foot, foot, fld.y)); |
| 386 | base = mix(base, field, farmW); |
| 387 | } |
| 388 | |
| 389 | // Lit by the sun wherever on the axis it is (SUN_GLSL). The rest of the world is lit |
| 390 | // land, and lit land is what fills the shadows. |
| 391 | vec3 lit = base * (0.34 + 0.80 * direct(vWorld, normalize(vNormal2))) * uExposure * lightShare(vWorld); |
| 392 | /** |
| 393 | * Each crop on its own day (farms.ts \`fieldLight\`): lit as at noon from sunrise for as |
| 394 | * many hours as the plant wants, on into the night if it wants more than the sun gives; |
| 395 | * taken down to the rod's glow by day if it wants fewer; dark by night. |
| 396 | */ |
| 397 | if (farmW > 0.01 && uFarm.w > 0.5) { |
| 398 | vec3 litF = lit; |
| 399 | if (uFarm.x < CROP_DAY[crop]) litF = base * 1.14 * uExposure * uFarm.z; |
| 400 | else if (uFarm.x < uFarm.y) litF = lit * ${(ROD_GLOW_BY_DAY ** 0.35).toFixed(4)}; |
| 401 | lit = mix(lit, litF, farmW); |
| 402 | } |
| 403 | |
| 404 | vec3 T = towerTransmittance(uCam, vWorld); |
| 405 | gl_FragColor = vec4(lit * T + uHaze * uExposure * (1.0 - T), 1.0); |
| 406 | #include <colorspace_fragment> |
| 407 | } |
| 408 | ` |
| 409 | |
| 410 | /** |
| 411 | * Water (world-generation.md §4.2–§4.4). Every lake, sea and river is a surface at its own |
| 412 | * level, drawn over the ground with the depth test deciding the shoreline. |
| 413 | * |
| 414 | * Waves are a sum of four Gerstner-like components around the body's wind, sized from |
| 415 | * its wind and fetch in the worker (water.ts). They displace the surface only near the |
| 416 | * eye, where the vertices are close enough to carry them; everywhere they shade it, and |
| 417 | * each component fades out once it is smaller than a few pixels, so distant water is |
| 418 | * calm rather than a shimmer of aliasing. A river's flow scrolls ripples downstream. |
| 419 | * |
| 420 | * The surface coordinates are metres on the unrolled floor, taken from the tower-frame |
| 421 | * position, so the pattern is continuous across tiles and around theta. |
| 422 | */ |
| 423 | const WAVES_GLSL = /* glsl */` |
| 424 | uniform float uTime; |
| 425 | const float G = 9.80665; |
| 426 | // Generated from ships.ts, which runs the same sum so a ship rides the waves drawn under it. |
| 427 | const float WAVE_LEN[${WAVE_LEN.length}] = float[](${WAVE_LEN.map((v) => v.toFixed(3)).join(', ')}); |
| 428 | const float WAVE_AMP[${WAVE_LEN.length}] = float[](${WAVE_AMP.map((v) => v.toFixed(3)).join(', ')}); |
| 429 | const float WAVE_TURN[${WAVE_LEN.length}] = float[](${WAVE_TURN.map((v) => v.toFixed(3)).join(', ')}); |
| 430 | const float WAVE_PHASE[${WAVE_LEN.length}] = float[](${WAVE_PHASE.map((v) => v.toFixed(3)).join(', ')}); |
| 431 | |
| 432 | /** |
| 433 | * Surface coordinates, metres, measured from a reference point near the eye. |
| 434 | * |
| 435 | * Around the ring the obvious coordinate is atan(y, x) × R, but in float32 that is good |
| 436 | * to only about 6 m at Tower's radius: noise and waves finer than that smear into radial |
| 437 | * streaks. So the angle is taken from a reference direction instead, where it is small |
| 438 | * and exact, and the reference moves with the eye in whole steps of SURF_SNAP_M — a |
| 439 | * multiple of every noise period used here — so patterns never jump (setCamera). Waves |
| 440 | * add back what the reference's own position contributes to their phase (uWaveOffset). |
| 441 | */ |
| 442 | uniform vec4 uSurfRef; // cos and sin of the reference angle, its z in metres, unused |
| 443 | vec2 surfaceMetres(vec3 world) { |
| 444 | float da = atan(uSurfRef.x * world.y - uSurfRef.y * world.x, uSurfRef.x * world.x + uSurfRef.y * world.y); |
| 445 | return vec2(world.z * 1000.0 - uSurfRef.z, da * ${(R * 1000).toFixed(1)}); |
| 446 | } |
| 447 | uniform vec4 uBodies[${MAX_BODIES}]; |
| 448 | uniform float uWaveOffset[${MAX_BODIES * WAVE_LEN.length}]; |
| 449 | float waveOffset(int body, int i) { |
| 450 | return body < 0 ? 0.0 : uWaveOffset[body * ${WAVE_LEN.length} + i]; |
| 451 | } |
| 452 | |
| 453 | // Height of the wave sum at surface position p (metres), and its gradient, fading each |
| 454 | // component out below 'footprint' metres of wavelength. |
| 455 | vec3 waves(vec2 p, float amp, float len, float dir, int body, float footprint) { |
| 456 | vec3 out3 = vec3(0.0); |
| 457 | if (amp <= 0.0 || len <= 0.0) return out3; |
| 458 | for (int i = 0; i < ${WAVE_LEN.length}; i++) { |
| 459 | float L = len * WAVE_LEN[i]; |
| 460 | float fade = smoothstep(4.0, 16.0, L / max(footprint, 1e-3)); |
| 461 | if (fade <= 0.0) continue; |
| 462 | float a = amp * WAVE_AMP[i] * fade; |
| 463 | float th = dir + WAVE_TURN[i]; |
| 464 | vec2 d = vec2(cos(th), sin(th)); |
| 465 | float k = 6.2831853 / L; |
| 466 | float w = sqrt(G * k); |
| 467 | // Time wraps every ten minutes: long enough that nobody sees it, short enough that |
| 468 | // float precision in the phase never degrades. |
| 469 | float ph = k * dot(d, p) - w * mod(uTime, 600.0) + WAVE_PHASE[i] + waveOffset(body, i); |
| 470 | out3.x += a * cos(ph); |
| 471 | out3.yz -= a * k * sin(ph) * d; |
| 472 | } |
| 473 | return out3; |
| 474 | } |
| 475 | |
| 476 | /** |
| 477 | * A water vertex's waves — amplitude, wavelength, direction, and the fraction of the |
| 478 | * surface in whitecaps. A river carries its own chop in the attribute and never breaks; a |
| 479 | * lake or sea carries -(index + 1), and its sea state, which follows the weather, is |
| 480 | * looked up here (sea-state.ts). |
| 481 | */ |
| 482 | /** |
| 483 | * The wind's own short sea alone, -1 trough to 1 crest: where whitecaps break. Keyed to |
| 484 | * these rather than the whole sum, which a storm's 600 m swell dominates, so caps spread |
| 485 | * over the sea instead of lining up along the swell's crests. |
| 486 | */ |
| 487 | float chop(vec2 p, float amp, float len, float dir, int body) { |
| 488 | if (amp <= 0.0 || len <= 0.0) return 0.0; |
| 489 | float h = 0.0, total = 0.0; |
| 490 | for (int i = ${WAVE_LEN.length - 3}; i < ${WAVE_LEN.length}; i++) { |
| 491 | float L = len * WAVE_LEN[i]; |
| 492 | float th = dir + WAVE_TURN[i]; |
| 493 | float k = 6.2831853 / L; |
| 494 | float ph = k * dot(vec2(cos(th), sin(th)), p) - sqrt(G * k) * mod(uTime, 600.0) + WAVE_PHASE[i] + waveOffset(body, i); |
| 495 | h += WAVE_AMP[i] * cos(ph); |
| 496 | total += WAVE_AMP[i]; |
| 497 | } |
| 498 | return h / total; |
| 499 | } |
| 500 | |
| 501 | int bodyOf(vec4 water) { return water.y >= 0.0 ? -1 : int(-water.y - 0.5); } |
| 502 | vec4 waveParams(vec4 water) { |
| 503 | if (water.y >= 0.0) return vec4(water.yzw, 0.0); |
| 504 | return uBodies[bodyOf(water)]; |
| 505 | } |
| 506 | ` |
| 507 | |
| 508 | /** |
| 509 | * Sea ice (D-090): the pack's floes and the fast ice's cracks, on the Deepward Sea's rings. |
| 510 | * |
| 511 | * A floe is a cell of a jittered grid — a Voronoi cell, which is the shape ice breaks into: |
| 512 | * straight edges, meeting at odd angles. Each cell is ice or open water by its own hash |
| 513 | * against the cover the tile carries (field.ts \`seaIce\`), so the pack loosens toward its |
| 514 | * edge on its own. Where two ice cells meet there is a crack; where ice meets open water |
| 515 | * the floe stops short of the cell's edge, which opens the leads. |
| 516 | * |
| 517 | * Floes are 110 m and bigger, so unlike the waves they need no surface reference: absolute |
| 518 | * metres along the axis and around are good to centimetres in float32. The grid around is a |
| 519 | * whole number of cells, so the world closes on itself without a seam, and the hash is a |
| 520 | * permutation mod 289 — exact in float, so the same floe on every GPU. |
| 521 | */ |
| 522 | /** The frost table: a texel per lattice cell, with room for every pane's (a, b) (frost.ts). */ |
| 523 | const FROST_SIZE = 32 |
| 524 | const FROST_OFFSET = 16 |
| 525 | |
| 526 | const FLOE_M = 110 |
| 527 | const ICE_LINEAR = `vec3(${ICE.map((c) => ((c / 255) ** 2.2).toFixed(4)).join(', ')})` |
| 528 | const FLOES_AROUND = Math.round((2 * Math.PI * R * 1000) / FLOE_M) |
| 529 | const FAST_CRACK_M = 660 |
| 530 | const FAST_AROUND = Math.round((2 * Math.PI * R * 1000) / FAST_CRACK_M) |
| 531 | const ICE_GLSL = /* glsl */` |
| 532 | float icePermute(float x) { return mod((34.0 * x + 1.0) * x, 289.0); } |
| 533 | float iceHash(vec2 c, float salt) { |
| 534 | vec2 m = mod(c, 289.0); |
| 535 | return icePermute(icePermute(icePermute(m.x) + m.y) + salt) / 289.0; |
| 536 | } |
| 537 | /** Waves under ice: the pack damps the sea, and fast ice stills it. */ |
| 538 | float iceCalm(float cover) { return 1.0 - smoothstep(0.0, 0.6, cover); } |
| 539 | |
| 540 | /** |
| 541 | * The two nearest cell centres to a point, in cells: x, y the nearest cell, z the distance |
| 542 | * to the edge between it and the second nearest (in cells), w that second cell's index |
| 543 | * packed as offset (0..8). |
| 544 | */ |
| 545 | vec4 iceCells(vec2 x, float around, out vec2 second) { |
| 546 | vec2 n = floor(x), f = fract(x); |
| 547 | float d1 = 9.0, d2 = 9.0; |
| 548 | vec2 c1 = n, c2 = n, p1 = vec2(0.0), p2 = vec2(0.0); |
| 549 | for (int j = -1; j <= 1; j++) { |
| 550 | for (int i = -1; i <= 1; i++) { |
| 551 | vec2 g = vec2(float(i), float(j)); |
| 552 | vec2 c = n + g; |
| 553 | c.y = mod(c.y, around); |
| 554 | vec2 pt = g + 0.1 + 0.8 * vec2(iceHash(c, 11.0), iceHash(c, 23.0)); |
| 555 | float d = dot(pt - f, pt - f); |
| 556 | if (d < d1) { d2 = d1; c2 = c1; p2 = p1; d1 = d; c1 = c; p1 = pt; } |
| 557 | else if (d < d2) { d2 = d; c2 = c; p2 = pt; } |
| 558 | } |
| 559 | } |
| 560 | // Distance to the bisector between the two nearest centres: the edge between the cells. |
| 561 | vec2 mid = 0.5 * (p1 + p2); |
| 562 | float edge = dot(mid - f, normalize(p2 - p1)); |
| 563 | second = c2; |
| 564 | return vec4(c1, edge, 0.0); |
| 565 | } |
| 566 | |
| 567 | /** |
| 568 | * The ice at a point of the sea: x how much of the pixel is ice, y how dark with cracks, |
| 569 | * z the shade of the floe it is on. \`cover\` is the tile's, 1 on fast ice. |
| 570 | */ |
| 571 | vec3 seaIce(vec3 world, float cover, float footprint) { |
| 572 | vec2 m = vec2(world.z * 1000.0, atan(world.y, world.x) * ${(R * 1000).toFixed(1)}); |
| 573 | if (m.y < 0.0) m.y += ${(2 * Math.PI * R * 1000).toFixed(1)}; |
| 574 | float fast = step(0.999, cover); |
| 575 | vec3 out3 = vec3(0.0, 0.0, 1.0); |
| 576 | |
| 577 | // The pack: floes over open leads. |
| 578 | vec2 x = vec2(m.x / ${FLOE_M.toFixed(1)}, m.y / ${(2 * Math.PI * R * 1000 / FLOES_AROUND).toFixed(4)}); |
| 579 | vec2 c2; |
| 580 | vec4 v = iceCells(x, ${FLOES_AROUND.toFixed(1)}, c2); |
| 581 | bool ice1 = fast > 0.5 || iceHash(v.xy, 37.0) < cover; |
| 582 | bool ice2 = fast > 0.5 || iceHash(c2, 37.0) < cover; |
| 583 | float edgeM = v.z * ${FLOE_M.toFixed(1)}; |
| 584 | float aa = max(footprint, 0.05); |
| 585 | // Leads open as the pack loosens: floes stop short of the cell edge by up to 12 m. |
| 586 | float lead = mix(12.0, 1.0, cover); |
| 587 | float onIce = ice1 ? (ice2 ? 1.0 : smoothstep(lead - aa, lead + aa, edgeM)) : 0.0; |
| 588 | // A crack where two floes meet: a metre and a half of darker, wetter ice. |
| 589 | float crack = (ice1 && ice2 && fast < 0.5) ? 1.0 - smoothstep(0.0, 1.5 + aa, edgeM) : 0.0; |
| 590 | // Each floe its own white: older floes whiter, younger greyer toward the edge of the pack. |
| 591 | float shade = 0.84 + 0.16 * iceHash(v.xy, 51.0); |
| 592 | shade *= mix(0.72, 1.0, smoothstep(0.3, 0.8, cover)); |
| 593 | |
| 594 | // Far off, a floe is under a pixel: the sea whitens by its share of ice instead. |
| 595 | float far = smoothstep(${(FLOE_M * 0.12).toFixed(1)}, ${(FLOE_M * 0.6).toFixed(1)}, footprint); |
| 596 | float share = fast > 0.5 ? 1.0 : cover * (1.0 - 0.02 * lead); |
| 597 | out3.x = mix(onIce, share, far); |
| 598 | out3.y = mix(crack, 0.0, far); |
| 599 | out3.z = mix(shade, 0.9 * mix(0.72, 1.0, smoothstep(0.3, 0.8, cover)), far); |
| 600 | |
| 601 | // The fast ice is one sheet, cracked across in long lines and ridged where it pressed. |
| 602 | if (fast > 0.5) { |
| 603 | vec2 xb = vec2(m.x / ${FAST_CRACK_M.toFixed(1)}, m.y / ${(2 * Math.PI * R * 1000 / FAST_AROUND).toFixed(4)}); |
| 604 | vec2 cb; |
| 605 | vec4 vb = iceCells(xb, ${FAST_AROUND.toFixed(1)}, cb); |
| 606 | float line = 1.0 - smoothstep(0.0, 2.0 + aa, vb.z * ${FAST_CRACK_M.toFixed(1)}); |
| 607 | out3.y = max(out3.y, line * 0.6 * (1.0 - smoothstep(20.0, 120.0, footprint))); |
| 608 | out3.z = 0.9 + 0.1 * iceHash(vb.xy, 51.0); |
| 609 | } |
| 610 | return out3; |
| 611 | } |
| 612 | ` |
| 613 | |
| 614 | const WATER_VERT = /* glsl */` |
| 615 | #include <common> |
| 616 | #include <logdepthbuf_pars_vertex> |
| 617 | uniform mat4 uFrame; |
| 618 | uniform vec3 uCam; |
| 619 | attribute vec4 aWater; |
| 620 | attribute vec4 aFlow; |
| 621 | varying vec3 vWorld; |
| 622 | varying vec4 vWater; |
| 623 | varying vec4 vFlow; |
| 624 | ${WAVES_GLSL} |
| 625 | ${ICE_GLSL} |
| 626 | void main() { |
| 627 | vec3 world = (uFrame * modelMatrix * vec4(position, 1.0)).xyz; |
| 628 | // Displace only close in, where vertices are metres apart; beyond, waves are shading. |
| 629 | float near = 1.0 - smoothstep(0.3, 2.5, distance(uCam, world)); |
| 630 | vec3 p = position; |
| 631 | vec4 wp = waveParams(aWater); |
| 632 | if (near > 0.0 && wp.x > 0.0) { |
| 633 | float h = waves(surfaceMetres(world), wp.x, wp.y, wp.z, bodyOf(aWater), 0.5).x; |
| 634 | h *= iceCalm(aFlow.w); |
| 635 | // Up is toward the axis. The tile mesh sits in the tower's frame, so its model |
| 636 | // matrix is a translation and 'up' can be taken from the world position directly. |
| 637 | vec3 up = -normalize(vec3(world.xy, 0.0)); |
| 638 | p += up * (h * near / 1000.0); |
| 639 | world += up * (h * near / 1000.0); |
| 640 | } |
| 641 | vWorld = world; |
| 642 | vWater = aWater; |
| 643 | vFlow = aFlow; |
| 644 | gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0); |
| 645 | #include <logdepthbuf_vertex> |
| 646 | } |
| 647 | ` |
| 648 | |
| 649 | const WATER_FRAG = /* glsl */` |
| 650 | #include <common> |
| 651 | #include <logdepthbuf_pars_fragment> |
| 652 | varying vec3 vWorld; |
| 653 | varying vec4 vWater; |
| 654 | varying vec4 vFlow; |
| 655 | ${HAZE_GLSL} |
| 656 | ${SUN_GLSL} |
| 657 | ${WAVES_GLSL} |
| 658 | ${ICE_GLSL} |
| 659 | |
| 660 | // Periodic every 256 cells, which is what lets surfaceMetres' reference step without a seam. |
| 661 | float wHash(vec2 p) { p = mod(p, 256.0); return fract(sin(dot(p, vec2(41.3, 289.1))) * 43758.5453); } |
| 662 | float wNoise(vec2 p) { |
| 663 | vec2 i = floor(p), f = fract(p); |
| 664 | f = f * f * (3.0 - 2.0 * f); |
| 665 | return mix(mix(wHash(i), wHash(i + vec2(1, 0)), f.x), mix(wHash(i + vec2(0, 1)), wHash(i + vec2(1, 1)), f.x), f.y); |
| 666 | } |
| 667 | |
| 668 | void main() { |
| 669 | #include <logdepthbuf_fragment> |
| 670 | vec2 sp = surfaceMetres(vWorld); |
| 671 | // Metres of floor under one pixel: what decides which waves can still be drawn. |
| 672 | float footprint = length(fwidth(sp)); |
| 673 | vec3 radial = normalize(vec3(vWorld.xy, 0.0)); |
| 674 | vec3 up = -radial; |
| 675 | vec3 spinward = vec3(-radial.y, radial.x, 0.0); |
| 676 | vec3 deepward = vec3(0.0, 0.0, 1.0); |
| 677 | |
| 678 | vec4 wp = waveParams(vWater); |
| 679 | vec3 w = waves(sp, wp.x, wp.y, wp.z, bodyOf(vWater), footprint); |
| 680 | vec2 grad = w.yz * iceCalm(vFlow.w); |
| 681 | |
| 682 | /** |
| 683 | * Flow: ripples carried downstream. Two copies of a noise field advected by the flow |
| 684 | * and cross-faded on a short cycle, so the pattern moves without ever stretching out. |
| 685 | */ |
| 686 | vec2 flow = vFlow.xy; |
| 687 | float speed = length(flow); |
| 688 | if (speed > 0.01) { |
| 689 | float cycle = 2.0; |
| 690 | float t = mod(uTime, cycle) / cycle; |
| 691 | vec2 q = sp / 1.6; |
| 692 | vec2 off = flow * cycle / 1.6; |
| 693 | float blend = abs(t * 2.0 - 1.0); |
| 694 | float scale = smoothstep(6.0, 1.0, footprint); |
| 695 | for (int k = 0; k < 2; k++) { |
| 696 | float tk = fract(t + float(k) * 0.5); |
| 697 | vec2 at = q - off * tk + float(k) * 7.3; |
| 698 | float e = 0.05; |
| 699 | float n0 = wNoise(at); |
| 700 | vec2 g = vec2(wNoise(at + vec2(e, 0.0)) - n0, wNoise(at + vec2(0.0, e)) - n0) / e; |
| 701 | float wk = k == 0 ? blend : 1.0 - blend; |
| 702 | grad += g * 0.06 * wk * scale * min(1.0, speed); |
| 703 | } |
| 704 | } |
| 705 | |
| 706 | vec3 n = normalize(up - deepward * grad.x - spinward * grad.y); |
| 707 | vec3 view = normalize(vWorld - uCam); |
| 708 | float cosV = max(0.0, dot(-view, n)); |
| 709 | // Schlick, with water's 2% at normal incidence. |
| 710 | float fresnel = 0.02 + 0.98 * pow(1.0 - cosV, 5.0); |
| 711 | |
| 712 | float light = uExposure * (0.4 + 0.6 * direct(vWorld, up)) * lightShare(vWorld); |
| 713 | |
| 714 | // Deep water is dark blue-green; what it reflects inside Tower is the far side of the |
| 715 | // world, which from here is haze-blue land. |
| 716 | vec3 deep = vec3(0.018, 0.045, 0.06); |
| 717 | vec3 body = deep * (0.5 + 0.5 * max(0.0, dot(n, up))) * light; |
| 718 | // What water mirrors in Tower is land, hazed — never an open sky — so it is not pure haze. |
| 719 | vec3 sky = mix(uHaze, vec3(0.30, 0.45, 0.56), 0.35) * light * 0.95; |
| 720 | vec3 col = mix(body, sky, fresnel); |
| 721 | |
| 722 | // Water glints where it mirrors the sun back — and by night the banked rod, a line |
| 723 | // down the axis. The rod's is kept faint: at full strength a rippled river wore the |
| 724 | // glitter as a white snake. |
| 725 | vec3 refl = reflect(view, n); |
| 726 | float toAxis = max(0.0, dot(normalize(vec3(refl.xy, 0.0) + 1e-6), up)); |
| 727 | col += vec3(1.0, 0.95, 0.85) * pow(toAxis, 5000.0) * 0.45 * light * uSun.z; |
| 728 | col += vec3(1.0, 0.96, 0.88) * pow(max(dot(refl, sunDir(vWorld)), 0.0), 2000.0) * 2.5 * uSun.y * uExposure * aimedShare(vWorld.z); |
| 729 | // And the moon, on a moonlit night: a path of light across the sea toward it. |
| 730 | col += vec3(0.85, 0.9, 1.0) * pow(max(dot(refl, moonDir(vWorld)), 0.0), 1500.0) * 1.2 * uMoon.y * uExposure * aimedShare(vWorld.z); |
| 731 | |
| 732 | // Foam: rapids and falls, and the edge where the water is only centimetres deep. |
| 733 | float depth = vWater.x; |
| 734 | float shore = 1.0 - smoothstep(0.0, 0.6, depth); |
| 735 | float churn = vFlow.z * (0.55 + 0.45 * wNoise(sp / 0.8 - flow * mod(uTime, 60.0) / 0.8)); |
| 736 | float foam = clamp(max(churn, shore * 0.6 * step(0.001, wp.x)), 0.0, 1.0); |
| 737 | /** |
| 738 | * Whitecaps: the crests break over the share of the sea the wind says (sea-state.ts), |
| 739 | * broken up so they read as patches rather than stripes along every crest. Far off, where |
| 740 | * single caps are under a pixel, the sea just whitens by that share. |
| 741 | */ |
| 742 | if (wp.w > 0.0) { |
| 743 | float crest = chop(sp, wp.x, wp.y, wp.z, bodyOf(vWater)) * 0.5 + 0.5; |
| 744 | float drift = mod(uTime, 600.0); |
| 745 | float patches = wNoise(sp / 9.6 + vec2(drift * 0.05, 0.0)) * 0.65 + wNoise(sp / 2.4 - drift * 0.2) * 0.35; |
| 746 | // The threshold is set so about wp.w of the surface clears it. |
| 747 | // Crest and patches together sit near 0.58 ± 0.18: 0.92 clears ~2% of it, 0.64 ~29%. |
| 748 | float t = 1.01 - 0.68 * sqrt(wp.w); |
| 749 | float cap = smoothstep(t, t + 0.06, crest * 0.7 + patches * 0.45); |
| 750 | // Foam is lace, not a sheet: broken up finely, and thinner toward the cap's edge. |
| 751 | /** |
| 752 | * Resolved only close in: beyond, its metre-scale cells drew as blocks, so it averages |
| 753 | * out. Two octaves, the second rotated, because one octave of value noise thresholded |
| 754 | * this hard reads as a honeycomb of its own lattice. |
| 755 | */ |
| 756 | vec2 lp = vec2(sp.x * 0.83 - sp.y * 0.56, sp.x * 0.56 + sp.y * 0.83); |
| 757 | float fine = wNoise(sp / 0.8 + drift * 0.1) * 0.6 + wNoise(lp / 2.1 - drift * 0.05) * 0.4; |
| 758 | float lace = mix(smoothstep(0.3, 0.8, fine), 0.5, smoothstep(0.05, 0.4, footprint)); |
| 759 | cap *= lace * 0.5 + 0.5 * cap; |
| 760 | float far = smoothstep(1.0, 8.0, footprint); |
| 761 | foam = max(foam, mix(cap, wp.w * 0.5, far) * 0.8); |
| 762 | } |
| 763 | col = mix(col, vec3(0.85, 0.88, 0.9) * light * (0.34 + 0.8 * dot(n, up)), foam); |
| 764 | |
| 765 | /** |
| 766 | * Sea ice (D-090): white over the water where the floes are, with the leads between them |
| 767 | * open and dark, and the fast ice one cracked sheet. Matte — snow on ice mirrors nothing — |
| 768 | * and lit like the foam, by the same light, which at the far end is a dusk. |
| 769 | */ |
| 770 | float ice = 0.0; |
| 771 | if (vFlow.w > 0.001) { |
| 772 | vec3 floe = seaIce(vWorld, vFlow.w, footprint); |
| 773 | ice = floe.x; |
| 774 | vec3 white = ${ICE_LINEAR} * floe.z * (1.0 - 0.45 * floe.y); |
| 775 | // Snow on it, drifted: a mottle close in, where a metre is a few pixels. |
| 776 | float drift = wNoise(sp / 2.4) * 0.6 + wNoise(sp / 9.6) * 0.4; |
| 777 | white *= 1.0 + (drift - 0.5) * 0.12 * (1.0 - smoothstep(0.5, 4.0, footprint)); |
| 778 | col = mix(col, white * light * 1.1, ice); |
| 779 | } |
| 780 | |
| 781 | // How much ground shows through: shallow water is clear, a few metres is not. |
| 782 | float alpha = clamp(1.0 - exp(-depth / 3.5), 0.2, 0.97); |
| 783 | alpha = max(alpha, fresnel); |
| 784 | alpha = max(alpha, foam); |
| 785 | alpha = max(alpha, ice); |
| 786 | |
| 787 | vec3 T = towerTransmittance(uCam, vWorld); |
| 788 | gl_FragColor = vec4(col * T + uHaze * uExposure * (1.0 - T), alpha); |
| 789 | #include <colorspace_fragment> |
| 790 | } |
| 791 | ` |
| 792 | |
| 793 | /** |
| 794 | * The forest canopy, seen from a distance (tile.ts `buildCanopy`, world-generation.md §4.6). |
| 795 | * |
| 796 | * Greatwood holds about three billion trees, so past a kilometre the *ground* under them is |
| 797 | * this: a skin over the tile, lifted by the canopy's height, with a top left rough by the |
| 798 | * tile. Clumps of trees stand in front of it out to nearly three kilometres |
| 799 | * (forest-view.ts) and it fills what they leave between them — a stand of cards seen from |
| 800 | * above is all gaps, and the wood has to be closed. Coming closer, the skin **sinks back |
| 801 | * onto the ground it was built from** and stops being drawn, where the trees themselves |
| 802 | * have taken over. |
| 803 | */ |
| 804 | const CANOPY_NEAR_M = 620 |
| 805 | const CANOPY_FAR_M = 950 |
| 806 | |
| 807 | const CANOPY_VERT = /* glsl */` |
| 808 | #include <common> |
| 809 | #include <logdepthbuf_pars_vertex> |
| 810 | uniform mat4 uFrame; |
| 811 | uniform vec3 uCam; |
| 812 | attribute vec3 aCanopy; |
| 813 | varying vec3 vWorld; |
| 814 | varying vec3 vCanopy; |
| 815 | varying float vLift; |
| 816 | void main() { |
| 817 | vec3 world = (uFrame * modelMatrix * vec4(position, 1.0)).xyz; |
| 818 | float lift = smoothstep(${CANOPY_NEAR_M.toFixed(1)}, ${CANOPY_FAR_M.toFixed(1)}, distance(uCam, world) * 1000.0); |
| 819 | vec3 up = -normalize(vec3(world.xy, 0.0)); |
| 820 | vec3 p = position + up * (aCanopy.x * lift / 1000.0); |
| 821 | vWorld = world + up * (aCanopy.x * lift / 1000.0); |
| 822 | vCanopy = aCanopy; |
| 823 | vLift = lift; |
| 824 | gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0); |
| 825 | #include <logdepthbuf_vertex> |
| 826 | } |
| 827 | ` |
| 828 | |
| 829 | const CANOPY_FRAG = /* glsl */` |
| 830 | #include <common> |
| 831 | #include <logdepthbuf_pars_fragment> |
| 832 | varying vec3 vWorld; |
| 833 | varying vec3 vCanopy; |
| 834 | varying float vLift; |
| 835 | ${HAZE_GLSL} |
| 836 | ${SUN_GLSL} |
| 837 | float canopyHash(vec2 p) { |
| 838 | return fract(sin(dot(mod(p, 512.0), vec2(37.1, 91.7))) * 24634.6345); |
| 839 | } |
| 840 | |
| 841 | void main() { |
| 842 | #include <logdepthbuf_fragment> |
| 843 | if (vLift < 0.03) discard; |
| 844 | // The shape of the canopy is the shape of this surface: its normal comes from the |
| 845 | // surface itself, so the rough top the tile built reads as tree crowns rather than felt. |
| 846 | vec3 n = normalize(cross(dFdx(vWorld), dFdy(vWorld))); |
| 847 | vec3 radial = normalize(vec3(vWorld.xy, 0.0)); |
| 848 | if (dot(n, -radial) < 0.0) n = -n; |
| 849 | float lambert = direct(vWorld, n); |
| 850 | |
| 851 | /** |
| 852 | * Seen from underneath — standing in the wood, looking out through it — a solid skin is |
| 853 | * a black ceiling over everything past the last drawn tree. A canopy is not solid: a |
| 854 | * third of it is gaps between crowns, and that is what lets you see any distance through |
| 855 | * a forest at all. So from below it is opened up, on a pattern fixed to the ground so it |
| 856 | * reads as gaps rather than as static on the screen. |
| 857 | */ |
| 858 | vec3 toEye = normalize(uCam - vWorld); |
| 859 | bool under = dot(n, toEye) < 0.0; |
| 860 | if (under) { |
| 861 | vec2 sp = vec2(vWorld.z * 1000.0, atan(vWorld.y, vWorld.x) * ${(R * 1000).toFixed(1)}); |
| 862 | if (canopyHash(floor(sp / 9.0)) > 0.45) discard; |
| 863 | } |
| 864 | |
| 865 | /** |
| 866 | * Closed canopy is the darkest ground in Tower — a crown swallows most of what lands on |
| 867 | * it, which is why a forest reads as a dark mass and its clearings as bright grass. So |
| 868 | * this has to come out darker than the forest floor the palette paints in the meadows, |
| 869 | * or the wood looks like mist lying over the grass, which is what it looked like first. |
| 870 | */ |
| 871 | vec3 conifer = vec3(0.037, 0.067, 0.043); |
| 872 | vec3 broadleaf = vec3(0.070, 0.098, 0.040); |
| 873 | vec3 base = mix(broadleaf, conifer, clamp(vCanopy.z, 0.0, 1.0)); |
| 874 | // Thinner stands let the floor show through, so they are lighter than closed canopy. |
| 875 | base *= 1.0 + 0.35 * (1.0 - clamp(vCanopy.y, 0.0, 1.0)); |
| 876 | // Under the canopy the light is what has come through it: green, and a tenth as much. |
| 877 | vec3 lit = base * (under ? 0.55 : 0.22 + 0.90 * lambert) * uExposure * lightShare(vWorld); |
| 878 | |
| 879 | vec3 T = towerTransmittance(uCam, vWorld); |
| 880 | gl_FragColor = vec4(lit * T + uHaze * uExposure * (1.0 - T), 1.0); |
| 881 | #include <colorspace_fragment> |
| 882 | } |
| 883 | ` |
| 884 | |
| 885 | /** |
| 886 | * Trees near the eye (forest-view.ts): instanced meshes of bark and foliage, lit from the |
| 887 | * axis like everything else. The kind of foliage is carried by the geometry, since all three |
| 888 | * tree meshes share this one material. |
| 889 | */ |
| 890 | const FOLIAGE_GLSL = /* glsl */` |
| 891 | vec3 foliageColour(float part, float tint) { |
| 892 | vec3 bark = vec3(0.055, 0.037, 0.025); |
| 893 | vec3 conifer = vec3(0.030, 0.062, 0.038); |
| 894 | vec3 broadleaf = vec3(0.062, 0.098, 0.035); |
| 895 | vec3 giant = vec3(0.035, 0.058, 0.030); |
| 896 | vec3 c = part < 0.5 ? bark : part < 1.5 ? conifer : part < 2.5 ? broadleaf : giant; |
| 897 | // No two trees the same: a little lighter or darker, and a little yellower or bluer. |
| 898 | return c * (0.78 + 0.44 * tint) + vec3(0.006, 0.004, -0.004) * (tint - 0.5); |
| 899 | } |
| 900 | ` |
| 901 | |
| 902 | const TREE_VERT = /* glsl */` |
| 903 | #include <common> |
| 904 | #include <logdepthbuf_pars_vertex> |
| 905 | uniform mat4 uFrame; |
| 906 | attribute float aPart; |
| 907 | varying vec3 vWorld; |
| 908 | varying vec3 vN; |
| 909 | varying float vPart; |
| 910 | varying float vTint; |
| 911 | void main() { |
| 912 | mat4 m = modelMatrix * instanceMatrix; |
| 913 | vWorld = (uFrame * m * vec4(position, 1.0)).xyz; |
| 914 | vN = mat3(uFrame * m) * normal; |
| 915 | vPart = aPart; |
| 916 | // The instance's own position is as good a per-tree number as any. |
| 917 | vTint = fract(dot(instanceMatrix[3].xyz, vec3(17.3, 13.7, 29.1))); |
| 918 | gl_Position = projectionMatrix * modelViewMatrix * instanceMatrix * vec4(position, 1.0); |
| 919 | #include <logdepthbuf_vertex> |
| 920 | } |
| 921 | ` |
| 922 | |
| 923 | const TREE_FRAG = /* glsl */` |
| 924 | #include <common> |
| 925 | #include <logdepthbuf_pars_fragment> |
| 926 | varying vec3 vWorld; |
| 927 | varying vec3 vN; |
| 928 | varying float vPart; |
| 929 | varying float vTint; |
| 930 | ${HAZE_GLSL} |
| 931 | ${SUN_GLSL} |
| 932 | ${FOLIAGE_GLSL} |
| 933 | void main() { |
| 934 | #include <logdepthbuf_fragment> |
| 935 | vec3 n = normalize(vN); |
| 936 | // A crown is lit on the side facing the sun, and its other side is its shadow. |
| 937 | float lambert = direct(vWorld, n); |
| 938 | // Ambient is high for foliage: under a crown the light is what the crowns around it have |
| 939 | // bounced, and a canopy with none of that reads as pitch black rather than as shade. |
| 940 | vec3 lit = foliageColour(vPart, vTint) * (0.34 + 0.85 * lambert) * uExposure * lightShare(vWorld); |
| 941 | vec3 T = towerTransmittance(uCam, vWorld); |
| 942 | gl_FragColor = vec4(lit * T + uHaze * uExposure * (1.0 - T), 1.0); |
| 943 | #include <colorspace_fragment> |
| 944 | } |
| 945 | ` |
| 946 | |
| 947 | /** |
| 948 | * A tree as a card: one quad, turned to face the eye about the tree's own upright, with its |
| 949 | * shape cut out of it in the fragment — a cone for a conifer, a round crown for a broadleaf, |
| 950 | * a column and a high crown for a giant. No texture: at this distance a tree is a few pixels |
| 951 | * and its silhouette is all of it that survives. |
| 952 | */ |
| 953 | const CARD_VERT = /* glsl */` |
| 954 | #include <common> |
| 955 | #include <logdepthbuf_pars_vertex> |
| 956 | uniform mat4 uFrame; |
| 957 | uniform vec3 uCam; |
| 958 | attribute vec2 aCard; |
| 959 | varying vec3 vWorld; |
| 960 | varying vec2 vUv; |
| 961 | varying vec2 vCard; |
| 962 | void main() { |
| 963 | vec3 base = (modelMatrix * vec4(instanceMatrix[3].xyz, 1.0)).xyz; |
| 964 | vec3 world = (uFrame * vec4(base, 1.0)).xyz; |
| 965 | // Up is toward the axis; the card turns about it to face the eye and never leans. |
| 966 | vec3 up = -normalize(vec3(world.xy, 0.0)); |
| 967 | vec3 toEye = uCam - world; |
| 968 | vec3 right = normalize(cross(toEye, up)); |
| 969 | // The tree stands on the ground, so the card grows along up from its foot. |
| 970 | vec3 p = instanceMatrix[3].xyz |
| 971 | + right * (position.x * instanceMatrix[0][0]) |
| 972 | + up * (position.y * instanceMatrix[1][1]); |
| 973 | vWorld = (uFrame * modelMatrix * vec4(p, 1.0)).xyz; |
| 974 | vUv = uv; |
| 975 | vCard = aCard; |
| 976 | gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0); |
| 977 | #include <logdepthbuf_vertex> |
| 978 | } |
| 979 | ` |
| 980 | |
| 981 | const CARD_FRAG = /* glsl */` |
| 982 | #include <common> |
| 983 | #include <logdepthbuf_pars_fragment> |
| 984 | varying vec3 vWorld; |
| 985 | varying vec2 vUv; |
| 986 | varying vec2 vCard; |
| 987 | ${HAZE_GLSL} |
| 988 | ${SUN_GLSL} |
| 989 | ${FOLIAGE_GLSL} |
| 990 | void main() { |
| 991 | #include <logdepthbuf_fragment> |
| 992 | float u = vUv.x - 0.5, v = vUv.y; |
| 993 | float kind = vCard.x; |
| 994 | bool inside; |
| 995 | if (kind > 2.5) { |
| 996 | /** |
| 997 | * A clump: everything in twenty-odd metres of wood, drawn as one card. Three crowns |
| 998 | * over a solid band, so its outline is lumpy the way a stand of trees is, rather than |
| 999 | * the single cone a lone tree would make at this distance. |
| 1000 | */ |
| 1001 | float bump = 0.0; |
| 1002 | bump = max(bump, 0.62 * (1.0 - pow(abs((u + 0.30) / 0.24), 1.7))); |
| 1003 | bump = max(bump, 0.95 * (1.0 - pow(abs((u - 0.02) / 0.26), 1.7))); |
| 1004 | bump = max(bump, 0.70 * (1.0 - pow(abs((u - 0.31) / 0.22), 1.7))); |
| 1005 | // A little noise on the tops, so a row of clumps is not a row of the same shape. |
| 1006 | bump *= 0.86 + 0.28 * fract(sin(vCard.y * 91.7 + u * 13.3) * 4371.3); |
| 1007 | inside = v < bump; |
| 1008 | } else if (kind > 1.5) { |
| 1009 | // A giant: a bare column for half its height, and a crown above. |
| 1010 | float crownHalf = 0.5 * (1.0 - smoothstep(0.55, 1.0, v)); |
| 1011 | inside = v < 0.55 ? abs(u) < 0.1 : abs(u) < crownHalf; |
| 1012 | } else if (kind > 0.5) { |
| 1013 | // A conifer: a narrow cone on a stem. |
| 1014 | inside = v < 0.16 ? abs(u) < 0.08 : abs(u) < 0.5 * (1.0 - (v - 0.16) / 0.84); |
| 1015 | } else { |
| 1016 | // A broadleaf: a round crown over a short stem. |
| 1017 | float dy = (v - 0.68) / 0.34; |
| 1018 | inside = v < 0.4 ? abs(u) < 0.07 : (u * u) / 0.25 + dy * dy < 1.0; |
| 1019 | } |
| 1020 | if (!inside) discard; |
| 1021 | float part = kind > 2.5 ? 1.0 : kind > 1.5 ? 3.0 : kind > 0.5 ? 1.0 : 2.0; |
| 1022 | // A clump is all crown: no trunk of its own, since it is a stand and not a tree. |
| 1023 | if (kind < 2.5 && v < (kind > 1.5 ? 0.55 : kind > 0.5 ? 0.16 : 0.4)) part = 0.0; |
| 1024 | // Lit from overhead: the top of a crown is bright and its foot is in its own shade. |
| 1025 | float shade = 0.22 + 0.52 * smoothstep(0.0, 0.9, v); |
| 1026 | // As bright as a tree mesh's crown at the handover: the same sun, the same curve. |
| 1027 | float sun = (0.34 + 0.85 * direct(vWorld, upAt(vWorld))) / 1.19; |
| 1028 | vec3 lit = foliageColour(part, vCard.y) * shade * sun * uExposure * lightShare(vWorld); |
| 1029 | vec3 T = towerTransmittance(uCam, vWorld); |
| 1030 | gl_FragColor = vec4(lit * T + uHaze * uExposure * (1.0 - T), 1.0); |
| 1031 | #include <colorspace_fragment> |
| 1032 | } |
| 1033 | ` |
| 1034 | |
| 1035 | /** |
| 1036 | * Ships (ships.ts): instanced hulls and sails, lit from the axis and hazed like everything |
| 1037 | * else. The instances are placed relative to the mesh, which sits at the eye, so their |
| 1038 | * matrices stay small numbers in float32. |
| 1039 | */ |
| 1040 | const SHIP_VERT = /* glsl */` |
| 1041 | #include <common> |
| 1042 | #include <logdepthbuf_pars_vertex> |
| 1043 | uniform mat4 uFrame; |
| 1044 | varying vec3 vWorld; |
| 1045 | varying vec3 vN; |
| 1046 | void main() { |
| 1047 | mat4 m = modelMatrix * instanceMatrix; |
| 1048 | vWorld = (uFrame * m * vec4(position, 1.0)).xyz; |
| 1049 | vN = mat3(uFrame * m) * normal; |
| 1050 | gl_Position = projectionMatrix * modelViewMatrix * instanceMatrix * vec4(position, 1.0); |
| 1051 | #include <logdepthbuf_vertex> |
| 1052 | } |
| 1053 | ` |
| 1054 | |
| 1055 | const SHIP_FRAG = /* glsl */` |
| 1056 | #include <common> |
| 1057 | #include <logdepthbuf_pars_fragment> |
| 1058 | uniform vec3 uColour; |
| 1059 | varying vec3 vWorld; |
| 1060 | varying vec3 vN; |
| 1061 | ${HAZE_GLSL} |
| 1062 | ${SUN_GLSL} |
| 1063 | void main() { |
| 1064 | #include <logdepthbuf_fragment> |
| 1065 | vec3 n = normalize(vN); |
| 1066 | // Both faces of a sail are lit, whichever way it has swung. |
| 1067 | float lambert = uSun.y * abs(dot(n, sunDir(vWorld))) + uSun.z * abs(dot(n, upAt(vWorld))) |
| 1068 | + uMoon.y * abs(dot(n, moonDir(vWorld))); |
| 1069 | // Three.Color already converts its hex to linear. |
| 1070 | vec3 lit = uColour * (0.34 + 0.8 * lambert) * uExposure * lightShare(vWorld); |
| 1071 | vec3 T = towerTransmittance(uCam, vWorld); |
| 1072 | gl_FragColor = vec4(lit * T + uHaze * uExposure * (1.0 - T), 1.0); |
| 1073 | #include <colorspace_fragment> |
| 1074 | } |
| 1075 | ` |
| 1076 | |
| 1077 | /** |
| 1078 | * The ships' shader for things that are not instanced: the tour's craft (craft.ts) and the |
| 1079 | * railways. Lit from the sun and the rod, and hazed, like everything else in here. |
| 1080 | */ |
| 1081 | const PLAIN_VERT = /* glsl */` |
| 1082 | #include <common> |
| 1083 | #include <logdepthbuf_pars_vertex> |
| 1084 | uniform mat4 uFrame; |
| 1085 | varying vec3 vWorld; |
| 1086 | varying vec3 vN; |
| 1087 | void main() { |
| 1088 | vWorld = (uFrame * modelMatrix * vec4(position, 1.0)).xyz; |
| 1089 | vN = mat3(uFrame * modelMatrix) * normal; |
| 1090 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); |
| 1091 | #include <logdepthbuf_vertex> |
| 1092 | } |
| 1093 | ` |
| 1094 | |
| 1095 | /** Metres between a railway's piers, where its deck stands clear of the ground. */ |
| 1096 | const PIER_EVERY_M = 100 |
| 1097 | /** Samples of a railway per mesh: meshes with their own origins keep float32 precise. */ |
| 1098 | const RAIL_CHUNK = 100 |
| 1099 | |
| 1100 | /** |
| 1101 | * A hull one unit long, bow toward +x, deck up +y: pointed at the bow, a transom at the |
| 1102 | * stern, sides tumbling in to a narrow keel, a sheer rising to the bow, and a deckhouse |
| 1103 | * aft. Scaled to each ship's length by its instance. |
| 1104 | */ |
| 1105 | function hullGeometry(): BufferGeometry { |
| 1106 | const STATIONS = 12 |
| 1107 | const halfBeam = (t: number): number => |
| 1108 | 0.15 * Math.sqrt(Math.max(0, 1 - ((t - 0.4) / 0.6) ** 2)) * (t < 0.4 ? 0.78 + 0.22 * (t / 0.4) : 1) |
| 1109 | const deckY = (t: number): number => 0.08 + 0.05 * t ** 3 |
| 1110 | const KEEL = -0.05 |
| 1111 | const deck: number[][] = [], keel: number[][] = [] |
| 1112 | for (let i = 0; i <= STATIONS; i++) { |
| 1113 | const t = i / STATIONS |
| 1114 | const x = t - 0.5 |
| 1115 | const w = i === STATIONS ? 0 : halfBeam(t) |
| 1116 | deck.push([x, deckY(t), -w], [x, deckY(t), w]) |
| 1117 | keel.push([x, KEEL, -w * 0.3], [x, KEEL, w * 0.3]) |
| 1118 | } |
| 1119 | const tris: number[] = [] |
| 1120 | const tri = (a: number[], b: number[], c: number[]) => tris.push(...a, ...b, ...c) |
| 1121 | const quad = (a: number[], b: number[], c: number[], d: number[]) => { tri(a, b, c); tri(a, c, d) } |
| 1122 | for (let i = 0; i < STATIONS; i++) { |
| 1123 | const [dl0, dr0, dl1, dr1] = [deck[2 * i]!, deck[2 * i + 1]!, deck[2 * i + 2]!, deck[2 * i + 3]!] |
| 1124 | const [kl0, kr0, kl1, kr1] = [keel[2 * i]!, keel[2 * i + 1]!, keel[2 * i + 2]!, keel[2 * i + 3]!] |
| 1125 | quad(dl0, dr0, dr1, dl1) // deck |
| 1126 | quad(dl0, dl1, kl1, kl0) // the -z side |
| 1127 | quad(dr0, kr0, kr1, dr1) // the +z side |
| 1128 | quad(kl0, kl1, kr1, kr0) // bottom |
| 1129 | } |
| 1130 | quad(deck[0]!, keel[0]!, keel[1]!, deck[1]!) // transom |
| 1131 | // A deckhouse aft: a box on the deck. |
| 1132 | const box = (x0: number, x1: number, w: number, y0: number, y1: number) => { |
| 1133 | const p = (x: number, y: number, z: number) => [x, y, z] |
| 1134 | const c = [p(x0, y0, -w), p(x1, y0, -w), p(x1, y0, w), p(x0, y0, w), p(x0, y1, -w), p(x1, y1, -w), p(x1, y1, w), p(x0, y1, w)] |
| 1135 | quad(c[4]!, c[7]!, c[6]!, c[5]!) // top |
| 1136 | quad(c[0]!, c[1]!, c[5]!, c[4]!) |
| 1137 | quad(c[1]!, c[2]!, c[6]!, c[5]!) |
| 1138 | quad(c[2]!, c[3]!, c[7]!, c[6]!) |
| 1139 | quad(c[3]!, c[0]!, c[4]!, c[7]!) |
| 1140 | } |
| 1141 | box(-0.38, -0.12, 0.07, deckY(0.2) - 0.01, deckY(0.2) + 0.07) |
| 1142 | const g = new BufferGeometry() |
| 1143 | g.setAttribute('position', new BufferAttribute(new Float32Array(tris), 3)) |
| 1144 | g.computeVertexNormals() |
| 1145 | return g |
| 1146 | } |
| 1147 | |
| 1148 | /** The jib, from the stemhead to high on the mast. Fixed: only the mainsail swings. */ |
| 1149 | function jibGeometry(): BufferGeometry { |
| 1150 | const g = new BufferGeometry() |
| 1151 | g.setAttribute('position', new BufferAttribute(new Float32Array([0.5, 0.13, 0, 0.12, 0.85, 0, 0.16, 0.1, 0]), 3)) |
| 1152 | g.computeVertexNormals() |
| 1153 | return g |
| 1154 | } |
| 1155 | |
| 1156 | /** |
| 1157 | * A mainsail on a unit hull: a triangle from the foot of the mast to its head and back |
| 1158 | * along the boom. The mast is at the origin, so an instance can swing the sail about it. |
| 1159 | */ |
| 1160 | function sailGeometry(): BufferGeometry { |
| 1161 | const positions = new Float32Array([ |
| 1162 | 0, 0.12, 0, 0, 0.95, 0, -0.44, 0.14, 0, // sail |
| 1163 | 0.004, 0.08, 0, 0.004, 1.0, 0, -0.004, 1.0, 0, // a mast, thin |
| 1164 | 0.004, 0.08, 0, -0.004, 1.0, 0, -0.004, 0.08, 0, |
| 1165 | ]) |
| 1166 | const g = new BufferGeometry() |
| 1167 | g.setAttribute('position', new BufferAttribute(positions, 3)) |
| 1168 | g.computeVertexNormals() |
| 1169 | return g |
| 1170 | } |
| 1171 | |
| 1172 | /** |
| 1173 | * How far the water's surface reference steps: 256 cells of every noise scale the water |
| 1174 | * shader uses (0.8, 1.6, 2.4 and 9.6 m), so a step lands the noise exactly on itself. |
| 1175 | */ |
| 1176 | const SURF_SNAP_M = 2457.6 |
| 1177 | |
| 1178 | /** Most ships drawn at once: two seas of two hundred each. */ |
| 1179 | const MAX_SHIPS = 800 |
| 1180 | /** Where the mast stands on a unit hull. */ |
| 1181 | const MAST_X = 0.12 |
| 1182 | |
| 1183 | /** |
| 1184 | * A waterfall's sheet (falls.ts). Streaks run down it at free-fall speed, faster the |
| 1185 | * further down they are, over a sheet that thins to spray toward its foot. |
| 1186 | */ |
| 1187 | const FALL_VERT = /* glsl */` |
| 1188 | #include <common> |
| 1189 | #include <logdepthbuf_pars_vertex> |
| 1190 | uniform mat4 uFrame; |
| 1191 | varying vec3 vWorld; |
| 1192 | varying vec2 vUv; |
| 1193 | void main() { |
| 1194 | vWorld = (uFrame * modelMatrix * vec4(position, 1.0)).xyz; |
| 1195 | vUv = uv; |
| 1196 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); |
| 1197 | #include <logdepthbuf_vertex> |
| 1198 | } |
| 1199 | ` |
| 1200 | |
| 1201 | const FALL_FRAG = /* glsl */` |
| 1202 | #include <common> |
| 1203 | #include <logdepthbuf_pars_fragment> |
| 1204 | uniform float uTime; |
| 1205 | uniform float uHeightM; |
| 1206 | varying vec3 vWorld; |
| 1207 | varying vec2 vUv; |
| 1208 | ${HAZE_GLSL} |
| 1209 | ${SUN_GLSL} |
| 1210 | float fHash(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); } |
| 1211 | float fNoise(vec2 p) { |
| 1212 | vec2 i = floor(p), f = fract(p); |
| 1213 | f = f * f * (3.0 - 2.0 * f); |
| 1214 | return mix(mix(fHash(i), fHash(i + vec2(1, 0)), f.x), mix(fHash(i + vec2(0, 1)), fHash(i + vec2(1, 1)), f.x), f.y); |
| 1215 | } |
| 1216 | void main() { |
| 1217 | #include <logdepthbuf_fragment> |
| 1218 | // Seconds a streak has been falling to reach this far down: y = g t² / 2. Scrolling in |
| 1219 | // that rather than in distance makes streaks accelerate down the sheet as water does. |
| 1220 | float y = vUv.y * uHeightM; |
| 1221 | float s = sqrt(2.0 * y / 9.80665) - mod(uTime, 30.0); |
| 1222 | float streak = fNoise(vec2(vUv.x * 18.0, s * 3.0)) * 0.6 + fNoise(vec2(vUv.x * 55.0, s * 9.0)) * 0.4; |
| 1223 | float edge = smoothstep(0.0, 0.12, vUv.x) * smoothstep(1.0, 0.88, vUv.x); |
| 1224 | float alpha = (0.55 + 0.45 * streak) * edge * (1.0 - 0.6 * vUv.y); |
| 1225 | vec3 col = vec3(0.86, 0.9, 0.93) * uExposure * (0.75 + 0.35 * streak) |
| 1226 | * (0.45 + 0.6 * direct(vWorld, upAt(vWorld))) * lightShare(vWorld); |
| 1227 | vec3 T = towerTransmittance(uCam, vWorld); |
| 1228 | gl_FragColor = vec4(col * T + uHaze * uExposure * (1.0 - T), alpha); |
| 1229 | #include <colorspace_fragment> |
| 1230 | } |
| 1231 | ` |
| 1232 | |
| 1233 | const FLAT_VERT = /* glsl */` |
| 1234 | #include <common> |
| 1235 | #include <logdepthbuf_pars_vertex> |
| 1236 | uniform mat4 uFrame; |
| 1237 | varying vec3 vWorld; |
| 1238 | void main() { |
| 1239 | vWorld = (uFrame * modelMatrix * vec4(position, 1.0)).xyz; |
| 1240 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); |
| 1241 | #include <logdepthbuf_vertex> |
| 1242 | } |
| 1243 | ` |
| 1244 | |
| 1245 | /** |
| 1246 | * The sky through the windows (§1.4, D-045), and the exterior's sky (D-060). |
| 1247 | * |
| 1248 | * **The stars are Earth's** (D-075): the Yale Bright Star Catalogue, every star to |
| 1249 | * magnitude 6.5, where they really are. The direction is turned about z by the spin phase |
| 1250 | * — the pinwheel, one turn every 7 min 29 s — and then into the catalogue's frame by |
| 1251 | * `uSkyFrame`, which follows the date: Tower's axis points at the sun, so the far cap |
| 1252 | * looks at the antisolar point, and that walks round the ecliptic once a year |
| 1253 | * (celestial.ts). Two nested motions, as §1.6 says. |
| 1254 | * |
| 1255 | * A pixel cannot visit 9,000 stars, so they are baked into cells on the faces of a cube |
| 1256 | * (`bakeStars`): the pixel finds its face and cell and reads the nine cells round it. |
| 1257 | * Each face's grid carries a border of its neighbours' stars, so the nine never leave |
| 1258 | * the face and a star by an edge is seen from both sides of it and counted once. |
| 1259 | * |
| 1260 | * **Why a star is a gaussian a pixel wide.** It used to be a spike, (1 − d/0.09)^4, about |
| 1261 | * a pixel across on a retina panel. The sky is sampled once per pixel at the pixel's |
| 1262 | * centre, so what got drawn was that spike read at one arbitrary point on it — and half a |
| 1263 | * pixel of head movement moved the reading from the peak to a third of it. That is |
| 1264 | * flicker, and it is wrong twice over: vacuum has no air to twinkle through, and the |
| 1265 | * interior is deliberately still. A curve as wide as the pixel barely changes between |
| 1266 | * neighbouring samples, and it is what a point source looks like through any real |
| 1267 | * optic. The footprint is |dd/dpixel| on the unit direction, the pixel's angular size, |
| 1268 | * which has no seam anywhere on the sphere. |
| 1269 | * |
| 1270 | * **Brightness.** Flux is 1 at magnitude 6.5, so Sirius is 1,500. The eye does not see |
| 1271 | * that range and a screen cannot show it; a power of 0.8 does most of the squeezing — |
| 1272 | * steep enough that the constellations stand out of the faint field, as they do outdoors — |
| 1273 | * and the brightest also grow a little wider, which is how bright stars read in any |
| 1274 | * photograph and what tells Sirius from Rigel across a room. |
| 1275 | */ |
| 1276 | export const STAR_GLSL = /* glsl */` |
| 1277 | uniform float uSpin; |
| 1278 | uniform float uStarGain; |
| 1279 | uniform mat3 uSkyFrame; |
| 1280 | uniform highp sampler2D uStars; |
| 1281 | |
| 1282 | const float STAR_CELLS = ${STAR_FACE_CELLS.toFixed(1)}; |
| 1283 | const int STAR_SIZE = ${STAR_FACE_SIZE}; |
| 1284 | |
| 1285 | /** |
| 1286 | * B−V to a naked-eye colour. The hot ones blue-white, the sun white, the cool ones |
| 1287 | * orange — and all of it pale, because starlight is too faint for the eye's colour |
| 1288 | * vision to make much of: a saturated sky reads as fairground lights. |
| 1289 | */ |
| 1290 | vec3 starTint(float bv) { |
| 1291 | vec3 c = mix(vec3(0.66, 0.76, 1.0), vec3(0.86, 0.9, 1.0), smoothstep(-0.3, 0.0, bv)); |
| 1292 | c = mix(c, vec3(1.0, 0.97, 0.92), smoothstep(0.0, 0.6, bv)); |
| 1293 | c = mix(c, vec3(1.0, 0.85, 0.66), smoothstep(0.6, 1.2, bv)); |
| 1294 | c = mix(c, vec3(1.0, 0.74, 0.5), smoothstep(1.2, 1.9, bv)); |
| 1295 | return c; |
| 1296 | } |
| 1297 | |
| 1298 | vec3 starField(vec3 dir) { |
| 1299 | vec3 d0 = normalize(dir); |
| 1300 | float cs = cos(uSpin); |
| 1301 | float sn = sin(uSpin); |
| 1302 | vec3 d = uSkyFrame * vec3(cs * d0.x - sn * d0.y, sn * d0.x + cs * d0.y, d0.z); |
| 1303 | |
| 1304 | // A pixel's angular size, and a star's width from it. Capped well inside a cell at a |
| 1305 | // face's corner (a seventh of a degree), or the nine cells would clip its skirt. |
| 1306 | float sg = clamp(length(fwidth(d)) * 0.85, 1e-5, 0.0008); |
| 1307 | |
| 1308 | // The face, and the pixel's place on it: the same rule as celestial.ts's project(). |
| 1309 | vec3 a = abs(d); |
| 1310 | int axis = a.x >= a.y && a.x >= a.z ? 0 : (a.y >= a.z ? 1 : 2); |
| 1311 | float n = axis == 0 ? d.x : (axis == 1 ? d.y : d.z); |
| 1312 | vec2 uv = axis == 0 ? d.yz : (axis == 1 ? d.zx : d.xy); |
| 1313 | uv /= abs(n); |
| 1314 | int face = 2 * axis + (n < 0.0 ? 1 : 0); |
| 1315 | ivec2 cell = ivec2(floor((uv + 1.0) * 0.5 * STAR_CELLS)) + 1; |
| 1316 | cell = clamp(cell, ivec2(1), ivec2(STAR_SIZE - 2)); |
| 1317 | // Compared in the face's own frame — (u, v, 1) — where the angle is the same as on |
| 1318 | // the sky and nothing needs the face's axes in 3D. |
| 1319 | vec3 here = normalize(vec3(uv, 1.0)); |
| 1320 | |
| 1321 | vec3 sum = vec3(0.0); |
| 1322 | for (int j = -1; j <= 1; j++) { |
| 1323 | for (int i = -1; i <= 1; i++) { |
| 1324 | ivec2 c = cell + ivec2(i, j); |
| 1325 | vec4 s = texelFetch(uStars, ivec2(c.x, face * STAR_SIZE + c.y), 0); |
| 1326 | if (s.z <= 0.0) continue; |
| 1327 | vec3 at = normalize(vec3(s.xy, 1.0)) - here; |
| 1328 | float w = sg * (1.0 + 0.15 * log2(max(s.z, 1.0))); |
| 1329 | float peak = min(0.012 * pow(s.z, 0.8), 1.8); |
| 1330 | sum += starTint(s.w) * peak * exp(-dot(at, at) * 0.5 / (w * w)); |
| 1331 | } |
| 1332 | } |
| 1333 | /** |
| 1334 | * The pinwheel's centre, marked. §1.4 promises one motionless star at the middle of |
| 1335 | * the turning sky, and it is the fixed point the whole phenomenon is read against — |
| 1336 | * but the real sky puts no star at the antisolar point, which walks a degree a day, |
| 1337 | * so this one is placed rather than catalogued. Not canon: on the backlog (design.md §5). |
| 1338 | */ |
| 1339 | float th = acos(clamp(d0.z, -1.0, 1.0)); |
| 1340 | float ps = sg * 1.6; |
| 1341 | sum += exp(-th * th * 0.5 / (ps * ps)) * vec3(1.0, 0.95, 0.86) * 1.6; |
| 1342 | return sum * uStarGain; |
| 1343 | } |
| 1344 | ` |
| 1345 | |
| 1346 | const FROST_NOISE_GLSL = /* glsl */` |
| 1347 | float frostHash(vec2 p) { return fract(sin(dot(mod(p, 289.0), vec2(127.1, 311.7))) * 43758.5453); } |
| 1348 | float frostNoise(vec2 p) { |
| 1349 | vec2 i = floor(p), f = fract(p); |
| 1350 | f = f * f * (3.0 - 2.0 * f); |
| 1351 | return mix(mix(frostHash(i), frostHash(i + vec2(1, 0)), f.x), |
| 1352 | mix(frostHash(i + vec2(0, 1)), frostHash(i + vec2(1, 1)), f.x), f.y); |
| 1353 | } |
| 1354 | ` |
| 1355 | |
| 1356 | /** |
| 1357 | * How high the ice wall stands against the far cap (D-093), metres above the datum, by the |
| 1358 | * angle round the axis: a strip the terrain fills in from the ground at the cap's foot |
| 1359 | * whenever the world changes (\`setCapIce\`). Zero where there is no grounded ice. |
| 1360 | */ |
| 1361 | const CAP_ICE_MAX_M = 4080 |
| 1362 | const CAP_ICE_SAMPLES = 2048 |
| 1363 | const CAP_ICE_GLSL = /* glsl */` |
| 1364 | uniform sampler2D uCapIce; |
| 1365 | float capIceTop(vec2 xy) { |
| 1366 | float u = fract(atan(xy.y, xy.x) / 6.2831853); |
| 1367 | return texture2D(uCapIce, vec2(u, 0.5)).r * ${CAP_ICE_MAX_M.toFixed(1)}; |
| 1368 | } |
| 1369 | ` |
| 1370 | |
| 1371 | /** |
| 1372 | * The bore between the world's end and the glazing. Plain structure, hazed — or, where the |
| 1373 | * ice wall stands against the cap (D-093), buried in it: the kilometre of bore is all below |
| 1374 | * the ice's top, so it is ice, lit like the rest of the far end. |
| 1375 | */ |
| 1376 | const REVEAL_FRAG = /* glsl */` |
| 1377 | #include <common> |
| 1378 | #include <logdepthbuf_pars_fragment> |
| 1379 | uniform vec3 uTint; |
| 1380 | varying vec3 vWorld; |
| 1381 | ${HAZE_GLSL} |
| 1382 | ${SUN_GLSL} |
| 1383 | ${CAP_ICE_GLSL} |
| 1384 | void main() { |
| 1385 | #include <logdepthbuf_fragment> |
| 1386 | vec3 T = towerTransmittance(uCam, vWorld); |
| 1387 | vec3 wall = uTint; |
| 1388 | if (capIceTop(vWorld.xy) > 1.0) wall = ${ICE_LINEAR} * lightShare(vWorld); |
| 1389 | gl_FragColor = vec4(wall * uExposure * T + uHaze * uExposure * (1.0 - T), 1.0); |
| 1390 | #include <colorspace_fragment> |
| 1391 | } |
| 1392 | ` |
| 1393 | |
| 1394 | /** |
| 1395 | * The sun cap: also a window, and what fills it is the back of the collector. |
| 1396 | * |
| 1397 | * §1.4 gave this cap "heavy glare management" and no view, on the assumption that |
| 1398 | * it faces the sun. It does not face the sun — it faces the **collector**, which is |
| 1399 | * 41.8° across from here against the sun's 0.53° and sits squarely between the two. |
| 1400 | * Tower is inside its umbra permanently, so no direct sunlight has ever reached |
| 1401 | * this cap and there is nothing here to manage (D-050). |
| 1402 | * |
| 1403 | * What there is instead is a 244 km disc of unlit structure. Its front is a mirror |
| 1404 | * pointed at the sun; its back radiates the collector's own waste heat at about |
| 1405 | * 245 K, which is infrared and therefore black. So the sky through this window is a |
| 1406 | * black disc with stars around the edge of it — and the disc does not turn with |
| 1407 | * them, because the collector flies in formation rather than spinning. |
| 1408 | * |
| 1409 | * Seen from outside none of that is drawn here: the pane is clear, and the real |
| 1410 | * collector and the real stars are behind it in the exterior's own scene. |
| 1411 | */ |
| 1412 | const SUN_CAP_FRAG = /* glsl */` |
| 1413 | #include <common> |
| 1414 | #include <logdepthbuf_pars_fragment> |
| 1415 | uniform vec3 uTint; |
| 1416 | uniform sampler2D uGlazing; |
| 1417 | varying vec3 vWorld; |
| 1418 | ${HAZE_GLSL} |
| 1419 | ${STAR_GLSL} |
| 1420 | void main() { |
| 1421 | #include <logdepthbuf_fragment> |
| 1422 | vec2 uv = vWorld.xy / ${(2 * R).toFixed(1)} + 0.5; |
| 1423 | vec4 frame = texture2D(uGlazing, uv); |
| 1424 | vec3 mullion = pow(frame.rgb, vec3(2.2)) * uTint * uExposure; |
| 1425 | |
| 1426 | vec3 sky = vec3(0.0); |
| 1427 | if (uSeeThrough < 0.5) { |
| 1428 | vec3 dir = normalize(vWorld - uCam); |
| 1429 | sky = starField(dir); |
| 1430 | /** |
| 1431 | * Cast on through the window to the collector's plane. Anything landing on the |
| 1432 | * disc sees its unlit back, which is as black as the sky is not — and the edge, |
| 1433 | * which is the opposite: structure standing proud of the mirror plane with the |
| 1434 | * sun striking it side-on (D-054). So the collector reads the way any backlit |
| 1435 | * thing does, as a hole in the stars with a lit rim, rather than as an absence. |
| 1436 | */ |
| 1437 | float t = (${COLLECTOR_Z.toFixed(1)} - vWorld.z) / min(dir.z, -1e-4); |
| 1438 | if (t > 0.0) { |
| 1439 | float r = length(vWorld.xy + dir.xy * t); |
| 1440 | sky = mix(sky, vec3(0.0), step(r, ${COLLECTOR_R.toFixed(2)})); |
| 1441 | // Tight core for the structure itself, and a wide dim skirt for the scatter |
| 1442 | // around it. One exponential alone reads as a drawn outline rather than light. |
| 1443 | float d = abs(r - ${COLLECTOR_R.toFixed(2)}); |
| 1444 | float rim = exp(-d / ${COLLECTOR_RIM.toFixed(3)}) |
| 1445 | + 0.10 * exp(-d / ${(COLLECTOR_RIM * 3).toFixed(3)}); |
| 1446 | /** |
| 1447 | * Not scaled by uExposure — the sun does not dim when Tower banks its beam. |
| 1448 | * It does fade as the eye adapts, like the stars, but from a much higher floor: |
| 1449 | * this is sunlit structure, and it stays visible at noon where a star does not. |
| 1450 | */ |
| 1451 | sky += vec3(1.0, 0.93, 0.78) * rim * (0.3 + 0.7 * uStarGain) * 0.42; |
| 1452 | } |
| 1453 | } |
| 1454 | |
| 1455 | gl_FragColor = windowColour(mix(sky, mullion, frame.a), mullion, frame.a); |
| 1456 | #include <colorspace_fragment> |
| 1457 | } |
| 1458 | ` |
| 1459 | |
| 1460 | /** |
| 1461 | * The far cap: a window, and the frame holding it. |
| 1462 | * |
| 1463 | * `uGlazing`'s alpha is the mask — opaque where there is a mullion, clear where |
| 1464 | * there is a pane. Through a pane is deep anti-sun space (§1.4): black, with stars |
| 1465 | * in it, and the haze in front doing the rest. By day the airlight term swamps |
| 1466 | * everything and the cap is simply blue, which is what §1.5 already said would |
| 1467 | * happen to anything 540 km away. At night the airlight collapses with the beam |
| 1468 | * and what is left is the sky. |
| 1469 | */ |
| 1470 | const GLAZED_CAP_FRAG = /* glsl */` |
| 1471 | #include <common> |
| 1472 | #include <logdepthbuf_pars_fragment> |
| 1473 | uniform vec3 uTint; |
| 1474 | uniform sampler2D uGlazing; |
| 1475 | uniform sampler2D uFrost; |
| 1476 | varying vec3 vWorld; |
| 1477 | ${HAZE_GLSL} |
| 1478 | ${SUN_GLSL} |
| 1479 | ${STAR_GLSL} |
| 1480 | |
| 1481 | /** |
| 1482 | * The frost on the pane a point is in (D-090, frost.ts): the lattice cell and which half |
| 1483 | * of it, looked up in a small table the clock rewrites — lower triangles in red, upper in |
| 1484 | * green, cell (a, b) at texel (a + ${FROST_OFFSET}, b + ${FROST_OFFSET}). |
| 1485 | */ |
| 1486 | float paneFrost(vec2 xy) { |
| 1487 | float B = xy.y / ${(PANE_KM * Math.sqrt(3) / 2).toFixed(6)}; |
| 1488 | float A = xy.x / ${PANE_KM.toFixed(6)} - B * 0.5; |
| 1489 | vec2 c = floor(vec2(A, B)); |
| 1490 | float up = step(1.0, (A - c.x) + (B - c.y)); |
| 1491 | vec4 t = texture2D(uFrost, (c + ${FROST_OFFSET.toFixed(1)} + 0.5) / ${FROST_SIZE.toFixed(1)}); |
| 1492 | return mix(t.r, t.g, up); |
| 1493 | } |
| 1494 | ${FROST_NOISE_GLSL} |
| 1495 | ${CAP_ICE_GLSL} |
| 1496 | |
| 1497 | void main() { |
| 1498 | #include <logdepthbuf_fragment> |
| 1499 | vec2 uv = vWorld.xy / ${(2 * R).toFixed(1)} + 0.5; |
| 1500 | vec4 frame = texture2D(uGlazing, uv); |
| 1501 | vec3 mullion = pow(frame.rgb, vec3(2.2)) * uTint * uExposure; |
| 1502 | /** |
| 1503 | * Frost (D-090). The glass is the coldest thing in the world and whitens on its inside; |
| 1504 | * Tower clears a pane a turn. Frost is feathered, not a sheet, so a pane half-built reads |
| 1505 | * as crystals spreading rather than as grey glass — and it is lit by what lights the room |
| 1506 | * at the cap, which at the far end is the rod's dusk. |
| 1507 | */ |
| 1508 | float thick = paneFrost(vWorld.xy); |
| 1509 | // Crystals at tens of metres, in streaks a few hundred long; beyond a pixel they average. |
| 1510 | vec2 fp = vec2(vWorld.x * 0.8 - vWorld.y * 0.6, vWorld.x * 0.6 + vWorld.y * 0.8); |
| 1511 | float feather = frostNoise(fp / vec2(0.05, 0.2)) * 0.6 + frostNoise(vWorld.xy / 0.03 + 9.1) * 0.4; |
| 1512 | float fine = smoothstep(0.02, 0.4, length(fwidth(vWorld.xy))); |
| 1513 | feather = mix(feather, 0.5, fine); |
| 1514 | float frost = clamp(thick * (0.8 + 0.4 * feather) * 1.1, 0.0, 0.96); |
| 1515 | vec3 rime = vec3(0.74, 0.79, 0.84) * uExposure * lightShare(vWorld) * (0.9 + 0.2 * feather); |
| 1516 | /** |
| 1517 | * The stars are NOT scaled by uExposure. Nothing Tower does to its own beam |
| 1518 | * changes how bright Vega is — the exposure models the beam lighting the world, |
| 1519 | * and the sky outside is not lit by it. uStarGain carries the other half of the |
| 1520 | * same fact: the eye adapts, so the stars are there at noon and unseeable. |
| 1521 | */ |
| 1522 | vec3 sky = uSeeThrough < 0.5 ? starField(vWorld - uCam) : vec3(0.0); |
| 1523 | vec3 face = mix(mix(sky, rime, frost), mullion, frame.a); |
| 1524 | // Seen from outside the frost covers the pane as the frame covers its own width. |
| 1525 | float cover = frame.a + (1.0 - frame.a) * frost; |
| 1526 | vec3 solid = (mullion * frame.a + rime * (1.0 - frame.a) * frost) / max(cover, 1e-4); |
| 1527 | /** |
| 1528 | * The ice wall (D-093) stands against the glass as high as it stands against the cap, so |
| 1529 | * the window's foot is buried in it — frame and panes alike — to the wall's own height |
| 1530 | * there, with a ragged top where meltwater from the panes above ran down and froze. |
| 1531 | */ |
| 1532 | float onIce = 0.0; |
| 1533 | float top = capIceTop(vWorld.xy); |
| 1534 | if (top > 1.0) { |
| 1535 | float elevM = (${R.toFixed(1)} - length(vWorld.xy)) * 1000.0; |
| 1536 | float around = atan(vWorld.y, vWorld.x) * ${(R * 1000).toFixed(1)}; |
| 1537 | float runs = frostNoise(vec2(around / 18.0, 0.5)) * 0.7 + frostNoise(vec2(around / 90.0, 3.5)) * 0.3; |
| 1538 | float edge = top + (runs - 0.4) * 40.0; |
| 1539 | float aa = max(length(fwidth(vWorld.xy)) * 1000.0, 1.0); |
| 1540 | onIce = 1.0 - smoothstep(edge - aa, edge + aa, elevM); |
| 1541 | vec3 ice = ${ICE_LINEAR} * uExposure * lightShare(vWorld) * (0.85 + 0.2 * runs); |
| 1542 | face = mix(face, ice, onIce); |
| 1543 | solid = mix(solid, ice, onIce / max(max(cover, onIce), 1e-4)); |
| 1544 | cover = max(cover, onIce); |
| 1545 | } |
| 1546 | /** |
| 1547 | * The bullseye mirror (D-048) is returning 1.7 MW/m² back up the column, so it |
| 1548 | * is lit by the beam rather than by the room: it blazes at noon, banks at night |
| 1549 | * and goes out entirely when the beam does. The stars behind it do none of |
| 1550 | * those, which is the whole difference between the two things in this shader. |
| 1551 | * |
| 1552 | * It is on the axis, but that is NOT the centre of the pinwheel: starField turns |
| 1553 | * about the direction parallel to the axis, which lands on the window at the |
| 1554 | * camera's own radius rather than at the cap's middle. The two are ~10° apart |
| 1555 | * from the floor and coincide only for an observer standing on the axis. |
| 1556 | */ |
| 1557 | float rc = length(vWorld.xy) / ${R.toFixed(1)}; |
| 1558 | float mirror = 1.0 - smoothstep(${(MIRROR_UV_RADIUS * 0.86).toFixed(5)}, |
| 1559 | ${MIRROR_UV_RADIUS.toFixed(5)}, rc); |
| 1560 | // Lit by whatever beam arrives: by night the banked beam, by day what gets past the |
| 1561 | // sun (D-079), so the rod ends on a lit mirror at every hour. |
| 1562 | vec3 glow = mirror * vec3(1.0, 0.96, 0.88) * uExposure * 2.6 * (uSun.z + uSun.y * BEAM_PAST_SUN); |
| 1563 | gl_FragColor = windowColour(face + glow, solid + glow, cover); |
| 1564 | #include <colorspace_fragment> |
| 1565 | } |
| 1566 | ` |
| 1567 | |
| 1568 | const BEAM_VERT = /* glsl */` |
| 1569 | #include <common> |
| 1570 | #include <logdepthbuf_pars_vertex> |
| 1571 | uniform mat4 uFrame; |
| 1572 | varying vec3 vWorld; |
| 1573 | varying vec3 vNormal2; |
| 1574 | void main() { |
| 1575 | vWorld = (uFrame * modelMatrix * vec4(position, 1.0)).xyz; |
| 1576 | vNormal2 = normalize(mat3(uFrame * modelMatrix) * normal); |
| 1577 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); |
| 1578 | #include <logdepthbuf_vertex> |
| 1579 | } |
| 1580 | ` |
| 1581 | |
| 1582 | /** |
| 1583 | * The beam is glowing air, not a surface, so it is brightest where the sightline |
| 1584 | * passes through the most of it — down the middle — and fades at the limb. A tube |
| 1585 | * shaded the usual way does the exact opposite and reads as a chrome pipe. |
| 1586 | */ |
| 1587 | const BEAM_FRAG = /* glsl */` |
| 1588 | #include <common> |
| 1589 | #include <logdepthbuf_pars_fragment> |
| 1590 | uniform vec3 uBeam; |
| 1591 | varying vec3 vWorld; |
| 1592 | varying vec3 vNormal2; |
| 1593 | ${HAZE_GLSL} |
| 1594 | ${SUN_GLSL} |
| 1595 | void main() { |
| 1596 | #include <logdepthbuf_fragment> |
| 1597 | vec3 view = normalize(vWorld - uCam); |
| 1598 | // Additive white clips long before the limb, so the falloff has to start early |
| 1599 | // or the shaft renders as a hard-edged slab instead of a glowing rod. |
| 1600 | float through = pow(clamp(abs(dot(vNormal2, view)), 0.0, 1.0), 1.1); |
| 1601 | /** |
| 1602 | * The rod runs from cap to cap, day and night (D-079). By day the sun's shoal takes part |
| 1603 | * of the beam where it gathers, so past the sun the rod is dimmer — BEAM_PAST_SUN of |
| 1604 | * itself — but never gone. By night the banked beam runs the whole length evenly. |
| 1605 | */ |
| 1606 | float spent = smoothstep(uSun.x - 1.5, uSun.x + 1.5, vWorld.z) * uSun.y * (1.0 - BEAM_PAST_SUN); |
| 1607 | vec3 T = towerTransmittance(uCam, vWorld); |
| 1608 | gl_FragColor = vec4(uBeam * uExposure * through * (1.0 - spent) * T, 1.0); |
| 1609 | #include <colorspace_fragment> |
| 1610 | } |
| 1611 | ` |
| 1612 | |
| 1613 | /** |
| 1614 | * Tower's sun (D-074): the swarm gathered into one shoal on the beam, throwing the beam's |
| 1615 | * light out sideways. It is drawn on a sphere well outside the disc, and every pixel works |
| 1616 | * out how close its line of sight passes to the sun's centre: inside the disc it is far |
| 1617 | * brighter than white, and outside it falls off as glare. One mesh, so the disc and its |
| 1618 | * glow cannot drift apart, and the same haze as everything else between it and the eye — |
| 1619 | * which is what makes it the same sun from the floor, from the air, and through a window. |
| 1620 | */ |
| 1621 | const SUN_FRAG = /* glsl */` |
| 1622 | #include <common> |
| 1623 | #include <logdepthbuf_pars_fragment> |
| 1624 | varying vec3 vWorld; |
| 1625 | ${HAZE_GLSL} |
| 1626 | ${SUN_GLSL} |
| 1627 | void main() { |
| 1628 | #include <logdepthbuf_fragment> |
| 1629 | // Each pixel once: the near face from outside the glow, the far face from inside it. |
| 1630 | bool outside = distance(uCam, sunPos()) > ${SUN_GLOW_R.toFixed(1)}; |
| 1631 | if (gl_FrontFacing != outside) discard; |
| 1632 | vec3 v = normalize(vWorld - uCam); |
| 1633 | vec3 oc = sunPos() - uCam; |
| 1634 | float along = dot(oc, v); |
| 1635 | if (along <= 0.0) discard; |
| 1636 | // How far the line of sight passes from the sun's centre, km. |
| 1637 | float d = length(oc - v * along); |
| 1638 | float disc = 1.0 - smoothstep(${(SUN_R * 0.9).toFixed(3)}, ${SUN_R.toFixed(3)}, d); |
| 1639 | float glare = 0.9 * exp(-max(d - ${SUN_R.toFixed(3)}, 0.0) / 1.6) |
| 1640 | + 0.22 * exp(-d / 7.0); |
| 1641 | glare *= 1.0 - smoothstep(${(SUN_GLOW_R * 0.6).toFixed(1)}, ${SUN_GLOW_R.toFixed(1)}, d); |
| 1642 | vec3 T = towerTransmittance(uCam, sunPos()); |
| 1643 | vec3 col = vec3(1.0, 0.96, 0.86) * (disc * 40.0 + glare * uExposure) * uSun.y; |
| 1644 | gl_FragColor = vec4(col * T, 1.0); |
| 1645 | #include <colorspace_fragment> |
| 1646 | } |
| 1647 | ` |
| 1648 | |
| 1649 | /** |
| 1650 | * Tower's moon (D-088): the sun's counterpart by night, a smaller gathering of the swarm on |
| 1651 | * the banked beam, drawn the way the sun is. Its disc shows Earth's Moon's phase: the lit |
| 1652 | * part is bounded by the terminator's ellipse, and lies toward the far cap while it waxes — |
| 1653 | * the end the sun went out at, as a waxing moon on Earth is lit on the side of the set sun — |
| 1654 | * and toward the sun cap while it wanes, the end the sun will light at. The dark part is not |
| 1655 | * drawn: those mirrorlets are simply turned away. |
| 1656 | */ |
| 1657 | const MOON_FRAG = /* glsl */` |
| 1658 | #include <common> |
| 1659 | #include <logdepthbuf_pars_fragment> |
| 1660 | varying vec3 vWorld; |
| 1661 | ${HAZE_GLSL} |
| 1662 | ${SUN_GLSL} |
| 1663 | void main() { |
| 1664 | #include <logdepthbuf_fragment> |
| 1665 | bool outside = distance(uCam, moonPos()) > ${MOON_GLOW_R.toFixed(1)}; |
| 1666 | if (gl_FrontFacing != outside) discard; |
| 1667 | vec3 v = normalize(vWorld - uCam); |
| 1668 | vec3 oc = moonPos() - uCam; |
| 1669 | float along = dot(oc, v); |
| 1670 | if (along <= 0.0) discard; |
| 1671 | vec3 off = v * along - oc; |
| 1672 | float d = length(off); |
| 1673 | // Across the disc: x toward the lit limb, along the axis as seen; y across it. |
| 1674 | vec3 axis = vec3(0.0, 0.0, 1.0) - v * v.z; |
| 1675 | axis = length(axis) > 1e-4 ? normalize(axis) : normalize(cross(v, vec3(1.0, 0.0, 0.0))); |
| 1676 | float x = dot(off, axis) / ${MOON_R.toFixed(3)} * sign(uMoon.w); |
| 1677 | float y = length(off - axis * dot(off, axis)) / ${MOON_R.toFixed(3)}; |
| 1678 | float e = 1.0 - 2.0 * abs(uMoon.w); |
| 1679 | float lit = step(e * sqrt(max(0.0, 1.0 - y * y)), x); |
| 1680 | float disc = (1.0 - smoothstep(0.9, 1.0, d / ${MOON_R.toFixed(3)})) * lit; |
| 1681 | float glare = 0.35 * exp(-max(d - ${MOON_R.toFixed(3)}, 0.0) / 1.2) * abs(uMoon.w); |
| 1682 | glare *= 1.0 - smoothstep(${(MOON_GLOW_R * 0.6).toFixed(1)}, ${MOON_GLOW_R.toFixed(1)}, d); |
| 1683 | vec3 T = towerTransmittance(uCam, moonPos()); |
| 1684 | vec3 col = vec3(0.86, 0.9, 1.0) * (disc * 3.0 + glare * uExposure) * uMoon.z; |
| 1685 | gl_FragColor = vec4(col * T, 1.0); |
| 1686 | #include <colorspace_fragment> |
| 1687 | } |
| 1688 | ` |
| 1689 | |
| 1690 | // ------------------------------------------------------------------------ model |
| 1691 | |
| 1692 | export interface InteriorModelOptions { |
| 1693 | /** How finely the ground is tiled. The exterior never needs more than the roots. */ |
| 1694 | lod: TerrainLODOptions |
| 1695 | /** |
| 1696 | * True when the model is looked at from outside, through its windows: the panes |
| 1697 | * go clear instead of painting the sky, and the caps blend with what is behind. |
| 1698 | */ |
| 1699 | seeThrough: boolean |
| 1700 | } |
| 1701 | |
| 1702 | /** Where the see-through windows draw among the transparent things (see `buildCaps`). */ |
| 1703 | export const WINDOW_RENDER_ORDER = 4 |
| 1704 | |
| 1705 | export class InteriorModel { |
| 1706 | /** Every material here, so a caller can set a shared uniform in one loop. */ |
| 1707 | readonly materials: ShaderMaterial[] = [] |
| 1708 | readonly groundMat: ShaderMaterial |
| 1709 | readonly waterMat: ShaderMaterial |
| 1710 | readonly canopyMat: ShaderMaterial |
| 1711 | /** The trees near the eye, and the cards beyond them (forest-view.ts). */ |
| 1712 | readonly forest: ForestView |
| 1713 | /** Tower's moon (D-088), moved along the axis by `setSky`, and out by day. */ |
| 1714 | readonly moon: Mesh |
| 1715 | /** Tower's sun, moved along the axis by `setSky`. */ |
| 1716 | readonly sun: Mesh |
| 1717 | /** Every waterfall in the world, rebuilt when the world is. */ |
| 1718 | readonly falls = new Group() |
| 1719 | /** The generated ground: a quadtree of tiles, refined around the eye. */ |
| 1720 | readonly lod: TerrainLOD |
| 1721 | readonly ground: Group |
| 1722 | /** Everything to add to a scene, the ground included. */ |
| 1723 | readonly meshes: Object3D[] |
| 1724 | /** The fleet: every hull, and every sail. Both sit at the eye (see SHIP_VERT). */ |
| 1725 | readonly hulls: InstancedMesh |
| 1726 | readonly sails: InstancedMesh |
| 1727 | readonly jibs: InstancedMesh |
| 1728 | /** The railways (terrain/rails.ts), set by the interior view: the exterior does not draw them. */ |
| 1729 | readonly rails = new Group() |
| 1730 | |
| 1731 | constructor(private opts: InteriorModelOptions) { |
| 1732 | this.groundMat = this.material(GROUND_VERT, GROUND_FRAG, { side: FrontSide, uniforms: {} }) |
| 1733 | /** |
| 1734 | * Water blends over the ground it covers, and writes no depth: a lake seen through |
| 1735 | * the shallows of another would otherwise punch holes in it. Both faces, since a wave |
| 1736 | * near the eye can tip a triangle away. |
| 1737 | */ |
| 1738 | this.waterMat = this.material(WATER_VERT, WATER_FRAG, { side: DoubleSide, uniforms: { |
| 1739 | uBodies: { value: this.bodies }, uWaveOffset: { value: this.waveOffsets }, uSurfRef: { value: this.surfRef }, |
| 1740 | } }) |
| 1741 | this.waterMat.transparent = true |
| 1742 | this.waterMat.depthWrite = false |
| 1743 | this.canopyMat = this.material(CANOPY_VERT, CANOPY_FRAG, { side: DoubleSide, uniforms: {} }) |
| 1744 | this.forest = new ForestView( |
| 1745 | terrain(), |
| 1746 | this.material(TREE_VERT, TREE_FRAG, { side: FrontSide, uniforms: {} }), |
| 1747 | this.material(CARD_VERT, CARD_FRAG, { side: DoubleSide, uniforms: {} })) |
| 1748 | this.lod = new TerrainLOD(terrain(), this.groundMat, this.waterMat, this.canopyMat, opts.lod) |
| 1749 | this.ground = this.lod.group |
| 1750 | this.falls.name = 'falls' |
| 1751 | const fleet = (geometry: BufferGeometry, colour: number, name: string): InstancedMesh => { |
| 1752 | const m = this.material(SHIP_VERT, SHIP_FRAG, { |
| 1753 | side: DoubleSide, uniforms: { uColour: { value: new Color(colour) } }, |
| 1754 | }) |
| 1755 | const mesh = new InstancedMesh(geometry, m, MAX_SHIPS) |
| 1756 | mesh.name = name |
| 1757 | mesh.count = 0 |
| 1758 | // The instances are placed around the eye every frame; the mesh's own bounds mean nothing. |
| 1759 | mesh.frustumCulled = false |
| 1760 | return mesh |
| 1761 | } |
| 1762 | this.hulls = fleet(hullGeometry(), 0x4a3526, 'hulls') |
| 1763 | this.sails = fleet(sailGeometry(), 0xe6ddc8, 'sails') |
| 1764 | this.jibs = fleet(jibGeometry(), 0xe6ddc8, 'jibs') |
| 1765 | this.sun = this.buildSun() |
| 1766 | this.moon = this.buildMoon() |
| 1767 | this.rails.name = 'rails' |
| 1768 | this.meshes = [this.ground, this.falls, this.forest.group, this.hulls, this.sails, this.jibs, this.rails, ...this.buildCaps(), this.buildBeam(), this.sun, this.moon] |
| 1769 | terrain().onWorld(() => this.buildFalls()) |
| 1770 | if (terrain().compiled) this.buildFalls() |
| 1771 | } |
| 1772 | |
| 1773 | /** |
| 1774 | * One sheet per fall. They are few — a handful per river — so each is its own mesh and |
| 1775 | * its own material, which carries the fall's height for the streaks' speed. |
| 1776 | */ |
| 1777 | private buildFalls(): void { |
| 1778 | for (const child of [...this.falls.children]) { |
| 1779 | const mesh = child as Mesh |
| 1780 | mesh.geometry.dispose() |
| 1781 | const m = mesh.material as ShaderMaterial |
| 1782 | this.materials.splice(this.materials.indexOf(m), 1) |
| 1783 | m.dispose() |
| 1784 | this.falls.remove(mesh) |
| 1785 | } |
| 1786 | for (const f of terrain().compiled?.falls ?? []) { |
| 1787 | const sheet = fallSheet(f) |
| 1788 | const g = new BufferGeometry() |
| 1789 | g.setAttribute('position', new BufferAttribute(sheet.positions, 3)) |
| 1790 | g.setAttribute('uv', new BufferAttribute(sheet.uvs, 2)) |
| 1791 | g.setIndex(new BufferAttribute(sheet.indices, 1)) |
| 1792 | g.computeBoundingSphere() |
| 1793 | const m = this.material(FALL_VERT, FALL_FRAG, { |
| 1794 | side: DoubleSide, uniforms: { uHeightM: { value: sheet.heightM } }, |
| 1795 | }) |
| 1796 | m.transparent = true |
| 1797 | m.depthWrite = false |
| 1798 | // Copy the shared uniforms from the ground, so a sheet built mid-session starts in |
| 1799 | // the light of the moment rather than at the defaults. |
| 1800 | const donor = this.groundMat.uniforms |
| 1801 | for (const key of ['uExposure', 'uStarGain', 'uSpin', 'uTime']) { |
| 1802 | m.uniforms[key]!.value = donor[key]!.value |
| 1803 | } |
| 1804 | ;(m.uniforms['uHaze']!.value as Color).copy(donor['uHaze']!.value as Color) |
| 1805 | ;(m.uniforms['uCam']!.value as Vector3).copy(donor['uCam']!.value as Vector3) |
| 1806 | ;(m.uniforms['uSun']!.value as Vector3).copy(donor['uSun']!.value as Vector3) |
| 1807 | ;(m.uniforms['uMoon']!.value as Vector4).copy(donor['uMoon']!.value as Vector4) |
| 1808 | ;(m.uniforms['uMoon']!.value as Vector4).copy(donor['uMoon']!.value as Vector4) |
| 1809 | ;(m.uniforms['uFrame']!.value as Matrix4).copy(donor['uFrame']!.value as Matrix4) |
| 1810 | const mesh = new Mesh(g, m) |
| 1811 | mesh.name = `fall ${f.river}` |
| 1812 | mesh.position.set(...sheet.origin) |
| 1813 | mesh.renderOrder = 2 |
| 1814 | this.falls.add(mesh) |
| 1815 | } |
| 1816 | } |
| 1817 | |
| 1818 | /** Each lake and sea's waves, as the shader reads them (sea-state.ts). */ |
| 1819 | readonly bodies = new Float32Array(MAX_BODIES * BODY_STRIDE) |
| 1820 | /** Each body's wave phases at the surface reference (see surfaceMetres in WAVES_GLSL). */ |
| 1821 | private readonly waveOffsets = new Float32Array(MAX_BODIES * WAVE_LEN.length) |
| 1822 | private readonly surfRef = new Vector4(1, 0, 0, 0) |
| 1823 | private surfRefA = 0 |
| 1824 | seaStates: SeaState[] = [] |
| 1825 | |
| 1826 | /** |
| 1827 | * Put the waves where the weather has them at `hours` since day 0. Cheap, but not free, |
| 1828 | * and the sea state changes over hours: callers need not run it every frame. |
| 1829 | */ |
| 1830 | setWeather(hours: number): void { |
| 1831 | const t = terrain() |
| 1832 | if (!t.world || !t.compiled) return |
| 1833 | this.seaStates = seaStateTable(t.world, t.compiled, hours, this.bodies) |
| 1834 | this.updateWaveOffsets() |
| 1835 | } |
| 1836 | |
| 1837 | /** What the surface reference's own position adds to each wave's phase, mod 2π. */ |
| 1838 | private updateWaveOffsets(): void { |
| 1839 | const n = WAVE_LEN.length |
| 1840 | const z = this.surfRef.z, a = this.surfRefA |
| 1841 | for (let b = 0; b < MAX_BODIES; b++) { |
| 1842 | const o = b * BODY_STRIDE |
| 1843 | const len = this.bodies[o + 1]!, dir = this.bodies[o + 2]! |
| 1844 | for (let i = 0; i < n; i++) { |
| 1845 | if (len <= 0) { this.waveOffsets[b * n + i] = 0; continue } |
| 1846 | const k = (2 * Math.PI) / (len * WAVE_LEN[i]!) |
| 1847 | const th = dir + WAVE_TURN[i]! |
| 1848 | const ph = k * (Math.cos(th) * z + Math.sin(th) * a) |
| 1849 | this.waveOffsets[b * n + i] = ph - Math.floor(ph / (2 * Math.PI)) * 2 * Math.PI |
| 1850 | } |
| 1851 | } |
| 1852 | } |
| 1853 | |
| 1854 | private railAt = new Vector3() |
| 1855 | /** How far out railways are drawn, km. */ |
| 1856 | private static readonly RAIL_RANGE_KM = 45 |
| 1857 | |
| 1858 | /** How far out ships are drawn, km. Past this a hull is under a pixel and all haze. */ |
| 1859 | private static readonly SHIP_RANGE_KM = 40 |
| 1860 | |
| 1861 | /** |
| 1862 | * Put the fleet where it is at `hours`, riding the waves drawn at `timeS` (the water |
| 1863 | * shader's clock). Cheap — a few dozen ships — so it runs every frame. |
| 1864 | */ |
| 1865 | setShips(hours: number, timeS: number, eye: Vector3): void { |
| 1866 | const t = terrain() |
| 1867 | const world = t.world, compiled = t.compiled |
| 1868 | const fleet = [this.hulls, this.sails, this.jibs] |
| 1869 | if (!world || !compiled) { for (const m of fleet) m.count = 0; return } |
| 1870 | const R_M = RADIUS_M |
| 1871 | const hull = new Matrix4(), sail = new Matrix4(), turn = new Matrix4() |
| 1872 | const up = new Vector3(), fwd = new Vector3(), side = new Vector3(), pos = new Vector3() |
| 1873 | const deep = new Vector3(0, 0, 1), spin = new Vector3() |
| 1874 | for (const m of fleet) m.position.copy(eye) |
| 1875 | let n = 0 |
| 1876 | for (const { lane, fix } of shipsAt(world, hours)) { |
| 1877 | if (n >= MAX_SHIPS) break |
| 1878 | const b = compiled.basins.find((x) => x.id === lane.sea) |
| 1879 | if (!b) continue |
| 1880 | const theta = fix.a / R_M |
| 1881 | const cos = Math.cos(theta), sin = Math.sin(theta) |
| 1882 | // Cull by distance first: the wave sum is the costly part. |
| 1883 | const rough = pos.set((R_M / 1000) * cos, (R_M / 1000) * sin, fix.z / 1000) |
| 1884 | if (rough.distanceTo(eye) > InteriorModel.SHIP_RANGE_KM) continue |
| 1885 | const o = b.index * BODY_STRIDE |
| 1886 | const amp = this.bodies[o]!, len = this.bodies[o + 1]!, dir = this.bodies[o + 2]! |
| 1887 | const w = waveAt(amp, len, dir, fix.z, fix.a, timeS) |
| 1888 | // A hull spans its waves: the longer it is against them, the less it pitches and rolls. |
| 1889 | const ride = len > 0 ? 1 / (1 + lane.lengthM / (0.35 * len)) : 0 |
| 1890 | const r = (R_M - b.levelM - w.h * (0.4 + 0.6 * ride)) / 1000 |
| 1891 | pos.set(r * cos, r * sin, fix.z / 1000).sub(eye) |
| 1892 | up.set(-cos, -sin, 0) |
| 1893 | spin.set(-sin, cos, 0) |
| 1894 | up.addScaledVector(deep, -w.gz * ride).addScaledVector(spin, -w.ga * ride).normalize() |
| 1895 | fwd.set(0, 0, 0).addScaledVector(deep, Math.cos(fix.headingRad)).addScaledVector(spin, Math.sin(fix.headingRad)) |
| 1896 | fwd.addScaledVector(up, -fwd.dot(up)).normalize() |
| 1897 | side.crossVectors(fwd, up) |
| 1898 | const scale = lane.lengthM / 1000 |
| 1899 | hull.makeBasis(fwd, up, side).scale(new Vector3(scale, scale, scale)).setPosition(pos) |
| 1900 | this.hulls.setMatrixAt(n, hull) |
| 1901 | this.jibs.setMatrixAt(n, hull) |
| 1902 | const state = this.seaStates[b.index] |
| 1903 | const windRad = ((state?.wind.directionDeg ?? 0) * Math.PI) / 180 |
| 1904 | // The instance's +z points to heading − 90°, and a positive boom angle swings toward |
| 1905 | // heading + 90°, so the rotation is negated. |
| 1906 | turn.makeRotationY(-boomAngle(fix.headingRad, windRad)) |
| 1907 | sail.makeTranslation(MAST_X, 0, 0).multiply(turn) |
| 1908 | this.sails.setMatrixAt(n, hull.clone().multiply(sail)) |
| 1909 | n++ |
| 1910 | } |
| 1911 | for (const m of fleet) { |
| 1912 | m.count = n |
| 1913 | m.instanceMatrix.needsUpdate = true |
| 1914 | } |
| 1915 | } |
| 1916 | |
| 1917 | /** |
| 1918 | * The eye, in the tower's frame. Call every frame: it is also what chooses which |
| 1919 | * tiles of ground to draw. |
| 1920 | */ |
| 1921 | setCamera(eye: Vector3): void { |
| 1922 | for (const m of this.materials) (m.uniforms['uCam']!.value as Vector3).copy(eye) |
| 1923 | // The water's surface reference, in whole steps near the eye, computed in float64. |
| 1924 | const aM = Math.atan2(eye.y, eye.x) * RADIUS_M |
| 1925 | const refA = Math.round(aM / SURF_SNAP_M) * SURF_SNAP_M |
| 1926 | const refZ = Math.round((eye.z * 1000) / SURF_SNAP_M) * SURF_SNAP_M |
| 1927 | this.surfRef.set(Math.cos(refA / RADIUS_M), Math.sin(refA / RADIUS_M), refZ, 0) |
| 1928 | this.surfRefA = refA |
| 1929 | this.updateWaveOffsets() |
| 1930 | // The trees around the eye, rebuilt only when it has moved far enough to matter. |
| 1931 | this.forest.update(eye) |
| 1932 | this.lod.update(eye) |
| 1933 | // A railway 7 m wide is under a pixel and all haze past a few tens of kilometres: those |
| 1934 | // chunks are not drawn at all, which keeps some 280 draw calls off every frame. |
| 1935 | for (const c of this.rails.children) { |
| 1936 | const m = c as Mesh |
| 1937 | const b = m.geometry.boundingSphere |
| 1938 | if (!b) continue |
| 1939 | this.railAt.copy(b.center).add(m.position) |
| 1940 | m.visible = this.railAt.distanceTo(eye) - b.radius < InteriorModel.RAIL_RANGE_KM |
| 1941 | } |
| 1942 | } |
| 1943 | |
| 1944 | /** |
| 1945 | * A lit, hazed material of one colour, for meshes that are neither ground nor water nor |
| 1946 | * instanced: the scenic tour's craft, and the railways. |
| 1947 | */ |
| 1948 | plainMaterial(colour: number): ShaderMaterial { |
| 1949 | const m = this.material(PLAIN_VERT, SHIP_FRAG, { side: DoubleSide, uniforms: { uColour: { value: new Color(colour) } } }) |
| 1950 | // Built mid-session, so take the light of the moment rather than the defaults. |
| 1951 | const donor = this.groundMat.uniforms |
| 1952 | for (const key of ['uExposure', 'uStarGain', 'uSpin', 'uTime']) m.uniforms[key]!.value = donor[key]!.value |
| 1953 | ;(m.uniforms['uHaze']!.value as Color).copy(donor['uHaze']!.value as Color) |
| 1954 | ;(m.uniforms['uCam']!.value as Vector3).copy(donor['uCam']!.value as Vector3) |
| 1955 | ;(m.uniforms['uSun']!.value as Vector3).copy(donor['uSun']!.value as Vector3) |
| 1956 | ;(m.uniforms['uFrame']!.value as Matrix4).copy(donor['uFrame']!.value as Matrix4) |
| 1957 | return m |
| 1958 | } |
| 1959 | |
| 1960 | private railMats: { deck: ShaderMaterial; girder: ShaderMaterial } | null = null |
| 1961 | |
| 1962 | /** |
| 1963 | * Lay the railways (D-086): a deck on a girder along each line at the height rails.ts gave |
| 1964 | * it, and a pier down to the ground every 100 m wherever it stands clear. Rebuilt whole when |
| 1965 | * the world changes; a few hundred kilometres of box is cheap. |
| 1966 | */ |
| 1967 | setRails(lines: readonly RailLine[]): void { |
| 1968 | for (const child of [...this.rails.children]) { |
| 1969 | ;(child as Mesh).geometry.dispose() |
| 1970 | this.rails.remove(child) |
| 1971 | } |
| 1972 | this.railMats ??= { deck: this.plainMaterial(0x57514a), girder: this.plainMaterial(0x9a958c) } |
| 1973 | const HALF = 3.5, GIRDER = 1.8, PIER = 1.6 |
| 1974 | for (const r of lines) { |
| 1975 | const n = r.line.s.length |
| 1976 | const P = (i: number) => { |
| 1977 | const k = r.loop ? ((i % (n - 1)) + (n - 1)) % (n - 1) : Math.max(0, Math.min(n - 1, i)) |
| 1978 | const th = r.line.pts[2 * k + 1]! / RADIUS_M, rad = RADIUS_M - r.deckM[k]! |
| 1979 | return { v: new Vector3(rad * Math.cos(th), rad * Math.sin(th), r.line.pts[2 * k]!), k } |
| 1980 | } |
| 1981 | for (let c0 = 0; c0 < n - 1; c0 += RAIL_CHUNK) { |
| 1982 | const c1 = Math.min(n - 1, c0 + RAIL_CHUNK) |
| 1983 | const origin = P(c0).v.clone().divideScalar(1000) |
| 1984 | const deck: number[] = [], girder: number[] = [] |
| 1985 | const at = (v: Vector3) => [v.x / 1000 - origin.x, v.y / 1000 - origin.y, v.z / 1000 - origin.z] |
| 1986 | const quad = (out: number[], a: Vector3, b: Vector3, c: Vector3, d: Vector3) => |
| 1987 | out.push(...at(a), ...at(b), ...at(c), ...at(a), ...at(c), ...at(d)) |
| 1988 | const edge = (i: number) => { |
| 1989 | const p = P(i).v, fwd = P(i + 1).v.clone().sub(P(i - 1).v).normalize() |
| 1990 | const up = new Vector3(-p.x, -p.y, 0).normalize() |
| 1991 | const side = new Vector3().crossVectors(fwd, up).normalize().multiplyScalar(HALF) |
| 1992 | const down = up.clone().multiplyScalar(-GIRDER) |
| 1993 | return { |
| 1994 | tl: p.clone().sub(side), tr: p.clone().add(side), |
| 1995 | bl: p.clone().sub(side).add(down), br: p.clone().add(side).add(down), p, up, |
| 1996 | } |
| 1997 | } |
| 1998 | let prev = edge(c0) |
| 1999 | for (let i = c0 + 1; i <= c1; i++) { |
| 2000 | const e = edge(i) |
| 2001 | quad(deck, prev.tl, prev.tr, e.tr, e.tl) |
| 2002 | quad(girder, prev.tl, e.tl, e.bl, prev.bl) |
| 2003 | quad(girder, prev.tr, prev.br, e.br, e.tr) |
| 2004 | quad(girder, prev.bl, e.bl, e.br, prev.br) |
| 2005 | prev = e |
| 2006 | } |
| 2007 | // Piers, where the deck stands clear of the ground. |
| 2008 | const every = Math.round(PIER_EVERY_M / (r.line.s[1]! - r.line.s[0]!)) || 2 |
| 2009 | for (let i = c0 - (c0 % every) + every; i < c1; i += every) { |
| 2010 | const { k } = P(i) |
| 2011 | const clear = r.deckM[k]! - r.groundM[k]! |
| 2012 | if (clear < GIRDER + 2) continue |
| 2013 | const e = edge(i) |
| 2014 | const fwd = P(i + 1).v.clone().sub(P(i - 1).v).normalize().multiplyScalar(PIER / 2) |
| 2015 | const side = new Vector3().crossVectors(fwd, e.up).normalize().multiplyScalar(PIER / 2) |
| 2016 | const top = e.p.clone().addScaledVector(e.up, -GIRDER) |
| 2017 | const foot = e.p.clone().addScaledVector(e.up, -(clear + 3)) |
| 2018 | const corners = [fwd.clone().add(side), fwd.clone().sub(side), fwd.clone().negate().sub(side), fwd.clone().negate().add(side)] |
| 2019 | for (let q = 0; q < 4; q++) { |
| 2020 | const a = corners[q]!, b = corners[(q + 1) % 4]! |
| 2021 | quad(girder, top.clone().add(a), top.clone().add(b), foot.clone().add(b), foot.clone().add(a)) |
| 2022 | } |
| 2023 | } |
| 2024 | for (const [pos, mat] of [[deck, this.railMats.deck], [girder, this.railMats.girder]] as const) { |
| 2025 | const g = new BufferGeometry() |
| 2026 | g.setAttribute('position', new BufferAttribute(new Float32Array(pos), 3)) |
| 2027 | g.computeVertexNormals() |
| 2028 | g.computeBoundingSphere() |
| 2029 | const mesh = new Mesh(g, mat) |
| 2030 | mesh.position.copy(origin) |
| 2031 | mesh.name = `rail ${r.id}` |
| 2032 | this.rails.add(mesh) |
| 2033 | } |
| 2034 | } |
| 2035 | } |
| 2036 | } |
| 2037 | |
| 2038 | /** World to tower coordinates. Only the exterior, which turns the hull, needs it. */ |
| 2039 | setFrame(worldToTower: Matrix4): void { |
| 2040 | for (const m of this.materials) (m.uniforms['uFrame']!.value as Matrix4).copy(worldToTower) |
| 2041 | } |
| 2042 | |
| 2043 | private material( |
| 2044 | vertexShader: string, fragmentShader: string, |
| 2045 | params: { side: typeof BackSide | typeof DoubleSide | typeof FrontSide; uniforms: Record<string, { value: unknown }>; additive?: boolean }, |
| 2046 | ): ShaderMaterial { |
| 2047 | const m = new ShaderMaterial({ |
| 2048 | vertexShader, fragmentShader, side: params.side, |
| 2049 | uniforms: { ...params.uniforms, ...this.sharedUniforms() }, |
| 2050 | ...(params.additive |
| 2051 | ? { transparent: true, blending: AdditiveBlending, depthWrite: false } |
| 2052 | : {}), |
| 2053 | }) |
| 2054 | this.materials.push(m) |
| 2055 | return m |
| 2056 | } |
| 2057 | |
| 2058 | private sharedUniforms(): Record<string, { value: unknown }> { |
| 2059 | return { |
| 2060 | uCam: { value: new Vector3() }, |
| 2061 | uFrame: { value: new Matrix4() }, |
| 2062 | uK: { value: new Vector3(...K_RGB) }, |
| 2063 | uHaze: { value: new Color(HAZE_DAY) }, |
| 2064 | uExposure: { value: 1 }, |
| 2065 | uSeeThrough: { value: this.opts.seeThrough ? 1 : 0 }, |
| 2066 | // Only the glazed caps read these, but every material carries them so the |
| 2067 | // per-frame update can walk one list instead of knowing who wants what. |
| 2068 | uSpin: { value: 0 }, |
| 2069 | // Seconds, for anything that moves: the water. |
| 2070 | uTime: { value: 0 }, |
| 2071 | uStarGain: { value: 0 }, |
| 2072 | // The real sky (D-075): the catalogue, and which way it lies at the clock's instant. |
| 2073 | uStars: { value: starTexture() }, |
| 2074 | uSkyFrame: { value: skyMatrix(Date.now(), new Matrix3()) }, |
| 2075 | // The sun's position (km), how lit it is, and the banked rod's share (SUN_GLSL). |
| 2076 | uSun: { value: new Vector3(L / 2, 1, 0) }, |
| 2077 | // The moon's position (km), its light, its ramp and its signed phase (SUN_GLSL). |
| 2078 | uMoon: { value: new Vector4(L / 2, 0, 0, 0) }, |
| 2079 | // The far end's dim, and the sky's light by where it comes from (SUN_GLSL, D-091). |
| 2080 | uDim: { value: new Vector4(0, 0, 0, 0) }, |
| 2081 | // The farms' own days (GROUND_FRAG, D-095). |
| 2082 | uFarm: { value: new Vector4(0, 12, 1, 1) }, |
| 2083 | uLightMix: { value: new Vector3(0, 1, 0) }, |
| 2084 | } |
| 2085 | } |
| 2086 | |
| 2087 | /** |
| 2088 | * Which caps are windows, and how they blend. Inside, a window is opaque to the |
| 2089 | * renderer — the sky it shows is computed, and nothing is drawn behind it. From |
| 2090 | * outside it has to be transparent, so it is sorted and blended with the scene |
| 2091 | * that really is behind it: the land beyond the near cap, the collector and the |
| 2092 | * stars beyond the far one. |
| 2093 | */ |
| 2094 | private windowMaterial(frag: string, tint: number, glazing: Texture, extra: Record<string, { value: unknown }> = {}): ShaderMaterial { |
| 2095 | const m = this.material(FLAT_VERT, frag, { |
| 2096 | side: DoubleSide, |
| 2097 | uniforms: { uTint: { value: new Color(tint) }, uGlazing: { value: glazing }, ...extra }, |
| 2098 | }) |
| 2099 | m.transparent = this.opts.seeThrough |
| 2100 | return m |
| 2101 | } |
| 2102 | |
| 2103 | /** |
| 2104 | * The caps close the world at both ends. From the midpoint they are 270 km away |
| 2105 | * and 4% transmitted (§1.8), so they are all but pure haze — which is the correct |
| 2106 | * and slightly unnerving result, and worth drawing precisely because it is. |
| 2107 | * |
| 2108 | * Both are windows (D-045, D-050). The far cap is set a kilometre into its own |
| 2109 | * frame so the wall depth of D-044 has somewhere to be visible from; the sun cap |
| 2110 | * is an annulus, because the beam enters through the hole in its middle. |
| 2111 | */ |
| 2112 | private buildCaps(): Mesh[] { |
| 2113 | // The tint is a multiplier over the pattern, not the colour of the cap. |
| 2114 | const sun = new Mesh( |
| 2115 | new RingGeometry(BEAM_R, R, 128), |
| 2116 | this.windowMaterial(SUN_CAP_FRAG, 0xffffff, sunCapGlazingTexture())) |
| 2117 | sun.name = 'sunCap' |
| 2118 | |
| 2119 | const far = new Mesh( |
| 2120 | new CircleGeometry(R, 128), |
| 2121 | this.windowMaterial(GLAZED_CAP_FRAG, 0xbcc2cc, capGlazingTexture(), { uFrost: { value: this.frostTexture }, uCapIce: { value: this.capIceTexture } })) |
| 2122 | far.name = 'farCap' |
| 2123 | far.position.z = L + HULL_D |
| 2124 | |
| 2125 | // The reveal: a kilometre of bore standing between the end of the world and |
| 2126 | // the glass. BackSide, because it is only ever seen from inside. |
| 2127 | const bore = new CylinderGeometry(R, R, HULL_D, 128, 1, true) |
| 2128 | bore.rotateX(Math.PI / 2) |
| 2129 | bore.translate(0, 0, L + HULL_D / 2) |
| 2130 | const reveal = new Mesh(bore, this.material(FLAT_VERT, REVEAL_FRAG, { |
| 2131 | side: BackSide, uniforms: { uTint: { value: new Color(0x9aa0aa) }, uCapIce: { value: this.capIceTexture } }, |
| 2132 | })) |
| 2133 | reveal.name = 'bore' |
| 2134 | /** |
| 2135 | * From outside, a window is drawn after everything behind it: the water (1), the |
| 2136 | * falls (2), the beam (3) and the sun (3.5). Left at 0 it went first, wrote its depth, |
| 2137 | * and whatever was behind the glass failed the depth test — looking in at the sun cap |
| 2138 | * showed the Sunward Sea's bed, brown, where the sea should have been, and the rod and |
| 2139 | * the sun were missing from a view that has to show the same sky as the one inside |
| 2140 | * (D-074). The exterior's own transparent things share this order (exterior.ts), so they |
| 2141 | * still sort against the glass by distance. |
| 2142 | */ |
| 2143 | if (this.opts.seeThrough) sun.renderOrder = far.renderOrder = WINDOW_RENDER_ORDER |
| 2144 | return [sun, far, reveal] |
| 2145 | } |
| 2146 | |
| 2147 | private buildBeam(): Mesh { |
| 2148 | const g = new CylinderGeometry(BEAM_R, BEAM_R, L, 48, 1, true) |
| 2149 | g.rotateX(Math.PI / 2) |
| 2150 | g.translate(0, 0, L / 2) |
| 2151 | const m = this.material(BEAM_VERT, BEAM_FRAG, { |
| 2152 | side: DoubleSide, additive: true, |
| 2153 | uniforms: { uBeam: { value: new Color(0xfff2d6).multiplyScalar(0.85) } }, |
| 2154 | }) |
| 2155 | const beam = new Mesh(g, m) |
| 2156 | beam.name = 'beam' |
| 2157 | /** |
| 2158 | * After the water (1) and the falls (2). Three draws lower render orders first, and the |
| 2159 | * beam is always in front of any water — water lies on the floor, the beam down the |
| 2160 | * axis — so drawn earlier, the far side's seas were blended over it and it went fuzzy |
| 2161 | * wherever it crossed them. |
| 2162 | */ |
| 2163 | beam.renderOrder = 3 |
| 2164 | return beam |
| 2165 | } |
| 2166 | |
| 2167 | /** After the beam it sits in, and before the windows, which are in front of it from outside. */ |
| 2168 | private buildSun(): Mesh { |
| 2169 | const m = this.material(FLAT_VERT, SUN_FRAG, { side: DoubleSide, additive: true, uniforms: {} }) |
| 2170 | const sun = new Mesh(new SphereGeometry(SUN_GLOW_R, 48, 24), m) |
| 2171 | sun.name = 'sun' |
| 2172 | sun.renderOrder = 3.5 |
| 2173 | sun.frustumCulled = false |
| 2174 | return sun |
| 2175 | } |
| 2176 | |
| 2177 | /** The moon, drawn as the sun is: after the beam, before the windows. */ |
| 2178 | private buildMoon(): Mesh { |
| 2179 | const m = this.material(FLAT_VERT, MOON_FRAG, { side: DoubleSide, additive: true, uniforms: {} }) |
| 2180 | const moon = new Mesh(new SphereGeometry(MOON_GLOW_R, 32, 16), m) |
| 2181 | moon.name = 'moon' |
| 2182 | moon.renderOrder = 3.5 |
| 2183 | moon.frustumCulled = false |
| 2184 | moon.visible = false |
| 2185 | return moon |
| 2186 | } |
| 2187 | |
| 2188 | /** |
| 2189 | * Light the model for a moment of Tower's day (sky.ts). Both views call this with the |
| 2190 | * same sky, so the sun, the rod and the light on the ground agree inside and out. |
| 2191 | * |
| 2192 | * Eyes adapt, so twilight is not 6.7x darker than temperate to look at: the exponent |
| 2193 | * compresses the canon watts into something a screen can show while keeping the ordering |
| 2194 | * honest. The stars are the other half of that — always exactly as bright as they are, |
| 2195 | * and whether you can see them depends on what the eye is set to: full at night, 2% at |
| 2196 | * temperate, which is the honest answer to why nobody looks out at lunchtime. |
| 2197 | */ |
| 2198 | private skyFrame = new Matrix3() |
| 2199 | |
| 2200 | /** |
| 2201 | * The far cap's frost, pane by pane (frost.ts), as the table GLAZED_CAP_FRAG reads. |
| 2202 | * Rewritten from the clock whenever it has moved: 282 panes, nothing to it. |
| 2203 | */ |
| 2204 | private readonly frostTexture = (() => { |
| 2205 | const t = new DataTexture(new Uint8Array(FROST_SIZE * FROST_SIZE * 4), FROST_SIZE, FROST_SIZE, RGBAFormat) |
| 2206 | t.magFilter = NearestFilter |
| 2207 | t.minFilter = NearestFilter |
| 2208 | t.needsUpdate = true |
| 2209 | return t |
| 2210 | })() |
| 2211 | private readonly frost = new Float32Array(panes().length) |
| 2212 | private frostAtS = NaN |
| 2213 | |
| 2214 | /** |
| 2215 | * How high the ice wall stands against the far cap, round the ring (CAP_ICE_GLSL, D-093): |
| 2216 | * read off the ground at the cap's foot once per generation of the world. 2048 samples is |
| 2217 | * one every 153 m of the circumference, filtered between. |
| 2218 | */ |
| 2219 | private readonly capIceTexture = (() => { |
| 2220 | const t = new DataTexture(new Uint8Array(CAP_ICE_SAMPLES * 4), CAP_ICE_SAMPLES, 1, RGBAFormat) |
| 2221 | t.magFilter = LinearFilter |
| 2222 | t.minFilter = LinearFilter |
| 2223 | t.wrapS = RepeatWrapping |
| 2224 | t.needsUpdate = true |
| 2225 | return t |
| 2226 | })() |
| 2227 | private capIceGeneration = -1 |
| 2228 | |
| 2229 | private updateCapIce(): void { |
| 2230 | const t = terrain() |
| 2231 | if (!t.compiled || t.generation === this.capIceGeneration) return |
| 2232 | this.capIceGeneration = t.generation |
| 2233 | const data = this.capIceTexture.image.data as Uint8Array |
| 2234 | for (let i = 0; i < CAP_ICE_SAMPLES; i++) { |
| 2235 | // Just short of the end: the ground there is the wall's top against the cap. |
| 2236 | const g = t.groundAt(((i + 0.5) / CAP_ICE_SAMPLES) * 2 * Math.PI, LENGTH_M - 20, 20) |
| 2237 | // Dry ground at the foot of the cap is the range: its ice, or a ridge of it standing bare. |
| 2238 | const top = g && !g.water ? Math.max(0, g.h) : 0 |
| 2239 | data[i * 4] = Math.round(Math.min(1, top / CAP_ICE_MAX_M) * 255) |
| 2240 | } |
| 2241 | this.capIceTexture.needsUpdate = true |
| 2242 | } |
| 2243 | |
| 2244 | private updateFrost(hours: number): void { |
| 2245 | const tS = hours * 3600 |
| 2246 | const seed = terrain().world?.seed ?? 0 |
| 2247 | if (tS === this.frostAtS) return |
| 2248 | this.frostAtS = tS |
| 2249 | frostAt(seed, tS, this.frost) |
| 2250 | const data = this.frostTexture.image.data as Uint8Array |
| 2251 | panes().forEach((p, i) => { |
| 2252 | const o = ((p.b + FROST_OFFSET) * FROST_SIZE + (p.a + FROST_OFFSET)) * 4 |
| 2253 | data[o + p.up] = Math.round(this.frost[i]! * 255) |
| 2254 | }) |
| 2255 | this.frostTexture.needsUpdate = true |
| 2256 | } |
| 2257 | |
| 2258 | setSky(sky: Sky, eyeZ?: number): { exposure: number; haze: Color } { |
| 2259 | this.updateFrost(sky.hours) |
| 2260 | this.updateCapIce() |
| 2261 | /** |
| 2262 | * The far end's dim (D-091): the sea Tower lets freeze, if there is one. The shaders |
| 2263 | * dim what is lit there (SUN_GLSL \`lightShare\`); the stars go by the light where the |
| 2264 | * eye is, so someone on the fast ice at dusk sees the brightest of them. |
| 2265 | */ |
| 2266 | const iced = terrain().compiled?.basins.find((b) => b.ice) ?? null |
| 2267 | const mix = lightMix(sky) |
| 2268 | const total = mix.sun + mix.rod + mix.moon |
| 2269 | const local = iced && eyeZ !== undefined ? lightShareAt(iced.ice!, iced.openM, eyeZ, mix) : 1 |
| 2270 | const exposure = (sky.watts / BRIGHTNESS_TEMPERATE_W_M2) ** 0.35 |
| 2271 | // The farms (D-095): where in its day each crop is, and how bright Tower's noon is now. |
| 2272 | const sinceSunrise = (((sky.hourOfDay - sky.times.sunriseH) % 24) + 24) % 24 |
| 2273 | const dayLen = sky.times.sunsetH - sky.times.sunriseH |
| 2274 | const farmBoost = (DIALS[sky.dial] / Math.max(sky.watts, BRIGHTNESS_NIGHT_W_M2)) ** 0.35 |
| 2275 | const starGain = (BRIGHTNESS_NIGHT_W_M2 / Math.max(sky.watts * local, BRIGHTNESS_NIGHT_W_M2)) ** 0.6 |
| 2276 | const haze = new Color(sky.watts <= BRIGHTNESS_TWILIGHT_W_M2 ? HAZE_DUSK : HAZE_DAY) |
| 2277 | const zKm = (sky.sunZ ?? 0) / KM |
| 2278 | // The banked rod lights the night; with the beam shut down there is not even that. |
| 2279 | const rod = sky.dial === 'dark' ? 0 : 1 - sky.power |
| 2280 | const moonKm = (sky.moonZ ?? 0) / KM |
| 2281 | const phase = sky.moonPhase.fraction * (sky.moonPhase.waxing ? 1 : -1) |
| 2282 | const frame = skyMatrix(sky.at, this.skyFrame) |
| 2283 | for (const m of this.materials) { |
| 2284 | ;(m.uniforms['uSkyFrame']!.value as Matrix3).copy(frame) |
| 2285 | m.uniforms['uExposure']!.value = exposure |
| 2286 | m.uniforms['uStarGain']!.value = starGain |
| 2287 | ;(m.uniforms['uHaze']!.value as Color).copy(haze) |
| 2288 | ;(m.uniforms['uSun']!.value as Vector3).set(zKm, sky.power, rod) |
| 2289 | ;(m.uniforms['uMoon']!.value as Vector4).set(moonKm, sky.moonLight, sky.moonRamp, phase) |
| 2290 | const dim = m.uniforms['uDim']!.value as Vector4 |
| 2291 | if (iced) dim.set(iced.openM / KM, iced.ice!.edgeM / KM, iced.ice!.fastM / KM, Math.sign(iced.ice!.fastM - iced.ice!.edgeM)) |
| 2292 | else dim.set(0, 0, 0, 0) |
| 2293 | ;(m.uniforms['uFarm']!.value as Vector4).set(sinceSunrise, dayLen, farmBoost, sky.dial === 'dark' ? 0 : 1) |
| 2294 | const lm = m.uniforms['uLightMix']!.value as Vector3 |
| 2295 | if (total > 0) lm.set(mix.sun / total, mix.rod / total, mix.moon / total) |
| 2296 | else lm.set(0, 1, 0) |
| 2297 | } |
| 2298 | this.sun.position.set(0, 0, zKm) |
| 2299 | this.sun.visible = sky.power > 0 |
| 2300 | this.moon.position.set(0, 0, moonKm) |
| 2301 | this.moon.visible = sky.moonRamp > 0 && sky.moonPhase.fraction > 0.005 |
| 2302 | return { exposure, haze } |
| 2303 | } |
| 2304 | } |