feat: reconstruct the chase-view grand tourer
This commit is contained in:
@@ -13,6 +13,8 @@ import { createTerrain } from '@/scene/world/terrain';
|
||||
import { createSun } from '@/scene/world/sun';
|
||||
import { createAtmosphere } from '@/scene/world/atmosphere';
|
||||
import { createPostProcessor } from '@/scene/post-processing';
|
||||
import { createCar } from '@/scene/world/car';
|
||||
import { createChaseCamera } from '@/scene/world/environment';
|
||||
|
||||
describe('procedural world models', () => {
|
||||
it('scales geometry and atmosphere work monotonically by live quality tier', () => {
|
||||
@@ -172,6 +174,10 @@ describe('procedural world models', () => {
|
||||
|
||||
expect(highStars.geometry.getAttribute('position').count).toBe(highQuality.starCount);
|
||||
expect(lowStars.geometry.getAttribute('position').count).toBe(lowQuality.starCount);
|
||||
expect(highStars.material.uniforms).toMatchObject({
|
||||
uPointScale: { value: 18 },
|
||||
uMaxPointSize: { value: 4.5 },
|
||||
});
|
||||
expect(highStreaks.geometry.getAttribute('position').count).toBe(highQuality.streakCount * 2);
|
||||
expect(lowStreaks.geometry.getAttribute('position').count).toBe(lowQuality.streakCount * 2);
|
||||
expect(high.group.getObjectByName('horizon-haze-near')).toBeInstanceOf(THREE.Mesh);
|
||||
@@ -228,6 +234,71 @@ describe('procedural world models', () => {
|
||||
expect(bloomPass.dispose).toHaveBeenCalledOnce();
|
||||
expect(composer.dispose).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('keeps the grand tourer anatomy readable in desktop and portrait chase views', () => {
|
||||
const resources = new SceneResourceBag();
|
||||
const car = createCar(resources);
|
||||
for (const name of [
|
||||
'rear-silhouette',
|
||||
'rear-body-face',
|
||||
'rear-light-bar',
|
||||
'rear-glass',
|
||||
'rear-glass-panel',
|
||||
'rear-fascia',
|
||||
'rear-wheel-left',
|
||||
'rear-wheel-right',
|
||||
'car-underglow',
|
||||
]) {
|
||||
expect(car.group.getObjectByName(name), name).toBeDefined();
|
||||
}
|
||||
|
||||
const desktopCamera = createChaseCamera(16 / 9);
|
||||
expect((desktopCamera as { fov: number }).fov).toBe(70);
|
||||
const desktop = projectedBounds(car.group, desktopCamera);
|
||||
expect(desktop.maxX - desktop.minX).toBeGreaterThan(0.28);
|
||||
expect(desktop.minX).toBeGreaterThan(-0.96);
|
||||
expect(desktop.maxX).toBeLessThan(0.96);
|
||||
expect(desktop.minY).toBeGreaterThan(-0.99);
|
||||
expect(desktop.maxY).toBeLessThan(-0.05);
|
||||
|
||||
car.setDetail('reduced');
|
||||
const portrait = projectedBounds(car.group, createChaseCamera(390 / 844));
|
||||
expect(portrait.minX).toBeGreaterThan(-0.99);
|
||||
expect(portrait.maxX).toBeLessThan(0.99);
|
||||
expect(portrait.minY).toBeGreaterThan(-0.99);
|
||||
expect(portrait.maxY).toBeLessThan(0.16);
|
||||
expect((car.group.getObjectByName('secondary-detail') as { visible: boolean }).visible).toBe(false);
|
||||
resources.dispose();
|
||||
});
|
||||
|
||||
it('damps squat and light response without allocating or changing the scene graph', () => {
|
||||
const resources = new SceneResourceBag();
|
||||
const car = createCar(resources);
|
||||
const light = car.group.getObjectByName('rear-light-bar') as {
|
||||
material: { emissiveIntensity: number };
|
||||
};
|
||||
const underglow = car.group.getObjectByName('car-underglow') as {
|
||||
material: { uniforms: Record<string, { value: number }> };
|
||||
};
|
||||
car.update(0.3, 0, 0);
|
||||
const restingY = (car.group as { position: { y: number } }).position.y;
|
||||
const restingLight = light.material.emissiveIntensity;
|
||||
const restingGlow = underglow.material.uniforms.uOpacity?.value ?? 0;
|
||||
const childCount = (car.group as { children: unknown[] }).children.length;
|
||||
const allocation = resources.snapshot();
|
||||
|
||||
for (let frame = 1; frame <= 60; frame += 1) {
|
||||
car.update(0.92, 1, frame / 60);
|
||||
}
|
||||
|
||||
expect((car.group as { position: { y: number } }).position.y).toBeLessThan(restingY);
|
||||
expect(light.material.emissiveIntensity).toBeGreaterThan(restingLight);
|
||||
expect(underglow.material.uniforms.uOpacity?.value).toBeGreaterThan(restingGlow);
|
||||
expect(Math.abs((car.group as { rotation: { z: number } }).rotation.z)).toBeLessThanOrEqual(0.006);
|
||||
expect((car.group as { children: unknown[] }).children).toHaveLength(childCount);
|
||||
expect(resources.snapshot()).toEqual(allocation);
|
||||
resources.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
function paletteSnapshot(palette: ReturnType<typeof createMutableChapterPalette>) {
|
||||
@@ -258,3 +329,34 @@ interface ShaderRenderable {
|
||||
fragmentShader: string;
|
||||
};
|
||||
}
|
||||
|
||||
function projectedBounds(group: unknown, camera: unknown) {
|
||||
const typedGroup = group as { updateMatrixWorld(force: boolean): void };
|
||||
const typedCamera = camera as { updateMatrixWorld(force: boolean): void };
|
||||
typedGroup.updateMatrixWorld(true);
|
||||
typedCamera.updateMatrixWorld(true);
|
||||
const bounds = new THREE.Box3().setFromObject(group);
|
||||
const projected = boxCorners(bounds).map((point) => point.project(camera));
|
||||
return {
|
||||
minX: Math.min(...projected.map((point) => point.x)),
|
||||
maxX: Math.max(...projected.map((point) => point.x)),
|
||||
minY: Math.min(...projected.map((point) => point.y)),
|
||||
maxY: Math.max(...projected.map((point) => point.y)),
|
||||
};
|
||||
}
|
||||
|
||||
function boxCorners(bounds: {
|
||||
min: { x: number; y: number; z: number };
|
||||
max: { x: number; y: number; z: number };
|
||||
}) {
|
||||
return [
|
||||
new THREE.Vector3(bounds.min.x, bounds.min.y, bounds.min.z),
|
||||
new THREE.Vector3(bounds.min.x, bounds.min.y, bounds.max.z),
|
||||
new THREE.Vector3(bounds.min.x, bounds.max.y, bounds.min.z),
|
||||
new THREE.Vector3(bounds.min.x, bounds.max.y, bounds.max.z),
|
||||
new THREE.Vector3(bounds.max.x, bounds.min.y, bounds.min.z),
|
||||
new THREE.Vector3(bounds.max.x, bounds.min.y, bounds.max.z),
|
||||
new THREE.Vector3(bounds.max.x, bounds.max.y, bounds.min.z),
|
||||
new THREE.Vector3(bounds.max.x, bounds.max.y, bounds.max.z),
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user