588 lines
20 KiB
TypeScript
588 lines
20 KiB
TypeScript
import * as THREE from 'three';
|
|
import type { JourneySnapshot } from '@/journey/types';
|
|
import { readMusicPulse } from '@/audio/music-pulse';
|
|
import type { SceneResourceBag } from '@/scene/resources';
|
|
import type { SceneBuildContext, SceneGeneration } from '@/scene/types';
|
|
import { renderQualityFor } from '@/scene/render-quality';
|
|
import { createHorizonAir } from '@/scene/world/horizon-air';
|
|
import { createExpressway } from '@/scene/world/expressway';
|
|
import { createDistricts } from '@/scene/world/districts';
|
|
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';
|
|
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 { createDriveDynamics } from '@/scene/drive-dynamics';
|
|
import { HORIZON_FOG_END, HORIZON_FOG_START } from '@/scene/world/metrics';
|
|
import { CHAPTER_COMPOSITIONS } from '@/scene/world/composition';
|
|
import { createCoachworkEnvironment } from '@/scene/world/coachwork-light';
|
|
import { createTravelDistance } from '@/scene/world/travel-distance';
|
|
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__;
|
|
|
|
export function createChaseCamera(aspect = 1) {
|
|
const c = CHAPTER_COMPOSITIONS.start;
|
|
const camera = new THREE.PerspectiveCamera(c.fov, aspect, 0.1, 1_200);
|
|
camera.position.set(c.x, c.y, c.z);
|
|
camera.lookAt(0, c.targetY, c.targetZ);
|
|
return camera;
|
|
}
|
|
|
|
export function createWorldFog(color: number) {
|
|
return new THREE.Fog(color, HORIZON_FOG_START, HORIZON_FOG_END);
|
|
}
|
|
|
|
export async function createProceduralSceneGeneration(
|
|
context: SceneBuildContext,
|
|
): Promise<SceneGeneration> {
|
|
if (TEST_BUILD && isTestFault('F1-shader')) {
|
|
recordTestFaultEvent('shader-rejected');
|
|
throw new Error('Injected shader failure');
|
|
}
|
|
if (TEST_BUILD && isTestFault('F1-texture')) {
|
|
recordTestFaultEvent('texture-rejected');
|
|
throw new Error('Injected texture failure');
|
|
}
|
|
if (TEST_BUILD && isTestFault('F1-car')) {
|
|
recordTestFaultEvent('car-rejected');
|
|
throw new Error('Injected car failure');
|
|
}
|
|
if (context.signal.aborted)
|
|
throw new DOMException('Scene build aborted', 'AbortError');
|
|
const canvas = context.canvas as HTMLCanvasElement;
|
|
if (typeof canvas.getContext !== 'function')
|
|
throw new Error('A canvas element is required');
|
|
const mobileOrTablet =
|
|
Math.max(canvas.clientWidth || 0, canvas.clientHeight || 0) < 1280;
|
|
const quality = renderQualityFor(
|
|
context.quality,
|
|
globalThis.devicePixelRatio ?? 1,
|
|
mobileOrTablet,
|
|
);
|
|
const renderer = context.resources.own(
|
|
'renderer',
|
|
new THREE.WebGLRenderer({
|
|
canvas,
|
|
alpha: false,
|
|
antialias: context.quality !== 'low',
|
|
powerPreference: 'high-performance',
|
|
}),
|
|
);
|
|
renderer.setPixelRatio(quality.pixelRatio);
|
|
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
renderer.toneMappingExposure = 1.05;
|
|
|
|
const scene = new THREE.Scene();
|
|
scene.name = 'world-root';
|
|
scene.environment = createCoachworkEnvironment(context.resources, renderer);
|
|
scene.environmentIntensity = 0.85;
|
|
const currentPalette = createMutableChapterPalette('deep-night');
|
|
const targetPalette = createMutableChapterPalette('deep-night');
|
|
const background = currentPalette.sky.clone();
|
|
scene.background = background;
|
|
scene.fog = createWorldFog(currentPalette.fog.getHex());
|
|
const camera = createChaseCamera();
|
|
camera.name = 'journey-camera';
|
|
scene.add(camera);
|
|
|
|
const ambient = new THREE.HemisphereLight(0x6aa9c5, 0x100812, 1.25);
|
|
scene.add(ambient);
|
|
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 speedWake = createSpeedWake(context.resources);
|
|
const expressway = createExpressway(context.resources, quality);
|
|
const districts = createDistricts(context.resources, quality, context.signal);
|
|
const signStage = createSignStage(context.resources);
|
|
const sun = createSun(context.resources);
|
|
const horizonAir = createHorizonAir(context.resources);
|
|
const authoredCar = await loadGrandTourer(context.signal);
|
|
const car = createCar(context.resources, authoredCar);
|
|
car.setDetail(mobileOrTablet ? 'reduced' : 'full');
|
|
await prepareSignTypography(context.signal);
|
|
const sign = createSign(context.resources, {
|
|
maximumAnisotropy: renderer.capabilities.getMaxAnisotropy(),
|
|
maximumTextureSize: renderer.capabilities.maxTextureSize,
|
|
});
|
|
const atmosphere = createAtmosphere(context.resources, quality);
|
|
scene.add(
|
|
atmosphere.group,
|
|
road.group,
|
|
expressway.group,
|
|
districts.group,
|
|
terrain.group,
|
|
sun.group,
|
|
horizonAir.group,
|
|
car.group,
|
|
speedWake.group,
|
|
sign.group,
|
|
);
|
|
|
|
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: 3.9,
|
|
targetZ: -90,
|
|
shift: CHAPTER_COMPOSITIONS.start.shift,
|
|
verticalShift: 0,
|
|
fov: camera.fov,
|
|
roll: 0,
|
|
};
|
|
let width = 0;
|
|
let height = 0;
|
|
const travelDistance = createTravelDistance();
|
|
let lastNow: number | null = null;
|
|
let disposed = false;
|
|
let renderedFrames = 0;
|
|
let atmosphericTime = 0;
|
|
let signCameraWeight = 0;
|
|
let overdrive = 0;
|
|
let terrainSurge = 0;
|
|
let musicPulse = 0;
|
|
let terrainSpectrumPhase = 0;
|
|
let firstFrameSuppressionRecorded = false;
|
|
|
|
const resize = (): void => {
|
|
const nextWidth = Math.max(1, canvas.clientWidth || canvas.width || 1);
|
|
const nextHeight = Math.max(1, canvas.clientHeight || canvas.height || 1);
|
|
if (nextWidth === width && nextHeight === height) return;
|
|
width = nextWidth;
|
|
height = nextHeight;
|
|
renderer.setSize(width, height, false);
|
|
camera.aspect = width / height;
|
|
camera.updateProjectionMatrix();
|
|
if (lastNow === null) {
|
|
const wideComposition = camera.aspect > 1.18;
|
|
cameraState.x = wideComposition ? CHAPTER_COMPOSITIONS.start.x : 0;
|
|
cameraState.targetX = 0;
|
|
cameraState.shift = wideComposition
|
|
? CHAPTER_COMPOSITIONS.start.shift
|
|
: 0;
|
|
cameraState.verticalShift = wideComposition ? 0 : 0.16;
|
|
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.setViewportSize(width, height);
|
|
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({
|
|
tier: context.quality,
|
|
defaultFramebuffer: {
|
|
width: Math.max(1, canvas.width),
|
|
height: Math.max(1, canvas.height),
|
|
bytesPerPixel: 4,
|
|
attachments: 2,
|
|
samples: context.quality === 'low' ? 1 : 4,
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
};
|
|
|
|
if (context.signal.aborted) {
|
|
context.resources.dispose();
|
|
throw new DOMException('Scene build aborted', 'AbortError');
|
|
}
|
|
|
|
return {
|
|
render(snapshot: Readonly<JourneySnapshot>, now: number) {
|
|
if (disposed) return;
|
|
if (TEST_BUILD && isTestFault('F2-first-frame-timeout')) {
|
|
if (!firstFrameSuppressionRecorded) {
|
|
firstFrameSuppressionRecorded = true;
|
|
recordTestFaultEvent('first-frame-suppressed');
|
|
}
|
|
return false;
|
|
}
|
|
if (
|
|
TEST_BUILD &&
|
|
isTestFault('F3-post-first-frame-exception') &&
|
|
renderedFrames > 0
|
|
) {
|
|
recordTestFaultEvent('post-first-frame-exception');
|
|
throw new Error('Injected post-first-frame exception');
|
|
}
|
|
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();
|
|
// 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)));
|
|
// Terrain feedback reads acceleration directly, with its own fast attack
|
|
// and long release. Audio and the shared drive responses stay untouched.
|
|
const surgeTarget = Math.sqrt(
|
|
THREE.MathUtils.clamp(snapshot.acceleration, 0, 1),
|
|
);
|
|
// Soundtrack coupling: the bass the visitor hears lifts the terrain and
|
|
// the sun. One analyser read per frame; zero when nothing is audible.
|
|
const pulseTarget = readMusicPulse();
|
|
musicPulse +=
|
|
(pulseTarget - musicPulse) *
|
|
(1 - Math.exp(-deltaSeconds / (pulseTarget > musicPulse ? 0.05 : 0.32)));
|
|
const beat = THREE.MathUtils.smoothstep(musicPulse, 0.42, 0.92);
|
|
terrainSurge +=
|
|
(Math.max(surgeTarget, beat * 0.55) - terrainSurge) *
|
|
(1 -
|
|
Math.exp(-deltaSeconds / (Math.max(surgeTarget, beat * 0.55) > terrainSurge ? 0.11 : 1.15)));
|
|
// Integrate color travel instead of multiplying elapsed time by speed:
|
|
// lifting off slows the spectrum to rest without rewinding its phase.
|
|
terrainSpectrumPhase =
|
|
(terrainSpectrumPhase + deltaSeconds * overdrive * 0.38) % 1;
|
|
writeChapterPalette(targetPalette, snapshot.chapter, snapshot.chapter, 0);
|
|
dampPalette(currentPalette, targetPalette, deltaSeconds);
|
|
const driveDistance = travelDistance.advance(
|
|
deltaSeconds,
|
|
snapshot.driveSpeed,
|
|
response.roadEnergy,
|
|
);
|
|
const visualSpeed = THREE.MathUtils.clamp(
|
|
0.18 + response.roadEnergy * 0.82,
|
|
0.18,
|
|
1,
|
|
);
|
|
road.update(
|
|
driveDistance,
|
|
visualSpeed,
|
|
response.roadEnergy,
|
|
currentPalette,
|
|
);
|
|
expressway.update(
|
|
driveDistance,
|
|
response.roadEnergy,
|
|
currentPalette.warmth,
|
|
snapshot.activeSectionId,
|
|
deltaSeconds,
|
|
);
|
|
districts.update(
|
|
driveDistance,
|
|
response.roadEnergy,
|
|
snapshot.activeSectionId,
|
|
atmosphericTime,
|
|
deltaSeconds,
|
|
);
|
|
terrain.update(
|
|
driveDistance,
|
|
visualSpeed,
|
|
snapshot.chapter,
|
|
snapshot.routeProgress,
|
|
currentPalette,
|
|
overdrive,
|
|
terrainSurge,
|
|
terrainSpectrumPhase,
|
|
);
|
|
atmosphericTime += deltaSeconds;
|
|
const elapsedSeconds = atmosphericTime;
|
|
horizonAir.update(currentPalette, elapsedSeconds);
|
|
sun.update(
|
|
currentPalette,
|
|
snapshot.routeProgress,
|
|
elapsedSeconds,
|
|
Math.min(1, response.bloomEnergy + beat * 0.35),
|
|
snapshot.arrived,
|
|
);
|
|
car.update(
|
|
visualSpeed,
|
|
response.suspensionEnergy,
|
|
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,
|
|
visualSpeed,
|
|
response.streakEnergy,
|
|
currentPalette,
|
|
atmosphericTime,
|
|
overdrive,
|
|
);
|
|
background.copy(currentPalette.sky);
|
|
scene.fog.color.copy(currentPalette.fog);
|
|
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.setHex(0xb2d5ff);
|
|
carKeyLight.intensity = 32 + response.bloomEnergy * 3.6 + overdrive * 8;
|
|
carFillLight.color.setHex(0xff62ad);
|
|
carFillLight.intensity = 12.5 + currentPalette.warmth * 2.8;
|
|
rearLight.intensity = 4.2 + response.suspensionEnergy * 1.4;
|
|
const wideComposition = camera.aspect > 1.18;
|
|
const baseComposition = CHAPTER_COMPOSITIONS[snapshot.activeSectionId];
|
|
const upcoming =
|
|
snapshot.sign && snapshot.sign.phase !== 'depart'
|
|
? CHAPTER_COMPOSITIONS[snapshot.sign.targetId]
|
|
: baseComposition;
|
|
const anticipation =
|
|
snapshot.sign?.phase === 'approach'
|
|
? Math.min(0.55, snapshot.sign.progress * 0.55)
|
|
: snapshot.sign?.phase === 'dwell'
|
|
? 0.55
|
|
: 0;
|
|
const composition = {
|
|
x: THREE.MathUtils.lerp(baseComposition.x, upcoming.x, anticipation),
|
|
y: THREE.MathUtils.lerp(baseComposition.y, upcoming.y, anticipation),
|
|
z: THREE.MathUtils.lerp(baseComposition.z, upcoming.z, anticipation),
|
|
targetY: THREE.MathUtils.lerp(
|
|
baseComposition.targetY,
|
|
upcoming.targetY,
|
|
anticipation,
|
|
),
|
|
targetZ: THREE.MathUtils.lerp(
|
|
baseComposition.targetZ,
|
|
upcoming.targetZ,
|
|
anticipation,
|
|
),
|
|
fov: THREE.MathUtils.lerp(
|
|
baseComposition.fov,
|
|
upcoming.fov,
|
|
anticipation,
|
|
),
|
|
shift: THREE.MathUtils.lerp(
|
|
baseComposition.shift,
|
|
upcoming.shift,
|
|
anticipation,
|
|
),
|
|
};
|
|
// 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
|
|
? THREE.MathUtils.lerp(
|
|
composition.shift,
|
|
composition.shift * 0.28,
|
|
signCameraWeight,
|
|
)
|
|
: 0;
|
|
const targetVerticalShift = wideComposition
|
|
? 0
|
|
: snapshot.activeSectionId === 'start'
|
|
? 0.16
|
|
: 0.07;
|
|
const roadVibration =
|
|
Math.sin(driveDistance * 0.105) * (0.012 + response.roadEnergy * 0.024);
|
|
const cameraMix =
|
|
1 -
|
|
Math.exp(
|
|
-deltaSeconds / (snapshot.activeSectionId === 'contact' ? 1.5 : 1.05),
|
|
);
|
|
cameraState.x = THREE.MathUtils.lerp(
|
|
cameraState.x,
|
|
cameraX - response.directionBias * 0.16,
|
|
cameraMix,
|
|
);
|
|
cameraState.y = THREE.MathUtils.lerp(
|
|
cameraState.y,
|
|
(wideComposition ? composition.y : 6.5) + roadVibration,
|
|
cameraMix,
|
|
);
|
|
cameraState.z = THREE.MathUtils.lerp(
|
|
cameraState.z,
|
|
(wideComposition ? composition.z : 31) + response.cameraEnergy * 0.9,
|
|
cameraMix,
|
|
);
|
|
cameraState.targetX = THREE.MathUtils.lerp(
|
|
cameraState.targetX,
|
|
targetX + response.directionBias * 0.32,
|
|
cameraMix,
|
|
);
|
|
cameraState.targetY = THREE.MathUtils.lerp(
|
|
cameraState.targetY,
|
|
composition.targetY + response.cameraEnergy * 0.32,
|
|
cameraMix,
|
|
);
|
|
cameraState.targetZ = THREE.MathUtils.lerp(
|
|
cameraState.targetZ,
|
|
composition.targetZ - response.cameraEnergy * 16,
|
|
cameraMix,
|
|
);
|
|
cameraState.fov = THREE.MathUtils.lerp(
|
|
cameraState.fov,
|
|
(wideComposition ? composition.fov : 61) + response.cameraEnergy * 5.5,
|
|
cameraMix,
|
|
);
|
|
cameraState.roll = THREE.MathUtils.lerp(
|
|
cameraState.roll,
|
|
-response.directionBias * 0.003 + roadVibration * 0.018,
|
|
cameraMix,
|
|
);
|
|
cameraState.shift = THREE.MathUtils.lerp(
|
|
cameraState.shift,
|
|
targetShift,
|
|
cameraMix,
|
|
);
|
|
cameraState.verticalShift = THREE.MathUtils.lerp(
|
|
cameraState.verticalShift,
|
|
targetVerticalShift,
|
|
cameraMix,
|
|
);
|
|
camera.setViewOffset(
|
|
width,
|
|
height,
|
|
-width * cameraState.shift,
|
|
-height * cameraState.verticalShift,
|
|
width,
|
|
height,
|
|
);
|
|
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();
|
|
}
|
|
const stage = signStage.update(width, height);
|
|
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) {
|
|
window.dispatchEvent(
|
|
new CustomEvent('portfolio-sign-frame', {
|
|
detail: {
|
|
now,
|
|
...snapshot.sign,
|
|
activeSectionId: snapshot.activeSectionId,
|
|
direction: snapshot.direction,
|
|
...presentation,
|
|
rendererPixelRatio: renderer.getPixelRatio(),
|
|
viewport: { width, height },
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
if (postProcessor) {
|
|
postProcessor.setDynamics(response.bloomEnergy);
|
|
postProcessor.render(scene, camera);
|
|
} else {
|
|
renderer.render(scene, camera);
|
|
}
|
|
renderedFrames += 1;
|
|
},
|
|
dispose() {
|
|
if (disposed) return;
|
|
disposed = true;
|
|
// Quality changes reuse the canvas context. Leave texture upload flags at
|
|
// defaults before the next renderer creates its empty array textures.
|
|
const gl = renderer.getContext();
|
|
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
|
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
|
scene.clear();
|
|
},
|
|
};
|
|
}
|
|
|
|
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,
|
|
);
|
|
}
|