tower / commit
feat(views): the far cap is a window, and the sky through it turns
README.md | 46 +++++++++++---- decisions.md | 52 +++++++++++++++++ design.md | 30 ++++++++-- index.html | 6 +- src/views/cap-pattern.ts | 82 ++++++++++++++++++++++++++ src/views/exterior.ts | 25 ++++++-- src/views/interior.ts | 149 ++++++++++++++++++++++++++++++++++++++++++++++- 7 files changed, 364 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index e5f08b1..88c43dd 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,19 @@ Companion piece to a novel set inside **Tower** — a fictional smart cylinder orbiting Earth, spun for 1 g, with an inner surface the size of Wisconsin, lit by 54 TW of sunlight -piped down its axis and scattered by mirrors that make its days and nights. The hull -is sealed: no windows, no stars, no view of the Earth it orbits. The end caps are -the only places in the world with a horizon that is not Tower. +piped down its axis and scattered by mirrors that make its days and nights. Its wall +is a kilometre of sealed hull: no windows along all 540 km, and no view of the Earth +it orbits. The far cap is the one exception — a 100 km window at the deepward end, +and the only place in the world with a horizon that is not Tower. Explore Tower from outside, pick a spot on the inner surface, and stand there — with the ground curving up on both sides and the far side of the world hanging 100 km overhead, clearer than the ground at your feet. Turn its sun up to Earth clear-sky -noon, or down to permanent dusk. +noon, down to permanent dusk, or shut the beam off entirely and watch the sky come +out: through the far cap the stars wheel once every 7 min 29 s about a single +motionless star on the axis. Nothing inside Tower rotates, because you and the +ground and the air all turn together — so that window is the only place the spin is +visible at all. Tower flies all 78 billion mirrors of its own sunlight, individually, because nothing passive would stay in the beam past mid-afternoon. It is a person. @@ -171,12 +176,33 @@ Drag to look, scroll to zoom in like a pair of binoculars, and select a place the list or by its caption — to **walk there**. Most of the world is beyond the haze horizon from any given spot, so travelling is more use than turning. -The end caps are checkered — the only structure in the world, and the only thing in -it with a known cell size, so they double as a rule for judging distance. The eye-height slider runs from 1.7 m to 25 km with the local gravity -beside it, the light has the three canon settings, and **Rain** is the only moving -thing in the sky — it leans 1.5° antispinward, which is all the evidence there is -that Tower turns. Nothing in the view rotates, because you, the ground and the air -all turn together. +**The far cap is a window** — 100 km of it, looking into deep anti-sun space. By day +it is simply a disc of blue, because 540 km of air in front of it scatters more +light than anything behind it sends; the stars are there at noon and unseeable, +which is a fact about eyes rather than about glass. Turn the light down and they +come out. The sun cap stays a checkered plate: it faces the sun, and needs glare +management rather than a view. + +The checker survives on the far cap as the window's **mullions** — the same polar +tessellation at the same pitch, so it is still the only object in the world with a +known cell size and still the only rule anyone has for judging distance. It is just +read against sky now instead of against a plate. + +The light has five settings. **Dark** shuts the 54 TW beam down entirely and is the +only one that is genuinely black — there is no airlight, no lit ground and no rod +overhead, and the only thing left in the world is the sky. **Night** banks the beam +to a thousandth instead of killing it, which is what night here has to be: the beam +glows by itself, so a lit beam lights the world whatever the mirrors are doing, and +aiming them away would leave the floor at about 100 W/m² — half of noon. + +The eye-height slider runs from 1.7 m to 25 km with the local gravity beside it, and +**Rain** leans 1.5° antispinward. Nothing *inside* rotates, because you, the ground +and the air all turn together — so for 539 of the 540 km that 1.5° is the only +evidence Tower turns at all. The exception is the window: the sky out there does not +co-rotate, and wheels once every 7 min 29 s about the one motionless star on the +axis. It is by a wide margin the plainest sign of the spin anywhere in the view, and +it is only available at night, at the deepward end, looking the one direction that +has ever had anything in it but Tower. ## Working in the map diff --git a/decisions.md b/decisions.md index 7eaba1e..a842028 100644 --- a/decisions.md +++ b/decisions.md @@ -1319,6 +1319,58 @@ near plane sat at 8 km — the depth was being drawn correctly and could not be close enough to to see. 60 km and a 2 km near plane make the choice legible; without that change the constant would have been a number in a file and nothing else. +## D-045 — The far cap is a window, and the checker becomes its frame + +§1.4 has said since the first draft that the caps are the only views out of Tower +and that the sky through them turns rather than sweeps — a pinwheel about the axis +with one motionless star at the middle, one revolution every 7 min 29 s. It was +written and never built: §2.4 said flatly "no stars", and D-041 had meanwhile made +the far cap a checkered structural plate. + +**Glazed whole, not windows at its foot.** §1.4 put the windows at the base of a +50 km cliff. A strip at the bottom of something 540 km away is nearly nothing to +look at, and the canon fact worth having in the view is the pinwheel, which needs +sky. So the whole far cap is a window. The sun cap is not — it faces the sun and +needs glare management rather than a view, and that asymmetry is already canon. + +**The checker survives as the mullions.** Deleting it would have retired the only +object in Tower with a known cell size, which is the only rule anyone has for +judging distance (D-041, §1.8). Instead the same polar tessellation at the same +pitch is drawn as the frame around transparent panes: the cells are exactly where +they were, and you look through them rather than at them. The rule now reads against +the sky, which is a better place for it than against a plate. + +**The physics fell out of the shader that was already there.** A pane is black with +stars in it, so the existing haze integration does the rest by itself: by day the +airlight term swamps everything and the cap is a disc of blue, which is what §1.5 +already said would happen to anything 540 km off; at night the airlight collapses +with the beam and what is left is the sky. Nothing needed special-casing. + +**The stars are not scaled by the exposure, and that is the whole point.** Nothing +Tower does to its own beam changes how bright Vega is. What does change is the eye, +so a separate gain carries that half: full at night and below, 2% at temperate. The +stars are there at noon and unseeable, which is a fact about eyes and not glass. + +**Two projections were wrong before the third.** Gnomonic (xy/z) about the axis puts +the pole star at the origin and blows up as the ray goes perpendicular to the axis — +which is the ordinary grazing view from the foot of the cap, not an edge case. +Painting the field on the cap plane instead is stable everywhere and has no fixed +angular scale: from a kilometre away the entire 60° field lands inside one star +cell and the sky comes out empty, which is exactly what the first screenshot showed. +Azimuthal-equidistant has neither failure. It is defined for every direction but +exactly sunward, stars keep the same angular size however far off the window is — +which is what being at infinity means — and the pinwheel is not animated but falls +out of the projection, because azimuth plus spin phase *is* the rotation. + +The pole star is placed rather than left to the hash. It is the fixed point the +whole phenomenon is read against, and a sky that happened not to roll one there +would be missing the only part of it anybody in Tower would have named. + +**Left open:** §1.3.1 puts 24% of the beam — 13 TW — on the far cap. Glazing it +means that power leaves through the window instead of being absorbed by it. Whether +that is a heat-rejection system Tower depends on, or a load the glass has to +survive, is not settled. + ## D-046 — Night is the beam banked, not the swarm aimed away The obvious way to make night in a habitat lit by a steerable mirror swarm is to diff --git a/design.md b/design.md index 2f2d035..4f82679 100644 --- a/design.md +++ b/design.md @@ -59,9 +59,12 @@ Consequences that the app should show or at least respect: - **Coriolis is mild but real.** 2ω = 0.0280 s⁻¹. Walking is unaffected (0.4 % g). A ball thrown at 30 m/s deflects at 8.6 % g — enough that Tower's sports differ from Earth's, and enough that thrown objects visibly curve. -- **Nothing in the sky moves.** The hull is sealed (§1.4), so there are no stars to - wheel past — and the far side of the world overhead co-rotates with you, hanging - perfectly still. The spin is invisible *upward*. +- **Nothing in the sky moves, except through the far cap.** The wall is sealed + (§1.4), so along 539 of the 540 km there is nothing to wheel past — the far side + of the world overhead co-rotates with you and hangs perfectly still. The one + exception is the far cap window, where the sky is *outside* and does not + co-rotate: it turns once every 7 min 29 s about a single motionless star on the + axis. The spin is invisible upward and obvious deepward, and only at night. - **But everything that falls leans.** Rain hangs at a permanent 1.47° slant, smoke drifts the other way, and a 1 km waterfall lands 133 m off its own cliff (§1.9.2). This inversion is the alien fact the interior view must sell: **the sky is still @@ -207,9 +210,24 @@ The only views out are at the **end caps**, and they are peculiar: - **The sky through them does not sweep — it turns.** A cap window looks along the spin axis, which is the celestial pole. Stars rotate about a fixed centre, one full turn every 7.5 minutes, with one motionless star at the middle. **A pinwheel - sky.** It is the only moving sky anyone in Tower ever sees. -- The **far cap** looks into deep anti-sun space. The **sun cap** faces the sun and - the collector, and needs heavy glare management. + sky.** It is the only moving sky anyone in Tower ever sees — and because the axis + is horizontal from the floor, it wheels about a point on the **horizon** rather + than overhead. +- The **far cap is glazed whole** — a 100 km window looking into deep anti-sun + space, not a wall with windows at its foot (D-045). Its structure survives as the + window's **mullions**, on the same polar tessellation the checker used and at the + same cell pitch, so the one object in the world with a known scale still is one: + the rule now reads against the sky instead of against a plate. The **sun cap** + faces the sun and the collector, keeps its checker, and needs heavy glare + management rather than a view. +- **By day the window is simply blue.** §1.5 puts the far cap 100–200 km into the + haze, so from anywhere but the deepward end the airlight in front of it swamps + whatever is behind, and the cap reads as a disc of sky. The stars are there at + noon and unseeable, which is a fact about eyes and not about the glass. +- **13 TW lands on it.** §1.3.1 puts 24% of the beam on the far cap, and glazing it + means that power now leaves through the window rather than being absorbed by it. + Whether that is a heat-rejection system Tower depends on or a load the glass has + to survive is not yet settled canon. **Nobody aboard can ever see Earth.** Tower holds a terminator orbit (D-004), which puts Earth roughly 90° off the axis — out the *side* of the cylinder, which is now diff --git a/index.html b/index.html index a7db194..ab55145 100644 --- a/index.html +++ b/index.html @@ -62,8 +62,10 @@ </select></label> <label class="row"><input id="f-rain" type="checkbox" /> Rain</label> <p class="hint">Drag to look, scroll to zoom in like a pair of binoculars. - Selecting a place walks you there. Nothing here rotates: you, the ground - and the air all turn together, so the spin only shows in the rain.</p> + Selecting a place walks you there. Nothing inside here rotates: you, the + ground and the air all turn together. The far cap is a window, so at + night the sky through it wheels once every 7 min 29 s about a single + motionless star — the only place the spin is obvious.</p> </div> </details> diff --git a/src/views/cap-pattern.ts b/src/views/cap-pattern.ts index 0e64a09..b0bdb89 100644 --- a/src/views/cap-pattern.ts +++ b/src/views/cap-pattern.ts @@ -32,6 +32,8 @@ const RIM_SECTORS = 32 const LIGHT = '#c3cad6' const DARK = '#8d96a5' const RIB = '#dae1eb' +/** The far cap's window frame. Lighter than the checker: it is seen against sky. */ +const MULLION = '#aeb7c6' /** Sectors at a given radius, chosen to keep cells about CELL_KM wide. */ const sectorsAt = (rKm: number): number => { @@ -87,6 +89,72 @@ export function drawCapPattern(size = 2048): HTMLCanvasElement { return c } +/** + * The far cap's glazing: the same polar tessellation, drawn as the frame around + * transparent panes instead of a filled checker. + * + * §1.4 makes the caps the only views out of Tower, and D-045 glazes the far one + * whole. The checker cannot simply be deleted, because it is the only object in + * the world with a known cell size and so the only rule anyone has for judging + * distance (D-041). So it becomes the mullions: the cells stay exactly where they + * were and at exactly the same pitch, and you look through them instead of at + * them. The rule now reads against the sky, which is a better place for it. + * + * Alpha is the whole point of this canvas — opaque where there is structure, + * clear where there is a pane. The colour channels carry the mullion's own tone. + */ +export function drawCapGlazing(size = 2048): HTMLCanvasElement { + const c = document.createElement('canvas') + c.width = size + c.height = size + const ctx = c.getContext('2d')! + const mid = size / 2 + const px = mid / RADIUS_KM + + // Transparent ground: every pixel not drawn below is a pane. + ctx.clearRect(0, 0, size, size) + ctx.strokeStyle = MULLION + ctx.lineCap = 'round' + + /** + * Mullions are 400 m of structure across a 100 km window — heavy for a window + * and slight for a wall, which is about right for something holding an + * atmosphere back. Below a pixel on screen the line simply fades, and the cap + * goes clear rather than moiring, which is the behaviour D-041 wanted. + */ + const mullionKm = 0.4 + ctx.lineWidth = Math.max(1, mullionKm * px) + + for (let i = 0; i < RINGS; i++) { + const r0 = i * CELL_KM + const r1 = r0 + CELL_KM + const n = sectorsAt(r1) + // The ring itself. + ctx.beginPath() + ctx.arc(mid, mid, r1 * px, 0, Math.PI * 2) + ctx.stroke() + // The radial members bounding each cell in it. + for (let s = 0; s < n; s++) { + const a = (s / n) * Math.PI * 2 + ctx.beginPath() + ctx.moveTo(mid + Math.cos(a) * r0 * px, mid + Math.sin(a) * r0 * px) + ctx.lineTo(mid + Math.cos(a) * r1 * px, mid + Math.sin(a) * r1 * px) + ctx.stroke() + } + } + + // The same two heavy rings the checker has, so the coarse scale survives when + // the cell pitch is too fine to resolve through 540 km of air. + ctx.lineWidth = Math.max(1.5, mullionKm * 2.5 * px) + for (const r of [25, 50]) { + ctx.beginPath() + ctx.arc(mid, mid, r * px - ctx.lineWidth / 2, 0, Math.PI * 2) + ctx.stroke() + } + + return c +} + /** The pattern as a texture. Built once; both views share the one instance. */ let cached: Texture | null = null export function capPatternTexture(): Texture { @@ -107,3 +175,17 @@ export function capPatternTexture(): Texture { cached = tex return cached } + +/** The glazing as a texture, on the same terms. Alpha is the pane mask. */ +let glazingCached: Texture | null = null +export function capGlazingTexture(): Texture { + if (glazingCached) return glazingCached + const tex = new CanvasTexture(drawCapGlazing()) + tex.colorSpace = SRGBColorSpace + // Mipmapped for the same reason the checker is, and it matters more here: an + // aliasing window frame twinkles, which reads as the stars behind it moving. + tex.anisotropy = 16 + tex.magFilter = LinearFilter + glazingCached = tex + return glazingCached +} diff --git a/src/views/exterior.ts b/src/views/exterior.ts index 2dc0d38..28485ab 100644 --- a/src/views/exterior.ts +++ b/src/views/exterior.ts @@ -21,7 +21,7 @@ import { } from '../canon/constants.js' import { type SurfacePoint, mapThetaOrigin, normalizeTheta } from '../canon/geometry.js' import type { Place } from '../world/schema.js' -import { capPatternTexture } from './cap-pattern.js' +import { capGlazingTexture, capPatternTexture } from './cap-pattern.js' import { KIND_COLOR } from './palette.js' const KM = 1000 @@ -243,15 +243,28 @@ export class ExteriorView { aperture.position.z = -HULL_D + 0.4 this.tower.add(aperture) + /** + * The far cap is a window (D-045). From outside it is dark glass in a light + * frame: the panes look into a hazed interior that is not lit from this side, + * so the honest reading is nearly black, with the mullions catching the only + * light there is. The backing disc stops the panes showing the inside of the + * tube, which would be a view through Tower rather than into it. + */ + const glass = new Mesh( + new CircleGeometry(R, 96), + new MeshStandardMaterial({ color: 0x0a0f18, roughness: 0.18, metalness: 0.5 })) + glass.position.z = L + HULL_D + this.tower.add(glass) + + const glazing = capGlazingTexture() const far = new Mesh( new CircleGeometry(R, 96), new MeshStandardMaterial({ - // Lighter than the bare cap used to be: the pattern supplies the tone now, - // and this face is turned away from every light in the scene. - color: 0xd8dee8, map: pattern, roughness: 0.72, metalness: 0.25, side: DoubleSide, - emissiveMap: pattern, emissive: new Color(0xffffff), emissiveIntensity: 0.5, + color: 0xd8dee8, map: glazing, transparent: true, side: DoubleSide, + roughness: 0.72, metalness: 0.25, + emissiveMap: glazing, emissive: new Color(0xffffff), emissiveIntensity: 0.5, })) - far.position.z = L + HULL_D + far.position.z = L + HULL_D + 0.02 this.tower.add(far) } diff --git a/src/views/interior.ts b/src/views/interior.ts index 0fbafc4..60a7b40 100644 --- a/src/views/interior.ts +++ b/src/views/interior.ts @@ -34,7 +34,7 @@ import { RAINDROP_TERMINAL_M_S, steadyFallLeanRad } from '../canon/motion.js' import { type SurfacePoint, mapThetaOrigin, normalizeTheta } from '../canon/geometry.js' import type { Place } from '../world/schema.js' import { EYE_MAX_M, EYE_MIN_M } from '../ui/scales.js' -import { capPatternTexture } from './cap-pattern.js' +import { capGlazingTexture, capPatternTexture } from './cap-pattern.js' import { COLOR_RASTER, HEIGHT_RASTER, RASTER_HEIGHT, RASTER_HEIGHT_SPAN_M, RASTER_MIN_HEIGHT_M, RASTER_WIDTH, @@ -264,6 +264,78 @@ void main() { } ` +/** + * The sky through the far cap window (§1.4, D-045). + * + * Sampled by view direction, in an azimuthal-equidistant projection about +z. + * + * The projection is the whole trick. +z is the spin axis and therefore the + * celestial pole, so polar angle becomes radius and azimuth becomes angle, and + * adding the spin phase to the azimuth *is* the pinwheel: every star runs a circle + * about the axis once every 7 min 29 s and the one on the axis does not move. + * + * Two projections were wrong before this one. Gnomonic (xy/z) blows up as the ray + * goes perpendicular to the axis, which is the ordinary grazing view from the foot + * of the cap. Painting the field on the cap plane instead is stable but has no + * fixed angular scale — from a kilometre away the whole 60° field lands inside one + * star cell, so the sky comes out empty. Equidistant has neither failure: it is + * defined for every direction but exactly sunward, and stars stay the same angular + * size however far away the window is, because they are at infinity and that is + * what being at infinity means. + */ +const STAR_GLSL = /* glsl */` +uniform float uSpin; +uniform float uStarGain; + +float starHash(vec2 p) { + vec3 q = fract(vec3(p.xyx) * 0.1031); + q += dot(q, q.yzx + 33.33); + return fract((q.x + q.y) * q.z); +} +vec2 starHash2(vec2 p) { + vec3 q = fract(vec3(p.xyx) * vec3(0.1031, 0.1030, 0.0973)); + q += dot(q, q.yzx + 33.33); + return fract((q.xx + q.yz) * q.zy); +} + +// Cells per radian. ~1.2 deg cells, which at 22% occupancy lands near the naked-eye +// sky's real density of one star per 7 square degrees. +const float STAR_SCALE = 46.0; + +vec3 starField(vec3 dir) { + vec3 d = normalize(dir); + float th = acos(clamp(d.z, -1.0, 1.0)); + float ph = atan(d.y, d.x) + uSpin; + vec2 p = vec2(cos(ph), sin(ph)) * th * STAR_SCALE; + vec2 cell = floor(p); + vec2 f = p - cell; + vec3 sum = vec3(0.0); + for (int j = -1; j <= 1; j++) { + for (int i = -1; i <= 1; i++) { + vec2 o = vec2(float(i), float(j)); + vec2 g = cell + o; + float mag = starHash(g); + if (mag < 0.78) continue; // most of the sky is empty + float b = pow(max(0.0, 1.0 - length(o + starHash2(g + 11.7) - f) / 0.09), 4.0) + * ((mag - 0.78) / 0.22); + // A few warm ones among the cool. Without it the field reads as noise. + sum += b * mix(vec3(0.72, 0.80, 1.0), vec3(1.0, 0.86, 0.68), starHash(g + 5.1)); + } + } + /** + * The pole star, placed rather than left to the hash. §1.4 promises one + * motionless star at the centre of the pinwheel; it is the fixed point the whole + * phenomenon is read against, and a sky that happened not to roll one there + * would be missing the only part of it anybody in Tower would have named. + * + * It lands at the observer's own radius on the window — the axis is horizontal + * from the floor, so this is a star on the horizon, not overhead. + */ + sum += pow(max(0.0, 1.0 - th * STAR_SCALE / 0.11), 4.0) * vec3(1.0, 0.95, 0.86) * 1.6; + return sum * 2.6 * uStarGain; +} +` + const CAP_FRAG = /* glsl */` #include <common> #include <logdepthbuf_pars_fragment> @@ -283,6 +355,44 @@ void main() { } ` +/** + * The far cap: a window, and the frame holding it. + * + * `uGlazing`'s alpha is the mask — opaque where there is a mullion, clear where + * there is a pane. Through a pane is deep anti-sun space (§1.4): black, with stars + * in it, and the haze in front doing the rest. By day the airlight term swamps + * everything and the cap is simply blue, which is what §1.5 already said would + * happen to anything 540 km away. At night the airlight collapses with the beam + * and what is left is the sky. + */ +const GLAZED_CAP_FRAG = /* glsl */` +#include <common> +#include <logdepthbuf_pars_fragment> +uniform vec3 uTint; +uniform sampler2D uGlazing; +varying vec3 vWorld; +${HAZE_GLSL} +${STAR_GLSL} +void main() { + #include <logdepthbuf_fragment> + vec2 uv = vWorld.xy / ${(2 * R).toFixed(1)} + 0.5; + vec4 frame = texture2D(uGlazing, uv); + vec3 mullion = pow(frame.rgb, vec3(2.2)) * uTint * uExposure; + /** + * The stars are NOT scaled by uExposure. Nothing Tower does to its own beam + * changes how bright Vega is — the exposure models the beam lighting the world, + * and the sky outside is not lit by it. uStarGain carries the other half of the + * same fact: the eye adapts, so the stars are there at noon and unseeable. + */ + vec3 sky = starField(vWorld - uCam); + vec3 face = mix(sky, mullion, frame.a); + vec3 T = towerTransmittance(uCam, vWorld); + gl_FragColor = vec4(face * T + uHaze * uExposure * (1.0 - T), 1.0); + #include <colorspace_fragment> +} +` + + const BEAM_VERT = /* glsl */` #include <common> #include <logdepthbuf_pars_vertex> @@ -350,6 +460,8 @@ export class InteriorView { pitch = 0 fovDeg = 60 lighting: Lighting = 'temperate' + /** Phase of the star pinwheel, radians. The only thing in the view that turns. */ + private spin = 0 showRain = false private labels: { place: Place; el: HTMLSpanElement }[] = [] @@ -413,6 +525,10 @@ export class InteriorView { uK: { value: new Vector3(...K_RGB) }, uHaze: { value: new Color(HAZE_DAY) }, uExposure: { value: 1 }, + // Only the glazed cap reads these, but every material carries them so the + // per-frame update can walk one list instead of knowing who wants what. + uSpin: { value: 0 }, + uStarGain: { value: 0 }, } } @@ -552,6 +668,19 @@ export class InteriorView { return m } + private glazedCapMaterial(): ShaderMaterial { + const m = new ShaderMaterial({ + vertexShader: FLAT_VERT, fragmentShader: GLAZED_CAP_FRAG, side: DoubleSide, + uniforms: { + uTint: { value: new Color(0xbcc2cc) }, + uGlazing: { value: capGlazingTexture() }, + ...this.sharedUniforms(), + }, + }) + this.shared.push(m) + return m + } + private revealMaterial(tint: number): ShaderMaterial { const m = new ShaderMaterial({ vertexShader: FLAT_VERT, fragmentShader: REVEAL_FRAG, side: BackSide, @@ -576,7 +705,7 @@ export class InteriorView { * kilometre of reveal standing in front of it — which is what the wall depth of * D-044 actually looks like from the only place it can be seen. */ - const far = new Mesh(new CircleGeometry(R, 128), this.capMaterial(0xbcc2cc)) + const far = new Mesh(new CircleGeometry(R, 128), this.glazedCapMaterial()) far.position.z = L + HULL_D this.scene.add(far) @@ -711,9 +840,17 @@ export class InteriorView { */ const watts = LIGHTING[this.lighting] const e = (watts / BRIGHTNESS_TEMPERATE_W_M2) ** 0.35 + /** + * The other half of "eyes adapt": the stars are always exactly as bright as + * they are, and whether you can see them depends entirely on what your eyes + * are currently set to. Full at night and below, gone by noon — 2% at + * temperate, which is the honest answer to why nobody looks out at lunchtime. + */ + const starGain = (BRIGHTNESS_NIGHT_W_M2 / Math.max(watts, BRIGHTNESS_NIGHT_W_M2)) ** 0.6 const haze = new Color(watts <= BRIGHTNESS_TWILIGHT_W_M2 ? HAZE_DUSK : HAZE_DAY) for (const m of this.shared) { m.uniforms['uExposure']!.value = e + m.uniforms['uStarGain']!.value = starGain ;(m.uniforms['uHaze']!.value as Color).copy(haze) } // Scaled, so `dark` clears to black rather than to a dusk-coloured sky that @@ -927,6 +1064,14 @@ export class InteriorView { const dt = Math.min(0.1, (now - this.lastFrame) / 1000) this.lastFrame = now if (this.showRain) this.stepRain(dt) + /** + * The pinwheel. Nothing inside Tower rotates — you, the ground and the air + * all turn together — but the sky through the far cap does not, so it wheels + * once every 7 min 29 s about the one motionless star on the axis. It is the + * only unambiguous evidence of the spin anywhere in the view (D-045). + */ + this.spin = (this.spin + OMEGA_RAD_S * dt) % (2 * Math.PI) + for (const m of this.shared) m.uniforms['uSpin']!.value = this.spin this.renderer.render(this.scene, this.camera) this.updateLabels() requestAnimationFrame(tick)