Files
skills/shader-dev/techniques/multipass-buffer.md
shihao 6487becf60 Initial commit: add all skills files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:52:49 +08:00

923 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
### 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)