feat: layer the nightdrive sky and atmosphere
This commit is contained in:
@@ -160,8 +160,9 @@ describe('scene render quality', () => {
|
||||
roadSegments: 192,
|
||||
terrainSegments: 96,
|
||||
terrainOctaves: 3,
|
||||
particleCount: 384,
|
||||
starCount: 384,
|
||||
streakCount: 48,
|
||||
postScale: 1,
|
||||
postEffects: true,
|
||||
});
|
||||
expect(medium).toEqual({
|
||||
@@ -169,8 +170,9 @@ describe('scene render quality', () => {
|
||||
roadSegments: 144,
|
||||
terrainSegments: 64,
|
||||
terrainOctaves: 2,
|
||||
particleCount: 192,
|
||||
starCount: 192,
|
||||
streakCount: 28,
|
||||
postScale: 0.7,
|
||||
postEffects: true,
|
||||
});
|
||||
expect(low).toEqual({
|
||||
@@ -178,8 +180,9 @@ describe('scene render quality', () => {
|
||||
roadSegments: 96,
|
||||
terrainSegments: 32,
|
||||
terrainOctaves: 1,
|
||||
particleCount: 64,
|
||||
starCount: 64,
|
||||
streakCount: 12,
|
||||
postScale: 0,
|
||||
postEffects: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -100,7 +100,7 @@ describe('scroll drive controller', () => {
|
||||
expect(snapshot.driveSpeed).toBeCloseTo(0.9490152503018645, 14);
|
||||
});
|
||||
|
||||
it('resets transient velocity and establishes a baseline after a gap over 250 ms', () => {
|
||||
it('resets transient velocity and establishes a baseline after a stationary gap over 250 ms', () => {
|
||||
const controller = createScrollDriveController();
|
||||
controller.sample({ scrollY: 0, now: 0, maxScrollY: 1000 });
|
||||
controller.sample({ scrollY: 500, now: 100, maxScrollY: 1000 });
|
||||
@@ -115,6 +115,19 @@ describe('scroll drive controller', () => {
|
||||
expect(controller.sample({ scrollY: 600, now: 451, maxScrollY: 1000 }).direction).toBe(1);
|
||||
});
|
||||
|
||||
it('preserves signed movement while dropping acceleration on the first delayed sample', () => {
|
||||
const controller = createScrollDriveController();
|
||||
controller.sample({ scrollY: 0, now: 0, maxScrollY: 1000 });
|
||||
|
||||
expect(controller.sample({ scrollY: 500, now: 351, maxScrollY: 1000 })).toEqual({
|
||||
scrollY: 500,
|
||||
routeProgress: 0.5,
|
||||
direction: 1,
|
||||
acceleration: 0,
|
||||
driveSpeed: 0.18,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
['downward', 0, 1000, 1],
|
||||
['upward', 1000, 0, -1],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import * as THREE from 'three';
|
||||
import type { EnvironmentChapter } from '@/journey/types';
|
||||
import { renderQualityFor } from '@/scene/render-quality';
|
||||
@@ -10,6 +10,9 @@ import {
|
||||
} from '@/scene/world/palette';
|
||||
import { createRoad } from '@/scene/world/road';
|
||||
import { createTerrain } from '@/scene/world/terrain';
|
||||
import { createSun } from '@/scene/world/sun';
|
||||
import { createAtmosphere } from '@/scene/world/atmosphere';
|
||||
import { createPostProcessor } from '@/scene/post-processing';
|
||||
|
||||
describe('procedural world models', () => {
|
||||
it('scales geometry and atmosphere work monotonically by live quality tier', () => {
|
||||
@@ -132,6 +135,99 @@ describe('procedural world models', () => {
|
||||
expect(target.warmth).toBeGreaterThan(CHAPTER_PALETTES['deep-night'].warmth);
|
||||
expect(target.warmth).toBeLessThan(CHAPTER_PALETTES.destination.warmth);
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
expect(disc).toBeInstanceOf(THREE.Mesh);
|
||||
expect(halo).toBeInstanceOf(THREE.Sprite);
|
||||
expect(disc.material.uniforms).toMatchObject({
|
||||
uScanBands: { value: 14 },
|
||||
uWarmth: { value: CHAPTER_PALETTES['deep-night'].warmth },
|
||||
});
|
||||
expect(disc.material.fragmentShader).toContain('scanBand');
|
||||
expect(resources.snapshot().owned.texture).toBe(1);
|
||||
|
||||
const palette = createMutableChapterPalette('destination');
|
||||
sun.update(palette, 1);
|
||||
expect(disc.material.uniforms.uWarmth?.value).toBe(1);
|
||||
expect(disc.material.uniforms.uRouteProgress?.value).toBe(1);
|
||||
resources.dispose();
|
||||
});
|
||||
|
||||
it('builds deterministic stars, haze layers, and quality-capped speed streaks', () => {
|
||||
const highResources = new SceneResourceBag();
|
||||
const lowResources = new SceneResourceBag();
|
||||
const highQuality = renderQualityFor('high', 1, false);
|
||||
const lowQuality = renderQualityFor('low', 1, true);
|
||||
const high = createAtmosphere(highResources, highQuality);
|
||||
const low = createAtmosphere(lowResources, lowQuality);
|
||||
const highStars = high.group.getObjectByName('atmosphere-stars') as ShaderRenderable;
|
||||
const lowStars = low.group.getObjectByName('atmosphere-stars') as ShaderRenderable;
|
||||
const highStreaks = high.group.getObjectByName('atmosphere-streaks') as ShaderRenderable;
|
||||
const lowStreaks = low.group.getObjectByName('atmosphere-streaks') as ShaderRenderable;
|
||||
|
||||
expect(highStars.geometry.getAttribute('position').count).toBe(highQuality.starCount);
|
||||
expect(lowStars.geometry.getAttribute('position').count).toBe(lowQuality.starCount);
|
||||
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);
|
||||
expect(high.group.getObjectByName('horizon-haze-far')).toBeInstanceOf(THREE.Mesh);
|
||||
|
||||
const highStarPositions = Array.from(highStars.geometry.getAttribute('position').array);
|
||||
const secondResources = new SceneResourceBag();
|
||||
const secondHigh = createAtmosphere(secondResources, highQuality);
|
||||
const secondStars = secondHigh.group.getObjectByName('atmosphere-stars') as ShaderRenderable;
|
||||
expect(Array.from(secondStars.geometry.getAttribute('position').array)).toEqual(highStarPositions);
|
||||
|
||||
const palette = createMutableChapterPalette('builds');
|
||||
const before = highResources.snapshot();
|
||||
for (let index = 0; index < 300; index += 1) {
|
||||
high.update(index, 0.7, 0.8, palette);
|
||||
}
|
||||
expect(highResources.snapshot()).toEqual(before);
|
||||
expect(highStreaks.material.uniforms.uAcceleration?.value).toBe(0.8);
|
||||
expect(highStreaks.material.uniforms.uVisibility?.value).toBeGreaterThan(0);
|
||||
|
||||
highResources.dispose();
|
||||
lowResources.dispose();
|
||||
secondResources.dispose();
|
||||
});
|
||||
|
||||
it('disposes a composer and its passes exactly once', () => {
|
||||
const resources = new SceneResourceBag();
|
||||
const composer = {
|
||||
addPass: vi.fn(),
|
||||
setPixelRatio: vi.fn(),
|
||||
setSize: vi.fn(),
|
||||
render: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
};
|
||||
const renderPass = { scene: null, camera: null, setSize: vi.fn(), dispose: vi.fn() };
|
||||
const bloomPass = { setSize: vi.fn(), dispose: vi.fn() };
|
||||
const processor = createPostProcessor(resources, {}, {
|
||||
createComposer: () => composer,
|
||||
createRenderPass: () => renderPass,
|
||||
createBloomPass: () => bloomPass,
|
||||
});
|
||||
|
||||
processor.resize(1_200, 800, 1.25);
|
||||
processor.render({}, {});
|
||||
processor.dispose();
|
||||
processor.dispose();
|
||||
resources.dispose();
|
||||
|
||||
expect(composer.addPass).toHaveBeenCalledTimes(2);
|
||||
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(composer.dispose).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
function paletteSnapshot(palette: ReturnType<typeof createMutableChapterPalette>) {
|
||||
|
||||
Reference in New Issue
Block a user