fuzzyworld

tower / commit

feat(exterior): the collector is dark on both sides, with a lit rim

472390e279db71fcc880f0e664e45bef14ea725b · Pavel Safronov · 2026-09-08 22:44:34 -0700 · parents b55cf01bac47

feat(exterior): the collector is dark on both sides, with a lit rim

Drawn pale, it contradicted the window it is seen through: the interior
view already renders it as a hole with a lit rim (D-050), because the
face pointed at Tower is a 245 K radiator and radiates in the infrared.
The face pointed at the sun is no brighter — a flat mirror in vacuum
shows the empty sky in front of it, not the sun behind the eye.

So it becomes two single-sided meshes rather than one double-sided disc,
and culling picks whichever the camera is entitled to see; the pair never
fight over a depth. What makes it legible is the sunlit rim of D-054,
4 km of structure standing proud of the mirror plane and drawn outboard
of its edge, which is what stops the black disc reading as a gap in the
stars.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
 decisions.md          | 29 ++++++++++++++++++++++
 design.md             |  7 +++++-
 src/views/exterior.ts | 66 ++++++++++++++++++++++++++++++++++++++++++---------
 3 files changed, 90 insertions(+), 12 deletions(-)

diff --git a/decisions.md b/decisions.md
index 29c1d02..e866ee9 100644
--- a/decisions.md
+++ b/decisions.md
@@ -1644,3 +1644,32 @@ seven times too wide, which washed the whole disc gold instead of tracing it.
 lamp, so the rim is outside `uExposure`, like the stars. It does fade as the eye
 adapts, but from a much higher floor than a star's — this is sunlit structure, and
 it stays faintly visible at noon where a star cannot.
+
+## D-055 — The collector looks the same from outside as it does from inside
+
+The exterior view drew the collector as a near-white disc with a pale blue glow, and
+the sun cap window drew it as black with a lit rim (D-054). Both cannot be right, and
+the window is the one that follows from the physics: the collector's mirror face is
+pointed at the sun and away from Tower, so what the world sees is the back of it, a
+46,938 km² radiator running at 245 K. That is infrared. In visible light it is black.
+
+**So the exterior draws two faces rather than one.** The disc is two meshes sharing a
+plane, one front-facing and one back-facing, and the camera's own position picks which
+one it gets — culling does the choosing, so they never fight over the same depth.
+
+- The Tower side is very dark and unlit, with just enough emissive to keep it a
+  surface. Not zero: against black sky, a wholly unlit disc is not a dark object, it
+  is a missing one.
+- The sun side is bare metal at almost no roughness, which under one directional light
+  is dark everywhere except a specular glint. That is what a flat mirror in vacuum
+  actually looks like: it shows the empty sky in front of it, not the sun behind you.
+
+**The rim is what makes it an object.** The 4 km of sunlit edge structure from D-054 is
+drawn here as a ring of geometry rather than as two exponentials in a shader, because
+out here it is being seen rather than looked through — and it is placed just outboard
+of the mirror, not overlapping its edge, so the two surfaces never share a depth
+either. It reads from both sides, which is right: it is structure, not a reflection.
+
+The cost is that the largest object in the scene is now a dark one. The label, the
+beam converging on it and the lit rim carry it — and being hard to see is the point,
+since Tower has never seen this thing lit.
diff --git a/design.md b/design.md
index bdbbe05..2958ae6 100644
--- a/design.md
+++ b/design.md
@@ -707,9 +707,14 @@ map's hillshade:
 - **A visible beam.** In vacuum there is nothing between collector and cap to
   scatter light, so the converging beam would not be visible at all. It is drawn
   as rays because it is the thing the view is about.
+- **A dark collector.** Both of its sides are dark, and for different reasons: the
+  face pointed at Tower is a 245 K radiator, and the face pointed at the sun is a
+  mirror, which shows the empty sky in front of it and not the sun behind the eye.
+  What makes it legible is the sunlit rim of D-054 — the same 4 km ring the sun cap
+  window sees from inside, drawn here as geometry rather than in a shader (D-055).
 
 Everything in the scene is captioned — collector, beam, both caps — with screen-space
-labels. A pale disc and a cone of light are not self-explanatory, and the collector
+labels. A dark disc and a cone of light are not self-explanatory, and the collector
 is the least intuitive object in Tower: it is larger than the world it feeds.
 
 The default camera frames the whole system rather than Tower alone: the collector
