feat: weave speed-reactive terrain and ground roadside signs

Anchor desktop signs beside the road and feather the compact reading edge with measured chapter landings. Add a continuous cyan-to-coral terrain grid and a bounded rear wake that builds with existing drive speed.

Preserve approved content, media, soundtrack, and canonical journey contracts.
This commit is contained in:
2026-09-05 06:22:49 +03:00
parent b188cac7ed
commit 51a9c743da
7 changed files with 416 additions and 53 deletions

View File

@@ -6,6 +6,8 @@ import { renderQualityFor } from '@/scene/render-quality';
import { createHorizonAir } from '@/scene/world/horizon-air';
import { createExpressway } from '@/scene/world/expressway';
import { createSignStage } from '@/scene/world/sign-stage';
import { usesRoadsideSign } from '@/scene/world/sign-layout';
import { createSpeedWake } from '@/scene/world/speed-wake';
import { createRoad } from '@/scene/world/road';
import { createTerrain } from '@/scene/world/terrain';
import { createSun } from '@/scene/world/sun';
@@ -111,6 +113,7 @@ export async function createProceduralSceneGeneration(
const road = createRoad(context.resources, quality);
const terrain = createTerrain(context.resources, quality);
const speedWake = createSpeedWake(context.resources);
const expressway = createExpressway(context.resources, quality);
const signStage = createSignStage(context.resources);
const sun = createSun(context.resources);
@@ -132,6 +135,7 @@ export async function createProceduralSceneGeneration(
sun.group,
horizonAir.group,
car.group,
speedWake.group,
sign.group,
);
@@ -158,6 +162,8 @@ export async function createProceduralSceneGeneration(
let disposed = false;
let renderedFrames = 0;
let atmosphericTime = 0;
let signCameraWeight = 0;
let overdrive = 0;
let firstFrameSuppressionRecorded = false;
const resize = (): void => {
@@ -240,6 +246,17 @@ export async function createProceduralSceneGeneration(
lastNow = now;
dynamics.update(snapshot.acceleration, snapshot.direction, deltaSeconds);
const response = dynamics.snapshot();
// Read the existing speed contract; visual charge has its own slow release
// and cannot change the soundtrack or the canonical drive dynamics.
const chargeTarget = THREE.MathUtils.smoothstep(
snapshot.driveSpeed,
0.68,
0.9,
);
overdrive +=
(chargeTarget - overdrive) *
(1 -
Math.exp(-deltaSeconds / (chargeTarget > overdrive ? 0.28 : 1.05)));
writeChapterPalette(targetPalette, snapshot.chapter, snapshot.chapter, 0);
dampPalette(currentPalette, targetPalette, deltaSeconds);
const driveDistance = travelDistance.advance(
@@ -271,6 +288,7 @@ export async function createProceduralSceneGeneration(
snapshot.chapter,
snapshot.routeProgress,
currentPalette,
overdrive,
);
atmosphericTime += deltaSeconds;
const elapsedSeconds = atmosphericTime;
@@ -287,6 +305,12 @@ export async function createProceduralSceneGeneration(
elapsedSeconds,
driveDistance,
);
speedWake.update(
overdrive,
elapsedSeconds,
car.group.position,
width < 1100 || height < 620 ? 2.32 : 2.78,
);
sign.update(snapshot.sign, now);
atmosphere.update(
driveDistance,
@@ -302,7 +326,7 @@ export async function createProceduralSceneGeneration(
ambient.groundColor.copy(currentPalette.fog);
ambient.intensity = 1.05 + currentPalette.warmth * 0.18;
carKeyLight.color.setHex(0xb9e3ef);
carKeyLight.intensity = 32 + response.bloomEnergy * 3.6;
carKeyLight.intensity = 32 + response.bloomEnergy * 3.6 + overdrive * 8;
carFillLight.color.setHex(0x809ec1);
carFillLight.intensity = 12.5 + currentPalette.warmth * 2.8;
rearLight.intensity = 4.2 + response.suspensionEnergy * 1.4;
@@ -346,11 +370,23 @@ export async function createProceduralSceneGeneration(
// The document stays native. Only the rendered lens eases to the chapter's safe zone.
const cameraX = wideComposition ? composition.x : 0;
const targetX = 0;
const signTarget =
usesRoadsideSign(width, height) && snapshot.sign !== null
? snapshot.sign.phase === 'depart'
? 1 - snapshot.sign.progress
: 1
: 0;
signCameraWeight +=
(signTarget - signCameraWeight) * (1 - Math.exp(-deltaSeconds / 0.48));
const targetShift =
height < 560 && width >= 768
? 0.22
: wideComposition
? composition.shift
? THREE.MathUtils.lerp(
composition.shift,
composition.shift * 0.28,
signCameraWeight,
)
: 0;
const targetVerticalShift = wideComposition
? 0
@@ -434,7 +470,14 @@ export async function createProceduralSceneGeneration(
camera.updateProjectionMatrix();
}
const stage = signStage.update(width, height);
sign.compose(camera, width, height, stage.navigationBottom, now);
sign.compose(
camera,
width,
height,
stage.navigationBottom,
now,
car.group.position,
);
if (TEST_BUILD && snapshot.sign !== null) {
const presentation = sign.inspect(camera, width, height);
if (presentation !== null) {