feat: add aurora borealis animated shader effect to skybox
Some checks failed
CI / validate (pull_request) Failing after 21s
CI / auto-merge (pull_request) Has been skipped

Two layered semi-transparent planes use a custom GLSL ShaderMaterial
with fractal Brownian motion (fBm) noise to simulate aurora curtains.
Colors cycle through green, teal, and violet via time-driven distortion.
Additive blending and depth-write disabled keep it composited cleanly
behind stars and constellation lines.

Fixes #105

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Alexander Whitestone
2026-03-24 00:13:26 -04:00
parent 36cc526df0
commit f08bd2e3fa

101
app.js
View File

@@ -116,6 +116,103 @@ function buildConstellationLines() {
const constellationLines = buildConstellationLines();
scene.add(constellationLines);
// === AURORA BOREALIS ===
const auroraVertexShader = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const auroraFragmentShader = `
uniform float uTime;
varying vec2 vUv;
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
}
float fbm(vec2 p) {
float v = 0.0;
float amp = 0.5;
for (int i = 0; i < 5; i++) {
v += amp * noise(p);
p *= 2.0;
amp *= 0.5;
}
return v;
}
void main() {
vec2 uv = vUv;
float t = uTime * 0.12;
// Distort UVs for flowing curtain motion
float dx = fbm(vec2(uv.x * 2.5 + t * 0.35, uv.y * 1.5 + t * 0.2)) * 0.28;
float dy = fbm(vec2(uv.x * 2.5 - t * 0.28, uv.y * 1.5 + t * 0.18)) * 0.18;
vec2 dUv = uv + vec2(dx, dy);
// Vertical curtain bands — shift along x over time
float band1 = fbm(vec2(dUv.x * 3.2 + t * 0.55, t * 0.3));
float band2 = fbm(vec2(dUv.x * 5.1 - t * 0.42, t * 0.22));
float bands = band1 * 0.65 + band2 * 0.35;
// Vertical envelope: bright in the upper portion, fade at edges
float yFade = smoothstep(0.0, 0.22, uv.y) * smoothstep(1.0, 0.45, uv.y);
float intensity = pow(bands * yFade, 1.6);
// Color palette: green <-> teal <-> violet
vec3 green = vec3(0.0, 1.0, 0.45);
vec3 teal = vec3(0.0, 0.78, 1.0);
vec3 violet = vec3(0.55, 0.1, 1.0);
float mix1 = fbm(vec2(dUv.x * 2.2 + t * 0.18, uv.y * 3.0));
float mix2 = fbm(vec2(uv.x * 4.0 + t * 0.3, uv.y * 2.0 - t * 0.12)) * 0.55;
vec3 color = mix(green, teal, mix1);
color = mix(color, violet, mix2);
float alpha = intensity * 0.6;
gl_FragColor = vec4(color * intensity, alpha);
}
`;
const auroraMaterial = new THREE.ShaderMaterial({
uniforms: { uTime: { value: 0.0 } },
vertexShader: auroraVertexShader,
fragmentShader: auroraFragmentShader,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
side: THREE.DoubleSide,
});
// Two overlapping planes for depth — slightly different positions/rotations
const auroraGeo = new THREE.PlaneGeometry(700, 220, 1, 1);
const aurora = new THREE.Mesh(auroraGeo, auroraMaterial);
aurora.position.set(0, 75, -130);
aurora.rotation.x = -0.25;
scene.add(aurora);
const auroraMaterial2 = auroraMaterial.clone();
auroraMaterial2.uniforms = { uTime: { value: 0.0 } };
const aurora2 = new THREE.Mesh(new THREE.PlaneGeometry(700, 220, 1, 1), auroraMaterial2);
aurora2.position.set(0, 90, -160);
aurora2.rotation.x = -0.18;
scene.add(aurora2);
// === MOUSE-DRIVEN ROTATION ===
let mouseX = 0;
let mouseY = 0;
@@ -252,6 +349,10 @@ function animate() {
constellationLines.rotation.x = stars.rotation.x;
constellationLines.rotation.y = stars.rotation.y;
// Advance aurora shader time
auroraMaterial.uniforms.uTime.value = elapsed;
auroraMaterial2.uniforms.uTime.value = elapsed + 8.3; // offset for variety
// Subtle pulse on constellation opacity
constellationLines.material.opacity = 0.12 + Math.sin(elapsed * 0.5) * 0.06;