From 10263a968b9b32fa38f8acddcddfffe1c8e4873d Mon Sep 17 00:00:00 2001 From: LucaP Date: Sat, 10 Oct 2026 20:20:18 +0300 Subject: [PATCH] fix(scene): land the opening crane on scroll or stutter and prewarm cut shaders --- src/scene/world/environment.ts | 28 ++++++++++++++++++++++------ src/scene/world/exit-gates.ts | 9 +++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/scene/world/environment.ts b/src/scene/world/environment.ts index c622465..417b048 100644 --- a/src/scene/world/environment.ts +++ b/src/scene/world/environment.ts @@ -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), diff --git a/src/scene/world/exit-gates.ts b/src/scene/world/exit-gates.ts index ad8676a..1df5bc4 100644 --- a/src/scene/world/exit-gates.ts +++ b/src/scene/world/exit-gates.ts @@ -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;