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:
+54
-2
@@ -19,7 +19,7 @@ import {
|
||||
type Atmosphere,
|
||||
type WeatherObservation,
|
||||
} from "./engine/atmosphere.ts";
|
||||
import { officeDaylight } from "./interiors/daylight.ts";
|
||||
import { officeDaylight, withHouseLights } from "./interiors/daylight.ts";
|
||||
import { createScene, type SceneHandle } from "./engine/scene.ts";
|
||||
import {
|
||||
regionOf,
|
||||
@@ -415,7 +415,39 @@ function officeLighting(site: NonNullable<Office["site"]>) {
|
||||
// rig here would be a flicker rather than a fix, so this is only ever called
|
||||
// where one exists.
|
||||
const state = officeAtmosphere?.apply(env);
|
||||
return state ? officeDaylight(state, site) : undefined;
|
||||
if (!state) return undefined;
|
||||
|
||||
/**
|
||||
* The building's own lights, on top of whatever is left of the sun.
|
||||
*
|
||||
* The order matters and is the only subtle thing here: the daylight
|
||||
* adaptation runs first, because it is about the *sun* — which way the
|
||||
* building faces and where the weather starts — and the house lights are
|
||||
* added to the result, because they are about the building. Doing it the
|
||||
* other way round would rotate the interior lighting by the building's
|
||||
* heading, which is meaningless: a ceiling does not face a compass point.
|
||||
*
|
||||
* `office` may not exist yet — this is called once at construction, before
|
||||
* there is a scene to ask — in which case the elevation is fed straight to the
|
||||
* ramp so the first frame is already correct rather than a lit room fading
|
||||
* down or a dark one fading up.
|
||||
*/
|
||||
office?.setSolarElevation(env.sun.elevation);
|
||||
const house = office?.houseLevel() ?? houseLevelFor(env.sun.elevation);
|
||||
return withHouseLights(officeDaylight(state, site), house);
|
||||
}
|
||||
|
||||
/**
|
||||
* The lights-on ramp, for the one moment there is no office to ask.
|
||||
*
|
||||
* Duplicating the two bounds from `luminaires.ts` is a smell and is the lesser
|
||||
* of the two available ones: the alternative is building the office scene with
|
||||
* a rig computed from a light level it cannot report yet, which means the room
|
||||
* is visibly wrong for exactly one frame at every entry. Kept in step by being
|
||||
* four lines long and named after the thing it mirrors.
|
||||
*/
|
||||
function houseLevelFor(solarElevationDeg: number): number {
|
||||
return 1 - Math.min(1, Math.max(0, solarElevationDeg / 6));
|
||||
}
|
||||
|
||||
function updateSun() {
|
||||
@@ -434,6 +466,17 @@ function updateSun() {
|
||||
const env = observe(active.center.lat, active.center.lng, currentInstant(), currentWeather());
|
||||
city.setLighting(atmosphere.apply(env));
|
||||
city.setSolarElevation(env.sun.elevation);
|
||||
/**
|
||||
* The sky's own cover, which is a different question from what it does to the
|
||||
* light and is why the scene takes it separately.
|
||||
*
|
||||
* `null` weather is "nobody was asked" — the state `currentWeather` is careful
|
||||
* to preserve — and for cloud the honest reading of that is a clear sky rather
|
||||
* than an invented overcast. The modelled marine layer already reaches the rig
|
||||
* through `observe`; this is the *observed* cover when a station reported one.
|
||||
*/
|
||||
city.setCloudCover(currentWeather()?.cloudCover ?? 0);
|
||||
city.setWind(currentWeather()?.windKph ?? null, currentWeather()?.windDirDeg ?? null);
|
||||
// The override itself, not `currentInstant()`. Handing over a resolved date
|
||||
// would peg the sky to whatever second this ran in, and this runs about once a
|
||||
// second — so an unscrubbed sky would advance in visible steps while the
|
||||
@@ -817,6 +860,15 @@ async function enterOffice() {
|
||||
...(pack.site ? {} : { background: 0x11161c }),
|
||||
...(pack.site ? { lighting: officeLighting(pack.site) } : {}),
|
||||
...(pack.site ? { horizon: { drop: pack.site.elevation } } : {}),
|
||||
// Two per floor, whatever floors this pack has — so the two-storey tower
|
||||
// gets four and the single-storey hangar gets two, without either pack
|
||||
// having to know about robots. Derived from the levels rather than
|
||||
// written down, because a pack that gains a storey should not need an
|
||||
// edit here to be staffed.
|
||||
robots: pack.levels.flatMap((level) => [
|
||||
{ levelId: level.id, id: `${level.id}-a` },
|
||||
{ levelId: level.id, id: `${level.id}-b` },
|
||||
]),
|
||||
depth,
|
||||
materials,
|
||||
// Ignored entirely at `"public"` depth, where no layer is built to colour.
|
||||
|
||||
Reference in New Issue
Block a user