feat: rebuild the nightdrive road and terrain

This commit is contained in:
2026-09-02 14:30:03 +03:00
parent f2dd3f85dd
commit 3c932cb840
7 changed files with 694 additions and 74 deletions

View File

@@ -0,0 +1,164 @@
import { describe, expect, it } from 'vitest';
import * as THREE from 'three';
import type { EnvironmentChapter } from '@/journey/types';
import { renderQualityFor } from '@/scene/render-quality';
import { SceneResourceBag } from '@/scene/resources';
import {
CHAPTER_PALETTES,
createMutableChapterPalette,
writeChapterPalette,
} from '@/scene/world/palette';
import { createRoad } from '@/scene/world/road';
import { createTerrain } from '@/scene/world/terrain';
describe('procedural world models', () => {
it('scales geometry and atmosphere work monotonically by live quality tier', () => {
const high = renderQualityFor('high', 3, false);
const medium = renderQualityFor('medium', 3, false);
const low = renderQualityFor('low', 3, false);
expect(high.roadSegments).toBeGreaterThan(medium.roadSegments);
expect(medium.roadSegments).toBeGreaterThan(low.roadSegments);
expect(high.terrainSegments).toBeGreaterThan(medium.terrainSegments);
expect(medium.terrainSegments).toBeGreaterThan(low.terrainSegments);
expect(high.streakCount).toBeGreaterThan(low.streakCount);
expect(low.postEffects).toBe(false);
});
it('builds a shader road whose motion is entirely uniform-driven', () => {
const resources = new SceneResourceBag();
const road = createRoad(resources, renderQualityFor('high', 1, false));
const surface = road.group.getObjectByName('road-surface') as ShaderRenderable;
expect(surface).toBeInstanceOf(THREE.Mesh);
expect(surface.geometry.index).not.toBeNull();
expect(surface.geometry.getAttribute('position').count).toBeGreaterThan(1_000);
expect(surface.material.uniforms).toMatchObject({
uDistance: { value: 0 },
uSpeed: { value: 0 },
uAcceleration: { value: 0 },
});
expect(surface.material.fragmentShader).toContain('centerLane');
expect(surface.material.fragmentShader).toContain('outerEdge');
const positionArray = surface.geometry.getAttribute('position').array;
const before = resources.snapshot();
for (let index = 0; index < 300; index += 1) {
road.update(index * 0.5, 0.65, 0.4);
}
expect(resources.snapshot()).toEqual(before);
expect(surface.geometry.getAttribute('position').array).toBe(positionArray);
expect(surface.material.uniforms.uDistance?.value).toBe(149.5);
expect(surface.material.uniforms.uSpeed?.value).toBe(0.65);
expect(surface.material.uniforms.uAcceleration?.value).toBe(0.4);
resources.dispose();
});
it('builds one wide indexed line terrain with a flat road-center mask', () => {
const resources = new SceneResourceBag();
const terrain = createTerrain(resources, renderQualityFor('medium', 1, false));
const lines = terrain.group.getObjectByName('terrain-lines') as ShaderRenderable;
expect(lines).toBeInstanceOf(THREE.LineSegments);
expect(lines.geometry.index).not.toBeNull();
lines.geometry.computeBoundingBox();
expect(lines.geometry.boundingBox).not.toBeNull();
expect(
(lines.geometry.boundingBox?.max.x ?? 0) - (lines.geometry.boundingBox?.min.x ?? 0),
).toBeGreaterThan(120);
const roadMask = lines.geometry.getAttribute('roadMask');
expect(roadMask).toBeDefined();
const maskValues = Array.from(roadMask.array as ArrayLike<number>);
expect(Math.min(...maskValues)).toBe(0);
expect(Math.max(...maskValues)).toBeGreaterThan(0.99);
expect(lines.material.uniforms).toMatchObject({
uDistance: { value: 0 },
uSpeed: { value: 0 },
uRouteProgress: { value: 0 },
});
expect(lines.material.vertexShader).toContain('multiOctaveNoise');
const indexArray = lines.geometry.index?.array;
const positionArray = lines.geometry.getAttribute('position').array;
const lowColor = lines.material.uniforms.uLowColor?.value;
const highColor = lines.material.uniforms.uHighColor?.value;
const before = resources.snapshot();
for (let index = 0; index < 300; index += 1) {
terrain.update(index * 0.4, 0.7, 'builds', index / 299);
}
expect(resources.snapshot()).toEqual(before);
expect(lines.geometry.index?.array).toBe(indexArray);
expect(lines.geometry.getAttribute('position').array).toBe(positionArray);
expect(lines.material.uniforms.uLowColor?.value).toBe(lowColor);
expect(lines.material.uniforms.uHighColor?.value).toBe(highColor);
expect(lines.material.uniforms.uRouteProgress?.value).toBe(1);
resources.dispose();
});
it('writes deterministic chapter blends into stable color allocations', () => {
const target = createMutableChapterPalette('deep-night');
const colorReferences = [
target.sky,
target.fog,
target.terrainLow,
target.terrainHigh,
target.sun,
target.haze,
];
expect(Object.keys(CHAPTER_PALETTES)).toEqual([
'deep-night',
'systems',
'infrastructure',
'builds',
'pre-dawn',
'destination',
] satisfies EnvironmentChapter[]);
expect(writeChapterPalette(target, 'deep-night', 'destination', 0.5)).toBe(target);
const first = paletteSnapshot(target);
expect(writeChapterPalette(target, 'deep-night', 'destination', 0.5)).toBe(target);
expect(paletteSnapshot(target)).toEqual(first);
expect([
target.sky,
target.fog,
target.terrainLow,
target.terrainHigh,
target.sun,
target.haze,
]).toEqual(colorReferences);
expect(target.warmth).toBeGreaterThan(CHAPTER_PALETTES['deep-night'].warmth);
expect(target.warmth).toBeLessThan(CHAPTER_PALETTES.destination.warmth);
});
});
function paletteSnapshot(palette: ReturnType<typeof createMutableChapterPalette>) {
return {
sky: palette.sky.getHex(),
fog: palette.fog.getHex(),
terrainLow: palette.terrainLow.getHex(),
terrainHigh: palette.terrainHigh.getHex(),
sun: palette.sun.getHex(),
haze: palette.haze.getHex(),
warmth: palette.warmth,
terrainEnergy: palette.terrainEnergy,
starVisibility: palette.starVisibility,
streakEnergy: palette.streakEnergy,
};
}
interface ShaderRenderable {
geometry: {
index: { array: ArrayLike<number> } | null;
boundingBox: { min: { x: number }; max: { x: number } } | null;
computeBoundingBox(): void;
getAttribute(name: string): { count: number; array: ArrayLike<number> };
};
material: {
uniforms: Record<string, { value: unknown }>;
vertexShader: string;
fragmentShader: string;
};
}