Shader development, camera keyframe interpolation, space transformations, and uniform UI controls for 3D rendering in p5.js v2 (WEBGL / WEBGL2 / WebGPU).
- Keyframes interpolation
- Space transformations
- Uniform UI
- Post-processing
- Utilities
- Drawing stuff
- Releases
- Usage
In p5.tree, matrix queries are immutable and cache-friendly: they never modify their arguments and always return new p5.Matrix instances.
let matrix = createMatrix(4)
let i = iMatrix(matrix)
// i !== matrixMost functions are available both as p5 helpers (global-style) and as renderer methods (p5.Renderer3D).
Camera path methods live on p5.Camera and are also exposed as p5 helpers that forward to the active camera.
Parameters may be provided in any order unless specified otherwise. When a function accepts a configuration/options object, it is always the last parameter.
A minimal camera-path API built on p5.Camera.copy() snapshots and p5.Camera.slerp() interpolation.
The path lives in user space as camera.path (an array of p5.Camera snapshots). You record keyframes, then play the path with a chosen speed and duration.
camera.addPath(...) appends a keyframe (camera snapshot) to camera.path.
Overloads
camera.addPath(eye, center, up, [opts])camera.addPath(view, [opts])camera.addPath([camera0, camera1, ...], [opts])camera.addPath([view0, view1, ...], [opts])camera.addPath([opts])
Notes
- In (1),
upis mandatory (no default assumed). - In (2),
viewis ap5.Matrix(4)or rawmat4[16]representing a world → camera transform. - (3) appends copies of existing camera snapshots.
- (4) appends copies of existing view matrices.
- (5) records the current camera state at call time.
Where:
eye,center,up→p5.Vectoror[x, y, z]view→p5.Matrix(4)or rawmat4[16]opts.reset(defaultfalse) clears the path before appending
Example
let cam
function setup() {
createCanvas(600, 400, WEBGL)
cam = createCamera()
cam.addPath([400, 0, 0], [0, 0, 0], [0, 1, 0])
cam.addPath(cam)
cam.addPath(cam.cameraMatrix)
}p5 wrappers
addPath(...) is also available as a p5 helper:
function setup() {
createCanvas(600, 400, WEBGL)
addPath([400, 0, 0], [0, 0, 0], [0, 1, 0])
}Start or update playback with:
camera.playPath(rate)
camera.playPath({ duration, loop, pingPong, onEnd, rate })Options:
duration→ frames per segment (default30)loop→ wrap at ends (defaultfalse)pingPong→ bounce at ends (defaultfalse)rate→ speed multiplier (default1)onEnd→ callback when playback finishes (non-looping)
If both loop and pingPong are true, pingPong takes precedence.
function setup() {
createCanvas(600, 400, WEBGL)
addPath([400, 0, 0], [0, 0, 0], [0, 1, 0], { reset: true })
playPath({ duration: 45, loop: true })
}Projection safety:
p5.Camera.slerp()requires identical projection matrices across keyframes.p5.treechecks compatibility while recording.
camera.seekPath(t) // t ∈ [0, 1]
camera.stopPath()
camera.resetPath()
camera.pathTime() // ∈ [0, 1]
camera.pathInfo() // snapshot objectseekPath(t)moves the camera along the path.stopPath()stops playback.resetPath()clears keyframes.pathTime()returns the current normalized path time.pathInfo()returns a snapshot of the current path state:keyframes(number) total keyframes in the path.segments(number) total segments (keyframes - 1).playing(boolean) whether playback is active.loop(boolean) whether looping is enabled.pingPong(boolean) whether ping-pong mode is enabled.rate(number) playback rate (signed).duration(number) frames per segment.time(number) normalized time in[0, 1]across the entire path.
Global helpers (seekPath, stopPath, resetPath, pathTime and pathInfo) forward to the active camera.
Matrix operations, matrix/frustum queries, and coordinate conversions.
createMatrix(...args): Explicit wrapper aroundnew p5.Matrix(...args)(identity creation).tMatrix(matrix): Returns the transpose ofmatrix.iMatrix(matrix): Returns the inverse ofmatrix.axbMatrix(a, b): Returns the product of theaandbmatrices.
Observation: all returned matrices are p5.Matrix instances.
pMatrix(): Returns the current projection matrix.mvMatrix([{ [vMatrix], [mMatrix] }]): Returns the modelview matrix.mMatrix(): Returns the model matrix (local → world), defined bytranslate/rotate/scaleand the currentpush/popstack.eMatrix(): Returns the current eye matrix (inverse ofvMatrix()). Also available onp5.Camera.vMatrix(): Returns the view matrix (inverse ofeMatrix()). Also available onp5.Camera.pvMatrix([{ [pMatrix], [vMatrix] }]): Returns projection × view.ipvMatrix([{ [pMatrix], [vMatrix], [pvMatrix] }]): Returns(pvMatrix)⁻¹.lMatrix([{ [from = createMatrix(4)], [to = this.eMatrix()], [matrix] }]): Returns the 4×4 matrix that transforms locations (points) fromfromtoto.dMatrix([{ [from = createMatrix(4)], [to = this.eMatrix()], [matrix] }]): Returns the 3×3 matrix that transforms directions (vectors) fromfromtoto(rotational part only).nMatrix([{ [vMatrix], [mMatrix], [mvMatrix] }]): Returns the normal matrix.
Observations
- All returned matrices are
p5.Matrixinstances. - Default values (
pMatrix,vMatrix,pvMatrix,eMatrix,mMatrix,mvMatrix) are those defined by the renderer at the moment the query is issued.
lPlane(),rPlane(),bPlane(),tPlane()nPlane(),fPlane()fov(): vertical field-of-view (radians).hfov(): horizontal field-of-view (radians).isOrtho():truefor orthographic,falsefor perspective.
mapLocation(point = p5.Tree.ORIGIN, [{ [from = p5.Tree.EYE], [to = p5.Tree.WORLD], [pMatrix], [vMatrix], [eMatrix], [pvMatrix], [ipvMatrix] }])mapDirection(vector = p5.Tree._k, [{ [from = p5.Tree.EYE], [to = p5.Tree.WORLD], [vMatrix], [eMatrix], [pMatrix] }])
Pass matrix parameters when you have cached those matrices (see Matrix queries) to speed up repeated conversions:
let cachedPVI
function draw() {
cachedPVI = ipvMatrix() // compute once per frame
// many fast conversions using the cached matrix
const a = mapLocation([0, 0, 0], { from: p5.Tree.WORLD, to: p5.Tree.SCREEN, ipvMatrix: cachedPVI })
const b = mapLocation([100, 0, 0], { from: p5.Tree.WORLD, to: p5.Tree.SCREEN, ipvMatrix: cachedPVI })
// ...
}You can also convert between local spaces by passing a p5.Matrix as from / to:
let modelMatrix
function draw() {
background(0)
push()
translate(80, 0, 0)
rotateY(frameCount * 0.01)
modelMatrix = mMatrix()
box(40)
pop()
// screen projection of the model origin
const s = mapLocation(p5.Tree.ORIGIN, { from: modelMatrix, to: p5.Tree.SCREEN })
beginHUD()
bullsEye({ x: s.x, y: s.y, size: 30 })
endHUD()
}Observations
- Returned vectors are
p5.Vectorinstances. fromandtomay be matrices or any of:p5.Tree.WORLD,p5.Tree.EYE,p5.Tree.SCREEN,p5.Tree.NDC,p5.Tree.MODEL.- When no matrix params are passed, current renderer values are used.
- The default
mapLocation()call (i.e. eye → world at origin) returns the camera world position. - The default
mapDirection()call returns the normalized camera viewing direction. - Useful vector constants:
p5.Tree.ORIGIN,p5.Tree._k,p5.Tree.i,p5.Tree.j,p5.Tree.k,p5.Tree._i,p5.Tree._j.
Draw directly in screen space, independent of the current camera and 3D transforms.
beginHUD()— enter HUD mode.endHUD()— restore normal 3D rendering.
In HUD mode, coordinates follow standard 2D conventions:
(x, y) ∈ [0, width] × [0, height], with origin at the top-left corner and y increasing downward. Rendering behavior matches image() and other 2D drawing functions.
Typical use cases include overlays, labels, debug markers, and screen-aligned UI elements.
beginHUD()
text('FPS: ' + frameRate().toFixed(1), 10, 20)
endHUD()A lightweight, renderer-agnostic system for managing shader uniforms and optional UI controls.
Separates:
- Core uniform logic (
createUniformUI) - Optional panel rendering (
ui.show())
Compatible with:
createFilterShader(GLSL)- WebGPU shaders
p5.strands
const ui = createUniformUI({
blurIntensity: { min: 0, max: 4, value: 2, step: 0.1 },
useLighting: { value: true },
tintColor: { value: '#ff8844' }
})Type inference:
- number → float slider
- boolean → checkbox
- color string → color picker
- array length 2/3/4 → vec2/3/4
options→ selectonClick→ button
Explicit override:
{ type: 'int', min: 0, max: 10 }ui.blurIntensity.value()
ui.blurIntensity.set(3)
ui.blurIntensity.reset()
const values = ui.values()Set the visibility of a specific control:
ui.blurIntensity.visible = false
ui.blurIntensity.visible = trueui.applyTo(shader)Optional remapping:
ui.applyTo(shader, {
blurIntensity: 'uBlur',
tintColor: {
uniform: 'uColor',
value: v => v.slice(0, 3)
}
})For p5.strands, bind explicitly inside .modify():
const blurIntensity = uniformFloat(() => ui.blurIntensity.value())No automatic applyTo() exists for strands.
ui.visible = true // show whole UI
ui.visible = false // hide whole UI
ui.remove()
ui.config({ x: 20, y: 20, width: 160, offset: 8 })You can mount the UI into a specific container (useful for Vue / Slidev / component setups):
const ui = createUniformUI(schema, {
parent: document.getElementById('sketch'),
x: 10,
y: 10
})When parent is provided, createUniformUI ensures the container has a proper positioning context so x/y anchoring behaves predictably.
Labels:
- omitted → uniform key
label: false→ no labellabel: 'Custom'→ custom text
A lightweight multi-pass post-processing pipeline for p5.Framebuffer, p5.strands, and standard WebGL rendering.
pipe() lets you chain one or more filter shaders (or strand-based filters), optionally display the result, and reuse internal ping/pong framebuffers efficiently.
Framebuffers are lazily allocated and cached, and automatically released when the sketch is removed.
pipe(source, passes, options)source→p5.Framebuffer, texture, image, or graphics.passes→ an array of filters or a single filter instance (e.g.baseFilterShader().modify(...)).options(optional):
| Option | Default | Description |
|---|---|---|
display |
true |
Draw final result to the main canvas. |
allocate |
true |
Allocate internal ping/pong framebuffers when missing. |
key |
'default' |
Cache key for internal ping/pong (advanced multi-pipeline use). |
ping, pong |
— | User-provided framebuffers (advanced override). |
clear |
true |
Clear ping/pong passes before drawing into them. |
clearDisplay |
true |
Clear canvas before final display. |
clearFn |
() => background(0) |
Clear strategy for passes. |
clearDisplayFn |
clearFn |
Clear strategy for display stage. |
draw |
full-canvas blit | Custom draw strategy per pass. |
pipe(layer, [noiseFilter, pixelFilter, blurFilter])Equivalent to:
pipe(layer, [noiseFilter, pixelFilter, blurFilter], {
display: true
})pipe(sceneFbo, scenePasses, { key: 'scene' })
pipe(minimapFbo, miniPasses, { key: 'mini', display: false })Each key maintains its own cached ping/pong pair.
Opaque passes + transparent final composite:
pipe(layer, passes, {
clearFn: () => background(0),
clearDisplayFn: () => clear()
})pipe(layer, passes, {
draw: tex => {
image(tex, -200, -150, 400, 300)
}
})- Internal ping/pong buffers are lazily resized to match the source.
- If only one pass is provided and no ping/pong are available, it falls back to
filter(). - When
display: false,pipe()returns the final framebuffer. - User-provided
ping/pongare never stored internally.
Release internally allocated ping/pong framebuffers.
releasePipe()
releasePipe('mini')
releasePipe(true)| Call | Effect |
|---|---|
releasePipe() |
Releases the default pipeline. |
releasePipe('key') |
Releases a specific keyed pipeline. |
releasePipe(true) |
Releases all cached pipelines. |
Internal resources are automatically released when the sketch is removed.
A small collection of helpers commonly needed in interactive 3D sketches:
texOffset(image):[1 / image.width, 1 / image.height]mousePosition([flip = true]): pixel-density-aware mouse position (optionally flips Y).pointerPosition(pointerX, pointerY, [flip = true]): pixel-density-aware pointer position (optionally flips Y).resolution(): pixel-density-aware canvas resolution[pd * width, pd * height]pixelRatio(location): world-to-pixel ratio at a world location.mousePicking([{ ... }])andpointerPicking(pointerX, pointerY, [{ ... }]): hit-test a screen-space circle/square tied to a model matrix.bounds([{ [eMatrix], [vMatrix] }]): frustum planes in general formax + by + cz + d = 0.visibility({ ... }): returnsp5.Tree.VISIBLE,p5.Tree.INVISIBLE, orp5.Tree.SEMIVISIBLE.
Primitives for visualizing common 3D concepts:
axes({ size, colors, bits })grid({ size, subdivisions })cross({ mMatrix, x, y, size, ... })bullsEye({ mMatrix, x, y, size, shape, ... })viewFrustum({ pg, bits, viewer, eMatrix, pMatrix, vMatrix })
Latest:
- https://cdn.jsdelivr.net/npm/p5.tree/dist/p5.tree.js
- https://cdn.jsdelivr.net/npm/p5.tree/dist/p5.tree.min.js
- https://cdn.jsdelivr.net/npm/p5.tree/dist/p5.tree.esm.js
- https://www.npmjs.com/package/p5.tree
Tagged example:
- https://cdn.jsdelivr.net/npm/p5.tree@0.0.14/dist/p5.tree.js
- https://cdn.jsdelivr.net/npm/p5.tree@0.0.14/dist/p5.tree.min.js
- https://cdn.jsdelivr.net/npm/p5.tree@0.0.14/dist/p5.tree.esm.js
<script src="https://cdn.jsdelivr.net/npm/p5/lib/p5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/p5.tree/dist/p5.tree.js"></script>
<script>
function setup() {
createCanvas(600, 400, WEBGL)
axes(100)
}
function draw() {
background(0.15)
orbitControl()
}
</script>Works in global and instance mode.
npm i p5 p5.treeimport p5 from 'p5'
import 'p5.tree'
const sketch = p => {
p.setup = () => {
p.createCanvas(600, 400, p.WEBGL)
p.axes(100)
}
p.draw = () => {
p.background(0.15)
p.orbitControl()
}
}
new p5(sketch)Modern, modular, instance-mode workflow.
