Files
Portfolio/src/scene/world/atmosphere.ts

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);
}