feat: ignition opening with cold start, lamps, crossfade and shader warm-up
This commit is contained in:
@@ -46,6 +46,8 @@ import { dawnLightUniforms } from '@/scene/world/dawn-light';
|
||||
import { isTestFault, recordTestFaultEvent } from '@/testing/fault-hooks';
|
||||
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';
|
||||
|
||||
declare const __PORTFOLIO_TEST_BUILD__: boolean;
|
||||
const TEST_BUILD =
|
||||
@@ -104,7 +106,7 @@ export async function createProceduralSceneGeneration(
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
// ACES plus the film grade in cinematic-grade.ts, shared by every tier.
|
||||
renderer.toneMapping = THREE.CustomToneMapping;
|
||||
renderer.toneMappingExposure = 1.12;
|
||||
renderer.toneMappingExposure = IGNITION.exposure;
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
scene.name = 'world-root';
|
||||
@@ -257,6 +259,9 @@ export async function createProceduralSceneGeneration(
|
||||
let dawnState: DawnStep | '' = '';
|
||||
// DASHCAM: the pointer drifts the lens and the letterbox closes while open.
|
||||
const dashcamLens = createDashcamLens();
|
||||
// IGNITION: the first frame is cold; lamps, beam, roll and exposure come up
|
||||
// over three seconds (src/scene/world/ignition.ts).
|
||||
const ignition = createIgnition();
|
||||
|
||||
const resize = (): void => {
|
||||
const nextWidth = Math.max(1, canvas.clientWidth || canvas.width || 1);
|
||||
@@ -367,6 +372,23 @@ export async function createProceduralSceneGeneration(
|
||||
// lifting off slows the spectrum to rest without rewinding its phase.
|
||||
terrainSpectrumPhase =
|
||||
(terrainSpectrumPhase + deltaSeconds * overdrive * 0.38) % 1;
|
||||
// IGNITION: the first three seconds. A scroll, a navigation, a sign, the
|
||||
// dashcam or a stuttering start (the crane's rule) cuts to driving. The
|
||||
// clock starts on the frame after the first: the first frame compiles
|
||||
// the shaders (hundreds of ms on a cold GPU) while the canvas dissolves
|
||||
// in on the compositor, and that stall must not eat the cold hold. A
|
||||
// later hitch stretches the timeline instead of skipping a beat.
|
||||
const ignited = ignition.update(renderedFrames <= 1 ? 0 : Math.min(0.1, wallSeconds), {
|
||||
cut: snapshot.scrollY > 40
|
||||
|| snapshot.activeSectionId !== 'start'
|
||||
|| snapshot.sign !== null
|
||||
|| readDashcam().open
|
||||
|| introSlowFrames >= 3,
|
||||
short: readIgnition().returning,
|
||||
});
|
||||
const roll = ignited.roll;
|
||||
renderer.toneMappingExposure = ignited.exposure;
|
||||
if (renderedFrames === 0) canvas.dataset.lit = '';
|
||||
writeChapterPalette(targetPalette, snapshot.chapter, snapshot.chapter, 0);
|
||||
// The 03:12 window: section-local scroll progress drives every beat.
|
||||
const service = maintenance.update(
|
||||
@@ -427,7 +449,7 @@ export async function createProceduralSceneGeneration(
|
||||
dawnLightUniforms.uDawnLight.value = dawnRims ? dawn : 0;
|
||||
dampPalette(currentPalette, targetPalette, deltaSeconds);
|
||||
const driveDistance = travelDistance.advance(
|
||||
deltaSeconds * (1 - engineOff),
|
||||
deltaSeconds * (1 - engineOff) * roll,
|
||||
snapshot.driveSpeed,
|
||||
response.roadEnergy,
|
||||
);
|
||||
@@ -435,11 +457,11 @@ export async function createProceduralSceneGeneration(
|
||||
0.18 + response.roadEnergy * 0.82,
|
||||
0.18,
|
||||
1,
|
||||
) * (1 - engineOff);
|
||||
) * (1 - engineOff) * roll;
|
||||
road.update(
|
||||
driveDistance,
|
||||
visualSpeed,
|
||||
response.roadEnergy,
|
||||
response.roadEnergy * roll,
|
||||
currentPalette,
|
||||
);
|
||||
expressway.update(
|
||||
@@ -473,7 +495,7 @@ export async function createProceduralSceneGeneration(
|
||||
skyline.update(currentPalette, elapsedSeconds);
|
||||
traffic.update(
|
||||
deltaSeconds,
|
||||
(18 + snapshot.driveSpeed * 54 + response.roadEnergy * 48) * (1 - engineOff),
|
||||
(18 + snapshot.driveSpeed * 54 + response.roadEnergy * 48) * (1 - engineOff) * roll,
|
||||
elapsedSeconds,
|
||||
);
|
||||
sun.update(
|
||||
@@ -490,9 +512,10 @@ export async function createProceduralSceneGeneration(
|
||||
elapsedSeconds,
|
||||
driveDistance,
|
||||
engineOff,
|
||||
ignited.lamps,
|
||||
);
|
||||
speedWake.update(
|
||||
overdrive,
|
||||
overdrive * roll,
|
||||
elapsedSeconds,
|
||||
car.group.position,
|
||||
width < 1100 || height < 620 ? 2.32 : 2.78,
|
||||
@@ -509,7 +532,7 @@ export async function createProceduralSceneGeneration(
|
||||
atmosphere.update(
|
||||
driveDistance,
|
||||
visualSpeed,
|
||||
response.streakEnergy,
|
||||
response.streakEnergy * roll,
|
||||
currentPalette,
|
||||
atmosphericTime,
|
||||
overdrive,
|
||||
@@ -658,7 +681,8 @@ export async function createProceduralSceneGeneration(
|
||||
introFrames += 1;
|
||||
if (introFrames > 3 && wallSeconds > 0.1) introSlowFrames += 1;
|
||||
if (introSlowFrames >= 3) introProgress = 1;
|
||||
introProgress = Math.min(1, introProgress + deltaSeconds / INTRO_SECONDS);
|
||||
// IGNITION: the crane holds its top position until the main beam is on.
|
||||
if (ignited.phase >= 2) introProgress = Math.min(1, introProgress + deltaSeconds / INTRO_SECONDS);
|
||||
}
|
||||
outroProgress = THREE.MathUtils.clamp(
|
||||
outroProgress
|
||||
@@ -699,8 +723,11 @@ export async function createProceduralSceneGeneration(
|
||||
camera.fov = lensFov;
|
||||
camera.updateProjectionMatrix();
|
||||
}
|
||||
const beam = headlights.update(snapshot.beam, snapshot, camera, 1 - engineOff, deltaSeconds);
|
||||
road.setBeam(beam.yaw, beam.lift);
|
||||
// IGNITION: the dipped level lights the road throw; the steerable main
|
||||
// beam (spot, haze cone, lift) only comes in above it.
|
||||
const mainBeam = THREE.MathUtils.clamp((ignited.lamps - IGNITION.dipped) / (1 - IGNITION.dipped), 0, 1);
|
||||
const beam = headlights.update(snapshot.beam, snapshot, camera, (1 - engineOff) * mainBeam, deltaSeconds);
|
||||
road.setBeam(beam.yaw, beam.lift, ignited.lamps);
|
||||
const stage = signStage.update(width, height);
|
||||
sign.compose(camera, width, height, stage.dock, now, stage.tilt);
|
||||
const gantry = sign.dockState();
|
||||
@@ -756,6 +783,37 @@ export async function createProceduralSceneGeneration(
|
||||
);
|
||||
}
|
||||
}
|
||||
if (renderedFrames === 0) {
|
||||
// IGNITION parks the wake, the streaks and the beam cone until the
|
||||
// pull-away, and the first scroll brings the sign: each would compile
|
||||
// its program as a 70-280 ms hitch inside the opening shot (three slow
|
||||
// frames end the crane; one over 250 ms is a quality downgrade that
|
||||
// rebuilds the scene). Draw everything once now, behind the poster,
|
||||
// through the real pipeline: renderer.compile() keys its programs for
|
||||
// the screen (sRGB output, tone mapping) and never matches the post
|
||||
// processor's linear targets, so every program compiled twice. The
|
||||
// drawing buffer is presented once per task: the cold frame below
|
||||
// is what the canvas shows.
|
||||
// Culling is off too: the car sits outside the crane's frustum.
|
||||
const hidden: { visible: boolean }[] = [];
|
||||
const culled: { frustumCulled: boolean }[] = [];
|
||||
scene.traverse((object: { visible: boolean; frustumCulled: boolean }) => {
|
||||
if (object.frustumCulled) {
|
||||
object.frustumCulled = false;
|
||||
culled.push(object);
|
||||
}
|
||||
if (object.visible) return;
|
||||
object.visible = true;
|
||||
hidden.push(object);
|
||||
});
|
||||
try {
|
||||
if (postProcessor) postProcessor.render(scene, camera);
|
||||
else renderer.render(scene, camera);
|
||||
} finally {
|
||||
for (const object of hidden) object.visible = false;
|
||||
for (const object of culled) object.frustumCulled = true;
|
||||
}
|
||||
}
|
||||
if (postProcessor) {
|
||||
postProcessor.setDynamics(response.bloomEnergy, dawnRims ? dawn : 0);
|
||||
postProcessor.setLens({
|
||||
@@ -791,6 +849,7 @@ export async function createProceduralSceneGeneration(
|
||||
delete document.documentElement.dataset.interlude;
|
||||
delete document.documentElement.dataset.interludeArmed;
|
||||
delete document.documentElement.dataset.dawn;
|
||||
ignition.dispose();
|
||||
dawnLightUniforms.uDawnLight.value = 0;
|
||||
// Quality changes reuse the canvas context. Leave texture upload flags at
|
||||
// defaults before the next renderer creates its empty array textures.
|
||||
|
||||
Reference in New Issue
Block a user