Multi-pass post-processing with p5.strands and pipe(). Three filter passes — depth-of-field blur, value-noise warp, and pixelation — are chained over a scene framebuffer. Each pass is a baseFilterShader().modify() callback; createPanel wires their uniforms automatically. Press 1, 2, or 3 to rotate the pass ordering at runtime.


Shader passes as strands callbacks

Each post-processing pass is a baseFilterShader().modify() callback — a plain JavaScript function using the p5.strands DSL. The framework compiles each callback to WebGL2 at startup; no raw GLSL strings are needed.

dofFilter   = baseFilterShader().modify(dofCallback)
noiseFilter = baseFilterShader().modify(noiseCallback)
pixelFilter = baseFilterShader().modify(pixelCallback)

Uniforms bound with a string name (uniformFloat('blurIntensity')) are matched by createPanel’s target option and pushed automatically every frame. Uniforms bound with a closure (uniformFloat(() => focusVal)) are evaluated on the CPU each draw call — useful for values computed from space transforms or framebuffer state.


Uniform panels with target

Passing target: shader to createPanel means the panel calls setUniform on every dirty frame with no code in draw():

uiDof = createPanel({
  blurIntensity: { min: 0, max: 4, value: 1.5, step: 0.1, label: 'blur' }
}, { target: dofFilter, x: 10, y: 10, width: 130, labels: true, title: 'DOF', color: 'white' })

The schema key must match the uniform string name exactly. Unmatched uniforms (e.g. focusVal, layer.depth) are handled through closures instead.


Post-processing with pipe

pipe sequences an array of filter shaders over a source framebuffer, reusing internal ping/pong buffers — zero allocation per frame. Ordering is just array order:

pipe(layer, [dofFilter, noiseFilter, pixelFilter])

Re-ordering the array changes the visual outcome significantly. Pressing 1/2/3 rotates through the three cyclic orderings of the three passes, making it easy to explore how DOF, noise, and pixelation interact in different sequences.

const passes = [[dofFilter, noiseFilter, pixelFilter],
                [noiseFilter, pixelFilter, dofFilter],
                [pixelFilter, dofFilter, noiseFilter]][order]
pipe(layer, passes)

HUD overlay

beginHUD() / endHUD() switch the canvas to screen-space coordinates — origin at the top-left, y increasing downward — independent of any 3D camera. The pass label is drawn after pipe(), on top of the composited result:

pipe(layer, passes)

beginHUD()
text(orderLabel + '  (1/2/3 to rotate)', 10, height - 10)
endHUD()

References

  1. p5.strands tutorial — Luke Plowden
  2. Writing shaders in JavaScript — Dave Pagurek
  3. Depth of field in p5.js — Dave Pagurek
  4. Diego Bulla’s post-effects study
  5. p5.FIP — 40+ post-effects by Darragh Nolan