feat(scene): add opening crane shot and exit-gate chapter cuts
This commit is contained in:
@@ -17,6 +17,10 @@ import { createCar, loadGrandTourer } from '@/scene/world/car';
|
||||
import { createSign, prepareSignTypography } from '@/scene/world/sign';
|
||||
import { createAtmosphere } from '@/scene/world/atmosphere';
|
||||
import { createPostProcessor } from '@/scene/post-processing';
|
||||
import { installCinematicGrade } from '@/scene/cinematic-grade';
|
||||
import { createSkyline } from '@/scene/world/skyline';
|
||||
import { createTraffic } from '@/scene/world/traffic';
|
||||
import { createExitGates } from '@/scene/world/exit-gates';
|
||||
import { createDriveDynamics } from '@/scene/drive-dynamics';
|
||||
import { HORIZON_FOG_END, HORIZON_FOG_START } from '@/scene/world/metrics';
|
||||
import { CHAPTER_COMPOSITIONS } from '@/scene/world/composition';
|
||||
@@ -72,6 +76,7 @@ export async function createProceduralSceneGeneration(
|
||||
globalThis.devicePixelRatio ?? 1,
|
||||
mobileOrTablet,
|
||||
);
|
||||
installCinematicGrade();
|
||||
const renderer = context.resources.own(
|
||||
'renderer',
|
||||
new THREE.WebGLRenderer({
|
||||
@@ -83,8 +88,9 @@ export async function createProceduralSceneGeneration(
|
||||
);
|
||||
renderer.setPixelRatio(quality.pixelRatio);
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
renderer.toneMappingExposure = 1.05;
|
||||
// ACES plus the film grade in cinematic-grade.ts, shared by every tier.
|
||||
renderer.toneMapping = THREE.CustomToneMapping;
|
||||
renderer.toneMappingExposure = 1.12;
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
scene.name = 'world-root';
|
||||
@@ -121,6 +127,9 @@ export async function createProceduralSceneGeneration(
|
||||
const signStage = createSignStage(context.resources);
|
||||
const sun = createSun(context.resources);
|
||||
const horizonAir = createHorizonAir(context.resources);
|
||||
const skyline = createSkyline(context.resources);
|
||||
const traffic = createTraffic(context.resources, quality);
|
||||
const exitGates = createExitGates(context.resources);
|
||||
const authoredCar = await loadGrandTourer(context.signal);
|
||||
const car = createCar(context.resources, authoredCar);
|
||||
car.setDetail(mobileOrTablet ? 'reduced' : 'full');
|
||||
@@ -138,6 +147,9 @@ export async function createProceduralSceneGeneration(
|
||||
terrain.group,
|
||||
sun.group,
|
||||
horizonAir.group,
|
||||
skyline.group,
|
||||
traffic.group,
|
||||
exitGates.group,
|
||||
car.group,
|
||||
speedWake.group,
|
||||
sign.group,
|
||||
@@ -172,6 +184,9 @@ export async function createProceduralSceneGeneration(
|
||||
let musicPulse = 0;
|
||||
let terrainSpectrumPhase = 0;
|
||||
let firstFrameSuppressionRecorded = false;
|
||||
// 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;
|
||||
|
||||
const resize = (): void => {
|
||||
const nextWidth = Math.max(1, canvas.clientWidth || canvas.width || 1);
|
||||
@@ -329,6 +344,13 @@ export async function createProceduralSceneGeneration(
|
||||
atmosphericTime += deltaSeconds;
|
||||
const elapsedSeconds = atmosphericTime;
|
||||
horizonAir.update(currentPalette, elapsedSeconds);
|
||||
skyline.update(currentPalette, elapsedSeconds);
|
||||
traffic.update(
|
||||
deltaSeconds,
|
||||
18 + snapshot.driveSpeed * 54 + response.roadEnergy * 48,
|
||||
elapsedSeconds,
|
||||
);
|
||||
const cut = exitGates.update(snapshot.activeSectionId, deltaSeconds);
|
||||
sun.update(
|
||||
currentPalette,
|
||||
snapshot.routeProgress,
|
||||
@@ -489,23 +511,41 @@ export async function createProceduralSceneGeneration(
|
||||
targetVerticalShift,
|
||||
cameraMix,
|
||||
);
|
||||
if (introProgress === null) {
|
||||
introProgress =
|
||||
snapshot.activeSectionId === 'start' && snapshot.scrollY < 40 ? 0 : 1;
|
||||
}
|
||||
if (introProgress < 1) {
|
||||
const hurry = snapshot.scrollY > 40 ? 5 : 1;
|
||||
introProgress = Math.min(1, introProgress + (deltaSeconds * hurry) / INTRO_SECONDS);
|
||||
}
|
||||
const crane = easeInOutCubic(
|
||||
THREE.MathUtils.clamp((introProgress - 0.06) / 0.94, 0, 1),
|
||||
);
|
||||
const craneShift = cameraState.shift * crane;
|
||||
camera.setViewOffset(
|
||||
width,
|
||||
height,
|
||||
-width * cameraState.shift,
|
||||
-width * craneShift,
|
||||
-height * cameraState.verticalShift,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
camera.position.set(cameraState.x, cameraState.y, cameraState.z);
|
||||
camera.lookAt(
|
||||
cameraState.targetX,
|
||||
cameraState.targetY,
|
||||
cameraState.targetZ,
|
||||
camera.position.set(
|
||||
THREE.MathUtils.lerp(0, cameraState.x, crane),
|
||||
THREE.MathUtils.lerp(46, cameraState.y, crane),
|
||||
THREE.MathUtils.lerp(92, cameraState.z, crane),
|
||||
);
|
||||
camera.rotation.z += cameraState.roll;
|
||||
if (Math.abs(camera.fov - cameraState.fov) > 0.001) {
|
||||
camera.fov = cameraState.fov;
|
||||
camera.lookAt(
|
||||
THREE.MathUtils.lerp(0, cameraState.targetX, crane),
|
||||
THREE.MathUtils.lerp(64, cameraState.targetY, crane),
|
||||
THREE.MathUtils.lerp(-720, cameraState.targetZ, crane),
|
||||
);
|
||||
camera.rotation.z += cameraState.roll * crane;
|
||||
const lensFov =
|
||||
THREE.MathUtils.lerp(30, cameraState.fov, crane) + cut * 6;
|
||||
if (Math.abs(camera.fov - lensFov) > 0.001) {
|
||||
camera.fov = lensFov;
|
||||
camera.updateProjectionMatrix();
|
||||
}
|
||||
const stage = signStage.update(width, height);
|
||||
@@ -536,7 +576,13 @@ export async function createProceduralSceneGeneration(
|
||||
}
|
||||
}
|
||||
if (postProcessor) {
|
||||
postProcessor.setDynamics(response.bloomEnergy);
|
||||
postProcessor.setDynamics(Math.max(response.bloomEnergy, cut * 0.45));
|
||||
postProcessor.setLens({
|
||||
time: elapsedSeconds,
|
||||
letterbox: 0.115 * (1 - THREE.MathUtils.smoothstep(introProgress, 0.5, 1)),
|
||||
flash: cut * 0.08,
|
||||
energy: Math.max(overdrive, cut),
|
||||
});
|
||||
postProcessor.render(scene, camera);
|
||||
} else {
|
||||
renderer.render(scene, camera);
|
||||
@@ -556,6 +602,12 @@ export async function createProceduralSceneGeneration(
|
||||
};
|
||||
}
|
||||
|
||||
const INTRO_SECONDS = 4.2;
|
||||
|
||||
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>,
|
||||
|
||||
Reference in New Issue
Block a user