From 968ac970bfb6d2d7022fc37397e5514ae880e07c Mon Sep 17 00:00:00 2001 From: Artem Date: Tue, 14 Jul 2026 20:12:30 -0400 Subject: [PATCH] revert: restore nebula reveal to single-stage fade Undo the staged sky-reveal split from 9efd048: the deferred sky wash and shortened opacity ramp did not look right. NebulaBackground and glsl.ts return to their pre-9efd048 state; the backup-folder deletions from that commit are kept. Co-Authored-By: Claude Opus 4.8 --- components/composites/NebulaBackground.tsx | 230 +++++++-------------- lib/nebula/glsl.ts | 6 +- 2 files changed, 75 insertions(+), 161 deletions(-) diff --git a/components/composites/NebulaBackground.tsx b/components/composites/NebulaBackground.tsx index 4c2c784..ad9d452 100644 --- a/components/composites/NebulaBackground.tsx +++ b/components/composites/NebulaBackground.tsx @@ -39,10 +39,6 @@ const DENSE_STAR_COUNT = 3200; const FRAME_INTERVAL_MS = 16; const SLOW_FRAME_MS = 55; const MIX_RATE = 2.5; // 1/s, ease toward the glyph target (slower morph) -// 1/s, ramp the sky wash in once its shader finishes compiling. The sky -// is by far the slowest program to build, so the scene reveals without -// it and it eases in a beat later rather than gating the whole canvas. -const SKY_FADE_RATE = 2.2; const MOUSE_FADE_RATE = 4; // 1/s, push fade only const MOUSE_VEL_RATE = 7; // 1/s, velocity smoothing for the wake // Pointer position smoothing. Raw pointer events arrive quantized to @@ -167,15 +163,6 @@ export default function NebulaBackground({ let fgProgram!: WebGLProgram; let denseProgram!: WebGLProgram; let activePrograms: WebGLProgram[] = []; - // The reveal waits on these; the 238-line sky shader is not among - // them. Gas, glyphs, and stars are small programs that build in a - // fraction of the sky's compile time, and the scene reads fine - // without the wash, so gating everything on the slowest program only - // bought a longer stare at an empty starfield. - let revealPrograms: WebGLProgram[] = []; - let skyPrograms: WebGLProgram[] = []; - let skyFrame = 0; - let seed: [number, number, number, number] = [0, 0, 0, 0]; let parallelCompile: KHR_parallel_shader_compile | null = null; const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; @@ -194,9 +181,6 @@ export default function NebulaBackground({ // programs still have zeroed uniforms. uploadedW: 0, uploadedH: 0, - // The sky wash lands after the reveal; skyFade eases it in. - skyReady: false, - skyFade: 0, mix: 0, activeShape: null as string | null, pendingShape: null as string | null, @@ -293,14 +277,8 @@ export default function NebulaBackground({ canvas.height = height; gl!.viewport(0, 0, canvas.width, canvas.height); if (!isMini) { - // Touching the sky program before its async compile finishes would - // force the driver to complete it synchronously — exactly the - // main-thread stall the staged reveal exists to avoid. startSky() - // uploads the size it missed. - if (state.skyReady) { - gl!.useProgram(bgProgram); - gl!.uniform2f(bgU.res, canvas.width, canvas.height); - } + gl!.useProgram(bgProgram); + gl!.uniform2f(bgU.res, canvas.width, canvas.height); gl!.useProgram(fgProgram); gl!.uniform2f(fgU.res, canvas.width, canvas.height); } @@ -358,31 +336,22 @@ export default function NebulaBackground({ gl!.disable(gl!.BLEND); gl!.clearColor(0, 0, 0, 0); gl!.clear(gl!.COLOR_BUFFER_BIT); + } else if (visible("background")) { + // Pass 1: background (sky, stars, H II, cirrus). + gl!.disable(gl!.BLEND); + gl!.useProgram(bgProgram); + gl!.uniform1f(bgU.time, time); + gl!.uniform2f(bgU.mouse, state.smoothMouse.x, state.smoothMouse.y); + gl!.uniform1f(bgU.mouseActive, state.mouseActive); + gl!.bindBuffer(gl!.ARRAY_BUFFER, quadBuffer); + const quadLoc = gl!.getAttribLocation(bgProgram, "aPosition"); + gl!.enableVertexAttribArray(quadLoc); + gl!.vertexAttribPointer(quadLoc, 2, gl!.FLOAT, false, 0, 0); + gl!.drawArrays(gl!.TRIANGLES, 0, 3); } else { - // Pass 1: background (sky, stars, H II, cirrus). The clear is the - // sky shader's own base color, so drawing the wash over it with a - // 0..1 alpha is continuous: at fade 0 the buffer already holds - // exactly what the sky would paint before anything is added to it. gl!.disable(gl!.BLEND); gl!.clearColor(0.004, 0.004, 0.006, 1); gl!.clear(gl!.COLOR_BUFFER_BIT); - - if (visible("background") && state.skyReady) { - state.skyFade += (1 - state.skyFade) * (1 - Math.exp(-SKY_FADE_RATE * dtSec)); - gl!.enable(gl!.BLEND); - gl!.blendFunc(gl!.SRC_ALPHA, gl!.ONE_MINUS_SRC_ALPHA); - gl!.useProgram(bgProgram); - gl!.uniform1f(bgU.time, time); - gl!.uniform2f(bgU.mouse, state.smoothMouse.x, state.smoothMouse.y); - gl!.uniform1f(bgU.mouseActive, state.mouseActive); - gl!.uniform1f(bgU.fade, state.skyFade); - gl!.bindBuffer(gl!.ARRAY_BUFFER, quadBuffer); - const quadLoc = gl!.getAttribLocation(bgProgram, "aPosition"); - gl!.enableVertexAttribArray(quadLoc); - gl!.vertexAttribPointer(quadLoc, 2, gl!.FLOAT, false, 0, 0); - gl!.drawArrays(gl!.TRIANGLES, 0, 3); - gl!.disable(gl!.BLEND); - } } // Pass 1.5: the dense dust of faint distant suns, as static @@ -697,13 +666,13 @@ export default function NebulaBackground({ // and the worker has delivered the particle buffers. const start = () => { try { - for (const program of revealPrograms) finishProgram(gl!, program); + for (const program of activePrograms) finishProgram(gl!, program); } catch (error) { console.error("Nebula shaders failed, keeping starfield only:", error); return; } - seed = [Math.random(), Math.random(), Math.random(), Math.random()]; + const seed = [Math.random(), Math.random(), Math.random(), Math.random()]; // Particle buffers, prebuilt by the worker in the common case. The // sync path covers a worker that failed to spawn or crashed. @@ -739,6 +708,50 @@ export default function NebulaBackground({ gl!.bindBuffer(gl!.ARRAY_BUFFER, quadBuffer); gl!.bufferData(gl!.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl!.STATIC_DRAW); + if (!isMini) { + // Uniforms: background program. The wash tints come from the + // profiles: A follows the lowest cloud, B the upper one. The + // portrait scene has no third cloud; the cool accent falls + // back to the first (the teal web). + const palA = profilePalettes[clouds[0].profile].bg; + const palB = profilePalettes[(clouds[1] ?? clouds[0]).profile].bg; + const palC = profilePalettes[(clouds[2] ?? clouds[0]).profile].bg; + gl!.useProgram(bgProgram); + bgU = { + res: gl!.getUniformLocation(bgProgram, "uRes"), + time: gl!.getUniformLocation(bgProgram, "uTime"), + mouse: gl!.getUniformLocation(bgProgram, "uMouse"), + mouseActive: gl!.getUniformLocation(bgProgram, "uMouseActive"), + }; + gl!.uniform4f(gl!.getUniformLocation(bgProgram, "uSeed"), seed[0], seed[1], seed[2], seed[3]); + clouds.forEach((c, i) => { + gl!.uniform4f(gl!.getUniformLocation(bgProgram, `uCloud${i}`), c.x, c.y, c.radius, 1); + }); + const hiiSpots: [number, number][] = [ + [0.5, 0.9], + [0.15, 0.5], + [0.85, 0.55], + [0.5, 0.1], + ]; + hiiSpots.sort(() => Math.random() - 0.5); + for (let i = 0; i < 2; i++) { + gl!.uniform4f( + gl!.getUniformLocation(bgProgram, `uHii${i}`), + hiiSpots[i][0] + jitter(), + hiiSpots[i][1] + jitter(), + 0.55 + Math.random() * 0.35, + 0.5 + Math.random() * 0.4 + ); + } + gl!.uniform3f(gl!.getUniformLocation(bgProgram, "uColCoreA"), ...palA.core); + gl!.uniform3f(gl!.getUniformLocation(bgProgram, "uColCoreB"), ...palB.core); + gl!.uniform3f(gl!.getUniformLocation(bgProgram, "uColMidA"), ...palA.mid); + gl!.uniform3f(gl!.getUniformLocation(bgProgram, "uColMidB"), ...palB.mid); + gl!.uniform3f(gl!.getUniformLocation(bgProgram, "uColFilA"), ...palA.fil); + gl!.uniform3f(gl!.getUniformLocation(bgProgram, "uColFilB"), ...palB.fil); + gl!.uniform3f(gl!.getUniformLocation(bgProgram, "uColBlue"), ...palC.core); + } + // Uniforms and attributes: particle program. gl!.useProgram(particleProgram); pU = { @@ -837,101 +850,20 @@ export default function NebulaBackground({ canvas.style.opacity = "1"; }; - /** - * Second stage: the sky wash, once its shader has finally linked. The - * scene is already on screen and animating by now, so a failure here - * is survivable — the near-black clear the sky paints over stands in - * for it, and the gas and stars carry the frame. - */ - const startSky = () => { - try { - for (const program of skyPrograms) finishProgram(gl!, program); - } catch (error) { - console.error("Nebula sky shader failed, keeping the bare scene:", error); - return; - } - - // The wash tints come from the profiles: A follows the lowest cloud, - // B the upper one. The portrait scene has no third cloud; the cool - // accent falls back to the first (the teal web). - const palA = profilePalettes[clouds[0].profile].bg; - const palB = profilePalettes[(clouds[1] ?? clouds[0]).profile].bg; - const palC = profilePalettes[(clouds[2] ?? clouds[0]).profile].bg; - gl!.useProgram(bgProgram); - bgU = { - res: gl!.getUniformLocation(bgProgram, "uRes"), - time: gl!.getUniformLocation(bgProgram, "uTime"), - mouse: gl!.getUniformLocation(bgProgram, "uMouse"), - mouseActive: gl!.getUniformLocation(bgProgram, "uMouseActive"), - fade: gl!.getUniformLocation(bgProgram, "uFade"), - }; - gl!.uniform4f(gl!.getUniformLocation(bgProgram, "uSeed"), seed[0], seed[1], seed[2], seed[3]); - clouds.forEach((c, i) => { - gl!.uniform4f(gl!.getUniformLocation(bgProgram, `uCloud${i}`), c.x, c.y, c.radius, 1); - }); - const hiiSpots: [number, number][] = [ - [0.5, 0.9], - [0.15, 0.5], - [0.85, 0.55], - [0.5, 0.1], - ]; - hiiSpots.sort(() => Math.random() - 0.5); - for (let i = 0; i < 2; i++) { - gl!.uniform4f( - gl!.getUniformLocation(bgProgram, `uHii${i}`), - hiiSpots[i][0] + jitter(), - hiiSpots[i][1] + jitter(), - 0.55 + Math.random() * 0.35, - 0.5 + Math.random() * 0.4 - ); - } - gl!.uniform3f(gl!.getUniformLocation(bgProgram, "uColCoreA"), ...palA.core); - gl!.uniform3f(gl!.getUniformLocation(bgProgram, "uColCoreB"), ...palB.core); - gl!.uniform3f(gl!.getUniformLocation(bgProgram, "uColMidA"), ...palA.mid); - gl!.uniform3f(gl!.getUniformLocation(bgProgram, "uColMidB"), ...palB.mid); - gl!.uniform3f(gl!.getUniformLocation(bgProgram, "uColFilA"), ...palA.fil); - gl!.uniform3f(gl!.getUniformLocation(bgProgram, "uColFilB"), ...palB.fil); - gl!.uniform3f(gl!.getUniformLocation(bgProgram, "uColBlue"), ...palC.core); - // resize() skipped this program while it was still compiling. - gl!.uniform2f(bgU.res, canvas.width, canvas.height); - - state.skyReady = true; - if (reducedMotion) { - // No render loop to ease it in; land on the finished sky. - state.skyFade = 1; - renderFrame(12000, 0.016); - } - }; - - const isCompiling = (programs: WebGLProgram[]) => - parallelCompile && - programs.some( - (program) => !gl!.getProgramParameter(program, parallelCompile!.COMPLETION_STATUS_KHR) - ); - - // Poll for the sky's compile on its own, after the scene is up. - const waitForSky = () => { - if (state.disposed) return; - if (isCompiling(skyPrograms)) { - skyFrame = requestAnimationFrame(waitForSky); - return; - } - startSky(); - }; - // Poll for compile and particle-generation completion without // blocking the main thread. The worker nulls itself when it delivers. - // Only the reveal programs are waited on here; the sky follows. const waitForCompile = () => { if (state.disposed) return; - if (isCompiling(revealPrograms) || particleWorker) { + const compiling = + parallelCompile && + activePrograms.some( + (program) => !gl!.getProgramParameter(program, parallelCompile!.COMPLETION_STATUS_KHR) + ); + if (compiling || particleWorker) { state.frame = requestAnimationFrame(waitForCompile); return; } start(); - // start() bails on a compile failure and leaves the scene unbooted; - // there is then nothing for the sky to draw over. - if (state.started && skyPrograms.length > 0) waitForSky(); }; // Compiles the programs and kicks off the async-compile poll. Runs @@ -964,26 +896,16 @@ export default function NebulaBackground({ } } - // A restored context starts over with programs that know nothing. - state.skyReady = false; - state.skyFade = 0; - try { particleProgram = beginProgram(gl!, PARTICLE_VERTEX_SRC, PARTICLE_FRAGMENT_SRC); glyphProgram = beginProgram(gl!, PARTICLE_VERTEX_SRC, GLYPH_FRAGMENT_SRC); denseProgram = beginProgram(gl!, DENSE_STAR_VERTEX_SRC, DENSE_STAR_FRAGMENT_SRC); - revealPrograms = [particleProgram, glyphProgram, denseProgram]; - skyPrograms = []; + activePrograms = [particleProgram, glyphProgram, denseProgram]; if (!isMini) { bgProgram = beginProgram(gl!, VERTEX_SRC, BACKGROUND_FRAGMENT_SRC); fgProgram = beginProgram(gl!, VERTEX_SRC, FOREGROUND_FRAGMENT_SRC); - // The foreground stars are a small program and draw over the - // clear just as happily as over the wash, so they reveal with - // the scene. Only the heavy sky is deferred. - revealPrograms.push(fgProgram); - skyPrograms.push(bgProgram); + activePrograms.push(bgProgram, fgProgram); } - activePrograms = [...revealPrograms, ...skyPrograms]; } catch (error) { console.error("Nebula shaders failed, keeping starfield only:", error); return; @@ -1024,7 +946,6 @@ export default function NebulaBackground({ state.disposed = true; state.running = false; cancelAnimationFrame(state.frame); - cancelAnimationFrame(skyFrame); particleWorker?.terminate(); particleWorker = null; canvas.removeEventListener("webglcontextlost", onContextLost); @@ -1060,16 +981,13 @@ export default function NebulaBackground({ ); } diff --git a/lib/nebula/glsl.ts b/lib/nebula/glsl.ts index d3c3a5b..120ee9f 100644 --- a/lib/nebula/glsl.ts +++ b/lib/nebula/glsl.ts @@ -48,10 +48,6 @@ uniform vec3 uColMidB; uniform vec3 uColFilA; uniform vec3 uColFilB; uniform vec3 uColBlue; // reflection-blue accent for the gradient wash -// 0..1 reveal. The sky compiles far slower than the particle programs, so -// the scene shows without it and this ramps the wash in once it lands. -// Blended over a clear of the same near-black base, so fade 0 is seamless. -uniform float uFade; float hash21(vec2 p) { p = fract(p * vec2(123.34, 456.21)); @@ -266,7 +262,7 @@ void main() { col *= vig; col = col / (1.0 + col); // Reinhard - gl_FragColor = vec4(col, uFade); + gl_FragColor = vec4(col, 1.0); } `;