282 lines
9.8 KiB
TypeScript
282 lines
9.8 KiB
TypeScript
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 },
|
|
uPointScale: { value: 18 },
|
|
uMaxPointSize: { value: 4.5 },
|
|
uColor: { value: new THREE.Color(0xb9ecf2) },
|
|
},
|
|
vertexShader: `
|
|
attribute float starSize;
|
|
attribute float brightness;
|
|
uniform float uDistance;
|
|
uniform float uPointScale;
|
|
uniform float uMaxPointSize;
|
|
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 = clamp(
|
|
starSize * (uPointScale / max(1.0, -viewPosition.z)),
|
|
0.75,
|
|
uMaxPointSize
|
|
);
|
|
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);
|
|
}
|