feat(scene): add destination skyline, traffic, wet-road light and a saturated sun
This commit is contained in:
@@ -27,6 +27,8 @@ interface WorldGroup {
|
|||||||
|
|
||||||
const ROAD_WIDTH = 20.4;
|
const ROAD_WIDTH = 20.4;
|
||||||
const FLOW_PERIOD = 5_694;
|
const FLOW_PERIOD = 5_694;
|
||||||
|
// Matches the pylon rhythm in expressway.ts: eight pairs over the world depth.
|
||||||
|
const PYLON_SPACING = WORLD_DEPTH / 8;
|
||||||
|
|
||||||
export function createRoad(
|
export function createRoad(
|
||||||
resources: SceneResourceBag,
|
resources: SceneResourceBag,
|
||||||
@@ -45,6 +47,7 @@ export function createRoad(
|
|||||||
const initialPalette = CHAPTER_PALETTES['deep-night'];
|
const initialPalette = CHAPTER_PALETTES['deep-night'];
|
||||||
const uniforms = {
|
const uniforms = {
|
||||||
uDistance: { value: 0 },
|
uDistance: { value: 0 },
|
||||||
|
uPylonPhase: { value: 0 },
|
||||||
uSpeed: { value: 0 },
|
uSpeed: { value: 0 },
|
||||||
uAcceleration: { value: 0 },
|
uAcceleration: { value: 0 },
|
||||||
uAsphalt: { value: new THREE.Color(0x090511) },
|
uAsphalt: { value: new THREE.Color(0x090511) },
|
||||||
@@ -77,6 +80,7 @@ export function createRoad(
|
|||||||
`,
|
`,
|
||||||
fragmentShader: `
|
fragmentShader: `
|
||||||
uniform float uDistance;
|
uniform float uDistance;
|
||||||
|
uniform float uPylonPhase;
|
||||||
uniform float uSpeed;
|
uniform float uSpeed;
|
||||||
uniform float uAcceleration;
|
uniform float uAcceleration;
|
||||||
uniform vec3 uAsphalt;
|
uniform vec3 uAsphalt;
|
||||||
@@ -173,6 +177,23 @@ export function createRoad(
|
|||||||
float sunReflection = exp(-pow(vRoadX / (0.55 + vViewDistance * 0.011), 2.0))
|
float sunReflection = exp(-pow(vRoadX / (0.55 + vViewDistance * 0.011), 2.0))
|
||||||
* smoothstep(36.0, 180.0, vViewDistance);
|
* smoothstep(36.0, 180.0, vViewDistance);
|
||||||
color += vec3(0.8, 0.15, 0.08) * sunReflection * film * 0.17;
|
color += vec3(0.8, 0.15, 0.08) * sunReflection * film * 0.17;
|
||||||
|
// Wet glare: the sun lays a broken column down the asphalt.
|
||||||
|
float glare = exp(-pow(vRoadX / (1.1 + vViewDistance * 0.02), 2.0))
|
||||||
|
* smoothstep(50.0, 220.0, vViewDistance) * (1.0 - smoothstep(360.0, 560.0, vViewDistance));
|
||||||
|
color += vec3(1.0, 0.32, 0.22) * glare * pow(film, 2.0) * asphaltMask * 0.24;
|
||||||
|
|
||||||
|
// Headlight throw: two warm cones ahead of the car merging into a pool.
|
||||||
|
float ahead = -vWorldZ - 5.0;
|
||||||
|
float throwZ = smoothstep(-3.0, 5.0, ahead) * (1.0 - smoothstep(26.0, 92.0, ahead));
|
||||||
|
float spread = 0.9 + max(0.0, ahead) * 0.07;
|
||||||
|
float cones = exp(-pow((vRoadX - 1.25) / spread, 2.0)) + exp(-pow((vRoadX + 1.25) / spread, 2.0));
|
||||||
|
color += vec3(1.0, 0.84, 0.66) * min(cones, 1.4) * throwZ * (0.07 + 0.16 * film) * asphaltMask;
|
||||||
|
|
||||||
|
// Pylon lamps reflect as long streaks towards the camera.
|
||||||
|
float pylonGap = mod(vWorldZ + 24.0 - uPylonPhase, ${PYLON_SPACING.toFixed(3)});
|
||||||
|
float pylonStreak = exp(-pylonGap * 0.075) * smoothstep(0.0, 1.5, pylonGap)
|
||||||
|
* exp(-pow((roadX - 6.6) * 1.1, 2.0));
|
||||||
|
color += edgeColor * pylonStreak * film * 0.5 * (1.0 - smoothstep(300.0, 520.0, vViewDistance));
|
||||||
|
|
||||||
float horizonPickup = smoothstep(280.0, 620.0, vViewDistance)
|
float horizonPickup = smoothstep(280.0, 620.0, vViewDistance)
|
||||||
* (1.0 - smoothstep(610.0, ${HORIZON_FOG_END.toFixed(1)}, vViewDistance))
|
* (1.0 - smoothstep(610.0, ${HORIZON_FOG_END.toFixed(1)}, vViewDistance))
|
||||||
@@ -203,6 +224,8 @@ export function createRoad(
|
|||||||
update(distance, speed, acceleration, palette) {
|
update(distance, speed, acceleration, palette) {
|
||||||
uniforms.uDistance.value =
|
uniforms.uDistance.value =
|
||||||
((finiteOrZero(distance) % FLOW_PERIOD) + FLOW_PERIOD) % FLOW_PERIOD;
|
((finiteOrZero(distance) % FLOW_PERIOD) + FLOW_PERIOD) % FLOW_PERIOD;
|
||||||
|
uniforms.uPylonPhase.value =
|
||||||
|
((finiteOrZero(distance) % PYLON_SPACING) + PYLON_SPACING) % PYLON_SPACING;
|
||||||
uniforms.uSpeed.value = clamp01(speed);
|
uniforms.uSpeed.value = clamp01(speed);
|
||||||
uniforms.uAcceleration.value = clamp01(acceleration);
|
uniforms.uAcceleration.value = clamp01(acceleration);
|
||||||
if (palette) {
|
if (palette) {
|
||||||
|
|||||||
137
src/scene/world/skyline.ts
Normal file
137
src/scene/world/skyline.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
import type { SceneResourceBag } from '@/scene/resources';
|
||||||
|
import type { MutableChapterPalette } from '@/scene/world/palette';
|
||||||
|
import { SUN_DISTANCE_Z } from '@/scene/world/metrics';
|
||||||
|
|
||||||
|
const WIDTH = 1_640;
|
||||||
|
const HEIGHT = 150;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The city at the end of the road: one procedural plane between the far ridge
|
||||||
|
* and the sun. Two layers of towers in silhouette against the sun and the
|
||||||
|
* horizon glow, sparse lit windows, sun-caught roof edges and blinking masts.
|
||||||
|
* One draw call, no texture.
|
||||||
|
*/
|
||||||
|
export function createSkyline(resources: SceneResourceBag) {
|
||||||
|
const group = new THREE.Group();
|
||||||
|
group.name = 'destination-skyline';
|
||||||
|
const uniforms = {
|
||||||
|
uTime: { value: 0 },
|
||||||
|
uSun: { value: new THREE.Color(0xff649d) },
|
||||||
|
uFog: { value: new THREE.Color(0x291236) },
|
||||||
|
uHaze: { value: new THREE.Color(0xaf287d) },
|
||||||
|
uWarmth: { value: 0 },
|
||||||
|
uLights: { value: 1 },
|
||||||
|
};
|
||||||
|
const material = resources.own(
|
||||||
|
'material',
|
||||||
|
new THREE.ShaderMaterial({
|
||||||
|
uniforms,
|
||||||
|
vertexShader: /* glsl */ `
|
||||||
|
varying vec2 vUv;
|
||||||
|
void main() {
|
||||||
|
vUv = uv;
|
||||||
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||||
|
}`,
|
||||||
|
fragmentShader: /* glsl */ `
|
||||||
|
uniform float uTime;
|
||||||
|
uniform vec3 uSun;
|
||||||
|
uniform vec3 uFog;
|
||||||
|
uniform vec3 uHaze;
|
||||||
|
uniform float uWarmth;
|
||||||
|
uniform float uLights;
|
||||||
|
varying vec2 vUv;
|
||||||
|
|
||||||
|
float hash(float n) { return fract(sin(n * 127.1) * 43758.5453); }
|
||||||
|
float hash2(vec2 p) { return fract(sin(dot(p, vec2(41.3, 289.1))) * 15731.743); }
|
||||||
|
|
||||||
|
// Height of a tower row at world x, with towers of jittered width.
|
||||||
|
vec4 towers(float x, float cell, float seed, float envelope, float tall) {
|
||||||
|
float id = floor(x / cell);
|
||||||
|
float local = fract(x / cell);
|
||||||
|
float r = hash(id + seed);
|
||||||
|
float r2 = hash(id * 1.7 + seed + 3.1);
|
||||||
|
float inset = 0.06 + r2 * 0.18;
|
||||||
|
float inside = step(inset, local) * step(local, 1.0 - inset * 0.5);
|
||||||
|
float h = (0.1 + pow(r, 2.4) * tall) * envelope;
|
||||||
|
// Stepped crowns on the tall ones.
|
||||||
|
float crown = step(0.62, r) * step(0.32, abs(local - 0.5)) * 0.06 * envelope;
|
||||||
|
return vec4(h - crown, inside, id, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float x = (vUv.x - 0.5) * ${WIDTH.toFixed(1)};
|
||||||
|
float y = vUv.y;
|
||||||
|
float xn = vUv.x - 0.5;
|
||||||
|
float core = exp(-pow(xn / 0.2, 2.0));
|
||||||
|
float outskirts = 0.28 + 0.72 * core + 0.2 * exp(-pow((xn + 0.27) / 0.06, 2.0))
|
||||||
|
+ 0.16 * exp(-pow((xn - 0.31) / 0.05, 2.0));
|
||||||
|
float fade = smoothstep(0.5, 0.36, abs(xn));
|
||||||
|
|
||||||
|
vec4 far = towers(x + 3.0, 15.0, 11.0, outskirts * fade, 0.74);
|
||||||
|
vec4 near = towers(x, 10.0, 47.0, (0.42 + 0.58 * core) * fade, 0.5);
|
||||||
|
|
||||||
|
float inNear = near.y * step(y, near.x);
|
||||||
|
float inFar = far.y * step(y, far.x);
|
||||||
|
float mast = step(0.9, far.w) * step(abs(fract((x + 3.0) / 15.0) - 0.5), 0.022)
|
||||||
|
* step(far.x, y) * step(y, far.x + 0.1 * outskirts);
|
||||||
|
if (inNear + inFar + mast < 0.5) discard;
|
||||||
|
|
||||||
|
bool isNear = inNear > 0.5;
|
||||||
|
vec4 t = isNear ? near : far;
|
||||||
|
float top = t.x;
|
||||||
|
// Silhouette: near is darkest, far takes more of the horizon air.
|
||||||
|
vec3 base = mix(vec3(0.012, 0.008, 0.03), uFog * 0.55, isNear ? 0.18 : 0.42);
|
||||||
|
base += uHaze * (isNear ? 0.02 : 0.05) * (1.0 - y);
|
||||||
|
vec3 color = base;
|
||||||
|
|
||||||
|
// Windows: a fixed grid in world units, sparse and warm or cool.
|
||||||
|
vec2 grid = vec2(x / 1.6, y * ${HEIGHT.toFixed(1)} / 2.1);
|
||||||
|
vec2 win = fract(grid);
|
||||||
|
vec2 wid = floor(grid);
|
||||||
|
float pane = step(0.28, win.x) * step(win.x, 0.78) * step(0.3, win.y) * step(win.y, 0.74);
|
||||||
|
float lit = step(isNear ? 0.86 : 0.9, hash2(wid + t.z * 3.7));
|
||||||
|
float flicker = 0.75 + 0.25 * step(0.5, fract(sin(dot(wid, vec2(7.1, 3.3))) * 91.7 + uTime * 0.07));
|
||||||
|
vec3 warm = mix(vec3(1.0, 0.62, 0.3), vec3(0.35, 0.9, 1.0), step(0.72, hash2(wid * 1.31)));
|
||||||
|
float belowRoof = step(y, top - 0.02);
|
||||||
|
color += warm * pane * lit * flicker * belowRoof * (isNear ? 1.1 : 0.7) * uLights;
|
||||||
|
|
||||||
|
// Roof edges catch the sun behind them.
|
||||||
|
float edge = smoothstep(top - 0.018, top, y) * step(y, top);
|
||||||
|
color += uSun * edge * (isNear ? 0.55 : 1.15) * (0.7 + 0.3 * core);
|
||||||
|
|
||||||
|
// Aircraft warning lights on masts and the tallest roofs.
|
||||||
|
float blink = step(0.82, fract(uTime * 0.55 + t.z * 0.13));
|
||||||
|
float beacon = mast * step(far.x + 0.09 * outskirts, y) * 4.0 * blink;
|
||||||
|
color = mix(color, vec3(1.0, 0.1, 0.12) * 3.0, beacon);
|
||||||
|
color = mix(color, base * 1.4, mast * (1.0 - beacon));
|
||||||
|
|
||||||
|
gl_FragColor = vec4(color, 1.0);
|
||||||
|
#include <tonemapping_fragment>
|
||||||
|
#include <colorspace_fragment>
|
||||||
|
}`,
|
||||||
|
depthWrite: true,
|
||||||
|
depthTest: true,
|
||||||
|
fog: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const geometry = resources.own('geometry', new THREE.PlaneGeometry(WIDTH, HEIGHT));
|
||||||
|
const plane = new THREE.Mesh(geometry, material);
|
||||||
|
plane.name = 'skyline-silhouette';
|
||||||
|
plane.position.set(0, HEIGHT / 2 - 8, SUN_DISTANCE_Z + 18);
|
||||||
|
plane.renderOrder = -1;
|
||||||
|
group.add(plane);
|
||||||
|
|
||||||
|
return {
|
||||||
|
group,
|
||||||
|
update(palette: Readonly<MutableChapterPalette>, time: number) {
|
||||||
|
uniforms.uTime.value = time;
|
||||||
|
uniforms.uSun.value.copy(palette.sun);
|
||||||
|
uniforms.uFog.value.copy(palette.fog);
|
||||||
|
uniforms.uHaze.value.copy(palette.haze);
|
||||||
|
uniforms.uWarmth.value = palette.warmth;
|
||||||
|
// Lit windows thin out towards dawn.
|
||||||
|
uniforms.uLights.value = 1 - palette.warmth * 0.55;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -80,9 +80,9 @@ export function createSun(resources: SceneResourceBag): SunWorld {
|
|||||||
float lip = exp(-abs(phase - gap - 0.025) * 90.0) * step(solar.y, 0.65);
|
float lip = exp(-abs(phase - gap - 0.025) * 90.0) * step(solar.y, 0.65);
|
||||||
float convection = sin(solar.x * 19.0 + sin(solar.y * 23.0 + uTime * 0.055))
|
float convection = sin(solar.x * 19.0 + sin(solar.y * 23.0 + uTime * 0.055))
|
||||||
* sin(solar.y * 31.0 - uTime * 0.032);
|
* sin(solar.y * 31.0 - uTime * 0.032);
|
||||||
vec3 vermilion = mix(vec3(0.85, 0.018, 0.28), vec3(1.0, 0.24, 0.12), uArrival);
|
vec3 vermilion = mix(vec3(1.0, 0.03, 0.4), vec3(1.0, 0.24, 0.12), uArrival);
|
||||||
vec3 coral = mix(uColor, vec3(1.0, 0.16, 0.13), 0.48);
|
vec3 coral = mix(uColor, vec3(1.0, 0.3, 0.06), 0.6);
|
||||||
vec3 crown = mix(vec3(1.0, 0.63, 0.24), vec3(1.0, 0.88, 0.64), uArrival);
|
vec3 crown = mix(vec3(1.0, 0.8, 0.12), vec3(1.0, 0.9, 0.6), uArrival);
|
||||||
vec3 core = mix(vermilion, coral, smoothstep(0.0, 0.58, solar.y));
|
vec3 core = mix(vermilion, coral, smoothstep(0.0, 0.58, solar.y));
|
||||||
core = mix(core, crown, smoothstep(0.47, 1.0, solar.y));
|
core = mix(core, crown, smoothstep(0.47, 1.0, solar.y));
|
||||||
float filament = pow(abs(sin(solar.x * 39.0 + solar.y * 25.0 + convection * 2.4)), 16.0);
|
float filament = pow(abs(sin(solar.x * 39.0 + solar.y * 25.0 + convection * 2.4)), 16.0);
|
||||||
@@ -91,7 +91,8 @@ export function createSun(resources: SceneResourceBag): SunWorld {
|
|||||||
core += vec3(1.0, 0.24, 0.095) * (limb * 0.22 + lip * 0.20);
|
core += vec3(1.0, 0.24, 0.095) * (limb * 0.22 + lip * 0.20);
|
||||||
float extinction = smoothstep(0.025, 0.19, solar.y);
|
float extinction = smoothstep(0.025, 0.19, solar.y);
|
||||||
if(softEdge * bandMask * extinction < 0.02) discard;
|
if(softEdge * bandMask * extinction < 0.02) discard;
|
||||||
gl_FragColor = vec4(core * (1.0 + uWarmth * 0.10), softEdge * bandMask * extinction);
|
// Kept below ACES's shoulder so the disc stays saturated instead of cream.
|
||||||
|
gl_FragColor = vec4(core * (0.74 + uWarmth * 0.08), softEdge * bandMask * extinction);
|
||||||
#include <tonemapping_fragment>
|
#include <tonemapping_fragment>
|
||||||
#include <colorspace_fragment>
|
#include <colorspace_fragment>
|
||||||
}
|
}
|
||||||
|
|||||||
171
src/scene/world/traffic.ts
Normal file
171
src/scene/world/traffic.ts
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
import type { SceneResourceBag } from '@/scene/resources';
|
||||||
|
import type { RenderQualitySettings } from '@/scene/render-quality';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Other cars on the expressway, seen only as their lamps: taillight pairs,
|
||||||
|
* long-exposure trails whose length follows the speed difference, and red
|
||||||
|
* pools on the wet asphalt. Scrolling faster than the traffic overtakes it;
|
||||||
|
* resting lets the faster lanes pull away. Instances wrap through a hidden band
|
||||||
|
* behind the camera, so nothing pops in view.
|
||||||
|
*/
|
||||||
|
const NEAR_Z = 46;
|
||||||
|
const FAR_Z = -660;
|
||||||
|
const SPAN = NEAR_Z - FAR_Z;
|
||||||
|
|
||||||
|
interface Vehicle {
|
||||||
|
x: number;
|
||||||
|
z: number;
|
||||||
|
speed: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
tint: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTraffic(
|
||||||
|
resources: SceneResourceBag,
|
||||||
|
quality: RenderQualitySettings,
|
||||||
|
) {
|
||||||
|
const group = new THREE.Group();
|
||||||
|
group.name = 'expressway-traffic';
|
||||||
|
const count = quality.postEffects ? (quality.postScale >= 1 ? 14 : 10) : 7;
|
||||||
|
const lanes = [-5.1, -5.1, -2.6, 4.9, 5.2];
|
||||||
|
const vehicles: Vehicle[] = [];
|
||||||
|
let seed = 7;
|
||||||
|
const random = () => {
|
||||||
|
seed = (seed * 16807) % 2147483647;
|
||||||
|
return (seed - 1) / 2147483646;
|
||||||
|
};
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const lane = lanes[i % lanes.length]!;
|
||||||
|
// Left lanes run faster than the cruise, the right lane is slower trucks.
|
||||||
|
const fast = lane < 0;
|
||||||
|
vehicles.push({
|
||||||
|
x: lane + (random() - 0.5) * 0.5,
|
||||||
|
z: FAR_Z + random() * (SPAN - 120) + 20,
|
||||||
|
speed: fast ? 26 + random() * 22 : 9 + random() * 6,
|
||||||
|
width: fast ? 0.78 + random() * 0.12 : 1.0,
|
||||||
|
height: fast ? 0.95 : 1.55 + random() * 0.4,
|
||||||
|
tint: random(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniforms = { uTime: { value: 0 } };
|
||||||
|
const vertexShader = /* glsl */ `
|
||||||
|
attribute float aLevel;
|
||||||
|
varying vec2 vUv;
|
||||||
|
varying float vDepth;
|
||||||
|
varying float vLevel;
|
||||||
|
void main() {
|
||||||
|
vUv = uv;
|
||||||
|
vLevel = aLevel;
|
||||||
|
vec4 view = modelViewMatrix * instanceMatrix * vec4(position, 1.0);
|
||||||
|
vDepth = -view.z;
|
||||||
|
gl_Position = projectionMatrix * view;
|
||||||
|
}`;
|
||||||
|
const fragment = (shape: string) => /* glsl */ `
|
||||||
|
varying vec2 vUv;
|
||||||
|
varying float vDepth;
|
||||||
|
varying float vLevel;
|
||||||
|
void main() {
|
||||||
|
float far = 1.0 - smoothstep(420.0, 660.0, vDepth);
|
||||||
|
float near = smoothstep(1.5, 7.0, vDepth);
|
||||||
|
${shape}
|
||||||
|
vec3 color = mix(vec3(1.0, 0.03, 0.06), vec3(1.0, 0.22, 0.05), vLevel);
|
||||||
|
gl_FragColor = vec4(color * glow * far * near, 1.0);
|
||||||
|
#include <tonemapping_fragment>
|
||||||
|
#include <colorspace_fragment>
|
||||||
|
}`;
|
||||||
|
const additive = (shape: string) =>
|
||||||
|
resources.own(
|
||||||
|
'material',
|
||||||
|
new THREE.ShaderMaterial({
|
||||||
|
uniforms,
|
||||||
|
vertexShader,
|
||||||
|
fragmentShader: fragment(shape),
|
||||||
|
transparent: true,
|
||||||
|
depthWrite: false,
|
||||||
|
depthTest: true,
|
||||||
|
blending: THREE.AdditiveBlending,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// Lamps: a hot core in a soft disc, bright enough to bloom.
|
||||||
|
const lampMaterial = additive(`
|
||||||
|
vec2 c = vUv - 0.5;
|
||||||
|
float r = length(c * vec2(1.0, 1.6));
|
||||||
|
float glow = exp(-r * r * 26.0) * 3.2 + exp(-r * r * 5.0) * 0.5;`);
|
||||||
|
// Trails: thin, brightest at the lamp, fading over their length.
|
||||||
|
const trailMaterial = additive(`
|
||||||
|
float across = exp(-pow((vUv.x - 0.5) * 5.0, 2.0));
|
||||||
|
float glow = across * pow(vUv.y, 1.6) * 1.1;`);
|
||||||
|
// Wet-road pools: long and soft under each car.
|
||||||
|
const poolMaterial = additive(`
|
||||||
|
float across = exp(-pow((vUv.x - 0.5) * 3.2, 2.0));
|
||||||
|
float glow = across * pow(vUv.y, 2.2) * 0.2;`);
|
||||||
|
|
||||||
|
const plane = resources.own('geometry', new THREE.PlaneGeometry(1, 1));
|
||||||
|
const lampCount = count * 2;
|
||||||
|
const mesh = (name: string, material: unknown, instances: number) => {
|
||||||
|
const instanced = new THREE.InstancedMesh(plane, material, instances);
|
||||||
|
instanced.name = name;
|
||||||
|
instanced.frustumCulled = false;
|
||||||
|
instanced.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
|
||||||
|
const levels = new Float32Array(instances);
|
||||||
|
for (let i = 0; i < instances; i++) levels[i] = vehicles[Math.floor(i / 2) % count]!.tint * 0.6;
|
||||||
|
instanced.geometry = plane.clone();
|
||||||
|
resources.own('geometry', instanced.geometry);
|
||||||
|
instanced.geometry.setAttribute('aLevel', new THREE.InstancedBufferAttribute(levels, 1));
|
||||||
|
group.add(instanced);
|
||||||
|
return instanced;
|
||||||
|
};
|
||||||
|
const lamps = mesh('traffic-taillights', lampMaterial, lampCount);
|
||||||
|
const trails = mesh('traffic-light-trails', trailMaterial, lampCount);
|
||||||
|
const pools = mesh('traffic-road-pools', poolMaterial, count);
|
||||||
|
|
||||||
|
const dummy = new THREE.Object3D();
|
||||||
|
return {
|
||||||
|
group,
|
||||||
|
/**
|
||||||
|
* @param travelSpeed world units per second the camera car is covering.
|
||||||
|
*/
|
||||||
|
update(deltaSeconds: number, travelSpeed: number, time: number) {
|
||||||
|
uniforms.uTime.value = time;
|
||||||
|
const dt = Math.min(0.1, Math.max(0, deltaSeconds));
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const v = vehicles[i]!;
|
||||||
|
const relative = travelSpeed - v.speed; // > 0: we are overtaking
|
||||||
|
v.z += relative * dt;
|
||||||
|
if (v.z > NEAR_Z) v.z -= SPAN;
|
||||||
|
else if (v.z < FAR_Z) v.z += SPAN;
|
||||||
|
const trailLength = THREE.MathUtils.clamp(Math.abs(relative) * 0.16, 0.3, 14);
|
||||||
|
// Trails stream away from the direction of relative travel.
|
||||||
|
const trailDirection = relative >= 0 ? -1 : 1;
|
||||||
|
for (let side = 0; side < 2; side++) {
|
||||||
|
const index = i * 2 + side;
|
||||||
|
const x = v.x + (side === 0 ? -v.width : v.width);
|
||||||
|
dummy.position.set(x, v.height, v.z);
|
||||||
|
dummy.rotation.set(0, 0, 0);
|
||||||
|
dummy.scale.set(0.62, 0.36, 1);
|
||||||
|
dummy.updateMatrix();
|
||||||
|
lamps.setMatrixAt(index, dummy.matrix);
|
||||||
|
// Horizontal ribbon at lamp height, uv.y = 1 at the lamp.
|
||||||
|
dummy.position.set(x, v.height - 0.02, v.z + (trailDirection * trailLength) / 2);
|
||||||
|
dummy.rotation.set(trailDirection < 0 ? Math.PI / 2 : -Math.PI / 2, 0, 0);
|
||||||
|
dummy.scale.set(0.2, trailLength, 1);
|
||||||
|
dummy.updateMatrix();
|
||||||
|
trails.setMatrixAt(index, dummy.matrix);
|
||||||
|
}
|
||||||
|
const poolLength = 9 + Math.min(10, Math.abs(relative) * 0.08);
|
||||||
|
dummy.position.set(v.x, 0.04, v.z + poolLength / 2);
|
||||||
|
dummy.rotation.set(-Math.PI / 2, 0, 0);
|
||||||
|
dummy.scale.set(v.width * 2.3, poolLength, 1);
|
||||||
|
dummy.updateMatrix();
|
||||||
|
pools.setMatrixAt(i, dummy.matrix);
|
||||||
|
}
|
||||||
|
lamps.instanceMatrix.needsUpdate = true;
|
||||||
|
trails.instanceMatrix.needsUpdate = true;
|
||||||
|
pools.instanceMatrix.needsUpdate = true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user