feat: layer the nightdrive sky and atmosphere
This commit is contained in:
273
src/scene/world/atmosphere.ts
Normal file
273
src/scene/world/atmosphere.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
import * as THREE from 'three';
|
||||
import type { RenderQualitySettings } from '@/scene/render-quality';
|
||||
import type { SceneResourceBag } from '@/scene/resources';
|
||||
import type { MutableChapterPalette } from '@/scene/world/palette';
|
||||
import { CHAPTER_PALETTES } from '@/scene/world/palette';
|
||||
|
||||
export interface AtmosphereWorld {
|
||||
group: WorldGroup;
|
||||
update(
|
||||
distance: number,
|
||||
speed: number,
|
||||
acceleration: number,
|
||||
palette: Readonly<MutableChapterPalette>,
|
||||
): void;
|
||||
}
|
||||
|
||||
interface WorldGroup {
|
||||
add(...objects: unknown[]): void;
|
||||
getObjectByName(name: string): unknown;
|
||||
}
|
||||
|
||||
export function createAtmosphere(
|
||||
resources: SceneResourceBag,
|
||||
quality: RenderQualitySettings,
|
||||
): AtmosphereWorld {
|
||||
const group = new THREE.Group();
|
||||
group.name = 'atmosphere-world';
|
||||
const night = CHAPTER_PALETTES['deep-night'];
|
||||
|
||||
const skyGeometry = resources.own('geometry', new THREE.PlaneGeometry(520, 280));
|
||||
const skyMaterial = resources.own('material', new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
uSkyColor: { value: new THREE.Color(night.sky) },
|
||||
uHazeColor: { value: new THREE.Color(night.haze) },
|
||||
uWarmth: { value: night.warmth },
|
||||
},
|
||||
vertexShader: `
|
||||
varying vec2 vSkyUv;
|
||||
void main() {
|
||||
vSkyUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
uniform vec3 uSkyColor;
|
||||
uniform vec3 uHazeColor;
|
||||
uniform float uWarmth;
|
||||
varying vec2 vSkyUv;
|
||||
void main() {
|
||||
float horizon = 1.0 - smoothstep(0.08, 0.72, vSkyUv.y);
|
||||
float upperNight = smoothstep(0.35, 1.0, vSkyUv.y);
|
||||
vec3 color = mix(uSkyColor, uHazeColor, horizon * (0.38 + uWarmth * 0.2));
|
||||
color *= mix(1.04, 0.72, upperNight);
|
||||
gl_FragColor = vec4(color, 1.0);
|
||||
}
|
||||
`,
|
||||
depthWrite: false,
|
||||
depthTest: false,
|
||||
fog: false,
|
||||
toneMapped: true,
|
||||
}));
|
||||
const sky = new THREE.Mesh(skyGeometry, skyMaterial);
|
||||
sky.name = 'atmosphere-sky';
|
||||
sky.position.set(0, 69, -300);
|
||||
sky.renderOrder = -100;
|
||||
group.add(sky);
|
||||
|
||||
const starGeometry = resources.own('geometry', createStarGeometry(quality.starCount));
|
||||
const starMaterial = resources.own('material', new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
uVisibility: { value: night.starVisibility },
|
||||
uDistance: { value: 0 },
|
||||
uColor: { value: new THREE.Color(0xb9ecf2) },
|
||||
},
|
||||
vertexShader: `
|
||||
attribute float starSize;
|
||||
attribute float brightness;
|
||||
uniform float uDistance;
|
||||
varying float vBrightness;
|
||||
void main() {
|
||||
vec3 star = position;
|
||||
star.x += sin(position.z * 0.017 + uDistance * 0.004) * 0.08;
|
||||
vec4 viewPosition = modelViewMatrix * vec4(star, 1.0);
|
||||
vBrightness = brightness;
|
||||
gl_PointSize = starSize * (250.0 / max(1.0, -viewPosition.z));
|
||||
gl_Position = projectionMatrix * viewPosition;
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
uniform vec3 uColor;
|
||||
uniform float uVisibility;
|
||||
varying float vBrightness;
|
||||
void main() {
|
||||
float radius = distance(gl_PointCoord, vec2(0.5));
|
||||
float point = 1.0 - smoothstep(0.2, 0.5, radius);
|
||||
gl_FragColor = vec4(uColor, point * vBrightness * uVisibility);
|
||||
}
|
||||
`,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
depthTest: true,
|
||||
blending: THREE.AdditiveBlending,
|
||||
toneMapped: true,
|
||||
}));
|
||||
const stars = new THREE.Points(starGeometry, starMaterial);
|
||||
stars.name = 'atmosphere-stars';
|
||||
stars.frustumCulled = false;
|
||||
group.add(stars);
|
||||
|
||||
const hazeGeometry = resources.own('geometry', new THREE.PlaneGeometry(260, 58));
|
||||
const nearHazeMaterial = resources.own('material', createHazeMaterial(night.haze, 0.2));
|
||||
const farHazeMaterial = resources.own('material', createHazeMaterial(night.haze, 0.12));
|
||||
const farHaze = new THREE.Mesh(hazeGeometry, farHazeMaterial);
|
||||
farHaze.name = 'horizon-haze-far';
|
||||
farHaze.position.set(0, 10, -214);
|
||||
farHaze.scale.set(1.35, 1.1, 1);
|
||||
group.add(farHaze);
|
||||
const nearHaze = new THREE.Mesh(hazeGeometry, nearHazeMaterial);
|
||||
nearHaze.name = 'horizon-haze-near';
|
||||
nearHaze.position.set(0, 8, -132);
|
||||
group.add(nearHaze);
|
||||
|
||||
const streakGeometry = resources.own('geometry', createStreakGeometry(quality.streakCount));
|
||||
const streakMaterial = resources.own('material', new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
uDistance: { value: 0 },
|
||||
uAcceleration: { value: 0 },
|
||||
uVisibility: { value: 0 },
|
||||
uColor: { value: new THREE.Color(0x79dae5) },
|
||||
},
|
||||
vertexShader: `
|
||||
attribute float endFactor;
|
||||
uniform float uDistance;
|
||||
uniform float uAcceleration;
|
||||
varying float vTail;
|
||||
void main() {
|
||||
vec3 streak = position;
|
||||
float depthCycle = mod(streak.z + uDistance * 1.4 + 280.0, 286.0) - 280.0;
|
||||
streak.z = depthCycle + endFactor * (1.2 + uAcceleration * 18.0);
|
||||
vTail = endFactor;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(streak, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
uniform vec3 uColor;
|
||||
uniform float uVisibility;
|
||||
varying float vTail;
|
||||
void main() {
|
||||
gl_FragColor = vec4(uColor, uVisibility * mix(0.15, 0.72, vTail));
|
||||
}
|
||||
`,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
depthTest: true,
|
||||
blending: THREE.AdditiveBlending,
|
||||
toneMapped: true,
|
||||
}));
|
||||
const streaks = new THREE.LineSegments(streakGeometry, streakMaterial);
|
||||
streaks.name = 'atmosphere-streaks';
|
||||
streaks.frustumCulled = false;
|
||||
group.add(streaks);
|
||||
|
||||
return {
|
||||
group,
|
||||
update(distance, speed, acceleration, palette) {
|
||||
const safeAcceleration = clamp01(acceleration);
|
||||
const visibility = smoothstep(0.18, 0.82, safeAcceleration) * palette.streakEnergy;
|
||||
starMaterial.uniforms.uDistance.value = finiteOrZero(distance);
|
||||
starMaterial.uniforms.uVisibility.value = palette.starVisibility;
|
||||
streakMaterial.uniforms.uDistance.value = finiteOrZero(distance) * (0.72 + clamp01(speed) * 0.28);
|
||||
streakMaterial.uniforms.uAcceleration.value = safeAcceleration;
|
||||
streakMaterial.uniforms.uVisibility.value = visibility;
|
||||
skyMaterial.uniforms.uSkyColor.value.copy(palette.sky);
|
||||
skyMaterial.uniforms.uHazeColor.value.copy(palette.haze);
|
||||
skyMaterial.uniforms.uWarmth.value = palette.warmth;
|
||||
nearHazeMaterial.uniforms.uColor.value.copy(palette.haze);
|
||||
farHazeMaterial.uniforms.uColor.value.copy(palette.haze);
|
||||
nearHazeMaterial.uniforms.uEnergy.value = 0.16 + palette.warmth * 0.08;
|
||||
farHazeMaterial.uniforms.uEnergy.value = 0.09 + palette.warmth * 0.06;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createStarGeometry(count: number) {
|
||||
const positions = new Float32Array(count * 3);
|
||||
const sizes = new Float32Array(count);
|
||||
const brightness = new Float32Array(count);
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const offset = index * 3;
|
||||
positions[offset] = seeded(index, 11) * 210 - 105;
|
||||
positions[offset + 1] = 8 + seeded(index, 23) * 72;
|
||||
positions[offset + 2] = -18 - seeded(index, 37) * 270;
|
||||
sizes[index] = 2.4 + seeded(index, 53) * 4.8;
|
||||
brightness[index] = 0.28 + seeded(index, 71) * 0.72;
|
||||
}
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||
geometry.setAttribute('starSize', new THREE.BufferAttribute(sizes, 1));
|
||||
geometry.setAttribute('brightness', new THREE.BufferAttribute(brightness, 1));
|
||||
return geometry;
|
||||
}
|
||||
|
||||
function createStreakGeometry(count: number) {
|
||||
const positions = new Float32Array(count * 2 * 3);
|
||||
const ends = new Float32Array(count * 2);
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const x = (seeded(index, 101) > 0.5 ? 1 : -1) * (12 + seeded(index, 103) * 52);
|
||||
const y = 0.8 + seeded(index, 107) * 12;
|
||||
const z = -8 - seeded(index, 109) * 272;
|
||||
const offset = index * 6;
|
||||
positions[offset] = x;
|
||||
positions[offset + 1] = y;
|
||||
positions[offset + 2] = z;
|
||||
positions[offset + 3] = x;
|
||||
positions[offset + 4] = y + seeded(index, 113) * 0.8;
|
||||
positions[offset + 5] = z;
|
||||
ends[index * 2] = 0;
|
||||
ends[index * 2 + 1] = 1;
|
||||
}
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||
geometry.setAttribute('endFactor', new THREE.BufferAttribute(ends, 1));
|
||||
return geometry;
|
||||
}
|
||||
|
||||
function createHazeMaterial(color: number, energy: number) {
|
||||
return new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
uColor: { value: new THREE.Color(color) },
|
||||
uEnergy: { value: energy },
|
||||
},
|
||||
vertexShader: `
|
||||
varying vec2 vHazeUv;
|
||||
void main() {
|
||||
vHazeUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
uniform vec3 uColor;
|
||||
uniform float uEnergy;
|
||||
varying vec2 vHazeUv;
|
||||
void main() {
|
||||
float vertical = 1.0 - smoothstep(0.04, 0.84, abs(vHazeUv.y - 0.42) * 1.9);
|
||||
float horizontal = 1.0 - smoothstep(0.26, 0.5, abs(vHazeUv.x - 0.5));
|
||||
gl_FragColor = vec4(uColor, vertical * horizontal * uEnergy);
|
||||
}
|
||||
`,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
depthTest: true,
|
||||
blending: THREE.AdditiveBlending,
|
||||
toneMapped: true,
|
||||
});
|
||||
}
|
||||
|
||||
function seeded(index: number, salt: number): number {
|
||||
const result = Math.sin((index + 1) * 91.713 + salt * 17.17) * 43_758.5453;
|
||||
return result - Math.floor(result);
|
||||
}
|
||||
|
||||
function finiteOrZero(value: number): number {
|
||||
return Number.isFinite(value) ? value : 0;
|
||||
}
|
||||
|
||||
function clamp01(value: number): number {
|
||||
return THREE.MathUtils.clamp(finiteOrZero(value), 0, 1);
|
||||
}
|
||||
|
||||
function smoothstep(edge0: number, edge1: number, value: number): number {
|
||||
const progress = THREE.MathUtils.clamp((value - edge0) / (edge1 - edge0), 0, 1);
|
||||
return progress * progress * (3 - 2 * progress);
|
||||
}
|
||||
Reference in New Issue
Block a user