The office keeps its lights on, and something walks around under them
**Lights.** A sited office follows the real sun, and the real sun spends
half its time below the horizon — which was producing a technically
correct and completely useless picture: an unlit floor plate at midnight
in a building whose whole premise is that you can see who is at which
desk. `luminaires.ts` brings the diffusers up as the sun goes down and
reports one scalar for how much interior light there is; `withHouseLights`
adds it to the rig. CONTRACT §4's rule that a fitting emits no light is
kept in full — nothing here is a light source, and the rig still has one
owner.
**And they notice you.** A fitting within four metres of somebody walking
underneath brightens and fades back as they leave, which is what an
occupancy-sensed floor actually does at night. They are one `InstancedMesh`
sharing one material, so `emissiveIntensity` cannot vary between them —
`instanceColor` can, but three multiplies it into the diffuse term only, so
six lines of `onBeforeCompile` carry it into the emissive term as well. The
alternative was one mesh per fitting: forty draw calls of ceiling in a
building that spends about twenty on everything.
**Optimus.** A posable Gen-3 humanoid — eleven articulating joints, pale
shells over a dark frame, a black visor — with a walk cycle driven by
*distance travelled* rather than wall-clock, so the feet do not slide when
a robot slows down. Two per floor, derived from the pack's levels, so the
two-storey tower gets four and the hangar gets two without either pack
knowing robots exist. They wander between reachable points using
`Plan.blocked` — the collider the wall split already produces — and they
are deliberately **not** gated on `depth`: the build-time-exclusion rule is
about occupancy, and a robot is nobody.
**Starlinks stop being pixels.** The sixty-four nearest the centre of view
grow real geometry — a flat bus with ONE large solar array, which is the
actual signature and the thing everybody draws symmetrically and wrong —
fading in so there is no pop where a point becomes a mesh. Two draw calls.
The sun for their attitude comes from `solar.ts` and not from the rig,
because `atmosphere.ts` floors the light direction to keep the shadow
camera usable, and a sun ten degrees *down* is exactly the dusk geometry
that makes a pass visible.
**Aircraft** are airliners now — swept wings, nacelles, a fin — instead of
an arrowhead, still one shared geometry facing +Z as `flights.ts` requires.
**Clouds** drift over the board, driven by observed cover, lit by the rig
rather than by themselves.
Four modules were built by subagents and reviewed by another; every one
came back `needs-work` and the reviews were right. Fixed before wiring:
- The walk cycle's arms were a quarter cycle out of step with its legs —
the legs are cosine-shaped and the arms were on `sin`, so at the
instant the left leg reached full forward the left shoulder was at dead
neutral. Uncanny, and hard to name until it is pointed at.
- Every Optimus shell used a `roundedBox` radius of 0.12–0.22, which that
primitive turns into a near-circular cross-section — the figure was
built out of lozenges, not panels. The rest of the library uses
0.02–0.09.
- The cloud material was `transparent` + `DoubleSide` without
`forceSinglePass`, so three rendered it twice per frame *and* bumped
`material.version` on each pass — rebuilding the program cache key
forever, on the one layer that is fill-rate bound.
- `starlinkMesh.dispose()` freed the geometries but not the
`InstancedMesh`es, orphaning their instance buffers on every city
switch.
- The airliner's tailplane roots sat outside the tail cone and hung in
free air over most of their chord.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+9
-37
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import * as THREE from "three";
|
||||
import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js";
|
||||
import { airlinerGeometry } from "./aircraftGeometry.ts";
|
||||
import type { Aircraft, City, FlightSource } from "./types.ts";
|
||||
import { seededRandom, type World } from "./world.ts";
|
||||
|
||||
@@ -470,7 +470,7 @@ const SAME_POSITION_EPSILON = 1e-4;
|
||||
* It is a real ceiling now. It was declared and then referenced only by the
|
||||
* buffer sizing, so `tracks` grew without limit and `rebuildTrails` silently
|
||||
* ran out of vertices — which mattered the moment the godmode dial could put
|
||||
* four hundred aircraft in the sky. Tracks past this many still get a dart;
|
||||
* four hundred aircraft in the sky. Tracks past this many still get an aeroplane;
|
||||
* what they do not get is a trail, which is the graceful half to drop.
|
||||
*/
|
||||
const MAX_TRACKS = 192;
|
||||
@@ -484,7 +484,7 @@ const TRACK_GRACE_SECONDS = 32;
|
||||
|
||||
/**
|
||||
* Opacity at the head of a trail, fading to nothing at the tail. Well under 1
|
||||
* on purpose: the trail is context for the dart, not a second subject, and a
|
||||
* on purpose: the trail is context for the aircraft, not a second subject, and a
|
||||
* dozen opaque lines over a city read as a wiring diagram.
|
||||
*/
|
||||
const TRAIL_ALPHA = 0.55;
|
||||
@@ -537,7 +537,7 @@ const CRUISE_METRES = 9000;
|
||||
/** Distinct materials along the ramp. Enough to look continuous, few enough to cache. */
|
||||
const COLOR_BANDS = 12;
|
||||
|
||||
/** Steepest nose-up or nose-down attitude a dart is drawn at, in radians. */
|
||||
/** Steepest nose-up or nose-down attitude an aircraft is drawn at, in radians. */
|
||||
const MAX_PITCH = 0.42;
|
||||
|
||||
interface TrailSample {
|
||||
@@ -571,12 +571,12 @@ interface Track {
|
||||
* longer drawn, while its history is still held. See `tick`.
|
||||
*/
|
||||
stale: boolean;
|
||||
/** Altitude at `head`, which is what the dart's colour is chosen from. */
|
||||
/** Altitude at `head`, which is what the aircraft's colour is chosen from. */
|
||||
headAltitude: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aircraft as small darts, each dragging a fading trail of where it has been.
|
||||
* Aircraft as small airliners, each dragging a fading trail of where it has been.
|
||||
*
|
||||
* Rendered at true altitude through the world's vertical exaggeration, so a jet
|
||||
* on approach sits visibly below one at cruise, and coloured by that altitude so
|
||||
@@ -593,7 +593,7 @@ export function createFlightLayer(world: World): FlightLayer {
|
||||
const group = new THREE.Group();
|
||||
group.name = "flights";
|
||||
|
||||
const geo = dartGeometry();
|
||||
const geo = airlinerGeometry();
|
||||
const materials = new Map<number, THREE.MeshLambertMaterial>();
|
||||
const tracks = new Map<string, Track>();
|
||||
|
||||
@@ -847,7 +847,7 @@ export function createFlightLayer(world: World): FlightLayer {
|
||||
track.headAltitude = from.altitude + (to.altitude - from.altitude) * alpha;
|
||||
|
||||
track.mesh.position.copy(track.head);
|
||||
// A heading of 0 is north, and north is -z, so a dart whose nose is
|
||||
// A heading of 0 is north, and north is -z, so an aircraft whose nose is
|
||||
// modelled along +z has to be turned all the way round before the compass
|
||||
// and the scene agree. The previous mapping was a bare negation of the
|
||||
// heading, which flew every aircraft tail-first and put an easterly
|
||||
@@ -871,7 +871,7 @@ export function createFlightLayer(world: World): FlightLayer {
|
||||
*
|
||||
* The spine is every observation except the newest, followed by the
|
||||
* interpolated head — the newest observation is where the aircraft is *going*,
|
||||
* and drawing to it would put the trail in front of the dart.
|
||||
* and drawing to it would put the trail in front of the aircraft.
|
||||
*/
|
||||
function rebuildTrails() {
|
||||
let vertex = 0;
|
||||
@@ -981,35 +981,7 @@ export function createFlightLayer(world: World): FlightLayer {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A dart: a five-sided body with a wing and a tailplane, merged into one
|
||||
* geometry so an aircraft is one draw call.
|
||||
*
|
||||
* The wing is what earns its keep. A bare cone at this scale is a bright speck
|
||||
* with no orientation, and the whole reason to draw traffic on a city map is
|
||||
* that it is going somewhere — the crossbar is the only part of the silhouette
|
||||
* that says which way.
|
||||
*/
|
||||
function dartGeometry(): THREE.BufferGeometry {
|
||||
const body = new THREE.ConeGeometry(0.09, 0.42, 5);
|
||||
body.rotateX(Math.PI / 2); // nose along +z, so heading is a rotation about Y
|
||||
const wing = new THREE.BoxGeometry(0.44, 0.016, 0.085);
|
||||
wing.translate(0, -0.005, -0.02);
|
||||
const tail = new THREE.BoxGeometry(0.15, 0.014, 0.055);
|
||||
tail.translate(0, 0.02, -0.165);
|
||||
|
||||
const parts = [body, wing, tail];
|
||||
const merged = mergeGeometries(parts);
|
||||
for (const part of parts) part.dispose();
|
||||
if (merged) return merged;
|
||||
|
||||
// `mergeGeometries` returns null when the inputs disagree about their
|
||||
// attributes, which three primitives from the same library cannot — but the
|
||||
// signature allows it, and a missing aircraft is worse than a plain one.
|
||||
const fallback = new THREE.ConeGeometry(0.09, 0.42, 5);
|
||||
fallback.rotateX(Math.PI / 2);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* The climb angle of a leg, from the real numbers rather than the scene's.
|
||||
|
||||
Reference in New Issue
Block a user