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 abaseFilterShader().modify()callback;createPanelwires their uniforms automatically. Press1,2, or3to 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
- p5.strands tutorial — Luke Plowden
- Writing shaders in JavaScript — Dave Pagurek
- Depth of field in p5.js — Dave Pagurek
- Diego Bulla’s post-effects study
- p5.FIP — 40+ post-effects by Darragh Nolan