feat: layer the nightdrive sky and atmosphere
This commit is contained in:
@@ -1,33 +1,153 @@
|
||||
import * as THREE from 'three';
|
||||
import type { EnvironmentChapter } from '@/journey/types';
|
||||
import type { SceneResourceBag } from '@/scene/resources';
|
||||
import type { MutableChapterPalette } from '@/scene/world/palette';
|
||||
import { CHAPTER_PALETTES } from '@/scene/world/palette';
|
||||
|
||||
const chapterWarmth: Readonly<Record<EnvironmentChapter, number>> = {
|
||||
'deep-night': 0,
|
||||
systems: 0.12,
|
||||
infrastructure: 0.24,
|
||||
builds: 0.42,
|
||||
'pre-dawn': 0.68,
|
||||
destination: 1,
|
||||
};
|
||||
export interface SunWorld {
|
||||
group: WorldGroup;
|
||||
update(palette: Readonly<MutableChapterPalette>, routeProgress: number): void;
|
||||
}
|
||||
|
||||
export function createSun(resources: SceneResourceBag) {
|
||||
const geometry = resources.own('geometry', new THREE.CircleGeometry(18, 64));
|
||||
const material = resources.own('material', new THREE.MeshBasicMaterial({
|
||||
color: 0xff6f83,
|
||||
interface WorldGroup {
|
||||
add(...objects: unknown[]): void;
|
||||
getObjectByName(name: string): unknown;
|
||||
}
|
||||
|
||||
export function createSun(resources: SceneResourceBag): SunWorld {
|
||||
const group = new THREE.Group();
|
||||
group.name = 'sun-world';
|
||||
const night = CHAPTER_PALETTES['deep-night'];
|
||||
const sunColor = new THREE.Color(night.sun);
|
||||
const uniforms = {
|
||||
uColor: { value: sunColor },
|
||||
uScanBands: { value: 14 },
|
||||
uWarmth: { value: night.warmth },
|
||||
uRouteProgress: { value: 0 },
|
||||
};
|
||||
const geometry = resources.own('geometry', new THREE.CircleGeometry(20, 96));
|
||||
const material = resources.own('material', new THREE.ShaderMaterial({
|
||||
uniforms,
|
||||
vertexShader: `
|
||||
varying vec2 vSunUv;
|
||||
|
||||
void main() {
|
||||
vSunUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
uniform vec3 uColor;
|
||||
uniform float uScanBands;
|
||||
uniform float uWarmth;
|
||||
uniform float uRouteProgress;
|
||||
varying vec2 vSunUv;
|
||||
|
||||
void main() {
|
||||
float radius = distance(vSunUv, vec2(0.5));
|
||||
float softEdge = 1.0 - smoothstep(0.435, 0.5, radius);
|
||||
float lowerDisc = 1.0 - smoothstep(0.42, 0.76, vSunUv.y);
|
||||
float scanBand = smoothstep(0.16, 0.28, fract(vSunUv.y * uScanBands));
|
||||
float bandCut = mix(1.0, 0.32 + scanBand * 0.68, lowerDisc * 0.82);
|
||||
float horizonFade = smoothstep(0.02, 0.14, vSunUv.y);
|
||||
float luminance = 0.72 + uWarmth * 0.18 + uRouteProgress * 0.06;
|
||||
gl_FragColor = vec4(uColor * luminance, softEdge * bandCut * horizonFade * 0.9);
|
||||
}
|
||||
`,
|
||||
transparent: true,
|
||||
opacity: 0.82,
|
||||
fog: false,
|
||||
depthWrite: false,
|
||||
depthTest: true,
|
||||
blending: THREE.AdditiveBlending,
|
||||
toneMapped: true,
|
||||
}));
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
mesh.position.set(0, 12, -128);
|
||||
const disc = new THREE.Mesh(geometry, material);
|
||||
disc.name = 'sun-disc';
|
||||
disc.position.set(0, 11.5, -142);
|
||||
group.add(disc);
|
||||
|
||||
const haloTexture = resources.own('texture', createHaloTexture());
|
||||
const haloMaterial = resources.own('material', new THREE.SpriteMaterial({
|
||||
map: haloTexture,
|
||||
color: sunColor.clone(),
|
||||
transparent: true,
|
||||
opacity: 0.24,
|
||||
depthWrite: false,
|
||||
depthTest: true,
|
||||
blending: THREE.AdditiveBlending,
|
||||
toneMapped: true,
|
||||
}));
|
||||
const halo = new THREE.Sprite(haloMaterial);
|
||||
halo.name = 'sun-halo';
|
||||
halo.position.copy(disc.position);
|
||||
halo.position.z -= 0.5;
|
||||
halo.scale.set(57, 43, 1);
|
||||
group.add(halo);
|
||||
|
||||
return {
|
||||
mesh,
|
||||
update(chapter: EnvironmentChapter, routeProgress: number) {
|
||||
const warmth = Math.max(chapterWarmth[chapter], routeProgress);
|
||||
material.color.setRGB(1, 0.34 + warmth * 0.24, 0.42 + warmth * 0.22);
|
||||
mesh.position.y = 12 + warmth * 5;
|
||||
group,
|
||||
update(palette, routeProgress) {
|
||||
const progress = clamp01(routeProgress);
|
||||
sunColor.copy(palette.sun);
|
||||
haloMaterial.color.copy(palette.sun);
|
||||
uniforms.uWarmth.value = clamp01(palette.warmth);
|
||||
uniforms.uRouteProgress.value = progress;
|
||||
haloMaterial.opacity = 0.18 + palette.warmth * 0.12;
|
||||
disc.position.y = 11.5 + palette.warmth * 4.8;
|
||||
halo.position.y = disc.position.y;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createHaloTexture() {
|
||||
const size = 64;
|
||||
if (typeof document !== 'undefined') {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
let context: CanvasRenderingContext2D | null = null;
|
||||
try {
|
||||
context = canvas.getContext('2d');
|
||||
} catch {
|
||||
context = null;
|
||||
}
|
||||
if (context) {
|
||||
const gradient = context.createRadialGradient(
|
||||
size / 2,
|
||||
size / 2,
|
||||
0,
|
||||
size / 2,
|
||||
size / 2,
|
||||
size / 2,
|
||||
);
|
||||
gradient.addColorStop(0, 'rgb(255 255 255 / 90%)');
|
||||
gradient.addColorStop(0.26, 'rgb(255 255 255 / 32%)');
|
||||
gradient.addColorStop(1, 'rgb(255 255 255 / 0%)');
|
||||
context.fillStyle = gradient;
|
||||
context.fillRect(0, 0, size, size);
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
texture.generateMipmaps = false;
|
||||
return texture;
|
||||
}
|
||||
}
|
||||
|
||||
const pixels = new Uint8Array(size * size * 4);
|
||||
for (let y = 0; y < size; y += 1) {
|
||||
for (let x = 0; x < size; x += 1) {
|
||||
const distance = Math.hypot(x - size / 2, y - size / 2) / (size / 2);
|
||||
const alpha = Math.round(255 * Math.max(0, 1 - distance) ** 2.4);
|
||||
const offset = (y * size + x) * 4;
|
||||
pixels[offset] = 255;
|
||||
pixels[offset + 1] = 255;
|
||||
pixels[offset + 2] = 255;
|
||||
pixels[offset + 3] = alpha;
|
||||
}
|
||||
}
|
||||
const texture = new THREE.DataTexture(pixels, size, size, THREE.RGBAFormat);
|
||||
texture.needsUpdate = true;
|
||||
texture.generateMipmaps = false;
|
||||
return texture;
|
||||
}
|
||||
|
||||
function clamp01(value: number): number {
|
||||
return Number.isFinite(value) ? THREE.MathUtils.clamp(value, 0, 1) : 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user