tower / commit
feat(exterior): the hull reads two ways, and they are different claims
README.md | 14 +++++++++---- decisions.md | 30 +++++++++++++++++++++++++++ index.html | 9 ++++++++ src/ui/app.ts | 16 ++++++++++----- src/views/exterior.ts | 57 ++++++++++++++++++++++++++++++++++++++------------- 5 files changed, 103 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 88c43dd..5700a7c 100644 --- a/README.md +++ b/README.md @@ -157,10 +157,16 @@ Any selection updates the URL, so a link is always shareable: ## Views -**Map** is the unrolled inner surface. **Exterior** is Tower from outside — hull, -checkered end caps, the 244 km collector flying ahead and the beam converging into -the sun cap, with the terrain wrapped onto the hull as an x-ray of what is inside. Each -piece is captioned. The active view is remembered and travels in the permalink, so +**Map** is the unrolled inner surface. **Exterior** is Tower from outside — a +kilometre of hull, the checkered sun cap, the glazed far cap, the 244 km collector +flying ahead and the beam converging into the sun cap. Each piece is captioned. + +The hull reads two ways and the **Hull** switch picks one. **True** is the outer +skin: bare structure, and the far cap a real window — look in through it and the +land is actually there, lit and receding 540 km down the tube. It is the only line +of sight into Tower that is not a diagram. **X-ray** wraps the terrain onto the +outside instead, which is a diagram of what is inside rather than a view of it, and +useful precisely because the wall is opaque. The active view is remembered and travels in the permalink, so `#view=exterior&place=the-narrows` opens where you left off. Drag to orbit, scroll to zoom, click the hull to drop a point; the spin runs at 1x to 1200x real time, and a point picked off the turning hull still comes back in canon coordinates. diff --git a/decisions.md b/decisions.md index a842028..00c25e2 100644 --- a/decisions.md +++ b/decisions.md @@ -1393,3 +1393,33 @@ having. The beam's brightness scales with the exposure already, so it banks and goes out on its own; neither setting needed the beam shader touched. + +## D-047 — The exterior hull reads two ways, and they are different claims + +The exterior had a "Show terrain" checkbox that wrapped the raster onto the hull. +It was doing something stronger than a checkbox admits: from outside Tower is +structure, so terrain on the outside is not what Tower looks like — it is a diagram +of what is inside, drawn on the outside because the wall is opaque. Useful, and not +a view. The two readings are different claims about the same object, so the control +is now a two-way switch that names both: + +- **True** — the outer skin. Bare hull, checkered sun cap, glazed far cap. +- **X-ray** — the inner surface projected onto the outside. + +**The wall is drawn as two meshes now, not one double-sided one.** Its faces show +different things: the outer skin changes with the mode and the inner surface is +always the land, and a single `DoubleSide` material has no way to say so. The inner +mesh is emissive for the same reason the x-ray always was — it is lit by a beam this +scene does not model, and unlit it reads as a hole rather than a world. + +**This is what made the far cap window worth having from outside.** D-045 glazed it +for the sake of the interior, where the payoff is the pinwheel. From outside the +payoff is different and better: the panes are clear, so in true mode you look in +through the glass and the world is really there, mountains and desert and lakes +receding 540 km down the tube behind a grid of mullions. It is the only line of +sight into Tower that is not a projection, and it exists only because the wall got a +window and the hull got two faces in the same week. + +The dark backing disc the first version of D-045 put behind the glazing is gone. It +was there to stop the panes showing the inside of the tube — which turns out to be +exactly the thing worth showing. diff --git a/index.html b/index.html index ab55145..1530efe 100644 --- a/index.html +++ b/index.html @@ -43,6 +43,15 @@ outside.</p> </div> <div id="exterior-options" class="hidden"> + <label>Hull + <select id="f-ext-view"> + <option value="true">True — what Tower looks like</option> + <option value="xray" selected>X-ray — the inner surface projected out</option> + </select></label> + <p class="hint">True shows the outer skin: bare hull, checkered sun cap, and + the far cap window — look in through it and the land is really there, + 540 km deep. X-ray wraps the terrain onto the outside instead, which is a + diagram of what is inside rather than a view of it.</p> <label class="row"><input id="f-spin" type="checkbox" checked /> Spin</label> <label>Speed <span id="speed-value" class="dim"></span> <input id="f-speed" type="range" min="0" max="4" step="1" value="2" /></label> diff --git a/src/ui/app.ts b/src/ui/app.ts index fe47138..812a1be 100644 --- a/src/ui/app.ts +++ b/src/ui/app.ts @@ -221,9 +221,12 @@ export class App { $('map-options').classList.toggle('hidden', !isMap) $('exterior-options').classList.toggle('hidden', !isExterior) $('interior-options').classList.toggle('hidden', !isInterior) - // The terrain raster is the ground itself in the interior; there is nothing to - // turn off, so the control goes away rather than sitting there doing nothing. - $('terrain-row').classList.toggle('hidden', isInterior) + /** + * "Show terrain" is the map's control. The interior has nothing to turn off — + * the raster is the ground itself — and the exterior now has its own two-mode + * switch instead of a checkbox (D-047), so the row belongs to the map alone. + */ + $('terrain-row').classList.toggle('hidden', !isMap) $('spin').classList.toggle('hidden', !isExterior) $('ext-labels').classList.toggle('hidden', !isExterior) $('standing').classList.toggle('hidden', !isInterior) @@ -258,7 +261,7 @@ export class App { const speed = $<HTMLInputElement>('f-speed') this.exterior.spinning = spin.checked this.exterior.timeScale = SPEEDS[Number(speed.value)]! - this.exterior.showTerrain = $<HTMLInputElement>('f-terrain').checked + this.exterior.showTerrain = $<HTMLSelectElement>('f-ext-view').value === 'xray' } this.exterior.resize() this.exterior.setPlaces(this.store.places, this.selectedId) @@ -533,7 +536,10 @@ export class App { box.onchange = () => { this.map.showTerrain = box.checked this.map.draw() - if (this.exterior) this.exterior.showTerrain = box.checked + } + const ext = $<HTMLSelectElement>('f-ext-view') + ext.onchange = () => { + if (this.exterior) this.exterior.showTerrain = ext.value === 'xray' } } diff --git a/src/views/exterior.ts b/src/views/exterior.ts index 28485ab..89bd686 100644 --- a/src/views/exterior.ts +++ b/src/views/exterior.ts @@ -9,6 +9,7 @@ import type { Texture } from 'three' import { AdditiveBlending, AmbientLight, BackSide, BufferGeometry, CircleGeometry, Color, + FrontSide, CylinderGeometry, DirectionalLight, DoubleSide, Float32BufferAttribute, Group, Mesh, LineBasicMaterial, LineSegments, MeshBasicMaterial, MeshStandardMaterial, PerspectiveCamera, Points, PointsMaterial, @@ -55,6 +56,7 @@ export class ExteriorView { private markers = new Group() private hull: Mesh private hullMat: MeshStandardMaterial + private innerMat!: MeshStandardMaterial private terrainMap: Texture | null = null private _showTerrain = true private raycaster = new Raycaster() @@ -79,7 +81,9 @@ export class ExteriorView { ) { this.renderer = new WebGLRenderer({ canvas, antialias: true }) this.renderer.setClearColor(0x05070d) - this.camera = new PerspectiveCamera(42, 1, 8, 30_000) + // Near plane at 2 km rather than 8: the wall is only a kilometre thick (D-044), + // so anywhere close enough to read it as depth is inside the old near plane. + this.camera = new PerspectiveCamera(42, 1, 2, 30_000) // Framed to hold the whole system: the collector is 244 km across and 320 km // ahead of the sun cap, so Tower alone is not the subject. this.camera.position.set(260, -1180, 300) @@ -87,7 +91,10 @@ export class ExteriorView { this.controls.target.set(0, 0, (L - COLLECTOR_STANDOFF) / 2) this.controls.enableDamping = true this.controls.dampingFactor = 0.08 - this.controls.minDistance = 120 + // 60 km, down from 120. A kilometre of wall is 1% of the cap's diameter, so it + // is a couple of pixels from anywhere the old limit allowed — the depth was + // being drawn and could not be got close enough to to see. + this.controls.minDistance = 60 this.controls.maxDistance = 6000 this.hullMat = new MeshStandardMaterial() @@ -116,6 +123,16 @@ export class ExteriorView { // ------------------------------------------------------------------ geometry + /** + * Two meshes, not one, because the two sides of Tower's wall show different + * things and a single DoubleSide material cannot say so. + * + * The outer skin is what Tower looks like: bare metal, or — in x-ray mode — the + * inner surface projected onto it, which is a diagram and not a view (D-047). + * The inner surface is the land itself, and since the far cap became a window + * (D-045) it is genuinely visible from outside: look in through the glass and + * the far wall of the world is right there, lit and 540 km deep. + */ private buildHull(): Mesh { const g = new CylinderGeometry(R, R, L, 160, 1, true) g.rotateX(Math.PI / 2) @@ -123,8 +140,15 @@ export class ExteriorView { this.remapHullUVs(g) this.hullMat = new MeshStandardMaterial({ - color: 0x8892a3, roughness: 0.62, metalness: 0.28, side: DoubleSide, + color: 0x8892a3, roughness: 0.62, metalness: 0.28, side: FrontSide, }) + // The land, seen from inside, through the window. Always the terrain — there + // is no mode in which the inside of Tower is bare metal. + this.innerMat = new MeshStandardMaterial({ + color: 0x2a2f38, roughness: 0.95, metalness: 0.0, side: BackSide, + }) + this.tower.add(new Mesh(g.clone(), this.innerMat)) + const hull = new Mesh(g, this.hullMat) this.loadTerrain() return hull @@ -195,6 +219,18 @@ export class ExteriorView { this.hullMat.metalness = on ? 0.06 : 0.28 this.hullMat.roughness = on ? 0.9 : 0.62 this.hullMat.needsUpdate = true + + // The inside carries the land in both modes, and always emissive: it is lit by + // a beam this scene does not model, and unlit it would read as a black hole + // behind the window rather than as a world. + if (this.terrainMap) { + this.innerMat.map = this.terrainMap + this.innerMat.emissiveMap = this.terrainMap + this.innerMat.emissive.set(0xffffff) + this.innerMat.emissiveIntensity = 0.5 + this.innerMat.color.set(0xffffff) + this.innerMat.needsUpdate = true + } } set showTerrain(v: boolean) { @@ -244,18 +280,11 @@ export class ExteriorView { 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. + * The far cap is a window (D-045), and from outside it is a window in the + * ordinary sense: the mullions are structure and the panes are clear, so what + * is behind them is the inside of Tower. That is the whole of the "true" view's + * payoff — the only line of sight into the world that is not a diagram. */ - 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),