923 lines
34 KiB
Markdown
923 lines
34 KiB
Markdown
|
|
### 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)
|