feat: scroll-driven maintenance window set-piece in the infrastructure chapter
This commit is contained in:
@@ -105,9 +105,11 @@ const VERTEX = `
|
||||
varying float vDepth;
|
||||
varying float vRise;
|
||||
varying float vHeight;
|
||||
varying float vStagger;
|
||||
void main() {
|
||||
vec4 origin = instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
||||
float stagger = fract(sin(dot(origin.xz, vec2(12.9898, 78.233))) * 43758.5453);
|
||||
vStagger = stagger;
|
||||
float rise = clamp(uRise * 1.45 - stagger * 0.45, 0.0, 1.0);
|
||||
rise = rise * rise * (3.0 - 2.0 * rise);
|
||||
vRise = rise;
|
||||
@@ -128,6 +130,32 @@ const FADES = `
|
||||
if (fade < 0.02) discard;
|
||||
`;
|
||||
|
||||
/** Pieces the 03:12 window powers down and re-lights (the Infrastructure district). */
|
||||
const SERVICED: ReadonlySet<DistrictPiece> = new Set(['rack-row', 'mast']);
|
||||
/** The infrastructure accent the service re-lights in. */
|
||||
export const SERVICE_ACCENT = 0x8be9ed;
|
||||
|
||||
/* The 03:12 window. Each piece (a rack row, a mast) powers down to about 10%
|
||||
on its own staggered beat, then re-lights lamp row by lamp row, bottom up,
|
||||
in the infrastructure accent. Scroll-driven, so it never flickers. */
|
||||
const SERVICE_PARS = `
|
||||
uniform float uServiceDim;
|
||||
uniform float uServiceRelight;
|
||||
uniform float uPieceHeight;
|
||||
uniform vec3 uServiceColor;
|
||||
varying float vStagger;
|
||||
float serviceRowDim() {
|
||||
return clamp(uServiceDim * 1.6 - vStagger * 0.6, 0.0, 1.0);
|
||||
}
|
||||
float serviceRelit() {
|
||||
float rows = 7.0;
|
||||
float row = floor(clamp(vHeight / max(0.001, uPieceHeight), 0.0, 0.999) * rows);
|
||||
float front = uServiceRelight * 1.3 - vStagger * 0.3;
|
||||
float at = (row + 0.5) / rows;
|
||||
return smoothstep(at - 0.05, at + 0.05, front);
|
||||
}
|
||||
`;
|
||||
|
||||
export interface DistrictsWorld {
|
||||
group: { name: string; add(...objects: unknown[]): void };
|
||||
update(
|
||||
@@ -139,6 +167,8 @@ export interface DistrictsWorld {
|
||||
): void;
|
||||
/** Pieces currently raised (for tests and diagnostics). */
|
||||
raised(): DistrictPiece[];
|
||||
/** The 03:12 window: power-down (0..1) and re-light (0..1). Uniform writes only. */
|
||||
setServiceState(dim: number, relight: number): void;
|
||||
}
|
||||
|
||||
export function createDistricts(
|
||||
@@ -154,6 +184,11 @@ export function createDistricts(
|
||||
uTime: { value: 0 },
|
||||
uAccent: { value: new THREE.Color(signAccentFor('start')) },
|
||||
};
|
||||
const service = {
|
||||
uServiceDim: { value: 0 },
|
||||
uServiceRelight: { value: 0 },
|
||||
uServiceColor: { value: new THREE.Color(SERVICE_ACCENT) },
|
||||
};
|
||||
const targetAccent = new THREE.Color(signAccentFor('start'));
|
||||
const state = new Map<
|
||||
DistrictPiece,
|
||||
@@ -162,11 +197,11 @@ export function createDistricts(
|
||||
const pieces = Object.keys(DISTRICT_PRESENCE) as DistrictPiece[];
|
||||
for (const piece of pieces) state.set(piece, { rise: { value: 0 }, meshes: [] });
|
||||
|
||||
const bodyMaterial = (rise: { value: number }) =>
|
||||
const bodyMaterial = (rise: { value: number }, serviced: boolean, height: { value: number }) =>
|
||||
resources.own(
|
||||
'material',
|
||||
new THREE.ShaderMaterial({
|
||||
uniforms: { ...shared, uRise: rise },
|
||||
uniforms: { ...shared, ...service, uRise: rise, uPieceHeight: height },
|
||||
vertexShader: VERTEX,
|
||||
fragmentShader: `
|
||||
uniform vec3 uAccent;
|
||||
@@ -174,6 +209,7 @@ export function createDistricts(
|
||||
varying float vDepth;
|
||||
varying float vRise;
|
||||
varying float vHeight;
|
||||
${serviced ? SERVICE_PARS : ''}
|
||||
void main() {
|
||||
${FADES}
|
||||
// Opaque dark steel, like the sign truss: fades dissolve through a
|
||||
@@ -191,7 +227,12 @@ export function createDistricts(
|
||||
// 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);
|
||||
${serviced ? `
|
||||
float dimmed = serviceRowDim();
|
||||
float relit = serviceRelit();
|
||||
float power = mix(1.0, mix(0.1, 1.0, relit), dimmed);
|
||||
color += mix(uAccent, uServiceColor, dimmed * relit) * (rim * 0.22 + crown * 0.05) * power;` : `
|
||||
color += uAccent * (rim * 0.22 + crown * 0.05);`}
|
||||
gl_FragColor = vec4(color, 1.0);
|
||||
#include <tonemapping_fragment>
|
||||
#include <colorspace_fragment>
|
||||
@@ -202,11 +243,11 @@ export function createDistricts(
|
||||
side: THREE.DoubleSide,
|
||||
}),
|
||||
);
|
||||
const lightMaterial = (rise: { value: number }, accentMix: number) =>
|
||||
const lightMaterial = (rise: { value: number }, accentMix: number, serviced: boolean, height: { value: number }) =>
|
||||
resources.own(
|
||||
'material',
|
||||
new THREE.ShaderMaterial({
|
||||
uniforms: { ...shared, uRise: rise },
|
||||
uniforms: { ...shared, ...service, uRise: rise, uPieceHeight: height },
|
||||
vertexShader: VERTEX,
|
||||
fragmentShader: `
|
||||
uniform vec3 uAccent;
|
||||
@@ -215,6 +256,8 @@ export function createDistricts(
|
||||
varying vec3 vWorld;
|
||||
varying float vDepth;
|
||||
varying float vRise;
|
||||
varying float vHeight;
|
||||
${serviced ? SERVICE_PARS : ''}
|
||||
void main() {
|
||||
${FADES}
|
||||
// Neon tubes warm up as the district finishes rising. A smooth
|
||||
@@ -226,6 +269,11 @@ export function createDistricts(
|
||||
// Close passes dim, so overhead neon never floods the bloom pass.
|
||||
float near = mix(0.3, 1.0, smoothstep(5.0, 32.0, vDepth));
|
||||
vec3 color = tone * (1.1 + uEnergy * 0.4) * near;
|
||||
${serviced ? `
|
||||
float dimmed = serviceRowDim();
|
||||
float relit = serviceRelit();
|
||||
color = mix(color, uServiceColor * (1.1 + uEnergy * 0.4) * near, dimmed * relit * 0.85);
|
||||
color *= mix(1.0, mix(0.1, 1.0, relit), dimmed);` : ''}
|
||||
gl_FragColor = vec4(color * on * fade, 1.0);
|
||||
#include <tonemapping_fragment>
|
||||
#include <colorspace_fragment>
|
||||
@@ -247,9 +295,16 @@ export function createDistricts(
|
||||
if (!entry) return;
|
||||
const placements = placementsFor(piece, quality.postEffects);
|
||||
const accentMix = piece === 'arch' ? 1 : 0.35;
|
||||
const serviced = SERVICED.has(piece);
|
||||
const height = { value: 1 };
|
||||
if (serviced) {
|
||||
const bounds = (body as Geometry & { computeBoundingBox?(): void; boundingBox?: { max: { y: number } } | null });
|
||||
bounds.computeBoundingBox?.();
|
||||
height.value = Math.max(0.5, bounds.boundingBox?.max.y ?? 1);
|
||||
}
|
||||
for (const [geometry, material] of [
|
||||
[body, bodyMaterial(entry.rise)],
|
||||
[light, lightMaterial(entry.rise, accentMix)],
|
||||
[body, bodyMaterial(entry.rise, serviced, height)],
|
||||
[light, lightMaterial(entry.rise, accentMix, serviced, height)],
|
||||
] as const) {
|
||||
resources.own('geometry', geometry);
|
||||
const mesh = resources.own(
|
||||
@@ -318,5 +373,9 @@ export function createDistricts(
|
||||
},
|
||||
raised: () =>
|
||||
pieces.filter((piece) => (DISTRICT_PRESENCE[piece][current] ?? 0) > 0),
|
||||
setServiceState(dim, relight) {
|
||||
service.uServiceDim.value = Number.isFinite(dim) ? Math.min(1, Math.max(0, dim)) : 0;
|
||||
service.uServiceRelight.value = Number.isFinite(relight) ? Math.min(1, Math.max(0, relight)) : 0;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user