diff --git a/src/views/exterior.ts b/src/views/exterior.ts
index 5681b74..84ff81c 100644
--- a/src/views/exterior.ts
+++ b/src/views/exterior.ts
@@ -18,7 +18,8 @@ import {
 } from 'three'
 import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
 import {
-  BEAM_RADIUS_M, COLLECTOR_DIAMETER_M, COLLECTOR_STANDOFF_M, HULL_DEPTH_M, LENGTH_M, OMEGA_RAD_S, RADIUS_M,
+  BEAM_RADIUS_M, COLLECTOR_DIAMETER_M, COLLECTOR_RIM_M, COLLECTOR_STANDOFF_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'
@@ -33,6 +34,7 @@ const BEAM_R = BEAM_RADIUS_M / KM
 const HULL_D = HULL_DEPTH_M / KM
 const COLLECTOR_R = COLLECTOR_DIAMETER_M / KM / 2
 const COLLECTOR_STANDOFF = COLLECTOR_STANDOFF_M / KM
+const COLLECTOR_RIM = COLLECTOR_RIM_M / KM
 /** How close to an end, in km, counts as being on a cap rather than on the wall. */
 const CAP_TOLERANCE = 8
 const isCapZ = (zKm: number): boolean => zKm < CAP_TOLERANCE || zKm > L - CAP_TOLERANCE
@@ -297,15 +299,57 @@ export class ExteriorView {
   }
 
   private buildSunSystem(): void {
-    // The collector flies ahead of the sun cap: 244 km across, wider than Tower.
-    const collector = new Mesh(
-      new CircleGeometry(COLLECTOR_R, 128),
-      new MeshStandardMaterial({
-        color: 0xe6edf8, roughness: 0.2, metalness: 0.5, side: DoubleSide,
-        emissive: new Color(0x93a2be), emissiveIntensity: 1.15,
-      }))
-    collector.position.z = -COLLECTOR_STANDOFF
-    this.scene.add(collector)
+    /**
+     * The collector flies ahead of the sun cap: 244 km across, wider than Tower.
+     *
+     * It has two sides and they do not look alike, so it is drawn as two meshes
+     * rather than one double-sided disc. The face pointed at the sun is a mirror;
+     * the side pointed at Tower — the one this camera almost always sees, since
+     * Tower is between it and the viewer — is the back of that mirror, radiating
+     * its waste heat at 245 K. That is infrared, so in visible light it is black
+     * (D-050), which is exactly how the interior view draws it: a hole with a lit
+     * rim. Drawing it pale from out here contradicted the window it is seen
+     * through. Culling does the choosing: only one of the two faces survives for
+     * any given camera, so the pair never fight over the same depth.
+     */
+    const disc = new CircleGeometry(COLLECTOR_R, 128)
+    // CircleGeometry faces +z, and +z from the collector is toward Tower.
+    const back = new Mesh(disc, new MeshStandardMaterial({
+      color: 0x0b0e14, roughness: 0.95, metalness: 0.0, side: FrontSide,
+      // Not black to the last digit: against black space a wholly unlit disc is
+      // not a dark object, it is a missing one. This is the least glow that still
+      // reads as a surface.
+      emissive: new Color(0x171d27), emissiveIntensity: 1.0,
+    }))
+    back.position.z = -COLLECTOR_STANDOFF
+    this.scene.add(back)
+    /**
+     * The mirror face. A flat mirror in vacuum shows what is in front of it, and
+     * what is in front of this one is empty sky — so it is dark too, with the
+     * sun's own reflection as a glint where the angle happens to line up. Metal
+     * with almost no roughness does exactly that on its own: no diffuse term, one
+     * tight specular lobe. The emissive is only enough to keep it a surface
+     * rather than a hole when the glint is elsewhere.
+     */
+    const front = new Mesh(disc, new MeshStandardMaterial({
+      color: 0xdfe8ff, roughness: 0.12, metalness: 1.0, side: BackSide,
+      emissive: new Color(0x1a2334), emissiveIntensity: 1.0,
+    }))
+    front.position.z = -COLLECTOR_STANDOFF
+    this.scene.add(front)
+    /**
+     * The sunlit rim (D-054): 4 km of structure standing proud of the mirror
+     * plane at the edge, struck side-on by a sun nothing else here can see. It is
+     * the only lit part of the collector from Tower's side, and it is what makes
+     * the black disc read as a disc rather than as a gap in the stars. Drawn
+     * outboard of the mirror instead of overlapping its edge, so the two surfaces
+     * never share a depth.
+     */
+    const rim = new Mesh(
+      new RingGeometry(COLLECTOR_R, COLLECTOR_R + COLLECTOR_RIM, 128),
+      new MeshBasicMaterial({ color: 0xffe6bb, side: DoubleSide }))
+    rim.position.z = -COLLECTOR_STANDOFF
+    this.scene.add(rim)
 
     /**
      * The converging beam, drawn as rays from the mirror's rim to the aperture.
@@ -362,7 +406,7 @@ export class ExteriorView {
   }
 
   /**
-   * A pale disc and a glowing cone are not self-explanatory, and the collector is
+   * A dark disc and a glowing cone are not self-explanatory, and the collector is
    * the least intuitive object in Tower: it is bigger than the world it feeds.
    */
   private buildLabels(): void {