feat: layer the nightdrive sky and atmosphere

This commit is contained in:
2026-09-02 14:52:03 +03:00
parent 3c932cb840
commit 5c52de9134
15 changed files with 801 additions and 168 deletions

View File

@@ -1,5 +1,5 @@
import * as THREE from 'three';
import type { EnvironmentChapter, JourneySnapshot } from '@/journey/types';
import type { JourneySnapshot } from '@/journey/types';
import type { SceneResourceBag } from '@/scene/resources';
import type { SceneBuildContext, SceneGeneration } from '@/scene/types';
import { renderQualityFor } from '@/scene/render-quality';
@@ -8,21 +8,18 @@ import { createTerrain } from '@/scene/world/terrain';
import { createSun } from '@/scene/world/sun';
import { createCar } from '@/scene/world/car';
import { createSign } from '@/scene/world/sign';
import { createParticles } from '@/scene/world/particles';
import { createAtmosphere } from '@/scene/world/atmosphere';
import { createPostProcessor } from '@/scene/post-processing';
import {
createMutableChapterPalette,
writeChapterPalette,
type MutableChapterPalette,
} from '@/scene/world/palette';
import { isTestFault, recordTestFaultEvent } from '@/testing/fault-hooks';
declare const __PORTFOLIO_TEST_BUILD__: boolean;
const TEST_BUILD = typeof __PORTFOLIO_TEST_BUILD__ !== 'undefined' && __PORTFOLIO_TEST_BUILD__;
const skyColors: Readonly<Record<EnvironmentChapter, number>> = {
'deep-night': 0x040713,
systems: 0x060918,
infrastructure: 0x080b1d,
builds: 0x0d1025,
'pre-dawn': 0x18152c,
destination: 0x2b2035,
};
export function createChaseCamera(aspect = 1) {
const camera = new THREE.PerspectiveCamera(52, aspect, 0.1, 320);
camera.position.set(0, 4.8, 12.5);
@@ -62,9 +59,11 @@ export async function createProceduralSceneGeneration(
renderer.toneMappingExposure = 1.05;
const scene = new THREE.Scene();
const background = new THREE.Color(skyColors['deep-night']);
const currentPalette = createMutableChapterPalette('deep-night');
const targetPalette = createMutableChapterPalette('deep-night');
const background = currentPalette.sky.clone();
scene.background = background;
scene.fog = new THREE.FogExp2(0x080c18, 0.012);
scene.fog = new THREE.FogExp2(currentPalette.fog.getHex(), 0.012);
const camera = createChaseCamera();
const ambient = new THREE.HemisphereLight(0x6aa9c5, 0x100812, 1.25);
@@ -78,43 +77,19 @@ export async function createProceduralSceneGeneration(
const sun = createSun(context.resources);
const car = createCar(context.resources);
const sign = createSign(context.resources);
const particles = createParticles(context.resources, quality.particleCount);
scene.add(road.group, terrain.group, sun.mesh, car.group, sign.group, particles.points);
const atmosphere = createAtmosphere(context.resources, quality);
scene.add(
atmosphere.group,
road.group,
terrain.group,
sun.group,
car.group,
sign.group,
);
const postTarget = quality.postEffects
? context.resources.own('target', new THREE.WebGLRenderTarget(1, 1, {
depthBuffer: true,
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
}))
const postProcessor = quality.postEffects
? createPostProcessor(context.resources, renderer)
: null;
const postScene = postTarget ? new THREE.Scene() : null;
const postCamera = postTarget ? new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1) : null;
if (postScene && postTarget) {
const postGeometry = context.resources.own('geometry', new THREE.PlaneGeometry(2, 2));
const postMaterial = context.resources.own('material', new THREE.ShaderMaterial({
uniforms: { source: { value: postTarget.texture } },
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position.xy, 0.0, 1.0);
}
`,
fragmentShader: `
uniform sampler2D source;
varying vec2 vUv;
void main() {
vec4 color = texture2D(source, vUv);
float vignette = smoothstep(0.92, 0.24, distance(vUv, vec2(0.5)));
gl_FragColor = vec4(color.rgb * mix(0.82, 1.0, vignette), color.a);
}
`,
depthTest: false,
depthWrite: false,
}));
postScene.add(new THREE.Mesh(postGeometry, postMaterial));
}
let width = 0;
let height = 0;
let driveDistance = 0;
@@ -132,10 +107,7 @@ export async function createProceduralSceneGeneration(
renderer.setSize(width, height, false);
camera.aspect = width / height;
camera.updateProjectionMatrix();
postTarget?.setSize(
Math.max(1, Math.floor(width * quality.pixelRatio)),
Math.max(1, Math.floor(height * quality.pixelRatio)),
);
postProcessor?.resize(width, height, quality.pixelRatio * quality.postScale);
if (TEST_BUILD && context.quality !== 'medium') {
window.__PORTFOLIO_PERFORMANCE__?.recordRuntimeAllocation(context.resources.gpuBudgetSnapshot({
tier: context.quality,
@@ -176,6 +148,8 @@ export async function createProceduralSceneGeneration(
resize();
const deltaSeconds = lastNow === null ? 0 : Math.min(0.1, Math.max(0, (now - lastNow) / 1000));
lastNow = now;
writeChapterPalette(targetPalette, snapshot.chapter, snapshot.chapter, 0);
dampPalette(currentPalette, targetPalette, deltaSeconds);
driveDistance += snapshot.driveSpeed * deltaSeconds * 18;
road.update(driveDistance, snapshot.driveSpeed, snapshot.acceleration);
terrain.update(
@@ -183,22 +157,27 @@ export async function createProceduralSceneGeneration(
snapshot.driveSpeed,
snapshot.chapter,
snapshot.routeProgress,
currentPalette,
);
sun.update(snapshot.chapter, snapshot.routeProgress);
sun.update(currentPalette, snapshot.routeProgress);
car.update(snapshot.driveSpeed, snapshot.acceleration, now / 1000);
sign.update(snapshot.sign);
particles.update(driveDistance, snapshot.driveSpeed, snapshot.acceleration);
background.setHex(skyColors[snapshot.chapter]);
atmosphere.update(
driveDistance,
snapshot.driveSpeed,
snapshot.acceleration,
currentPalette,
);
background.copy(currentPalette.sky);
scene.fog.color.copy(currentPalette.fog);
scene.fog.density = 0.0105 + (1 - currentPalette.warmth) * 0.0025;
const fov = 52 + Math.min(7, snapshot.acceleration * 4.5);
if (camera.fov !== fov) {
camera.fov = fov;
camera.updateProjectionMatrix();
}
if (postTarget) {
renderer.setRenderTarget(postTarget);
renderer.render(scene, camera);
renderer.setRenderTarget(null);
renderer.render(postScene, postCamera);
if (postProcessor) {
postProcessor.render(scene, camera);
} else {
renderer.render(scene, camera);
}
@@ -211,3 +190,21 @@ export async function createProceduralSceneGeneration(
},
};
}
function dampPalette(
current: MutableChapterPalette,
target: Readonly<MutableChapterPalette>,
deltaSeconds: number,
): void {
const mix = 1 - Math.exp(-Math.max(0, deltaSeconds) * 2.8);
current.sky.lerp(target.sky, mix);
current.fog.lerp(target.fog, mix);
current.terrainLow.lerp(target.terrainLow, mix);
current.terrainHigh.lerp(target.terrainHigh, mix);
current.sun.lerp(target.sun, mix);
current.haze.lerp(target.haze, mix);
current.warmth = THREE.MathUtils.lerp(current.warmth, target.warmth, mix);
current.terrainEnergy = THREE.MathUtils.lerp(current.terrainEnergy, target.terrainEnergy, mix);
current.starVisibility = THREE.MathUtils.lerp(current.starVisibility, target.starVisibility, mix);
current.streakEnergy = THREE.MathUtils.lerp(current.streakEnergy, target.streakEnergy, mix);
}