feat: refine nightdrive world and authored car
Integrate the Blender grand tourer with a cohesive shader-driven road, terrain, atmosphere, lighting, and post-processing pass. Refine responsive drive dynamics and sign lifecycle behavior, and add durable visual, accessibility, and performance verification evidence.
This commit is contained in:
@@ -6,10 +6,11 @@ import { renderQualityFor } from '@/scene/render-quality';
|
||||
import { createRoad } from '@/scene/world/road';
|
||||
import { createTerrain } from '@/scene/world/terrain';
|
||||
import { createSun } from '@/scene/world/sun';
|
||||
import { createCar } from '@/scene/world/car';
|
||||
import { createCar, loadGrandTourer } from '@/scene/world/car';
|
||||
import { createSign } from '@/scene/world/sign';
|
||||
import { createAtmosphere } from '@/scene/world/atmosphere';
|
||||
import { createPostProcessor } from '@/scene/post-processing';
|
||||
import { createDriveDynamics } from '@/scene/drive-dynamics';
|
||||
import {
|
||||
createMutableChapterPalette,
|
||||
writeChapterPalette,
|
||||
@@ -21,9 +22,9 @@ declare const __PORTFOLIO_TEST_BUILD__: boolean;
|
||||
const TEST_BUILD = typeof __PORTFOLIO_TEST_BUILD__ !== 'undefined' && __PORTFOLIO_TEST_BUILD__;
|
||||
|
||||
export function createChaseCamera(aspect = 1) {
|
||||
const camera = new THREE.PerspectiveCamera(70, aspect, 0.1, 320);
|
||||
camera.position.set(0, 4.8, 12.5);
|
||||
camera.lookAt(0, 1.6, -22);
|
||||
const camera = new THREE.PerspectiveCamera(66, aspect, 0.1, 900);
|
||||
camera.position.set(0, 8.6, 22);
|
||||
camera.lookAt(0, 4.8, -128);
|
||||
return camera;
|
||||
}
|
||||
|
||||
@@ -59,11 +60,12 @@ export async function createProceduralSceneGeneration(
|
||||
renderer.toneMappingExposure = 1.05;
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
scene.name = 'world-root';
|
||||
const currentPalette = createMutableChapterPalette('deep-night');
|
||||
const targetPalette = createMutableChapterPalette('deep-night');
|
||||
const background = currentPalette.sky.clone();
|
||||
scene.background = background;
|
||||
scene.fog = new THREE.FogExp2(currentPalette.fog.getHex(), 0.012);
|
||||
scene.fog = new THREE.FogExp2(currentPalette.fog.getHex(), 0.0024);
|
||||
const camera = createChaseCamera();
|
||||
|
||||
const ambient = new THREE.HemisphereLight(0x6aa9c5, 0x100812, 1.25);
|
||||
@@ -71,11 +73,20 @@ export async function createProceduralSceneGeneration(
|
||||
const rearLight = new THREE.PointLight(0xff365f, 5, 18);
|
||||
rearLight.position.set(0, 1.2, 7);
|
||||
scene.add(rearLight);
|
||||
const carKeyLight = new THREE.PointLight(0x72dce8, 18, 34, 2);
|
||||
carKeyLight.name = 'car-key-light';
|
||||
carKeyLight.position.set(-4.8, 5.4, 9.5);
|
||||
scene.add(carKeyLight);
|
||||
const carFillLight = new THREE.PointLight(0xb83f93, 11, 28, 2);
|
||||
carFillLight.name = 'car-fill-light';
|
||||
carFillLight.position.set(4.2, 3.4, 8);
|
||||
scene.add(carFillLight);
|
||||
|
||||
const road = createRoad(context.resources, quality);
|
||||
const terrain = createTerrain(context.resources, quality);
|
||||
const sun = createSun(context.resources);
|
||||
const car = createCar(context.resources);
|
||||
const authoredCar = await loadGrandTourer(context.signal);
|
||||
const car = createCar(context.resources, authoredCar);
|
||||
car.setDetail(mobileOrTablet ? 'reduced' : 'full');
|
||||
const sign = createSign(context.resources);
|
||||
const atmosphere = createAtmosphere(context.resources, quality);
|
||||
@@ -91,6 +102,17 @@ export async function createProceduralSceneGeneration(
|
||||
const postProcessor = quality.postEffects
|
||||
? createPostProcessor(context.resources, renderer)
|
||||
: null;
|
||||
const dynamics = createDriveDynamics();
|
||||
const cameraState = {
|
||||
x: camera.position.x,
|
||||
y: camera.position.y,
|
||||
z: camera.position.z,
|
||||
targetX: 0,
|
||||
targetY: 4.8,
|
||||
targetZ: -128,
|
||||
fov: camera.fov,
|
||||
roll: 0,
|
||||
};
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
let driveDistance = 0;
|
||||
@@ -108,9 +130,21 @@ export async function createProceduralSceneGeneration(
|
||||
renderer.setSize(width, height, false);
|
||||
camera.aspect = width / height;
|
||||
camera.updateProjectionMatrix();
|
||||
if (lastNow === null) {
|
||||
const wideComposition = camera.aspect > 1.18;
|
||||
cameraState.x = wideComposition ? -1.35 : 0;
|
||||
cameraState.targetX = wideComposition ? 1.6 : 0;
|
||||
camera.position.set(cameraState.x, cameraState.y, cameraState.z);
|
||||
camera.lookAt(
|
||||
cameraState.targetX,
|
||||
cameraState.targetY,
|
||||
cameraState.targetZ,
|
||||
);
|
||||
}
|
||||
car.setDetail(width < 1_100 || height < 620 ? 'reduced' : 'full');
|
||||
car.group.visible = height >= 340;
|
||||
sign.setViewportAspect(camera.aspect);
|
||||
atmosphere.setViewportAspect(camera.aspect);
|
||||
postProcessor?.resize(width, height, quality.pixelRatio * quality.postScale);
|
||||
if (TEST_BUILD && context.quality !== 'medium') {
|
||||
window.__PORTFOLIO_PERFORMANCE__?.recordRuntimeAllocation(context.resources.gpuBudgetSnapshot({
|
||||
@@ -152,35 +186,105 @@ export async function createProceduralSceneGeneration(
|
||||
resize();
|
||||
const deltaSeconds = lastNow === null ? 0 : Math.min(0.1, Math.max(0, (now - lastNow) / 1000));
|
||||
lastNow = now;
|
||||
dynamics.update(snapshot.acceleration, snapshot.direction, deltaSeconds);
|
||||
const response = dynamics.snapshot();
|
||||
writeChapterPalette(targetPalette, snapshot.chapter, snapshot.chapter, 0);
|
||||
dampPalette(currentPalette, targetPalette, deltaSeconds);
|
||||
driveDistance += snapshot.driveSpeed * deltaSeconds * 18;
|
||||
road.update(driveDistance, snapshot.driveSpeed, snapshot.acceleration);
|
||||
driveDistance += deltaSeconds * (
|
||||
18
|
||||
+ snapshot.driveSpeed * 54
|
||||
+ response.roadEnergy * 48
|
||||
);
|
||||
const visualSpeed = THREE.MathUtils.clamp(
|
||||
0.18 + response.roadEnergy * 0.82,
|
||||
0.18,
|
||||
1,
|
||||
);
|
||||
road.update(driveDistance, visualSpeed, response.roadEnergy);
|
||||
terrain.update(
|
||||
driveDistance,
|
||||
snapshot.driveSpeed,
|
||||
visualSpeed,
|
||||
snapshot.chapter,
|
||||
snapshot.routeProgress,
|
||||
currentPalette,
|
||||
);
|
||||
sun.update(currentPalette, snapshot.routeProgress);
|
||||
car.update(snapshot.driveSpeed, snapshot.acceleration, now / 1000);
|
||||
const elapsedSeconds = now / 1000;
|
||||
sun.update(currentPalette, snapshot.routeProgress, elapsedSeconds);
|
||||
car.update(visualSpeed, response.suspensionEnergy, elapsedSeconds);
|
||||
sign.update(snapshot.sign);
|
||||
atmosphere.update(
|
||||
driveDistance,
|
||||
snapshot.driveSpeed,
|
||||
snapshot.acceleration,
|
||||
visualSpeed,
|
||||
response.streakEnergy,
|
||||
currentPalette,
|
||||
);
|
||||
background.copy(currentPalette.sky);
|
||||
scene.fog.color.copy(currentPalette.fog);
|
||||
scene.fog.density = 0.0105 + (1 - currentPalette.warmth) * 0.0025;
|
||||
const fov = 70 + Math.min(5, snapshot.acceleration * 4.2);
|
||||
if (camera.fov !== fov) {
|
||||
camera.fov = fov;
|
||||
scene.fog.density = 0.0019 + (1 - currentPalette.warmth) * 0.0008;
|
||||
ambient.color.copy(currentPalette.haze).lerp(currentPalette.terrainHigh, 0.26);
|
||||
ambient.groundColor.copy(currentPalette.fog);
|
||||
ambient.intensity = 1.05 + currentPalette.warmth * 0.18;
|
||||
carKeyLight.color.copy(currentPalette.terrainHigh).lerp(currentPalette.haze, 0.22);
|
||||
carKeyLight.intensity = 21 + response.bloomEnergy * 3.6;
|
||||
carFillLight.color.copy(currentPalette.haze).lerp(currentPalette.sun, 0.18);
|
||||
carFillLight.intensity = 12.5 + currentPalette.warmth * 2.8;
|
||||
rearLight.intensity = 4.2 + response.suspensionEnergy * 1.4;
|
||||
const wideComposition = camera.aspect > 1.18;
|
||||
const cameraX = wideComposition ? -1.35 : 0;
|
||||
const targetX = wideComposition ? 1.6 : 0;
|
||||
const roadVibration = Math.sin(driveDistance * 0.105) * (
|
||||
0.012 + response.roadEnergy * 0.024
|
||||
);
|
||||
const cameraMix = 1 - Math.exp(-deltaSeconds / 0.2);
|
||||
cameraState.x = THREE.MathUtils.lerp(
|
||||
cameraState.x,
|
||||
cameraX - response.directionBias * 0.16,
|
||||
cameraMix,
|
||||
);
|
||||
cameraState.y = THREE.MathUtils.lerp(
|
||||
cameraState.y,
|
||||
8.6 + roadVibration,
|
||||
cameraMix,
|
||||
);
|
||||
cameraState.z = THREE.MathUtils.lerp(
|
||||
cameraState.z,
|
||||
22 - response.cameraEnergy * 1.65,
|
||||
cameraMix,
|
||||
);
|
||||
cameraState.targetX = THREE.MathUtils.lerp(
|
||||
cameraState.targetX,
|
||||
targetX + response.directionBias * 0.32,
|
||||
cameraMix,
|
||||
);
|
||||
cameraState.targetY = THREE.MathUtils.lerp(
|
||||
cameraState.targetY,
|
||||
4.8 + response.cameraEnergy * 0.58,
|
||||
cameraMix,
|
||||
);
|
||||
cameraState.targetZ = THREE.MathUtils.lerp(
|
||||
cameraState.targetZ,
|
||||
-128 - response.cameraEnergy * 24,
|
||||
cameraMix,
|
||||
);
|
||||
cameraState.fov = THREE.MathUtils.lerp(
|
||||
cameraState.fov,
|
||||
66 + response.cameraEnergy * 13,
|
||||
cameraMix,
|
||||
);
|
||||
cameraState.roll = THREE.MathUtils.lerp(
|
||||
cameraState.roll,
|
||||
-response.directionBias * 0.003 + roadVibration * 0.018,
|
||||
cameraMix,
|
||||
);
|
||||
camera.position.set(cameraState.x, cameraState.y, cameraState.z);
|
||||
camera.lookAt(cameraState.targetX, cameraState.targetY, cameraState.targetZ);
|
||||
camera.rotation.z += cameraState.roll;
|
||||
if (Math.abs(camera.fov - cameraState.fov) > 0.001) {
|
||||
camera.fov = cameraState.fov;
|
||||
camera.updateProjectionMatrix();
|
||||
}
|
||||
if (postProcessor) {
|
||||
postProcessor.setDynamics(response.bloomEnergy);
|
||||
postProcessor.render(scene, camera);
|
||||
} else {
|
||||
renderer.render(scene, camera);
|
||||
|
||||
Reference in New Issue
Block a user