feat: underpass overpasses carry the chapter transitions
This commit is contained in:
@@ -26,12 +26,13 @@ import { createCoachworkEnvironment } from '@/scene/world/coachwork-light';
|
||||
import { createTravelDistance } from '@/scene/world/travel-distance';
|
||||
import {
|
||||
blendDaybreak,
|
||||
dampPalette,
|
||||
engineOffStarScale,
|
||||
paletteDampRate,
|
||||
createMutableChapterPalette,
|
||||
STORM_STAR_SCALE,
|
||||
STORM_TINT,
|
||||
writeChapterPalette,
|
||||
type MutableChapterPalette,
|
||||
} from '@/scene/world/palette';
|
||||
import { createMaintenanceWindow } from '@/scene/world/maintenance-window';
|
||||
import { createHeadlights } from '@/scene/world/headlights';
|
||||
@@ -48,6 +49,9 @@ import { readDashcam, takeDashcamCapture } from '@/journey/dashcam-feed';
|
||||
import { createDashcamLens } from '@/scene/world/dashcam-lens';
|
||||
import { createIgnition, IGNITION } from '@/scene/world/ignition';
|
||||
import { readIgnition } from '@/journey/ignition-feed';
|
||||
import { createUnderpass, underpassUniforms } from '@/scene/world/underpass';
|
||||
import { DEFAULT_LEAD_VH, nextSectionId, shadeDepth } from '@/journey/underpass';
|
||||
import { signRouteFor, signTriggerMaximumVhFor } from '@/journey/sign-routes';
|
||||
|
||||
declare const __PORTFOLIO_TEST_BUILD__: boolean;
|
||||
const TEST_BUILD =
|
||||
@@ -195,7 +199,19 @@ export async function createProceduralSceneGeneration(
|
||||
maintenanceTier === 'full'
|
||||
&& (globalThis.navigator as { connection?: { saveData?: boolean } } | undefined)?.connection?.saveData !== true,
|
||||
);
|
||||
// UNDERPASS: every chapter boundary is a deck the car drives beneath.
|
||||
// High/medium: shade, sodium pool, palette boost and the pass swell (the
|
||||
// swell only with the soundtrack audible). Low tier: deck and strips only.
|
||||
const underpass = createUnderpass(context.resources, {
|
||||
full: context.quality !== 'low'
|
||||
&& (globalThis.navigator as { connection?: { saveData?: boolean } } | undefined)?.connection?.saveData !== true,
|
||||
fogColor: scene.fog.color,
|
||||
});
|
||||
let underpassShade = 0;
|
||||
let underpassKey = '';
|
||||
let underpassAttr = false;
|
||||
scene.add(
|
||||
underpass.group,
|
||||
relay.group,
|
||||
billboard.mesh,
|
||||
maintenance.group,
|
||||
@@ -447,7 +463,9 @@ export async function createProceduralSceneGeneration(
|
||||
targetPalette.starVisibility *= engineOffStarScale(engineOff, dawn);
|
||||
}
|
||||
dawnLightUniforms.uDawnLight.value = dawnRims ? dawn : 0;
|
||||
dampPalette(currentPalette, targetPalette, deltaSeconds);
|
||||
// UNDERPASS: deep in the deck's shade the palette settles x4 (last
|
||||
// frame's shade: the deck moves after the camera below).
|
||||
dampPalette(currentPalette, targetPalette, deltaSeconds, paletteDampRate(shadeDepth(underpassShade), service.storm));
|
||||
const driveDistance = travelDistance.advance(
|
||||
deltaSeconds * (1 - engineOff) * roll,
|
||||
snapshot.driveSpeed,
|
||||
@@ -540,6 +558,11 @@ export async function createProceduralSceneGeneration(
|
||||
);
|
||||
background.copy(currentPalette.sky);
|
||||
scene.fog.color.copy(currentPalette.fog);
|
||||
if (underpassShade > 0) {
|
||||
// The deck occludes the sky: at most 35% darker for a beat.
|
||||
background.lerp(ENGINE_OFF_NIGHT, underpassShade);
|
||||
scene.fog.color.lerp(ENGINE_OFF_NIGHT, underpassShade);
|
||||
}
|
||||
ambient.color
|
||||
.copy(currentPalette.haze)
|
||||
.lerp(currentPalette.terrainHigh, 0.26);
|
||||
@@ -731,6 +754,29 @@ export async function createProceduralSceneGeneration(
|
||||
const stage = signStage.update(width, height);
|
||||
sign.compose(camera, width, height, stage.dock, now, stage.tilt);
|
||||
const gantry = sign.dockState();
|
||||
// UNDERPASS: after the sign so the deck holds behind it while it comes in.
|
||||
const nextRoute = nextSectionId(snapshot.activeSectionId);
|
||||
const passage = underpass.update(
|
||||
snapshot.activeSectionId,
|
||||
null,
|
||||
snapshot.driveSpeed,
|
||||
deltaSeconds,
|
||||
now,
|
||||
{
|
||||
activation: snapshot.sectionActivation,
|
||||
leadVh: nextRoute === null ? DEFAULT_LEAD_VH : signTriggerMaximumVhFor(signRouteFor(nextRoute), width, height),
|
||||
cameraZ: camera.position.z,
|
||||
signZ: gantry === 'incoming' ? sign.group.position.z : null,
|
||||
explicit: snapshot.sign?.navigationArrival === true,
|
||||
},
|
||||
);
|
||||
underpassShade = passage.shade;
|
||||
const under = shadeDepth(passage.shade) > 0.2;
|
||||
if (under !== underpassAttr) {
|
||||
underpassAttr = under;
|
||||
if (under) document.documentElement.dataset.underpass = 'under';
|
||||
else delete document.documentElement.dataset.underpass;
|
||||
}
|
||||
if (gantry !== gantryState) {
|
||||
gantryState = gantry;
|
||||
document.documentElement.dataset.gantry = gantry;
|
||||
@@ -765,6 +811,16 @@ export async function createProceduralSceneGeneration(
|
||||
}));
|
||||
}
|
||||
}
|
||||
if (TEST_BUILD) {
|
||||
const deck = underpass.inspect();
|
||||
const key = `${deck.exit}|${deck.z === null ? '' : Math.round(deck.z / 5)}|${Math.round(deck.shade * 100)}|${deck.passes}|${deck.held}|${deck.visible}`;
|
||||
if (key !== underpassKey) {
|
||||
underpassKey = key;
|
||||
window.dispatchEvent(new CustomEvent('portfolio-underpass-frame', {
|
||||
detail: { now, activeSectionId: snapshot.activeSectionId, direction: snapshot.direction, driveSpeed: snapshot.driveSpeed, ...deck },
|
||||
}));
|
||||
}
|
||||
}
|
||||
if (TEST_BUILD && snapshot.sign !== null) {
|
||||
const presentation = sign.inspect(camera, width, height);
|
||||
if (presentation !== null) {
|
||||
@@ -849,6 +905,8 @@ export async function createProceduralSceneGeneration(
|
||||
delete document.documentElement.dataset.interlude;
|
||||
delete document.documentElement.dataset.interludeArmed;
|
||||
delete document.documentElement.dataset.dawn;
|
||||
delete document.documentElement.dataset.underpass;
|
||||
underpassUniforms.uShade.value = 0;
|
||||
ignition.dispose();
|
||||
dawnLightUniforms.uDawnLight.value = 0;
|
||||
// Quality changes reuse the canvas context. Leave texture upload flags at
|
||||
@@ -876,33 +934,3 @@ const INTERLUDE_RISE_MS = 800;
|
||||
function easeInOutCubic(t: number): number {
|
||||
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||
}
|
||||
|
||||
function dampPalette(
|
||||
current: MutableChapterPalette,
|
||||
target: Readonly<MutableChapterPalette>,
|
||||
deltaSeconds: number,
|
||||
): void {
|
||||
const mix = 1 - Math.exp(-Math.max(0, deltaSeconds) * 2.8);
|
||||
current.sky.lerp(target.sky, mix);
|
||||
current.fog.lerp(target.fog, mix);
|
||||
current.terrainLow.lerp(target.terrainLow, mix);
|
||||
current.terrainHigh.lerp(target.terrainHigh, mix);
|
||||
current.sun.lerp(target.sun, mix);
|
||||
current.haze.lerp(target.haze, mix);
|
||||
current.warmth = THREE.MathUtils.lerp(current.warmth, target.warmth, mix);
|
||||
current.terrainEnergy = THREE.MathUtils.lerp(
|
||||
current.terrainEnergy,
|
||||
target.terrainEnergy,
|
||||
mix,
|
||||
);
|
||||
current.starVisibility = THREE.MathUtils.lerp(
|
||||
current.starVisibility,
|
||||
target.starVisibility,
|
||||
mix,
|
||||
);
|
||||
current.streakEnergy = THREE.MathUtils.lerp(
|
||||
current.streakEnergy,
|
||||
target.streakEnergy,
|
||||
mix,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user