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:
2026-09-02 23:16:25 +03:00
parent 7dae835ac5
commit bc18bc2043
73 changed files with 1555 additions and 293 deletions

View File

@@ -20,11 +20,12 @@ interface WorldGroup {
getObjectByName(name: string): unknown;
}
const TERRAIN_WIDTH = 160;
const TERRAIN_DEPTH = 300;
const NEAR_Z = 14;
const ROAD_CLEAR_HALF_WIDTH = 11.4;
const RIDGE_START = 24;
const TERRAIN_WIDTH = 360;
const TERRAIN_DEPTH = 438;
const NEAR_Z = 28;
const ROAD_CLEAR_HALF_WIDTH = 9.4;
const RIDGE_START = 46;
const NOISE_PERIOD = 5_694;
export function createTerrain(
resources: SceneResourceBag,
@@ -42,6 +43,8 @@ export function createTerrain(
uRouteProgress: { value: 0 },
uTerrainEnergy: { value: initialPalette.terrainEnergy },
uOctaves: { value: quality.terrainOctaves },
uRelief: { value: 34 },
uFar: { value: 430 },
uLowColor: { value: lowColor },
uHighColor: { value: highColor },
};
@@ -54,12 +57,15 @@ export function createTerrain(
uniform float uRouteProgress;
uniform float uTerrainEnergy;
uniform float uOctaves;
uniform float uRelief;
uniform float uFar;
varying float vHeight;
varying float vRoadMask;
varying float vDepthFade;
varying float vFlow;
float hash21(vec2 point) {
point = mod(point, 256.0);
point = fract(point * vec2(123.34, 456.21));
point += dot(point, point + 45.32);
return fract(point.x * point.y);
@@ -85,19 +91,22 @@ export function createTerrain(
void main() {
vec3 displaced = position;
float wrappedPhase = mod(uDistance * (0.18 + uSpeed * 0.06), 96.0);
float ridgeNoise = multiOctaveNoise(vec2(position.x * 0.038, (position.z - wrappedPhase) * 0.028));
float macroRidge = multiOctaveNoise(vec2(position.x * 0.014 + 31.0, position.z * 0.012));
float edgeGrowth = mix(0.6, 1.0, smoothstep(0.0, 72.0, abs(position.x)));
float height = roadMask * (0.25 + ridgeNoise * ridgeNoise * 8.8 + macroRidge * 2.4);
height *= edgeGrowth * uTerrainEnergy;
displaced.y += height - 0.22;
float wrappedPhase = mod(uDistance, ${NOISE_PERIOD.toFixed(1)});
const float S = 0.04495961;
vec2 world = vec2(position.x, position.z - wrappedPhase);
float ridgeNoise = multiOctaveNoise(world * S);
float edgeGrowth = mix(7.0, uRelief, smoothstep(14.0, 150.0, abs(position.x)));
float height = roadMask * ridgeNoise * edgeGrowth * uTerrainEnergy;
displaced.y += height;
vHeight = clamp(height / 10.0, 0.0, 1.0);
vHeight = clamp(height / max(1.0, uRelief), 0.0, 1.0);
vRoadMask = roadMask;
vDepthFade = smoothstep(-286.0, -205.0, position.z) * (1.0 - smoothstep(7.0, 14.0, position.z));
vec4 viewPosition = modelViewMatrix * vec4(displaced, 1.0);
float viewDistance = -viewPosition.z;
vDepthFade = smoothstep(4.0, 18.0, viewDistance)
* (1.0 - smoothstep(uFar * 0.32, uFar * 0.88, viewDistance));
vFlow = fract((-position.z + uDistance * (0.9 + uSpeed * 0.25)) / 11.0);
gl_Position = projectionMatrix * modelViewMatrix * vec4(displaced, 1.0);
gl_Position = projectionMatrix * viewPosition;
}
`,
fragmentShader: `
@@ -112,10 +121,10 @@ export function createTerrain(
void main() {
float ridgeMix = smoothstep(0.16, 0.78, vHeight);
vec3 color = mix(uLowColor, uHighColor, ridgeMix);
float pulse = 0.78 + 0.22 * smoothstep(0.18, 0.52, vFlow);
float pulse = 0.72 + 0.28 * smoothstep(0.16, 0.55, vFlow);
float routeCalm = mix(1.0, 0.9, smoothstep(0.82, 1.0, uRouteProgress));
float alpha = vRoadMask * vDepthFade * pulse * routeCalm * mix(0.34, 0.66, ridgeMix);
gl_FragColor = vec4(color, alpha);
float alpha = vRoadMask * vDepthFade * pulse * routeCalm * mix(0.32, 0.82, ridgeMix);
gl_FragColor = vec4(color * mix(0.78, 1.55, ridgeMix), alpha);
}
`,
transparent: true,
@@ -126,7 +135,7 @@ export function createTerrain(
}));
const lines = new THREE.LineSegments(geometry, material);
lines.name = 'terrain-lines';
lines.name = 'terrain-grid';
lines.frustumCulled = false;
group.add(lines);
@@ -134,7 +143,7 @@ export function createTerrain(
group,
update(distance, speed, chapter, routeProgress, palette) {
const chapterPalette = CHAPTER_PALETTES[chapter];
uniforms.uDistance.value = finiteOrZero(distance);
uniforms.uDistance.value = ((finiteOrZero(distance) % NOISE_PERIOD) + NOISE_PERIOD) % NOISE_PERIOD;
uniforms.uSpeed.value = clamp01(speed);
uniforms.uRouteProgress.value = clamp01(routeProgress);
uniforms.uTerrainEnergy.value = palette?.terrainEnergy ?? chapterPalette.terrainEnergy;
@@ -151,7 +160,8 @@ export function createTerrain(
function createIndexedTerrainGeometry(segments: number) {
const zSegments = Math.max(16, Math.floor(segments));
const xSegments = Math.max(24, Math.floor(zSegments * 0.75 / 2) * 2);
const approximateCell = TERRAIN_DEPTH / zSegments;
const xSegments = Math.max(48, Math.floor((TERRAIN_WIDTH / approximateCell) / 2) * 2);
const columnCount = xSegments + 1;
const rowCount = zSegments + 1;
const positions = new Float32Array(columnCount * rowCount * 3);