fix(scene): land the opening crane on scroll or stutter and prewarm cut shaders

This commit is contained in:
2026-10-10 20:20:18 +03:00
parent af14e5910b
commit 10263a968b
2 changed files with 31 additions and 6 deletions

View File

@@ -187,6 +187,9 @@ export async function createProceduralSceneGeneration(
// Opening shot: a crane down from the sun to the chase position. It plays
// once, only when the visit starts at the top, and a scroll hurries it.
let introProgress: number | null = null;
let introHurried = false;
let introFrames = 0;
let introSlowFrames = 0;
const resize = (): void => {
const nextWidth = Math.max(1, canvas.clientWidth || canvas.width || 1);
@@ -261,10 +264,8 @@ export async function createProceduralSceneGeneration(
throw new Error('Injected post-first-frame exception');
}
resize();
const deltaSeconds =
lastNow === null
? 0
: Math.min(0.1, Math.max(0, (now - lastNow) / 1000));
const wallSeconds = lastNow === null ? 0 : Math.max(0, (now - lastNow) / 1000);
const deltaSeconds = Math.min(0.1, wallSeconds);
lastNow = now;
dynamics.update(snapshot.acceleration, snapshot.direction, deltaSeconds);
const response = dynamics.snapshot();
@@ -514,10 +515,25 @@ export async function createProceduralSceneGeneration(
if (introProgress === null) {
introProgress =
snapshot.activeSectionId === 'start' && snapshot.scrollY < 40 ? 0 : 1;
// The crane opens on the sky, so the road, car and roadside would
// otherwise compile their shaders mid-drive when they first enter the
// frame. Compile every visible object now, as the chase framing would.
if (introProgress === 0) renderer.compile(scene, camera);
}
if (introProgress < 1) {
const hurry = snapshot.scrollY > 40 ? 5 : 1;
introProgress = Math.min(1, introProgress + (deltaSeconds * hurry) / INTRO_SECONDS);
// The shot belongs to a visitor who waits at the top. Once they scroll
// or navigate it lands within about 0.4 s of wall time, so the chase
// framing (and every sign placed against it) is back before arrival.
if (snapshot.scrollY > 40 || snapshot.activeSectionId !== 'start') introHurried = true;
// A crane that stutters is worse than none: on a device that cannot
// hold the shot smoothly, land it straight away.
// The first frames carry shader compilation and a lone hitch (a late
// texture upload) is forgiven; three slow frames end the shot.
introFrames += 1;
if (introFrames > 3 && wallSeconds > 0.1) introSlowFrames += 1;
if (introSlowFrames >= 3) introHurried = true;
const step = introHurried ? Math.min(0.5, wallSeconds) / 0.4 : deltaSeconds / INTRO_SECONDS;
introProgress = Math.min(1, introProgress + step);
}
const crane = easeInOutCubic(
THREE.MathUtils.clamp((introProgress - 0.06) / 0.94, 0, 1),

View File

@@ -76,6 +76,7 @@ export function createExitGates(resources: SceneResourceBag) {
let progress = 1;
let previous: SectionId | null = null;
let warmFrames = 2;
const place = (index: number, x: number, y: number, z: number, sx: number, sy: number) => {
dummy.position.set(x, y, z);
@@ -94,6 +95,14 @@ export function createExitGates(resources: SceneResourceBag) {
if (progress >= 0.7) progress = 0;
}
previous = section;
if (warmFrames > 0) {
// Draw the gates unlit for the first frames so the shader program
// compiles at the on-ramp, not as a stall on the first exit.
warmFrames -= 1;
uniforms.uIntensity.value = 0;
group.visible = true;
return 0;
}
if (progress >= 1) {
group.visible = false;
return 0;