feat: retire the gantry and billboard districts and light the rest as steel

This commit is contained in:
2026-10-11 00:43:25 +03:00
parent 4ff42288a8
commit fc897e534c
4 changed files with 36 additions and 35 deletions

View File

@@ -18,8 +18,6 @@ import { WORLD_DEPTH, WORLD_FAR_Z } from './metrics';
* like the expressway furniture. Districts at rest are hidden, so only the
* current chapter's pieces cost draw calls. */
export type DistrictPiece =
| 'gantry'
| 'billboard'
| 'rack-row'
| 'mast'
| 'workshop'
@@ -34,7 +32,7 @@ interface Placement {
/** Yaw in radians, applied before mirroring. */
ry?: number;
s?: number;
/** Extra vertical stretch (gantries clear the chase camera). */
/** Extra vertical stretch (the arch clears the chase camera). */
sy?: number;
}
@@ -42,18 +40,10 @@ interface Placement {
export const DISTRICT_PRESENCE: Readonly<
Record<DistrictPiece, Partial<Record<SectionId, number>>>
> = {
gantry: {
profile: 1,
experience: 1,
infrastructure: 1,
'selected-builds': 1,
'additional-work': 1,
toolkit: 1,
},
billboard: { profile: 1, experience: 1 },
// Pieces rise fully or not at all: a part-raised piece is squashed flat.
'rack-row': { infrastructure: 1 },
mast: { infrastructure: 0.75, toolkit: 1 },
workshop: { 'selected-builds': 1, 'additional-work': 0.5 },
mast: { infrastructure: 1, toolkit: 1 },
workshop: { 'selected-builds': 1 },
crane: { 'selected-builds': 1, 'additional-work': 1 },
container: { 'additional-work': 1 },
arch: { contact: 1 },
@@ -67,14 +57,6 @@ const span = (count: number, from: number, to: number) =>
function placementsFor(piece: DistrictPiece, dense: boolean): Placement[] {
const n = (full: number) => (dense ? full : Math.ceil(full / 2));
switch (piece) {
case 'gantry':
return span(n(3), -70, -70 - WORLD_DEPTH).map((z) => ({ x: 0, z, sy: 1.42 }));
case 'billboard':
return span(n(10), -40, -40 - WORLD_DEPTH).map((z, i) => ({
x: (i % 2 ? 1 : -1) * 16,
z,
ry: (i % 2 ? 1 : -1) * 0.28,
}));
case 'rack-row':
return span(n(18), -26, -26 - WORLD_DEPTH).flatMap((z, i) => {
const side = i % 2 ? 1 : -1;
@@ -194,17 +176,28 @@ export function createDistricts(
varying float vHeight;
void main() {
${FADES}
// Opaque dark steel, like the sign truss: fades dissolve through a
// fixed screen-door pattern instead of alpha, so pieces never read
// as translucent sheets over the sky and the sun.
float cover = fade * smoothstep(0.0, 0.25, vRise);
vec2 cell = mod(floor(gl_FragCoord.xy), 4.0);
float threshold = (mod(cell.x + cell.y * 2.0, 4.0) * 4.0 + mod(cell.y, 2.0) * 2.0 + mod(cell.x, 2.0) + 0.5) / 16.0;
if (cover < threshold) discard;
vec3 n = normalize(cross(dFdx(vWorld), dFdy(vWorld)));
float key = max(0.0, dot(n, normalize(vec3(-0.45, 0.85, 0.35))));
float rim = pow(1.0 - abs(n.y), 3.0) * 0.5;
vec3 color = vec3(0.03, 0.016, 0.05) + vec3(0.085, 0.06, 0.14) * key;
color += uAccent * rim * 0.06 * (0.4 + 0.6 * smoothstep(0.0, 12.0, vHeight));
gl_FragColor = vec4(color, fade * smoothstep(0.0, 0.25, vRise));
float fill = max(0.0, dot(n, normalize(vec3(0.6, 0.2, -0.4))));
vec3 color = vec3(0.022, 0.018, 0.034) + vec3(0.07, 0.06, 0.1) * key + vec3(0.02, 0.015, 0.035) * fill;
// Emissive edge: the silhouette catches the chapter colour, and
// the tops of the pieces carry a thin lit band.
float rim = pow(1.0 - abs(dot(n, normalize(cameraPosition - vWorld))), 4.0);
float crown = smoothstep(0.92, 1.0, n.y) * smoothstep(1.0, 6.0, vHeight);
color += uAccent * (rim * 0.22 + crown * 0.05);
gl_FragColor = vec4(color, 1.0);
#include <tonemapping_fragment>
#include <colorspace_fragment>
}
`,
transparent: true,
transparent: false,
depthWrite: true,
side: THREE.DoubleSide,
}),
@@ -253,7 +246,7 @@ export function createDistricts(
const entry = state.get(piece);
if (!entry) return;
const placements = placementsFor(piece, quality.postEffects);
const accentMix = piece === 'gantry' || piece === 'arch' || piece === 'billboard' ? 1 : 0.35;
const accentMix = piece === 'arch' ? 1 : 0.35;
for (const [geometry, material] of [
[body, bodyMaterial(entry.rise)],
[light, lightMaterial(entry.rise, accentMix)],