/** * Unknown Pleasures WebGL Visualizer * * Uses GPU-accelerated rendering with: * - Geometry-based thick lines (quads instead of LINE_STRIP) * - Shader-based glow effect (post-processing blur) * - Prepared for future ambient haze effects */ export class UnknownPleasuresWebGL { // Propagation speed: controls how fast waves propagate between lines // Higher = faster propagation (1.0 = default, 0.5 = slower, 2.0 = faster) static PROPAGATION_SPEED = 0.7; // Glow intensity: controls how strong the glow effect is static GLOW_INTENSITY = 5.0; static NOISE_STRENGTH = 0.04; constructor() { this.name = 'Unknown Pleasures'; this.contextType = 'webgl'; this.historySize = 25; this.dataPoints = 96; this.history = []; this.writeIndex = 0; this.pLookup = new Float32Array(this.dataPoints); this.xLookup = new Float32Array(this.dataPoints); // WebGL state this.gl = null; this.lineProgram = null; this.glowProgram = null; this.quadBuffer = null; this.framebuffer = null; this.sceneTexture = null; // Cached values this._paletteColor = ''; this._paletteRGB = null; this.rotationAngle = Math.PI / 6; this._cos = Math.cos(this.rotationAngle); this._sin = Math.sin(this.rotationAngle); // Propagation timing this._propagationAccum = 0; this.reset(); this._precompute(); } reset() { this.history.length = 0; for (let i = 0; i < this.historySize; i++) { this.history.push(new Float32Array(this.dataPoints)); } this.writeIndex = 0; } resize(width, height) { if (this.gl && this.sceneTexture) { this._resizeFramebuffer(this.gl, width, height); } } destroy() { this.history.length = 0; if (this.gl) { if (this.lineProgram) this.gl.deleteProgram(this.lineProgram); if (this.glowProgram) this.gl.deleteProgram(this.glowProgram); if (this.quadBuffer) this.gl.deleteBuffer(this.quadBuffer); if (this.framebuffer) this.gl.deleteFramebuffer(this.framebuffer); if (this.sceneTexture) this.gl.deleteTexture(this.sceneTexture); } this.gl = null; this.lineProgram = null; this.glowProgram = null; } _precompute() { const pts = this.dataPoints; const inv = 1 / (pts - 1); for (let i = 0; i < pts; i++) { const p = Math.abs(i * inv - 0.5) * 2; this.pLookup[i] = 1 - p * p * p; this.xLookup[i] = i * inv; } } _createBuffers() { this.quadBuffer = this.gl.createBuffer(); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.quadBuffer); this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), this.gl.STATIC_DRAW); this.lineBuffer = this.gl.createBuffer(); // Pre-allocate vertex buffer (max possible size: historySize * dataPoints * 6 vertices * 3 floats) const maxVertices = this.historySize * this.dataPoints * 6; // 6 vertices per segment this.vertexBuffer = new Float32Array(maxVertices * 3); // 3 floats per vertex (x,y,edge) } _initGL(gl, width, height) { if (this.lineProgram) return; this.gl = gl; // === LINE SHADER (draws thick colored lines as quads with AA edges) === const lineVS = ` attribute vec3 a_posEdge; // xy = position, z = edge distance (-1 to +1) varying float v_edge; void main() { gl_Position = vec4(a_posEdge.xy, 0.0, 1.0); v_edge = a_posEdge.z; } `; const lineFS = ` precision mediump float; uniform vec3 u_color; varying float v_edge; void main() { // Smooth antialiasing at edges float edge = abs(v_edge); float aa = 1.0 - smoothstep(0.6, 1.0, edge); gl_FragColor = vec4(u_color * aa, aa); } `; this.lineProgram = this._createProgram(gl, lineVS, lineFS); if (!this.lineProgram) return; this.line_a_posEdge = gl.getAttribLocation(this.lineProgram, 'a_posEdge'); this.line_u_color = gl.getUniformLocation(this.lineProgram, 'u_color'); // === BRIGHTNESS EXTRACTION SHADER === // This is KEY for bloom - extract bright pixels, blur them, add back const brightnessVS = ` attribute vec2 a_position; varying vec2 v_uv; void main() { v_uv = a_position * 0.5 + 0.5; gl_Position = vec4(a_position, 0.0, 1.0); } `; const brightnessFS = ` precision mediump float; varying vec2 v_uv; uniform sampler2D u_texture; uniform float u_threshold; uniform float u_isDarkTheme; void main() { // Since Pass 1 now clears to transparent, the scene texture only contains the isolated lines. // We don't need to extract brightness by darkening the background anymore. // Just pass the lines through so they can be blurred. gl_FragColor = texture2D(u_texture, v_uv); } `; this.brightnessProgram = this._createProgram(gl, brightnessVS, brightnessFS); if (!this.brightnessProgram) return; this.brightness_a_position = gl.getAttribLocation(this.brightnessProgram, 'a_position'); this.brightness_u_texture = gl.getUniformLocation(this.brightnessProgram, 'u_texture'); this.brightness_u_threshold = gl.getUniformLocation(this.brightnessProgram, 'u_threshold'); this.brightness_u_isDarkTheme = gl.getUniformLocation(this.brightnessProgram, 'u_isDarkTheme'); // === BLUR SHADER (two-pass separable Gaussian) === const blurVS = ` attribute vec2 a_position; varying vec2 v_uv; void main() { v_uv = a_position * 0.5 + 0.5; gl_Position = vec4(a_position, 0.0, 1.0); } `; // 9-tap Gaussian blur with small fixed steps for smooth gradients // Use multiple passes to extend blur radius const blurFS = ` precision mediump float; varying vec2 v_uv; uniform sampler2D u_texture; uniform vec2 u_resolution; uniform vec2 u_direction; uniform float u_spread; // Used instead of u_radius // 9-tap Gaussian with expanding offsets void main() { // Expanding offsets for stronger glow (Thread Ripper Style) vec2 off1 = vec2(1.3846153846) * u_direction * u_spread; vec2 off2 = vec2(3.2307692308) * u_direction * u_spread; vec4 color = texture2D(u_texture, v_uv) * 0.2270270270; color += texture2D(u_texture, v_uv + (off1 / u_resolution)) * 0.3162162162; color += texture2D(u_texture, v_uv - (off1 / u_resolution)) * 0.3162162162; color += texture2D(u_texture, v_uv + (off2 / u_resolution)) * 0.0702702703; color += texture2D(u_texture, v_uv - (off2 / u_resolution)) * 0.0702702703; gl_FragColor = color; } `; this.blurProgram = this._createProgram(gl, blurVS, blurFS); if (!this.blurProgram) return; this.blur_a_position = gl.getAttribLocation(this.blurProgram, 'a_position'); this.blur_u_texture = gl.getUniformLocation(this.blurProgram, 'u_texture'); this.blur_u_resolution = gl.getUniformLocation(this.blurProgram, 'u_resolution'); this.blur_u_direction = gl.getUniformLocation(this.blurProgram, 'u_direction'); this.blur_u_spread = gl.getUniformLocation(this.blurProgram, 'u_spread'); // === COMPOSITE SHADER (combines original + blurred glow) === // === COMPOSITE SHADER (exact copy from Thread Ripper) === const compositeFS = ` precision mediump float; varying vec2 v_uv; uniform sampler2D u_scene; uniform sampler2D u_blur; uniform float u_glowStrength; uniform float u_noiseStrength; uniform float u_isDarkTheme; // Kept for compatibility but unused in logic below uniform float u_time; float rand(vec2 co) { return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453); } void main() { vec4 original = texture2D(u_scene, v_uv); vec4 blur = texture2D(u_blur, v_uv); // Additive glow on top of original lines vec3 rgb = original.rgb + blur.rgb * u_glowStrength; // Vignette: blur edges for depth float dist = distance(v_uv, vec2(0.5)); float vignette = smoothstep(0.4, 0.8, dist); // We handle scaling in the final mix later to avoid breaking the HDR mapping above. // The rgb here is the base scene before the final exponential glow math. float noise = rand(v_uv * 10.0); float noiseStrength = 0.06; rgb += (noise - 0.5) * noiseStrength; // In light mode (u_isDarkTheme == 0.0), the additive glow effect naturally appears weaker // against the bright background. We apply a 1.5x perceptual boost to match dark mode intensity. float themeBoost = mix(1.5, 1.0, u_isDarkTheme); // Using 1.0 - exp(-x) gives butter-smooth HDR-like falloff, eliminating harsh banding. // We square the intensity (gamma 2.0) to dramatically increase the "core" opacity of the glow // making it much more visible while preserving the smooth edges. vec3 rawGlow = blur.rgb * (u_glowStrength * themeBoost); float glowIntensity = max(rawGlow.r, max(rawGlow.g, rawGlow.b)); // Boost density significantly before applying HDR curve float density = glowIntensity * glowIntensity * 1.5; float smoothGlowAlpha = 1.0 - exp(-density); // Keep the color strictly within valid premultiplied alpha bounds (rgb <= alpha) vec3 safeGlowRgb = glowIntensity > 0.0 ? (rawGlow / glowIntensity) * smoothGlowAlpha : vec3(0.0); // Additive over the core lines rgb = original.rgb + safeGlowRgb; // Final alpha is the line's alpha plus the glow's alpha float finalAlpha = clamp(original.a + smoothGlowAlpha, 0.0, 1.0); // Output RGB and Alpha for PREMULTIPLIED alpha blending gl_FragColor = vec4(rgb, finalAlpha); } `; this.compositeProgram = this._createProgram(gl, blurVS, compositeFS); if (!this.compositeProgram) return; this.composite_a_position = gl.getAttribLocation(this.compositeProgram, 'a_position'); this.composite_u_scene = gl.getUniformLocation(this.compositeProgram, 'u_scene'); this.composite_u_blur = gl.getUniformLocation(this.compositeProgram, 'u_blur'); this.composite_u_glowStrength = gl.getUniformLocation(this.compositeProgram, 'u_glowStrength'); this.composite_u_noiseStrength = gl.getUniformLocation(this.compositeProgram, 'u_noiseStrength'); this.composite_u_isDarkTheme = gl.getUniformLocation(this.compositeProgram, 'u_isDarkTheme'); this.composite_u_time = gl.getUniformLocation(this.compositeProgram, 'u_time'); this._createBuffers(); // Use helper this._createFramebuffer(gl, width, height); gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); } _createProgram(gl, vsSource, fsSource) { const vs = this._compileShader(gl, gl.VERTEX_SHADER, vsSource); const fs = this._compileShader(gl, gl.FRAGMENT_SHADER, fsSource); if (!vs || !fs) return null; const program = gl.createProgram(); gl.attachShader(program, vs); gl.attachShader(program, fs); gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { console.error('WebGL program link failed:', gl.getProgramInfoLog(program)); return null; } return program; } _compileShader(gl, type, source) { const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { console.error('Shader compile error:', gl.getShaderInfoLog(shader)); gl.deleteShader(shader); return null; } return shader; } _createFramebuffer(gl, width, height) { // Framebuffer 1: Scene (lines) - FULL RESOLUTION this.framebuffer = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); this.sceneTexture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, this.sceneTexture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.sceneTexture, 0); // Blur Resolution (Half size for performance) const blurW = Math.max(1, width >> 1); const blurH = Math.max(1, height >> 1); // Framebuffer 2: Blur intermediate this.blurFramebuffer = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, this.blurFramebuffer); this.blurTexture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, this.blurTexture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, blurW, blurH, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.blurTexture, 0); // Framebuffer 3: Blur final this.blurFinalFramebuffer = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, this.blurFinalFramebuffer); this.blurFinalTexture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, this.blurFinalTexture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, blurW, blurH, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.blurFinalTexture, 0); gl.bindFramebuffer(gl.FRAMEBUFFER, null); } _resizeFramebuffer(gl, width, height) { const blurW = Math.max(1, width >> 1); const blurH = Math.max(1, height >> 1); gl.bindTexture(gl.TEXTURE_2D, this.sceneTexture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.bindTexture(gl.TEXTURE_2D, this.blurTexture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, blurW, blurH, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.bindTexture(gl.TEXTURE_2D, this.blurFinalTexture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, blurW, blurH, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); } _buildPalette(color) { // Parse color exactly like Canvas2D version const r = parseInt(color.slice(1, 3), 16); const g = parseInt(color.slice(3, 5), 16); const b = parseInt(color.slice(5, 7), 16); // perceptual grayscale (same weights browsers use) const gray = 0.299 * r + 0.587 * g + 0.114 * b; this._paletteRGB = []; for (let i = 0; i < this.historySize; i++) { const p = i / (this.historySize - 1); // === Saturation gradient (HSL-like) - match Canvas2D exactly === const sat = 3.0 - 2 * p; // Clamp to 0-255 like Canvas2D does with | 0 const rr = Math.max(0, Math.min(255, (gray + (r - gray) * sat) | 0)) / 255; const gg = Math.max(0, Math.min(255, (gray + (g - gray) * sat) | 0)) / 255; const bb = Math.max(0, Math.min(255, (gray + (b - gray) * sat) | 0)) / 255; this._paletteRGB.push([rr, gg, bb]); } this._paletteColor = color; } _generateLineQuads(points, thickness, width, height, outBuffer, offset) { if (points.length < 2) return 0; const n = points.length; let ptr = offset; // Precompute normals (reuse internal arrays if possible, but for now stack var is fine) // Optimization: Single pass miter calculation // Helper to clip X,Y const wInv = 2 / width; const hInv = 2 / height; for (let i = 0; i < n - 1; i++) { const p1 = points[i]; const p2 = points[i + 1]; // Calculate segment normal let dx = p2.x - p1.x; let dy = p2.y - p1.y; let len = Math.sqrt(dx * dx + dy * dy); let nx, ny; if (len < 0.001) { nx = 0; ny = -1; } else { nx = -dy / len; ny = dx / len; } // Previous normal (for miter) let prevNx = nx, prevNy = ny; if (i > 0) { const p0 = points[i - 1]; const dx0 = p1.x - p0.x; const dy0 = p1.y - p0.y; const len0 = Math.sqrt(dx0 * dx0 + dy0 * dy0); if (len0 >= 0.001) { prevNx = -dy0 / len0; prevNy = dx0 / len0; } } // Miter at P1 let m1x = nx + prevNx; let m1y = ny + prevNy; let m1l = Math.sqrt(m1x * m1x + m1y * m1y); if (m1l > 0.001) { m1x /= m1l; m1y /= m1l; } // Next normal (for P2 miter) let nextNx = nx, nextNy = ny; if (i < n - 2) { const p3 = points[i + 2]; const dx2 = p3.x - p2.x; const dy2 = p3.y - p2.y; const len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2); if (len2 >= 0.001) { nextNx = -dy2 / len2; nextNy = dx2 / len2; } } // Miter at P2 let m2x = nx + nextNx; let m2y = ny + nextNy; let m2l = Math.sqrt(m2x * m2x + m2y * m2y); if (m2l > 0.001) { m2x /= m2l; m2y /= m2l; } // Generate vertices // P1 Top const x1a = (p1.x - m1x * thickness) * wInv - 1; const y1a = 1 - (p1.y - m1y * thickness) * hInv; // P1 Bottom const x1b = (p1.x + m1x * thickness) * wInv - 1; const y1b = 1 - (p1.y + m1y * thickness) * hInv; // P2 Top const x2a = (p2.x - m2x * thickness) * wInv - 1; const y2a = 1 - (p2.y - m2y * thickness) * hInv; // P2 Bottom const x2b = (p2.x + m2x * thickness) * wInv - 1; const y2b = 1 - (p2.y + m2y * thickness) * hInv; // Triangle 1 outBuffer[ptr++] = x1a; outBuffer[ptr++] = y1a; outBuffer[ptr++] = -1.0; outBuffer[ptr++] = x1b; outBuffer[ptr++] = y1b; outBuffer[ptr++] = 1.0; outBuffer[ptr++] = x2a; outBuffer[ptr++] = y2a; outBuffer[ptr++] = -1.0; // Triangle 2 outBuffer[ptr++] = x1b; outBuffer[ptr++] = y1b; outBuffer[ptr++] = 1.0; outBuffer[ptr++] = x2b; outBuffer[ptr++] = y2b; outBuffer[ptr++] = 1.0; outBuffer[ptr++] = x2a; outBuffer[ptr++] = y2a; outBuffer[ptr++] = -1.0; } return ptr - offset; } draw(ctx, canvas, analyser, dataArray, params) { const gl = ctx; const { width, height } = canvas; const isDark = document.documentElement.getAttribute('data-theme') !== 'white'; canvas.style.mixBlendMode = 'normal'; if (!this.lineProgram) { this._initGL(gl, width, height); } if (this.history.length === 0) { this.reset(); } if (!params.paused) { this._propagationAccum += UnknownPleasuresWebGL.PROPAGATION_SPEED; const pts = this.dataPoints; if (this._propagationAccum >= 1.0) { this._propagationAccum -= 1.0; const sampleRate = analyser.context.sampleRate; const nyquist = sampleRate / 2; const targetFreq = 22000; const scale = Math.min(1.0, targetFreq / nyquist); const len = Math.floor(dataArray.length * scale); const line = this.history[this.writeIndex]; if (line) { for (let i = 0; i < pts; i++) { line[i] = (dataArray[(this.xLookup[i] * len) | 0] / 255) * this.pLookup[i]; } } this.writeIndex = (this.writeIndex + 1) % this.historySize; } } if (this._paletteColor !== params.primaryColor) { this._buildPalette(params.primaryColor); } // === PASS 1: Scene === gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); gl.viewport(0, 0, width, height); gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); // Constants const size = Math.max(Math.abs(width * this._cos) + Math.abs(height * this._sin), Math.abs(width * this._sin) + Math.abs(height * this._cos)) * 1.15; const horizonY = size * 0.05; const frontY = size * 0.9; const depth = 2.0; const totalH = frontY - horizonY; const B = totalH / (1 - 1 / (1 + depth)); const A = frontY - B; // --- BATCH GEOMETRY GENERATION --- // Fill the vertex buffer with ALL lines for this frame let bufferOffset = 0; // Store draw commands to execute later: { start, count, colorIndex } const drawCommands = []; // Reuse temporary points array if (!this._tempPoints) this._tempPoints = []; const points = this._tempPoints; const pts = this.dataPoints; const cx = width / 2; const cy = height / 2; const cosR = this._cos; const sinR = this._sin; const offsetX = -size / 2; const offsetY = -size / 2; for (let i = this.historySize - 1; i >= 0; i--) { const idx = (this.writeIndex + i) % this.historySize; const historyLine = this.history[idx]; const p = 1 - i / (this.historySize - 1); const z = 1 + p * depth; const scale = 1 / z; const y = A + B / z; const lw = size * scale * 1.5; const margin = (size - lw) * 0.5; const amp = 200 * scale; const lineWidth = Math.max(1, 8 * scale + params.kick * 3); // Generate points points.length = 0; for (let j = 0; j < pts; j++) { const rx = margin + this.xLookup[j] * lw; const ry = y - historyLine[j] * amp; const dx = rx + offsetX; const dy = ry + offsetY; points.push({ x: dx * cosR - dy * sinR + cx, y: dx * sinR + dy * cosR + cy }); } // Write to buffer const vertexCount = this._generateLineQuads(points, lineWidth / 2, width, height, this.vertexBuffer, bufferOffset); if (vertexCount > 0) { drawCommands.push({ start: bufferOffset / 3, // Start vertex index count: vertexCount / 3, // Number of vertices colorIndex: i }); bufferOffset += vertexCount; // Advance by number of floats } } // --- UPLOAD ONCE --- gl.bindBuffer(gl.ARRAY_BUFFER, this.lineBuffer); // Upload only the used portion of the pre-allocated buffer gl.bufferData(gl.ARRAY_BUFFER, this.vertexBuffer.subarray(0, bufferOffset), gl.DYNAMIC_DRAW); gl.enableVertexAttribArray(this.line_a_posEdge); gl.vertexAttribPointer(this.line_a_posEdge, 3, gl.FLOAT, false, 0, 0); // --- DRAW BATCH --- gl.useProgram(this.lineProgram); gl.enable(gl.BLEND); if (isDark) { gl.blendFunc(gl.ONE, gl.ONE); } else { gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); } for (const cmd of drawCommands) { const color = this._paletteRGB[cmd.colorIndex] || [1, 1, 1]; gl.uniform3f(this.line_u_color, color[0], color[1], color[2]); gl.drawArrays(gl.TRIANGLES, cmd.start, cmd.count); } gl.disable(gl.BLEND); // === PASS 2: Bloom (Half Res) === const blurW = Math.max(1, width >> 1); const blurH = Math.max(1, height >> 1); gl.bindFramebuffer(gl.FRAMEBUFFER, this.blurFramebuffer); gl.viewport(0, 0, blurW, blurH); gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); gl.useProgram(this.brightnessProgram); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this.sceneTexture); gl.uniform1i(this.brightness_u_texture, 0); gl.uniform1f(this.brightness_u_threshold, 0.0); gl.uniform1f(this.brightness_u_isDarkTheme, isDark ? 1.0 : 0.0); gl.bindBuffer(gl.ARRAY_BUFFER, this.quadBuffer); gl.enableVertexAttribArray(this.brightness_a_position); gl.vertexAttribPointer(this.brightness_a_position, 2, gl.FLOAT, false, 0, 0); gl.drawArrays(gl.TRIANGLES, 0, 6); // === PASS 3: Gaussian Blur (Ping Pong) === gl.useProgram(this.blurProgram); const iterations = 4; let horizontal = true; for (let i = 0; i < iterations * 2; i++) { const destFBO = horizontal ? this.blurFinalFramebuffer : this.blurFramebuffer; const srcTex = horizontal ? this.blurTexture : this.blurFinalTexture; const spread = 1.0 + i * 0.75; gl.bindFramebuffer(gl.FRAMEBUFFER, destFBO); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, srcTex); gl.uniform1i(this.blur_u_texture, 0); gl.uniform2f(this.blur_u_resolution, blurW, blurH); gl.uniform2f(this.blur_u_direction, horizontal ? 1.0 : 0.0, horizontal ? 0.0 : 1.0); gl.uniform1f(this.blur_u_spread, spread); gl.drawArrays(gl.TRIANGLES, 0, 6); horizontal = !horizontal; } // === PASS 4: Composite === gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport(0, 0, width, height); if (params.mode !== 'blended') { const bg = isDark ? [0.02, 0.02, 0.02, 1] : [0.9, 0.9, 0.9, 1]; gl.clearColor(bg[0], bg[1], bg[2], bg[3]); } else if (isDark) { gl.clearColor(0, 0, 0, 0.4); } else { gl.clearColor(0.95, 0.95, 0.95, 0.4); } gl.clear(gl.COLOR_BUFFER_BIT); gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); gl.useProgram(this.compositeProgram); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this.sceneTexture); gl.uniform1i(this.composite_u_scene, 0); gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, horizontal ? this.blurTexture : this.blurFinalTexture); gl.uniform1i(this.composite_u_blur, 1); const glowBoost = 1.0 + params.kick; gl.uniform1f(this.composite_u_glowStrength, UnknownPleasuresWebGL.GLOW_INTENSITY * glowBoost); gl.uniform1f(this.composite_u_noiseStrength, UnknownPleasuresWebGL.NOISE_STRENGTH); gl.uniform1f(this.composite_u_isDarkTheme, isDark ? 1.0 : 0.0); gl.uniform1f(this.composite_u_time, performance.now() / 1000.0); gl.bindBuffer(gl.ARRAY_BUFFER, this.quadBuffer); gl.enableVertexAttribArray(this.composite_a_position); gl.vertexAttribPointer(this.composite_a_position, 2, gl.FLOAT, false, 0, 0); gl.drawArrays(gl.TRIANGLES, 0, 6); } }