tower / commit
feat(canon): Tower's wall is a kilometre thick
decisions.md | 24 ++++++++++++++ design.md | 1 + docs/screenshots/desktop-exterior-true.png | Bin 0 -> 480301 bytes docs/screenshots/desktop-exterior.png | Bin 260378 -> 277525 bytes docs/screenshots/desktop-interior-night.png | Bin 0 -> 237262 bytes docs/screenshots/desktop-interior-overhead.png | Bin 374948 -> 448992 bytes docs/screenshots/desktop-interior.png | Bin 446455 -> 452858 bytes docs/screenshots/mobile-exterior.png | Bin 72930 -> 70244 bytes docs/screenshots/mobile-interior.png | Bin 143542 -> 145358 bytes docs/screenshots/mobile-panel.png | Bin 69817 -> 72871 bytes docs/screenshots/overview.png | Bin 683709 -> 856785 bytes src/canon/constants.ts | 8 +++++ src/views/exterior.ts | 28 +++++++++++++++-- src/views/interior.ts | 42 +++++++++++++++++++++++-- 14 files changed, 98 insertions(+), 5 deletions(-) diff --git a/decisions.md b/decisions.md index dbefc9a..24cc875 100644 --- a/decisions.md +++ b/decisions.md @@ -1294,3 +1294,27 @@ breakpoint (a phone turned on its side) resets to closed, which is also what cle of aligned columns and overran 390 px at 11 px type. It shrinks to 9 px on the same breakpoint rather than wrapping — wrapping those columns destroys the thing they are for. + +## D-044 — Tower's wall is a kilometre thick + +Canon had no figure for it. Both 3D views drew the hull, the caps and the collector +as zero-thickness surfaces, which is invisible until something is seen edge-on and +then reads as a paper cutout rather than a structure. + +**1 km, floor to outer skin** — 2% of the radius. The alternatives were a 200 m +technical minimum, which is defensible on shielding and services and is sub-pixel at +every exterior zoom, and 5 km, which is unmistakable and a great deal of mass to +justify later. A kilometre reads as built without making Tower a pipe. + +**It is only visible at the openings**, which is worth stating because it took a +false start to notice. A closed can has no visible thickness from outside — the +silhouette of a 50 km cylinder does not change when you give its wall a kilometre. +Depth shows only where there is a hole: the beam aperture, and now the far cap +window. So the caps sit at the **outer** face of the wall with a 1 km collar +standing between them and the inner surface, and it is the collar you see down. + +**The exterior camera had to be let closer.** A kilometre is 1% of the cap's +diameter, so at the old 120 km minimum distance the collar was two pixels and the +near plane sat at 8 km — the depth was being drawn correctly and could not be got +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. diff --git a/design.md b/design.md index 6cbdb0f..7523ded 100644 --- a/design.md +++ b/design.md @@ -32,6 +32,7 @@ inner surface matches Wisconsin. |---|---|---| | Radius | **50 km** | floor to axis | | Length | **540 km** | sun cap to far cap | +| Wall depth | **1 km** | floor to outer skin, D-044 | | Circumference | **314.16 km** | the world's "width" | | Inner surface area | **169,646 km²** | Wisconsin total area, 169,635 km² | | Habitable land area | **169,646 km²** | all of it — the hull is sealed (§1.4) | diff --git a/docs/screenshots/desktop-exterior-true.png b/docs/screenshots/desktop-exterior-true.png new file mode 100644 index 0000000..1ac9f25 Binary files /dev/null and b/docs/screenshots/desktop-exterior-true.png differ diff --git a/docs/screenshots/desktop-exterior.png b/docs/screenshots/desktop-exterior.png index c920665..df28393 100644 Binary files a/docs/screenshots/desktop-exterior.png and b/docs/screenshots/desktop-exterior.png differ diff --git a/docs/screenshots/desktop-interior-night.png b/docs/screenshots/desktop-interior-night.png new file mode 100644 index 0000000..66e9524 Binary files /dev/null and b/docs/screenshots/desktop-interior-night.png differ diff --git a/docs/screenshots/desktop-interior-overhead.png b/docs/screenshots/desktop-interior-overhead.png index 7798a3a..16924d7 100644 Binary files a/docs/screenshots/desktop-interior-overhead.png and b/docs/screenshots/desktop-interior-overhead.png differ diff --git a/docs/screenshots/desktop-interior.png b/docs/screenshots/desktop-interior.png index c6044f6..65fd822 100644 Binary files a/docs/screenshots/desktop-interior.png and b/docs/screenshots/desktop-interior.png differ diff --git a/docs/screenshots/mobile-exterior.png b/docs/screenshots/mobile-exterior.png index 9d9d465..f88fd92 100644 Binary files a/docs/screenshots/mobile-exterior.png and b/docs/screenshots/mobile-exterior.png differ diff --git a/docs/screenshots/mobile-interior.png b/docs/screenshots/mobile-interior.png index bcbcab9..72eaf1b 100644 Binary files a/docs/screenshots/mobile-interior.png and b/docs/screenshots/mobile-interior.png differ diff --git a/docs/screenshots/mobile-panel.png b/docs/screenshots/mobile-panel.png index f9c4ce6..b935c1a 100644 Binary files a/docs/screenshots/mobile-panel.png and b/docs/screenshots/mobile-panel.png differ diff --git a/docs/screenshots/overview.png b/docs/screenshots/overview.png index 1a90b57..1be84ad 100644 Binary files a/docs/screenshots/overview.png and b/docs/screenshots/overview.png differ diff --git a/src/canon/constants.ts b/src/canon/constants.ts index d87dbc3..0cca227 100644 --- a/src/canon/constants.ts +++ b/src/canon/constants.ts @@ -84,6 +84,14 @@ export const BRIGHTNESS_TWILIGHT_W_M2 = 30 export const BRIGHTNESS_TEMPERATE_W_M2 = 200 export const BRIGHTNESS_FULL_W_M2 = BEAM_POWER_W / INNER_AREA_M2 +// --------------------------------------------------------- structure (design.md §1.1) +/** + * Wall depth, floor to outer skin. Canon had no figure for this; 1 km is 2% of the + * radius — enough to read as built from outside and to give the cap windows a + * reveal you can see the depth of, without making Tower a pipe. D-044. + */ +export const HULL_DEPTH_M = 1000 + // --------------------------------------------------- atmosphere (design.md §1.5) export const SPECIFIC_GAS_CONSTANT = 287 export const TEMPERATURE_K = 288 diff --git a/src/views/exterior.ts b/src/views/exterior.ts index 67acd12..2dc0d38 100644 --- a/src/views/exterior.ts +++ b/src/views/exterior.ts @@ -17,7 +17,7 @@ import { } from 'three' import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' import { - BEAM_RADIUS_M, COLLECTOR_DIAMETER_M, LENGTH_M, OMEGA_RAD_S, RADIUS_M, + BEAM_RADIUS_M, COLLECTOR_DIAMETER_M, HULL_DEPTH_M, LENGTH_M, OMEGA_RAD_S, RADIUS_M, } from '../canon/constants.js' import { type SurfacePoint, mapThetaOrigin, normalizeTheta } from '../canon/geometry.js' import type { Place } from '../world/schema.js' @@ -28,6 +28,8 @@ const KM = 1000 const R = RADIUS_M / KM const L = LENGTH_M / KM const BEAM_R = BEAM_RADIUS_M / KM +/** Wall depth in km (D-044). The caps sit at the outer face of it. */ +const HULL_D = HULL_DEPTH_M / KM const COLLECTOR_R = COLLECTOR_DIAMETER_M / KM / 2 /** How far ahead of the sun cap the collector flies. Not fixed by canon. */ const COLLECTOR_STANDOFF = 320 @@ -92,6 +94,7 @@ export class ExteriorView { this.hull = this.buildHull() this.tower.add(this.hull, this.markers) this.scene.add(this.tower) + this.buildCollars() this.buildCaps() this.buildSunSystem() this.buildStars() @@ -199,6 +202,24 @@ export class ExteriorView { this.applyTerrain() } + /** + * The kilometre of wall (D-044), which from outside is only visible as depth at + * the two ends: the inner surface stops at z = 0 and z = L, the caps sit a + * kilometre further out at the skin, and what stands between them is a collar + * you can see down. There is no terrain on a collar — it is wall, not world. + */ + private buildCollars(): void { + const mat = new MeshStandardMaterial({ + color: 0x6f7787, roughness: 0.55, metalness: 0.42, side: DoubleSide, + }) + for (const z0 of [-HULL_D, L]) { + const g = new CylinderGeometry(R, R, HULL_D, 160, 1, true) + g.rotateX(Math.PI / 2) + g.translate(0, 0, z0 + HULL_D / 2) + this.tower.add(new Mesh(g, mat)) + } + } + private buildCaps(): void { // The same pattern the interior view paints on their far side (D-041). A cap is // structure, not landscape, and the checker is what says so from either side. @@ -213,12 +234,13 @@ export class ExteriorView { // is being looked at. Enough emissive to keep the pattern legible. emissiveMap: pattern, emissive: new Color(0xffffff), emissiveIntensity: 0.42, })) + sun.position.z = -HULL_D this.tower.add(sun) const aperture = new Mesh( new CircleGeometry(BEAM_R, 48), new MeshBasicMaterial({ color: 0xfff0cf })) - aperture.position.z = 0.4 + aperture.position.z = -HULL_D + 0.4 this.tower.add(aperture) const far = new Mesh( @@ -229,7 +251,7 @@ export class ExteriorView { color: 0xd8dee8, map: pattern, roughness: 0.72, metalness: 0.25, side: DoubleSide, emissiveMap: pattern, emissive: new Color(0xffffff), emissiveIntensity: 0.5, })) - far.position.z = L + far.position.z = L + HULL_D this.tower.add(far) } diff --git a/src/views/interior.ts b/src/views/interior.ts index 8a00abb..fba9095 100644 --- a/src/views/interior.ts +++ b/src/views/interior.ts @@ -26,7 +26,8 @@ import { } from 'three' import { BEAM_RADIUS_M, BRIGHTNESS_FULL_W_M2, BRIGHTNESS_TEMPERATE_W_M2, - BRIGHTNESS_TWILIGHT_W_M2, G0, LENGTH_M, OMEGA_RAD_S, RADIUS_M, RAYLEIGH_FLOOR_PER_M, + BRIGHTNESS_TWILIGHT_W_M2, G0, HULL_DEPTH_M, LENGTH_M, OMEGA_RAD_S, RADIUS_M, + RAYLEIGH_FLOOR_PER_M, SPECIFIC_GAS_CONSTANT, TEMPERATURE_K, gravityAtHeight, } from '../canon/constants.js' import { RAINDROP_TERMINAL_M_S, steadyFallLeanRad } from '../canon/motion.js' @@ -43,6 +44,8 @@ const KM = 1000 const R = RADIUS_M / KM const L = LENGTH_M / KM const BEAM_R = BEAM_RADIUS_M / KM +/** Wall depth in km. The far cap sits at the outer face of it. */ +const HULL_D = HULL_DEPTH_M / KM /** * Rayleigh extinction per kilometre at floor density, per channel. @@ -127,6 +130,21 @@ vec3 towerTransmittance(vec3 from, vec3 to) { } ` +/** The bore between the world's end and the cap. Plain structure, hazed. */ +const REVEAL_FRAG = /* glsl */` +#include <common> +#include <logdepthbuf_pars_fragment> +uniform vec3 uTint; +varying vec3 vWorld; +${HAZE_GLSL} +void main() { + #include <logdepthbuf_fragment> + vec3 T = towerTransmittance(uCam, vWorld); + gl_FragColor = vec4(uTint * uExposure * T + uHaze * uExposure * (1.0 - T), 1.0); + #include <colorspace_fragment> +} +` + const GROUND_VERT = /* glsl */` #include <common> #include <logdepthbuf_pars_vertex> @@ -520,6 +538,15 @@ export class InteriorView { return m } + private revealMaterial(tint: number): ShaderMaterial { + const m = new ShaderMaterial({ + vertexShader: FLAT_VERT, fragmentShader: REVEAL_FRAG, side: BackSide, + uniforms: { uTint: { value: new Color(tint) }, ...this.sharedUniforms() }, + }) + this.shared.push(m) + return m + } + /** * The caps close the world at both ends. From the midpoint they are 270 km away * and 4% transmitted (§1.8), so they are all but pure haze — which is the correct @@ -530,9 +557,20 @@ export class InteriorView { // The sun cap sits closer to the beam's brightest end; the far cap is dimmer. const sun = new Mesh(new RingGeometry(BEAM_R, R, 128), this.capMaterial(0xffffff)) this.scene.add(sun) + /** + * The far cap sits at the outer face of the wall, so from inside there is a + * 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)) - far.position.z = L + far.position.z = L + HULL_D this.scene.add(far) + + // The bore. BackSide, because it is only ever seen from inside. + const bore = new CylinderGeometry(R, R, HULL_D, 128, 1, true) + bore.rotateX(Math.PI / 2) + bore.translate(0, 0, L + HULL_D / 2) + this.scene.add(new Mesh(bore, this.revealMaterial(0x9aa0aa))) } private buildBeam(): void {