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

@@ -16,6 +16,7 @@ import { createPostProcessor } from "@/scene/post-processing";
import { createCar } from "@/scene/world/car";
import { createChaseCamera } from "@/scene/world/environment";
import { createSign, routeLabelFor } from "@/scene/world/sign";
import { createDriveDynamics } from "@/scene/drive-dynamics";
describe("procedural world models", () => {
it("scales geometry and atmosphere work monotonically by live quality tier", () => {
@@ -72,7 +73,7 @@ describe("procedural world models", () => {
renderQualityFor("medium", 1, false),
);
const lines = terrain.group.getObjectByName(
"terrain-lines",
"terrain-grid",
) as ShaderRenderable;
expect(lines).toBeInstanceOf(THREE.LineSegments);
@@ -93,8 +94,10 @@ describe("procedural world models", () => {
uDistance: { value: 0 },
uSpeed: { value: 0 },
uRouteProgress: { value: 0 },
uRelief: { value: expect.any(Number) },
});
expect(lines.material.vertexShader).toContain("multiOctaveNoise");
expect(lines.material.vertexShader).toContain("mod(point, 256.0)");
const indexArray = lines.geometry.index?.array;
const positionArray = lines.geometry.getAttribute("position").array;
@@ -158,20 +161,21 @@ describe("procedural world models", () => {
it("renders the sun as a scan-banded disc with a separately owned halo", () => {
const resources = new SceneResourceBag();
const sun = createSun(resources);
const disc = sun.group.getObjectByName("sun-disc") as ShaderRenderable;
const halo = sun.group.getObjectByName("sun-halo");
const disc = sun.group.getObjectByName("horizon-sun") as ShaderRenderable;
const halo = sun.group.getObjectByName("horizon-halo");
expect(disc).toBeInstanceOf(THREE.Mesh);
expect(halo).toBeInstanceOf(THREE.Sprite);
expect(disc.material.uniforms).toMatchObject({
uScanBands: { value: 14 },
uScanBands: { value: 11 },
uWarmth: { value: CHAPTER_PALETTES["deep-night"].warmth },
uTime: { value: 0 },
});
expect(disc.material.fragmentShader).toContain("scanBand");
expect(resources.snapshot().owned.texture).toBe(1);
const palette = createMutableChapterPalette("destination");
sun.update(palette, 1);
sun.update(palette, 1, 3.2);
expect(disc.material.uniforms.uWarmth?.value).toBe(1);
expect(disc.material.uniforms.uRouteProgress?.value).toBe(1);
resources.dispose();
@@ -191,10 +195,10 @@ describe("procedural world models", () => {
"atmosphere-stars",
) as ShaderRenderable;
const highStreaks = high.group.getObjectByName(
"atmosphere-streaks",
"speed-streaks",
) as ShaderRenderable;
const lowStreaks = low.group.getObjectByName(
"atmosphere-streaks",
"speed-streaks",
) as ShaderRenderable;
expect(highStars.geometry.getAttribute("position").count).toBe(
@@ -213,11 +217,32 @@ describe("procedural world models", () => {
expect(lowStreaks.geometry.getAttribute("position").count).toBe(
lowQuality.streakCount * 2,
);
expect(high.group.getObjectByName("horizon-haze-near")).toBeInstanceOf(
THREE.Mesh,
expect(high.group.getObjectByName("horizon-haze")).toBeInstanceOf(
THREE.Group,
);
expect(high.group.getObjectByName("horizon-haze-far")).toBeInstanceOf(
THREE.Mesh,
const sky = high.group.getObjectByName("atmosphere-sky") as ShaderRenderable & {
frustumCulled: boolean;
};
sky.geometry.computeBoundingBox();
expect(sky.geometry.boundingBox?.min.x).toBeCloseTo(-1, 6);
expect(sky.geometry.boundingBox?.max.x).toBeCloseTo(1, 6);
expect(sky.material.vertexShader).toContain(
"gl_Position = vec4(position.xy, 1.0, 1.0)",
);
expect(sky.frustumCulled).toBe(false);
expect(sky.material.depthTest).toBe(false);
const haze = high.group.getObjectByName(
"horizon-haze-near",
) as ShaderRenderable;
expect(haze.material.vertexShader).toContain(
"gl_Position = vec4(position.xy, 1.0, 1.0)",
);
expect(
(high.group.getObjectByName("distant-silhouettes") as { children: unknown[] })
.children,
).toHaveLength(3);
expect(high.group.getObjectByName("roadside-anchors")).toBeInstanceOf(
THREE.InstancedMesh,
);
const highStarPositions = Array.from(
@@ -241,11 +266,40 @@ describe("procedural world models", () => {
expect(highStreaks.material.uniforms.uAcceleration?.value).toBe(0.8);
expect(highStreaks.material.uniforms.uVisibility?.value).toBeGreaterThan(0);
high.setViewportAspect(32 / 9);
expect(sky.material.uniforms.uAspect?.value).toBeCloseTo(32 / 9, 6);
highResources.dispose();
lowResources.dispose();
secondResources.dispose();
});
it("keeps independent inertial response curves for the road, chase rig, streaks, bloom, and suspension", () => {
const dynamics = createDriveDynamics();
const stableSnapshot = dynamics.snapshot();
dynamics.update(1, 1, 0.1);
const attack = { ...dynamics.snapshot() };
expect(dynamics.snapshot()).toBe(stableSnapshot);
expect(attack.roadEnergy).toBeGreaterThan(attack.cameraEnergy);
expect(attack.cameraEnergy).toBeGreaterThan(attack.bloomEnergy);
expect(attack.streakEnergy).toBeGreaterThan(0);
expect(attack.suspensionEnergy).toBeGreaterThan(0);
expect(attack.directionBias).toBeGreaterThan(0);
dynamics.update(0, 0, 0.1);
const coast = dynamics.snapshot();
expect(coast.roadEnergy).toBeLessThan(attack.roadEnergy);
expect(coast.streakEnergy / attack.streakEnergy).toBeGreaterThan(
coast.roadEnergy / attack.roadEnergy,
);
expect(coast.bloomEnergy).toBeGreaterThan(0);
dynamics.update(1, -1, 10);
expect(Object.values(dynamics.snapshot()).every(Number.isFinite)).toBe(true);
expect(dynamics.snapshot().directionBias).toBeLessThan(1);
});
it("disposes a composer and its passes exactly once", () => {
const resources = new SceneResourceBag();
const target = (width: number, height: number, depthBuffer: boolean) => ({
@@ -271,12 +325,19 @@ describe("procedural world models", () => {
dispose: vi.fn(),
};
const bloomPass = {
strength: 0.54,
radius: 0.42,
threshold: 0.88,
setSize: vi.fn(),
dispose: vi.fn(),
renderTargetBright: target(750, 500, false),
renderTargetsHorizontal: [target(375, 250, false)],
renderTargetsVertical: [target(375, 250, false)],
};
const outputPass = {
setSize: vi.fn(),
dispose: vi.fn(),
};
const processor = createPostProcessor(
resources,
{},
@@ -284,10 +345,12 @@ describe("procedural world models", () => {
createComposer: () => composer,
createRenderPass: () => renderPass,
createBloomPass: () => bloomPass,
createOutputPass: () => outputPass,
},
);
processor.resize(1_200, 800, 1.25);
processor.setDynamics(0.75);
processor.render({}, {});
const allocations = resources.gpuBudgetSnapshot({
tier: "high",
@@ -314,13 +377,16 @@ describe("procedural world models", () => {
processor.dispose();
resources.dispose();
expect(composer.addPass).toHaveBeenCalledTimes(2);
expect(composer.addPass).toHaveBeenCalledTimes(3);
expect(composer.addPass).toHaveBeenLastCalledWith(outputPass);
expect(composer.setPixelRatio).toHaveBeenCalledWith(1.25);
expect(composer.setSize).toHaveBeenCalledWith(1_200, 800);
expect(composer.render).toHaveBeenCalledOnce();
expect(renderPass.dispose).toHaveBeenCalledOnce();
expect(bloomPass.dispose).toHaveBeenCalledOnce();
expect(outputPass.dispose).toHaveBeenCalledOnce();
expect(composer.dispose).toHaveBeenCalledOnce();
expect(bloomPass.strength).toBeGreaterThan(0.54);
});
it("keeps the grand tourer anatomy readable in desktop and portrait chase views", () => {
@@ -336,12 +402,13 @@ describe("procedural world models", () => {
"rear-wheel-left",
"rear-wheel-right",
"car-underglow",
"car-contact-shadow",
]) {
expect(car.group.getObjectByName(name), name).toBeDefined();
}
const desktopCamera = createChaseCamera(16 / 9);
expect((desktopCamera as { fov: number }).fov).toBe(70);
expect((desktopCamera as { fov: number }).fov).toBe(66);
const desktop = projectedBounds(car.group, desktopCamera);
expect(desktop.maxX - desktop.minX).toBeGreaterThan(0.28);
expect(desktop.minX).toBeGreaterThan(-0.96);
@@ -362,6 +429,50 @@ describe("procedural world models", () => {
resources.dispose();
});
it("wraps the authored GLB hierarchy without rebuilding it during animation", () => {
const resources = new SceneResourceBag();
const authored = new THREE.Group();
authored.name = "chase-car";
for (const name of [
"car-body",
"rear-glass",
"rear-light-bar",
"rear-diffuser",
"car-underglow-source",
]) {
const material = name === "rear-light-bar"
? new THREE.MeshStandardMaterial({ emissive: 0xff174f, emissiveIntensity: 8 })
: new THREE.MeshStandardMaterial();
const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), material);
mesh.name = name;
authored.add(mesh);
}
const car = createCar(resources, authored);
const childCount = (car.group as { children: unknown[] }).children.length;
expect(
(car.group.getObjectByName("grand-tourer-model") as { rotation: { y: number } })
.rotation.y,
).toBeCloseTo(Math.PI, 6);
expect(car.group.getObjectByName("car-body")).toBeDefined();
expect(
(car.group.getObjectByName("car-body") as {
material: { emissive: { getHex(): number } };
}).material.emissive.getHex(),
).toBe(0x0b2738);
expect(car.group.getObjectByName("rear-glass")).toBeDefined();
expect(car.group.getObjectByName("rear-light-bar")).toBeDefined();
expect(car.group.getObjectByName("car-underglow")).toBeDefined();
expect(car.group.getObjectByName("car-contact-shadow")).toBeDefined();
for (let frame = 0; frame < 60; frame += 1) {
car.update(0.8, 0.7, frame / 60);
}
expect((car.group as { children: unknown[] }).children).toHaveLength(childCount);
expect(resources.snapshot().owned.geometry).toBeGreaterThanOrEqual(6);
resources.dispose();
});
it("damps squat and light response without allocating or changing the scene graph", () => {
const resources = new SceneResourceBag();
const car = createCar(resources);
@@ -485,6 +596,7 @@ interface ShaderRenderable {
uniforms: Record<string, { value: unknown }>;
vertexShader: string;
fragmentShader: string;
depthTest: boolean;
};
}