### Standalone HTML Complete Shader Template (Must Be Strictly Followed) **IMPORTANT: The following template can be copied directly; every line must be strictly followed**: **Vertex Shader** (common to all shaders): ```glsl #version 300 es in vec4 iPosition; void main() { gl_Position = iPosition; } ``` **Fragment Shader Buffer A Example** (particle physics simulation): ```glsl #version 300 es precision highp float; // IMPORTANT: Critical: uniforms must be declared; ShaderToy's iTime/iResolution etc. are global variables uniform float iTime; uniform vec2 iResolution; uniform int iFrame; uniform vec4 iMouse; // IMPORTANT: Critical: mainImage parameters need manual extraction // ShaderToy: void mainImage(out vec4 fragColor, in vec2 fragCoord) // Adapted to: out vec4 fragColor; void main() { vec2 fragCoord = gl_FragCoord.xy; vec2 uv = fragCoord / iResolution; // IMPORTANT: Critical: texture2D → texture vec4 prev = texture(iChannel0, uv); // ... particle physics logic ... fragColor = vec4(pos, vel); } ``` **Fragment Shader Image Example**: ```glsl #version 300 es precision highp float; uniform float iTime; uniform vec2 iResolution; uniform int iFrame; uniform vec4 iMouse; uniform sampler2D iChannel0; out vec4 fragColor; void main() { vec2 fragCoord = gl_FragCoord.xy; vec2 uv = fragCoord / iResolution; // IMPORTANT: Critical: texture2D → texture, mainImage → standard main vec4 col = texture(iChannel0, uv); // Rendering logic col = col / (1.0 + col); // Tone mapping fragColor = col; } ``` **IMPORTANT: Common GLSL ES 3.00 Errors** (must be avoided): 1. **#version must be on the first line** - Any comments/blank lines will cause "version directive must occur on the first line" error 2. **in/out qualifiers** - WebGL1's attribute/varying must be changed to in/out in ES3 3. **texture function** - ES3 uses `texture(sampler, uv)`, not `texture2D(sampler, uv)` 4. **Type strictness** - `vec4 = float` is illegal, must use `vec4(v, v, v, v)` or `vec4(v)` or `vec4(vec3(v), 1.0)` ## Standalone HTML Multi-Channel Framebuffer Implementation **IMPORTANT: Multi-Channel Rendering Pipeline Core Pitfalls**: ShaderToy code requires manual Framebuffer rendering pipeline implementation. The following template demonstrates the correct approach: ```javascript // Correct multi-channel Framebuffer creation const NUM_BUFFERS = 2; // Buffer A, Buffer B const buffers = []; const textures = []; // Check float texture linear filtering extension const ext = gl.getExtension('EXT_color_buffer_float'); const floatLinear = gl.getExtension('OES_texture_float_linear'); // Each Buffer needs an independent Framebuffer + texture for (let i = 0; i < NUM_BUFFERS; i++) { const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); // IMPORTANT: Critical: Must use UNSIGNED_BYTE format without EXT_color_buffer_float extension! // RGBA16F/RGBA32F require the extension, otherwise GL_INVALID_OPERATION // Float textures need EXT_color_buffer_float; RGBA16F supports HDR data if (ext) { gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, width, height, 0, gl.RGBA, gl.FLOAT, null); } else { gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); } // IMPORTANT: Critical: Texture parameters must be set, otherwise GL_INVALID_FRAMEBUFFER // IMPORTANT: Float textures use NEAREST, or require OES_texture_float_linear extension for LINEAR // IMPORTANT: Critical: Float textures must use CLAMP_TO_EDGE wrap mode; REPEAT is not supported for float textures // IMPORTANT: Critical: Must fall back to UNSIGNED_BYTE format without EXT_color_buffer_float extension const filterMode = (ext && floatLinear) ? gl.LINEAR : gl.NEAREST; gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filterMode); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filterMode); // IMPORTANT: Must use CLAMP_TO_EDGE: float textures do not support REPEAT 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); const fbo = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); // IMPORTANT: Critical: Check Framebuffer completeness const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); if (status !== gl.FRAMEBUFFER_COMPLETE) { console.error("Framebuffer incomplete:", status); } textures.push(texture); buffers.push(fbo); } gl.bindFramebuffer(gl.FRAMEBUFFER, null); // Render loop: render to Buffer first, then render to screen function render() { // 1. Render to Buffer A (self-feedback reads previous Buffer) gl.bindFramebuffer(gl.FRAMEBUFFER, buffers[0]); gl.viewport(0, 0, width, height); // Bind previous frame texture to iChannel0 gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, textures[1]); // Read from other Buffer // Set uniforms etc... // Execute shader rendering // 2. Swap Buffers (simulate self-feedback) // IMPORTANT: Critical: Must swap textures for next frame reading; FBO handles remain unchanged [textures[0], textures[1]] = [textures[1], textures[0]]; // 3. Render to screen gl.bindFramebuffer(gl.FRAMEBUFFER, null); // Bind Buffer result to texture gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, textures[0]); // Execute Image pass shader } ``` **IMPORTANT: Common Errors** (JavaScript/WebGL side): 1. **Missing texture parameters** - Must set `TEXTURE_MIN_FILTER`, `TEXTURE_MAG_FILTER`, `TEXTURE_WRAP_S/T` 2. **Missing Framebuffer completeness check** - `gl.checkFramebufferStatus()` must return `FRAMEBUFFER_COMPLETE` before use 3. **Float texture extension** - `gl.RGBA16F` requires `EXT_color_buffer_float` extension, otherwise fall back to `gl.UNSIGNED_BYTE` 4. **Buffer ping-pong error** - Self-feedback must use 2 independent FBOs alternating read/write; a single FBO + texture swap causes "Feedback loop" error 5. **Particle system empty texture initialization** - Textures are empty before the first frame; shaders reading default values cause render failure — must execute initPass() to pre-render # Multi-Pass Buffer Techniques ## Use Cases When single-frame computation cannot achieve the desired effect and cross-frame data persistence or multi-stage processing pipelines are needed, use multi-pass buffers: - **Temporal accumulation**: Motion blur, TAA, progressive rendering - **Physics simulation**: Fluids, reaction-diffusion, particle systems - **Persistent state**: Game state, particle positions/velocities, interaction history - **Deferred rendering**: G-Buffer → post-processing → compositing - **Post-processing chains**: HDR Bloom (downsample → blur → composite) - **Iterative solvers**: Poisson solver, vorticity confinement, multi-scale computation ## Core Principles Multi-pass buffers split the rendering pipeline into multiple Buffers, each outputting a texture as input for the next stage. ### Self-Feedback A Buffer reads its own previous frame output, achieving cross-frame state persistence: `x(n+1) = f(x(n))` ``` Buffer A (frame N) reads → Buffer A (frame N-1) output ``` ### Pipeline Chaining Multiple Buffers process in sequence: ``` Buffer A (geometry) → Buffer B (blur H) → Buffer C (blur V) → Image (compositing) ``` ### Structured Data Storage Specific pixels serve as data registers, read precisely via `texelFetch`: ``` texel (0,0) = ball position+velocity (vec4) texel (1,0) = paddle position texel (x,1)-(x,12) = brick grid state ``` ### Key Mathematical Patterns - **Fluid self-advection**: `newPos = texture(buf, uv - dt * velocity * texelSize)` - **Gaussian blur**: `sum += texture(buf, uv + offset_i) * weight_i` - **Temporal blending**: `result = mix(newFrame, prevFrame, blendWeight)` - **Vorticity confinement**: `vortForce = curl × normalize(gradient(|curl|))` ## Implementation Steps ### Step 1: Minimal Self-Feedback Loop Buffer A (iChannel0 → Buffer A self-feedback): ```glsl void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = fragCoord / iResolution.xy; vec4 prev = texture(iChannel0, uv); // New content: procedural noise contour lines float n = noise(vec3(uv * 8.0, 0.1 * iTime)); float v = sin(6.2832 * 10.0 * n); v = smoothstep(1.0, 0.0, 0.5 * abs(v) / fwidth(v)); vec4 newContent = 0.5 + 0.5 * sin(12.0 * n + vec4(0, 2.1, -2.1, 0)); // Decay + offset blending vec4 decayed = exp(-33.0 / iResolution.y) * texture(iChannel0, (fragCoord + vec2(1.0, sin(iTime))) / iResolution.xy); fragColor = mix(decayed, newContent, v); // Initialization guard if (iFrame < 4) fragColor = vec4(0.5); } ``` Image (iChannel0 → Buffer A): ```glsl void mainImage(out vec4 fragColor, in vec2 fragCoord) { fragColor = texture(iChannel0, fragCoord / iResolution.xy); } ``` ### Step 2: Fluid Self-Advection Buffer A (iChannel0 → Buffer A self-feedback): ```glsl #define ROT_NUM 5 #define SCALE_NUM 20 const float ang = 6.2832 / float(ROT_NUM); mat2 m = mat2(cos(ang), sin(ang), -sin(ang), cos(ang)); float getRot(vec2 pos, vec2 b) { vec2 p = b; float rot = 0.0; for (int i = 0; i < ROT_NUM; i++) { rot += dot(texture(iChannel0, fract((pos + p) / iResolution.xy)).xy - vec2(0.5), p.yx * vec2(1, -1)); p = m * p; } return rot / float(ROT_NUM) / dot(b, b); } void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 pos = fragCoord; float rnd = fract(sin(float(iFrame) * 12.9898) * 43758.5453); vec2 b = vec2(cos(ang * rnd), sin(ang * rnd)); // Multi-scale rotation sampling vec2 v = vec2(0); float bbMax = 0.7 * iResolution.y; bbMax *= bbMax; for (int l = 0; l < SCALE_NUM; l++) { if (dot(b, b) > bbMax) break; vec2 p = b; for (int i = 0; i < ROT_NUM; i++) { v += p.yx * getRot(pos + p, b); p = m * p; } b *= 2.0; } // Self-advection fragColor = texture(iChannel0, fract((pos + v * vec2(-1, 1) * 2.0) / iResolution.xy)); // Center driving force vec2 scr = (fragCoord / iResolution.xy) * 2.0 - 1.0; fragColor.xy += 0.01 * scr / (dot(scr, scr) / 0.1 + 0.3); if (iFrame <= 4) fragColor = texture(iChannel1, fragCoord / iResolution.xy); } ``` ### Step 3-4: Navier-Stokes Solver + Chained Acceleration Buffer A / B / C use identical code (via Common tab's `solveFluid`): ```glsl void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = fragCoord / iResolution.xy; vec2 w = 1.0 / iResolution.xy; vec4 lastMouse = texelFetch(iChannel0, ivec2(0, 0), 0); vec4 data = solveFluid(iChannel0, uv, w, iTime, iMouse.xyz, lastMouse.xyz); if (iFrame < 20) data = vec4(0.5, 0, 0, 0); if (fragCoord.y < 1.0) data = iMouse; // Mouse state storage fragColor = data; } ``` iChannel bindings: A→C(prev frame), B→A, C→B — 3 iterations per frame. ### Step 5: Separable Gaussian Blur Buffer B (horizontal, iChannel0 → source Buffer) — Buffer C vertical direction is analogous, using y-axis offset: ```glsl void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 pixelSize = 1.0 / iResolution.xy; vec2 uv = fragCoord * pixelSize; float h = pixelSize.x; vec4 sum = vec4(0.0); // 9-tap Gaussian (sigma ≈ 2.0) sum += texture(iChannel0, fract(vec2(uv.x - 4.0*h, uv.y))) * 0.05; sum += texture(iChannel0, fract(vec2(uv.x - 3.0*h, uv.y))) * 0.09; sum += texture(iChannel0, fract(vec2(uv.x - 2.0*h, uv.y))) * 0.12; sum += texture(iChannel0, fract(vec2(uv.x - 1.0*h, uv.y))) * 0.15; sum += texture(iChannel0, fract(vec2(uv.x, uv.y))) * 0.16; sum += texture(iChannel0, fract(vec2(uv.x + 1.0*h, uv.y))) * 0.15; sum += texture(iChannel0, fract(vec2(uv.x + 2.0*h, uv.y))) * 0.12; sum += texture(iChannel0, fract(vec2(uv.x + 3.0*h, uv.y))) * 0.09; sum += texture(iChannel0, fract(vec2(uv.x + 4.0*h, uv.y))) * 0.05; fragColor = vec4(sum.xyz / 0.98, 1.0); } ``` ### Step 6: Structured State Storage ```glsl // Register address definitions const ivec2 txBallPosVel = ivec2(0, 0); const ivec2 txPaddlePos = ivec2(1, 0); const ivec2 txPoints = ivec2(2, 0); const ivec2 txState = ivec2(3, 0); const ivec4 txBricks = ivec4(0, 1, 13, 12); vec4 loadValue(ivec2 addr) { return texelFetch(iChannel0, addr, 0); } void storeValue(ivec2 addr, vec4 val, inout vec4 fragColor, ivec2 currentPixel) { fragColor = (currentPixel == addr) ? val : fragColor; } void storeValue(ivec4 rect, vec4 val, inout vec4 fragColor, ivec2 currentPixel) { fragColor = (currentPixel.x >= rect.x && currentPixel.y >= rect.y && currentPixel.x <= rect.z && currentPixel.y <= rect.w) ? val : fragColor; } void mainImage(out vec4 fragColor, in vec2 fragCoord) { ivec2 px = ivec2(fragCoord - 0.5); if (fragCoord.x > 14.0 || fragCoord.y > 14.0) discard; vec4 ballPosVel = loadValue(txBallPosVel); float paddlePos = loadValue(txPaddlePos).x; float points = loadValue(txPoints).x; if (iFrame == 0) { ballPosVel = vec4(0.0, -0.8, 0.6, 1.0); paddlePos = 0.0; points = 0.0; } // ... game logic update ... fragColor = loadValue(px); storeValue(txBallPosVel, ballPosVel, fragColor, px); storeValue(txPaddlePos, vec4(paddlePos, 0, 0, 0), fragColor, px); storeValue(txPoints, vec4(points, 0, 0, 0), fragColor, px); } ``` ### Step 7: Mouse State Inter-Frame Tracking ```glsl // Method 1: First-row pixel storage void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = fragCoord / iResolution.xy; vec2 w = 1.0 / iResolution.xy; vec4 lastMouse = texelFetch(iChannel0, ivec2(0, 0), 0); // ... simulation logic ... if (fragCoord.y < 1.0) fragColor = iMouse; } // Method 2: Fixed UV region storage vec2 mouseDelta() { vec2 pixelSize = 1.0 / iResolution.xy; float eighth = 1.0 / 8.0; vec4 oldMouse = texture(iChannel2, vec2(7.5 * eighth, 2.5 * eighth)); vec4 nowMouse = vec4(iMouse.xy / iResolution.xy, iMouse.zw / iResolution.xy); if (oldMouse.z > pixelSize.x && oldMouse.w > pixelSize.y && nowMouse.z > pixelSize.x && nowMouse.w > pixelSize.y) { return nowMouse.xy - oldMouse.xy; } return vec2(0.0); } ``` ## Complete Code Template A fully runnable fluid simulation shader (self-feedback + vorticity confinement + mouse interaction + color advection). ### Common tab ```glsl #define DT 0.15 #define VORTICITY_AMOUNT 0.11 #define VISCOSITY 0.55 #define PRESSURE_K 0.2 #define FORCE_RADIUS 0.001 #define FORCE_STRENGTH 0.001 #define VELOCITY_DECAY 1e-4 float mag2(vec2 p) { return dot(p, p); } vec2 emitter1(float t) { t *= 0.62; return vec2(0.12, 0.5 + sin(t) * 0.2); } vec2 emitter2(float t) { t *= 0.62; return vec2(0.88, 0.5 + cos(t + 1.5708) * 0.2); } vec4 solveFluid(sampler2D smp, vec2 uv, vec2 w, float time, vec3 mouse, vec3 lastMouse) { vec4 data = textureLod(smp, uv, 0.0); vec4 tr = textureLod(smp, uv + vec2(w.x, 0), 0.0); vec4 tl = textureLod(smp, uv - vec2(w.x, 0), 0.0); vec4 tu = textureLod(smp, uv + vec2(0, w.y), 0.0); vec4 td = textureLod(smp, uv - vec2(0, w.y), 0.0); vec3 dx = (tr.xyz - tl.xyz) * 0.5; vec3 dy = (tu.xyz - td.xyz) * 0.5; vec2 densDif = vec2(dx.z, dy.z); data.z -= DT * dot(vec3(densDif, dx.x + dy.y), data.xyz); vec2 laplacian = tu.xy + td.xy + tr.xy + tl.xy - 4.0 * data.xy; vec2 viscForce = vec2(VISCOSITY) * laplacian; data.xyw = textureLod(smp, uv - DT * data.xy * w, 0.0).xyw; vec2 newForce = vec2(0); newForce += 0.75 * vec2(0.0003, 0.00015) / (mag2(uv - emitter1(time)) + 0.0001); newForce -= 0.75 * vec2(0.0003, 0.00015) / (mag2(uv - emitter2(time)) + 0.0001); if (mouse.z > 1.0 && lastMouse.z > 1.0) { vec2 vv = clamp((mouse.xy * w - lastMouse.xy * w) * 400.0, -6.0, 6.0); newForce += FORCE_STRENGTH / (mag2(uv - mouse.xy * w) + FORCE_RADIUS) * vv; } data.xy += DT * (viscForce - PRESSURE_K / DT * densDif + newForce); data.xy = max(vec2(0), abs(data.xy) - VELOCITY_DECAY) * sign(data.xy); data.w = (tr.y - tl.y - tu.x + td.x); vec2 vort = vec2(abs(tu.w) - abs(td.w), abs(tl.w) - abs(tr.w)); vort *= VORTICITY_AMOUNT / length(vort + 1e-9) * data.w; data.xy += vort; data.y *= smoothstep(0.5, 0.48, abs(uv.y - 0.5)); data = clamp(data, vec4(vec2(-10), 0.5, -10.0), vec4(vec2(10), 3.0, 10.0)); return data; } ``` ### Buffer A / B / C (Fluid Sub-Steps 1/2/3) iChannel bindings: A←C(prev frame), B←A, C←B ```glsl void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = fragCoord / iResolution.xy; vec2 w = 1.0 / iResolution.xy; vec4 lastMouse = texelFetch(iChannel0, ivec2(0, 0), 0); vec4 data = solveFluid(iChannel0, uv, w, iTime, iMouse.xyz, lastMouse.xyz); if (iFrame < 20) data = vec4(0.5, 0, 0, 0); if (fragCoord.y < 1.0) data = iMouse; fragColor = data; } ``` ### Buffer D (Color Advection, iChannel0 → Buffer C, iChannel1 → Buffer D self-feedback) ```glsl #define COLOR_DECAY 0.004 #define COLOR_ADVECT_SCALE 3.0 vec3 getPalette(float x, vec3 c1, vec3 c2, vec3 p1, vec3 p2) { float x2 = fract(x / 2.0); x = fract(x); mat3 m = mat3(c1, p1, c2); mat3 m2 = mat3(c2, p2, c1); float omx = 1.0 - x; vec3 pws = vec3(omx * omx, 2.0 * omx * x, x * x); return clamp(mix(m * pws, m2 * pws, step(x2, 0.5)), 0.0, 1.0); } vec4 palette1(float x) { return vec4(getPalette(-x, vec3(0.2, 0.5, 0.7), vec3(0.9, 0.4, 0.1), vec3(1.0, 1.2, 0.5), vec3(1.0, -0.4, 0.0)), 1.0); } vec4 palette2(float x) { return vec4(getPalette(-x, vec3(0.4, 0.3, 0.5), vec3(0.9, 0.75, 0.4), vec3(0.1, 0.8, 1.3), vec3(1.25, -0.1, 0.1)), 1.0); } void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = fragCoord / iResolution.xy; vec2 w = 1.0 / iResolution.xy; vec2 velo = textureLod(iChannel0, uv, 0.0).xy; vec4 col = textureLod(iChannel1, uv - DT * velo * w * COLOR_ADVECT_SCALE, 0.0); vec2 mo = iMouse.xy / iResolution.xy; vec4 lastMouse = texelFetch(iChannel1, ivec2(0, 0), 0); if (iMouse.z > 1.0 && lastMouse.z > 1.0) { float str = smoothstep(-0.5, 1.0, length(mo - lastMouse.xy / iResolution.xy)); col += str * 0.0009 / (pow(length(uv - mo), 1.7) + 0.002) * palette2(-iTime * 0.7); } col += 0.0025 / (0.0005 + pow(length(uv - emitter1(iTime)), 1.75)) * DT * 0.12 * palette1(iTime * 0.05); col += 0.0025 / (0.0005 + pow(length(uv - emitter2(iTime)), 1.75)) * DT * 0.12 * palette2(iTime * 0.05 + 0.675); if (iFrame < 20) col = vec4(0.0); col = clamp(col, 0.0, 5.0); col = max(col - (0.0001 + col * COLOR_DECAY) * 0.5, 0.0); if (fragCoord.y < 1.0 && fragCoord.x < 1.0) col = iMouse; fragColor = col; } ``` ### Image (iChannel0 → Buffer D) ```glsl void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec4 col = textureLod(iChannel0, fragCoord / iResolution.xy, 0.0); if (fragCoord.y < 1.0 || fragCoord.y >= iResolution.y - 1.0) col = vec4(0); fragColor = col; } ``` ## Common Variants ### Variant 1: TAA Temporal Accumulation Anti-Aliasing ```glsl // Buffer A: Sub-pixel jittered rendering vec2 jitter = vec2(rand(uv + sin(iTime)), rand(uv + 1.0 + sin(iTime))) / iResolution.xy; vec3 eyevec = normalize(vec3(((uv + jitter) * 2.0 - 1.0) * vec2(aspect, 1.0), fov)); float blendWeight = 0.9; color = mix(color, texture(iChannel_self, uv).rgb, blendWeight); // Buffer C (TAA): YCoCg neighborhood clamping to prevent ghosting vec3 newYCC = RGBToYCoCg(newFrame); vec3 histYCC = RGBToYCoCg(history); vec3 colorAvg = ...; vec3 colorVar = ...; vec3 sigma = sqrt(max(vec3(0), colorVar - colorAvg * colorAvg)); histYCC = clamp(histYCC, colorAvg - 0.75 * sigma, colorAvg + 0.75 * sigma); result = YCoCgToRGB(mix(newYCC, histYCC, 0.95)); ``` ### Variant 2: Deferred Rendering G-Buffer ```glsl // Buffer A: G-Buffer output col.xy = (normal * camMat * 0.5 + 0.5).xy; // Normal col.z = 1.0 - abs((t * rd) * camMat).z / DMAX; // Depth col.w = dot(lightDir, nor) * 0.5 + 0.5; // Diffuse // Buffer B: Edge detection float checkSame(vec4 center, vec4 sample) { vec2 diffNormal = abs(center.xy - sample.xy) * Sensitivity.x; float diffDepth = abs(center.z - sample.z) * Sensitivity.y; return (diffNormal.x + diffNormal.y < 0.1 && diffDepth < 0.1) ? 1.0 : 0.0; } ``` ### Variant 3: HDR Bloom ```glsl // Buffer B: MIP pyramid (multi-level downsampling packed into one texture) vec2 CalcOffset(float octave) { vec2 offset = vec2(0); vec2 padding = vec2(10.0) / iResolution.xy; offset.x = -min(1.0, floor(octave / 3.0)) * (0.25 + padding.x); offset.y = -(1.0 - 1.0 / exp2(octave)) - padding.y * octave; offset.y += min(1.0, floor(octave / 3.0)) * 0.35; return offset; } // Image: Accumulate multi-level bloom + Reinhard tone mapping bloom += Grab(coord, 1.0, CalcOffset(0.0)) * 1.0; bloom += Grab(coord, 2.0, CalcOffset(1.0)) * 1.5; color = pow(color, vec3(1.5)); color = color / (1.0 + color); ``` ### Variant 4: Reaction-Diffusion System ```glsl // Buffer A: Gray-Scott reaction-diffusion vec2 uv_red = uv + vec2(dx.x, dy.x) * pixelSize * 8.0; float new_val = texture(iChannel0, fract(uv_red)).x; new_val += (noise.x - 0.5) * 0.0025 - 0.002; new_val -= (texture(iChannel_blur, fract(uv_red)).x - texture(iChannel_self, fract(uv_red)).x) * 0.047; ``` ### Variant 5: Multi-Scale MIP Fluid ```glsl for (int i = 0; i < NUM_SCALES; i++) { float mip = float(i); float stride = float(1 << i); vec4 t = stride * vec4(texel, -texel.y, 0); vec2 d = textureLod(sampler, fract(uv + t.ww), mip).xy; float w = WEIGHT_FUNCTION; result += w * computation(neighbors); } ``` ### Variant 6: Particle System (Position-Velocity Storage) **IMPORTANT: Particle System Implementation Key**: Particle state is stored in texture pixels, one particle per pixel. Rendering must iterate over the particle texture for sampling. **Buffer A (Particle Physics Simulation)**: ```glsl // Each texture pixel stores one particle: xy=position, zw=velocity // IMPORTANT: Critical: hash function must return vec2! Returning float causes type mismatch errors vec2 hash2(vec2 p) { return fract(sin(mat2(127.1, 311.7, 269.5, 183.3) * p) * 43758.5453); } void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = fragCoord / iResolution.xy; vec4 prev = texture(iChannel0, uv); vec2 pos = prev.xy; vec2 vel = prev.zw; // IMPORTANT: Initialization guard: use integer comparison + pixel-coordinate-based random (avoids particle overlap when time is too small) if (iFrame < 3) { // Use fragCoord (pixel coordinates) to ensure each particle has a unique position, independent of time // IMPORTANT: Critical: hash2 returns vec2, assign directly to pos/vel pos = hash2(fragCoord * 0.01 + vec2(1.7, 9.3)); vel = (hash2(fragCoord * 0.01 + vec2(5.3, 2.8)) - 0.5) * 0.02; fragColor = vec4(pos, vel); return; } // Physics update vel *= 0.98; // Damping // Mouse interaction vec2 mouse = iMouse.xy / iResolution.xy; if (iMouse.z > 0.0) { vec2 toMouse = mouse - pos; vel += normalize(toMouse + 0.001) * 0.0005 / (length(toMouse) + 0.1); } // Motion pos += vel * 60.0 * 0.016; pos = fract(pos); // Boundary wrapping fragColor = vec4(pos, vel); } ``` **Image (Render Particles)**: ```glsl void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = fragCoord / iResolution.xy; vec2 w = 1.0 / iResolution.xy; vec3 color = vec3(0.02, 0.02, 0.05); // Dark background // Iterate over particle texture for sampling (performance-sensitive, balance sample count) float glow = 0.0; for (float y = 0.0; y < 1.0; y += 0.02) { // IMPORTANT: Step size determines sampling density for (float x = 0.0; x < 1.0; x += 0.02) { vec4 particle = texture(iChannel0, vec2(x, y)); vec2 pPos = particle.xy; float dist = length(uv - pPos); float size = 0.01 + length(particle.zw) * 0.3; glow += exp(-dist * dist / (size * size)) * 0.15; } } // Particle glow color += vec3(0.3, 0.6, 1.0) * glow; // Vignette color *= 1.0 - length(uv - 0.5) * 0.8; // Tone mapping color = color / (1.0 + color); fragColor = vec4(color, 1.0); } ``` **Key Points**: - Buffer A self-feedback: iChannel0 → Buffer A - Image reads: iChannel0 → Buffer A (particle state) - Step size 0.02 produces 2500 samples; adjust based on performance - Particle size varies with velocity: `size = 0.01 + length(vel) * 0.3` **Complete JavaScript Rendering Pipeline (Particle System 3-Pass)**: ```javascript // Particle system needs 4 Framebuffers (2 each for Buffer A and Buffer B ping-pong) + screen output // Buffer A: Particle physics (self-feedback) - uses FBO 0/1 ping-pong // Buffer B: Density accumulation (reads Buffer A) - uses FBO 2/3 ping-pong // Image: Final rendering (reads Buffer A + Buffer B) // IMPORTANT: Critical: Must use 2 FBOs for ping-pong! Single FBO + texture swap causes // "Feedback loop formed between Framebuffer and active Texture" error const buffers = [null, null, null, null]; // [A_FBO0, A_FBO1, B_FBO0, B_FBO1] const textures = [null, null, null, null]; // [A_tex0, A_tex1, B_tex0, B_tex1] function createBuffers() { // Buffer A: 2 FBOs for ping-pong for (let i = 0; i < 2; i++) { const tex = createTexture(); textures[i] = tex; const fbo = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0); buffers[i] = fbo; } // Buffer B: 2 FBOs for ping-pong for (let i = 0; i < 2; i++) { const tex = createTexture(); textures[2 + i] = tex; const fbo = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0); buffers[2 + i] = fbo; } gl.bindFramebuffer(gl.FRAMEBUFFER, null); } // IMPORTANT: Critical: Initialization pre-rendering - must execute before the first frame! // Empty textures cause particle initialization failure (reading 0,0,0,0 makes all particles overlap) let aReadIdx = 0; // Current read FBO index (0 or 1) let bReadIdx = 0; // Buffer B current read FBO index (0 or 1) function initPass() { // ===== Buffer A Initialization ===== // Render first frame using FBO 0 gl.bindFramebuffer(gl.FRAMEBUFFER, buffers[0]); gl.viewport(0, 0, width, height); gl.useProgram(programBufferA); setupAttribute(programBufferA); // Bind FBO 1's texture as input (not yet rendered, but avoids binding errors) gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, textures[1]); gl.uniform1i(gl.getUniformLocation(programBufferA, 'iChannel0'), 0); gl.uniform2f(gl.getUniformLocation(programBufferA, 'iResolution'), width, height); gl.uniform1f(gl.getUniformLocation(programBufferA, 'iTime'), 0); gl.uniform1i(gl.getUniformLocation(programBufferA, 'iFrame'), 0); gl.uniform4f(gl.getUniformLocation(programBufferA, 'iMouse'), 0, 0, 0, 0); gl.drawArrays(gl.TRIANGLES, 0, 6); // Render second frame using FBO 1 (iFrame=1) gl.bindFramebuffer(gl.FRAMEBUFFER, buffers[1]); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, textures[0]); // Read FBO 0's result gl.uniform1i(gl.getUniformLocation(programBufferA, 'iFrame'), 1); gl.drawArrays(gl.TRIANGLES, 0, 6); // Render one more frame to ensure initialization is complete gl.bindFramebuffer(gl.FRAMEBUFFER, buffers[0]); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, textures[1]); gl.uniform1i(gl.getUniformLocation(programBufferA, 'iFrame'), 2); gl.drawArrays(gl.TRIANGLES, 0, 6); // ===== Buffer B Initialization ===== gl.bindFramebuffer(gl.FRAMEBUFFER, buffers[2]); // B_FBO0 gl.viewport(0, 0, width, height); gl.useProgram(programBufferB); setupAttribute(programBufferB); // Bind latest Buffer A result (FBO 0's result) gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, textures[0]); gl.uniform1i(gl.getUniformLocation(programBufferB, 'iChannel0'), 0); // Bind Buffer B previous frame (FBO 3's texture, not yet rendered) gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, textures[3]); gl.uniform1i(gl.getUniformLocation(programBufferB, 'iChannel1'), 1); gl.uniform2f(gl.getUniformLocation(programBufferB, 'iResolution'), width, height); gl.uniform1f(gl.getUniformLocation(programBufferB, 'iTime'), 0); gl.uniform1i(gl.getUniformLocation(programBufferB, 'iFrame'), 0); gl.drawArrays(gl.TRIANGLES, 0, 6); // Buffer B second frame gl.bindFramebuffer(gl.FRAMEBUFFER, buffers[3]); // B_FBO1 gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, textures[1]); // Buffer A latest gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, textures[2]); // Buffer B FBO0 result gl.uniform1i(gl.getUniformLocation(programBufferB, 'iFrame'), 1); gl.drawArrays(gl.TRIANGLES, 0, 6); // Initialize ping-pong indices aReadIdx = 0; // Next frame reads FBO 0 bReadIdx = 0; // Next frame reads FBO 2 gl.bindFramebuffer(gl.FRAMEBUFFER, null); } function render() { // ===== Pass 1: Buffer A (Particle Physics Self-Feedback) ===== // aReadIdx = 0: read FBO 0, write FBO 1 // aReadIdx = 1: read FBO 1, write FBO 0 const aWriteIdx = 1 - aReadIdx; // Write to target FBO (not the current read FBO) gl.bindFramebuffer(gl.FRAMEBUFFER, buffers[aWriteIdx]); gl.viewport(0, 0, width, height); // Read previous frame Buffer A texture (from current read FBO's texture) gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, textures[aReadIdx]); gl.uniform1i(uniformsBufferA.iChannel0, 0); gl.uniform2f(uniformsBufferA.iResolution, width, height); gl.uniform1f(uniformsBufferA.iTime, time); gl.uniform1i(uniformsBufferA.iFrame, frameCount); gl.uniform4f(uniformsBufferA.iMouse, mouse.x, mouse.y, mouse.z, mouse.w); // Render particle physics gl.useProgram(programBufferA); gl.drawArrays(gl.TRIANGLES, 0, 6); // Switch read index aReadIdx = aWriteIdx; // ===== Pass 2: Buffer B (Density Field) ===== const bWriteIdx = 1 - bReadIdx; gl.bindFramebuffer(gl.FRAMEBUFFER, buffers[2 + bWriteIdx]); // B_FBO0 or B_FBO1 gl.viewport(0, 0, width, height); // Bind current Buffer A particle state (use latest Buffer A result) gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, textures[aReadIdx]); // A latest result gl.uniform1i(uniformsBufferB.iChannel0, 0); // Bind previous frame Buffer B density (for accumulation) gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, textures[2 + bReadIdx]); // B_read gl.uniform1i(uniformsBufferB.iChannel1, 1); gl.uniform2f(uniformsBufferB.iResolution, width, height); gl.uniform1f(uniformsBufferB.iTime, time); gl.uniform1i(uniformsBufferB.iFrame, frameCount); // Render density accumulation gl.useProgram(programBufferB); gl.drawArrays(gl.TRIANGLES, 0, 6); // Switch Buffer B read index bReadIdx = bWriteIdx; // ===== Pass 3: Image (Final Rendering to Screen) ===== gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport(0, 0, width, height); // Bind Buffer A particles (use latest Buffer A result) gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, textures[aReadIdx]); gl.uniform1i(uniformsImage.iChannel0, 0); // Bind Buffer B density (use latest Buffer B result) gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, textures[2 + bReadIdx]); gl.uniform1i(uniformsImage.iChannel1, 1); gl.uniform2f(uniformsImage.iResolution, width, height); gl.uniform1f(uniformsImage.iTime, time); gl.uniform1i(uniformsImage.iFrame, frameCount); gl.uniform4f(uniformsImage.iMouse, mouse.x, mouse.y, mouse.z, mouse.w); // Render to screen gl.useProgram(programImage); gl.drawArrays(gl.TRIANGLES, 0, 6); } ``` **IMPORTANT: Key Points**: - **Must use 2 FBOs for ping-pong**: Each Buffer needs two independent FBOs (read FBO + write FBO); a single FBO + texture swap causes "Feedback loop" error - Use FBO index switching (not texture swapping): bind target FBO when writing, bind source texture when reading - Image pass binds the latest Buffer results (obtained via read index) ## Performance & Composition **Performance Optimization**: - Separable blur: N² → 2N samples - Bilinear tap trick: 5 samples replace 9-tap Gaussian - MIP sampling replaces large kernels: `textureLod` at high MIP levels ≈ large-range average - `discard` outside data regions to skip unnecessary computation - RGBA channel packing: velocity(xy) + density(z) + curl(w) in one vec4 - Chained sub-steps: A→B→C same code for 3x simulation speed - `if (dot(b,b) > bbMax) break;` adaptive early exit - `iFrame < 20` progressive initialization to prevent explosion **Typical Composition Patterns**: - **Fluid + Lighting**: Fluid buffer → Image computes gradient normals → diffuse + specular - **Fluid + Color Advection**: Separate Buffer tracks color field, advected by velocity field - **Scene + Bloom + TAA**: 4-Buffer pipeline (render → downsample → blur → composite tone mapping) - **G-Buffer + Screen-Space Effects**: 2-Buffer without temporal feedback (geometry → edge/SSAO/SSR → stylized compositing) - **State Storage + Visualization Separation**: Buffer A pure logic + Image pure rendering (`texelFetch` reads state + distance field drawing) ## Further Reading For complete step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/multipass-buffer.md)