Shader development and space transformations WEBGL p5.js library.
- Shaders
- Space transformations
- Utilities
- Drawing stuff
- Installation
- vs-code & vs-codium & gitpod hacking instructions
In p5.treegl
, all matrix operations are immutable. For example, invMatrix
does not modify its parameter but returns a new matrix:
let matrix = new p5.Matrix()
// invMatrix doesn't modify its matrix param, it gives a new value
let iMatrix = invMatrix(matrix)
// iMatrix !== matrix
Note that functions in the Shaders and Matrix operations sections are available only to p5
; those in the Matrix Queries, Space Transformations, Heads Up Display, Utilities, and Drawing Stuff sections are accessible to both p5
and p5.RendererGL instances; functions in the Frustum Queries section are available to p5
, p5.RendererGL, and p5.Matrix instances.
Parameters for p5.treegl
functions can be provided in any order, unless specified otherwise.
p5.treegl
simplifies the creation and application of shaders in WEBGL
. It covers the essentials from setting up shaders with Setup
, managing shader uniforms through a uniforms user interface, applying shaders using Apply shader
, enhancing visuals with Post-effects
, and setting common uniform variables using several Macros
.
Have a look at the toon shading, blur with focal point, post-effects, and gpu-based photomosaic examples.
The readShader
, makeShader
, and parseShader
functions take a fragment shader —specified in either GLSL ES 1.00
or GLSL ES 3.00
— to create and return a p5.Shader
object. They parse the fragment shader and use the matrices
param to infer the corresponding vertex shader, which is then logged to the console if no vertex shader source is provided. These functions also create a uniformsUI user interface with p5.Elements from the fragment shader's uniform variables' comments, and if a key
is provided, bind the shader to it, enabling its use as a Post-effect.
readShader(fragFilename, [vertFilename], [matrices = Tree.NONE], [uniformsUIConfig], [key], [successCallback], [failureCallback])
: Akin to loadShader, this function reads a fragment shader (and optionally a vertex shader) from a file, generates and logs a vertex shader if none is provided, and returns ap5.Shader
instance. It builds auniformsUI
user interface usinguniformsUIConfig
and, if akey
is given, it binds the shader to thiskey
for potential use as a Post-effect. Note thatreadShader
should be called within p5 preload. If both callbacks are provided, the first is used assuccessCallback
and the second asfailureCallback
.makeShader(fragStr, [vertStr], [matrices = Tree.NONE], [uniformsUIConfig], [key])
: Akin to createShader, this function takes a fragment shader source string (and optionally a vertex shader source string), generates and logs a vertex shader if none is provided, and returns ap5.Shader
. It also sets up auniformsUI
user interface withuniformsUIConfig
and, if akey
is provided, binds the shader to this key for potential use as a Post-effect. Note thatmakeShader
should be called within p5 setup.parseShader(fragSrc, [vertSrc], [matrices = Tree.NONE], [uniformsUIConfig], [key], [successCallback], [failureCallback])
: A high-level dispatcher function that determines whether to callreadShader
ormakeShader
based on the input parameters and should be called within p5 preload or p5 setup, accordingly. If both callbacks are provided, the first is used assuccessCallback
and the second asfailureCallback
.
Vertex shader generation observations
- The
matrices
parameter uses the following mask bit fieldsTree.vMatrix
,Tree.pMatrix
,Tree.mvMatrix
,Tree.pmvMatrix
,Tree.mMatrix
andTree.NONE
which is the default, to determine how vertices are projected onto NDC, according to the following rules:Mask bit fields gl_Position
Tree.NONE
aPosition
Tree.pmvMatrix
uModelViewProjectionMatrix * aPosition
Tree.mvMatrix
|Tree.pMatrix
uProjectionMatrix * uModelViewMatrix * aPosition
Tree.pMatrix
|Tree.vMatrix
|Tree.mMatrix
uProjectionMatrix * uViewMatrix * uModelMatrix * aPosition
Tree.vMatrix
|Tree.pMatrix
uProjectionMatrix * uViewMatrix * aPosition
Tree.pMatrix
|Tree.mMatrix
uProjectionMatrix * uModelMatrix * aPosition
Tree.mvMatrix
uModelViewMatrix * aPosition
Tree.vMatrix
|Tree.mMatrix
uViewMatrix * uModelMatrix * aPosition
Tree.pMatrix
uProjectionMatrix * aPosition
Tree.vMatrix
uViewMatrix * aPosition
Tree.mMatrix
uModelMatrix * aPosition
- The fragment shader's
varyings
variables are parsed to determine which and how vertex attributes should be interpolated from the vertex shader, following these naming conventions:type name space vec4
color4
color vec2
texcoords2
texture vec2
position2
local vec3
position3
local vec4
position4
eye vec3
normal3
eye
Examples:
-
Example 1:
parseShader(fragSrc)
WEBGL2
(GLSL ES 3.00
)fragSrc
, with novaryings
andhighp
precision
:// inferred vertex shader #version 300 es precision highp float; in vec3 aPosition; void main() { gl_Position = vec4(aPosition, 1.0); }
-
Example 2: Similar to Example 1 but with
WEBGL
(GLSL ES 1.00
):// inferred vertex shader precision highp float; attribute vec3 aPosition; void main() { gl_Position = vec4(aPosition, 1.0); }
-
Example 3:
parseShader(fragSrc, Tree.pmvMatrix)
WEBGL2
fragSrc
definingnormal3
andposition4
varyings, andmediump
precision
:// shader.frag excerpt #version 300 es precision mediump float; in vec3 normal3; in vec4 position4; // ...
infers the following vertex shader:
// inferred vertex shader #version 300 es precision mediump float; in vec3 aPosition; in vec3 aNormal; uniform mat3 uNormalMatrix; uniform mat4 uModelViewMatrix; uniform mat4 uModelViewProjectionMatrix; out vec3 normal3; out vec4 position4; void main() { normal3 = normalize(uNormalMatrix * aNormal); position4 = uModelViewMatrix * vec4(aPosition, 1.0); gl_Position = uModelViewProjectionMatrix * vec4(aPosition, 1.0); }
By parsing comments within glsl
shader
code, a shader.uniformsUI
object is built, mapping uniform variable names to p5.Element instances for interactively adjusting their values.
Supported elements include sliders for int
and float
types, color pickers for vec4
types, and checkboxes for bool
types, as highlighted in the following examples:
-
Sliders: Create a slider by annotating a uniform
float
orint
declaration in your shader code. The comment should specify the minimum value, maximum value, default value, and step value.Example:
uniform float speed; // 1, 10, 5, 0.1
This creates a slider for
speed
with a range from 1 to 10, a default value of 5, and a step of 0.1. The speed slider may be accessed ascustom_shader.uniformsUI.speed
. -
Color Picker: To create a color picker, annotate a
vec4
uniform. The comment can specify the default color using a CSS color name.Example:
uniform vec4 color; // 'magenta'
This creates a color picker for
color
with a default value of magenta. -
Checkboxes: For
bool
uniforms, a checkbox is created. The comment can specify the default state as true or false.Example:
uniform bool isActive; // true
This creates a checkbox for
isActive
that is checked by default.
These functions manipulate the uniformsUI
:
parseUniformsUI(shader, [{ [x = 0], [y = 0], [offset = 0], [width = 120], [color] }])
: Parsesshader
uniform variable comments into theshader.uniformsUI
map. It automatically callsconfigUniformsUI
with the provideduniformsUIConfig
object. This function should be invoked on custom shaders created with loadShader or createShader, whilereadShader
andmakeShader
already call it internally.configUniformsUI(shader, [{ [x = 0], [y = 0], [offset = 0], [width = 120], [color] }])
: Configures the layout and appearance of theshader.uniformsUI
elements based on the provided parameters:x
andy
: Set the initial position of the first UI element.offset
: Determines the spacing between consecutive UI elements.width
: Sets the width of the sliders and color pickers.color
: Specifies the text color for the UI elements' labels.
showUniformsUI(shader)
: Displays theshader.uniformsUI
elements associated with theshader
's uniforms. It attaches necessary event listeners to update the shader uniforms based on user interactions.hideUniformsUI(shader)
: Hides theshader.uniformsUI
elements and removes the event listeners, stopping any further updates to theshader
uniforms from ui interactions.resetUniformsUI(shader)
: Hides and resets theshader.uniformsUI
which should be restored with a call toparseUniformsUI(shader, configUniformsUI)
.setUniformsUI(shader)
: Iterates over theuniformsUI
map and sets the shader's uniforms based on the current values of the corresponding UI elements. This method should be called within thedraw
loop to ensure the shader uniforms are continuously updated. Note thatapplyShader
automatically calls this method.
The applyShader
function applies a shader
to a given scene
and target
, invoking setUniformsUI(shader)
and enabling the passing of custom uniform values not specified in uniformsUI.
applyShader(shader, [{ [target], [uniforms], [scene], [options] }])
appliesshader
to the specifiedtarget
(which can be the current context, a p5.Framebuffer or a p5.Graphics), emits theshader
uniformsUI
(callingshader.setUniformsUI()
) and theuniforms
object (formatted as{ uniform_1_name: value_1, ..., uniform_n_name: value_n }
), renders geometry by executingscene(options)
(defaults to an overlayingquad
if not specified), and returns thetarget
for method chaining.overlay(flip)
: A default rendering method used byapplyShader
, which covers the screen with a quad. It can also be called between beginHUD and endHUD to specify the scene geometry in screen space.
Post-effects1 play a key role in dynamic visual rendering, allowing for the interactive blending of various shader effects such as bloom, motion blur, ambient occlusion, and color grading, into a rendered scene. A user-space array of effects
may be sequentially applied to a source
with applyEffects(source, effects, [uniforms], [flip])
. Example usage:
// noise_shader
uniform sampler2D blender; // <- shared source should be named 'blender'
uniform float time;
// bloom_shader
uniform sampler2D blender; // <- shared source should be named 'blender'
uniform sampler2D depth;
// p5 setup
let layer
let effects[] // user space array of shaders
function setup() {
createCanvas(600, 400, WEBGL)
layer = createFramebuffer()
// instantiate shaders with keys for later
// uniform settings and add them to effects
effects.push(makeShader(noise_shader, 'noise'))
effects.push(makeShader(bloom_shader, 'bloom'))
}
// p5 draw
function draw() {
layer.begin()
// render scene into layer
layer.end()
// render target by applying effects to layer
let uniforms = { // emit uniforms to shaders (besides uniformsUI)
bloom: { depth: layer.depth }, // <- use bloom key
noise: { time: millis() / 1000 } // <- use noise key
}
const target = applyEffects(layer, effects, uniforms)
// display target using screen space coords
beginHUD()
image(target, 0, 0)
endHUD()
}
// p5 keyPressed
function keyPressed() {
// swap effects
[effects[0], effects[1]] = [effects[1], effects[0]]
}
applyEffects(source, effects, [uniforms = {}], [flip = true])
: Sequentially applies all effects (in the order they were added) to the source, which can be a p5.Framebuffer, p5.Graphics, p5.Image, or video p5.MediaElement. Theuniforms
param maps shaderkeys
to their respective uniform values, formatted as{ uniform_1_name: value_1, ..., uniform_n_name: value_n }
, provided that asampler2D uniform blender
variable is declared in each shader effect as a common fbo layer. Theflip
boolean indicates whether the final image should be vertically flipped. This method processes each effect, applying its shader with the corresponding uniforms (usingapplyShader
), and returns the final processed source, now modified by all effects.createBlender(effects, [options={}])
: Creates and attaches an fbo layer with specifiedoptions
to each shader in theeffects
array. IfcreateBlender
is not called,applyEffects
automatically generates a blender layer for each shader, utilizing default options.removeBlender(effects)
: Removes the individual fbo layers associated with each shader in theeffects
array, freeing up resources by invoking remove.
Retrieve image offset, mouse position, pointer position and screen resolution which are common uniform vec2
variables
texOffset(image)
which is the same as:return [1 / image.width, 1 / image.height]
.mousePosition([flip = true])
which is the same as:return [this.pixelDensity() * this.mouseX, this.pixelDensity() * (flip ? this.height - this.mouseY : this.mouseY)]
.pointerPosition(pointerX, pointerY, [flip = true])
which is the same as:return [this.pixelDensity() * pointerX, this.pixelDensity() * (flip ? this.height - pointerY : pointerY)]
. Available to both, thep5
object and p5.RendererGL instances. Note thatpointerX
should always be the first parameter andpointerY
the second.resolution()
which is the same as:return [this.pixelDensity() * this.width, this.pixelDensity() * this.height]
. Available to both, thep5
object and p5.RendererGL instances.
This section delves into matrix manipulations and queries which are essential for 3D rendering. It includes functions for matrix operations like creation, inversion, and multiplication in the Matrix operations subsection, and offers methods to retrieve transformation matrices and perform space conversions in Matrix queries, Frustum queries, and Coordinate Space conversions, facilitating detailed control over 3D scene transformations.
Have a look at the blur with focal point, post-effects, and visualizing perspective transformation to NDC examples.
iMatrix()
: Returns the identity matrix.tMatrix(matrix)
: Returns the tranpose ofmatrix
.invMatrix(matrix)
: Returns the inverse ofmatrix
.axbMatrix(a, b)
: Returns the product of thea
andb
matrices.
Observation: All returned matrices are instances of p5.Matrix.
pMatrix()
: Returns the current projection matrix.mvMatrix([{[vMatrix], [mMatrix]}])
: Returns the modelview matrix.mMatrix()
: Returns the model matrix. This matrix defines a local space transformation according to translate, rotate and scale commands. Refer also to push and pop.eMatrix()
: Returns the current eye matrix (the inverse ofvMatrix()
). In addition top5
and p5.RendererGL instances, this method is also available to p5.Camera objects.vMatrix()
: Returns the view matrix (the inverse ofeMatrix()
). In addition top5
and p5.RendererGL instances, this method is also available to p5.Camera objects.pvMatrix([{[pMatrix], [vMatrix]}])
: Returns the projection times view matrix.pvInvMatrix([{[pMatrix], [vMatrix], [pvMatrix]}])
: Returns thepvMatrix
inverse.lMatrix([{[from = iMatrix()], [to = this.eMatrix()]}])
: Returns the 4x4 matrix that transforms locations (points) from matrixfrom
to matrixto
.dMatrix([{[from = iMatrix()], [to = this.eMatrix()]}])
: Returns the 3x3 matrix (only rotational part is needed) that transforms directions (vectors) from matrixfrom
to matrixto
. ThenMatrix
below is a special case of this one.nMatrix([{[vMatrix], [mMatrix], [mvMatrix]}])
: Returns the normal matrix.
Observations
- All returned matrices are instances of p5.Matrix.
- The
pMatrix
,vMatrix
,pvMatrix
,eMatrix
,mMatrix
andmvMatrix
default values are those defined by the renderer at the moment the query is issued.
lPlane()
: Returns the left clipping plane.rPlane()
: Returns the right clipping plane.bPlane()
: Returns the bottom clipping plane.tPlane()
: Returns the top clipping plane.nPlane()
: Returns the near clipping plane.fPlane()
: Returns the far clipping plane.fov()
: Returns the vertical field-of-view (fov) in radians.hfov()
: Returns the horizontal field-of-view (hfov) in radians.isOrtho()
: Returns the camera projection type:true
for orthographic andfalse
for perspective.
parsePosition(vector = Tree.ORIGIN, [{[from = Tree.EYE], [to = Tree.WORLD], [pMatrix], [vMatrix], [eMatrix], [pvMatrix], [pvInvMatrix]}])
: transforms locations (points) from matrixfrom
to matrixto
.parseDirection(vector = Tree._k, [{[from = Tree.EYE], [to = Tree.WORLD], [vMatrix], [eMatrix], [pMatrix]}])
: transforms directions (vectors) from matrixfrom
to matrixto
.
Pass matrix params when you cached those matrices (see the previous section), either to speedup computations, e.g.,
let pvInv
function draw() {
// cache pvInv at the beginning of the rendering loop
// note that this matrix rarely change within the iteration
pvInv = pvInvMatrix()
// ...
// speedup parsePosition
parsePosition(vector, { from: Tree.WORLD, to: Tree.SCREEN, pvInvMatrix: pvInv })
parsePosition(vector, { from: Tree.WORLD, to: Tree.SCREEN, pvInvMatrix: pvInv })
// ... many more parsePosition calls....
// ... all the above parsePosition calls used the (only computed once) cached pvInv matrix
}
or to transform points (and vectors) between local spaces, e.g.,
let model
function draw() {
// ...
// save model matrix as it is set just before drawing your model
model = mMatrix()
drawModel()
// continue drawing your tree...
// let's draw a bulls eye at the model origin screen projection
push()
let screenProjection = parsePosition(Tree.ORIGIN, { from: model, to: Tree.SCREEN })
// which is the same as:
// let screenProjection = parsePosition(createVector(0, 0, 0), { from: model, to: Tree.SCREEN });
// or,
// let screenProjection = parsePosition([0, 0, 0], { from: model, to: Tree.SCREEN });
// or, more simply:
// let screenProjection = parsePosition({ from: model, to: Tree.SCREEN });
bullsEye({ x: screenProjection.x, y: screenProjection.y })
pop()
}
Observations
- Returned transformed vectors are instances of p5.Vector.
from
andto
may also be specified as either:Tree.WORLD
,Tree.EYE
,Tree.SCREEN
,Tree.NDC
orTree.MODEL
.- When no matrix params (
eMatrix
,pMatrix
,...) are passed the renderer current values are used instead. - The default
parsePosition
call (i.e.,parsePosition(Tree.ORIGIN, {from: Tree.EYE, to: Tree.WORLD)
) returns the camera world position. - Note that the default
parseDirection
call (i.e.,parseDirection(Tree._k, {from: Tree.EYE, to: Tree.WORLD)
) returns the normalized camera viewing direction. - Other useful vector constants, different than
Tree.ORIGIN
(i.e.,[0, 0, 0]
) andTree._k
(i.e.,[0, 0, -1]
), are:Tree.i
(i.e.,[1, 0, 0]
),Tree.j
(i.e.,[0, 1, 0]
),Tree.k
(i.e.,[0, 0, 1]
),Tree._i
(i.e.,[-1, 0, 0]
) andTree._j
(i.e.,[0, -1, 0]
).
beginHUD()
: Begins Heads Up Display, so that geometry specified betweenbeginHUD()
andendHUD()
is defined in window space. Should always be used in conjunction withendHUD
.endHUD()
: Ends Heads Up Display, so that geometry specified betweenbeginHUD()
andendHUD()
is defined in window space. Should always be used in conjunction withbeginHUD
.
This section comprises a collection of handy functions designed to facilitate common tasks in 3D graphics, such as pixel ratio calculations, mouse picking and visibility determination.
pixelRatio(location)
: Returns the world to pixel ratio units at given world location, i.e., a line ofn * pixelRatio(location)
world units will be projected with a length ofn
pixels on screen.mousePicking([{[mMatrix = this.mMatrix()], [x], [y], [size = 50], [shape = Tree.CIRCLE], [eMatrix], [pMatrix], [vMatrix], [pvMatrix]}])
: same asreturn this.pointerPicking(this.mouseX, this.mouseY, { mMatrix: mMatrix, x: x, y: y, size: size, shape: shape, eMatrix: eMatrix, pMatrix: pMatrix, vMatrix: vMatrix, pvMatrix: pvMatrix })
(see below).pointerPicking(pointerX, pointerY, [{[mMatrix = this.mMatrix()], [x], [y], [size = 50], [shape = Tree.CIRCLE], [eMatrix], [pMatrix], [vMatrix], [pvMatrix]}])
: Returnstrue
ifpointerX
,pointerY
lies within the screen space circle centered at (x
,y
) and havingsize
diameter. PassmMatrix
to compute (x
,y
) as the screen space projection of the local space origin (defined bymMatrix
), havingsize
as its bounding sphere diameter. UseTree.SQUARE
to use a squared shape instead of a circled one. Note thatpointerX
should always be specified beforepointerY
.visibility([{[center], [radius], [corner1], [corner2], [bounds]}])
: Returns object visibility, either asTree.VISIBLE
,Tree.INVISIBLE
, orTree.SEMIVISIBLE
. Object may be either a point:visibility({ center, [bounds = this.bounds([{[eMatrix = this.eMatrix()], [vMatrix = this.vMatrix()]}])]})
, a ball:visibility({ center, radius, [bounds = this.bounds()]})
or an axis-aligned box:visibility({ corner1, corner2, [bounds = this.bounds()]})
.bounds([{[eMatrix], [vMatrix]}])
: Returns the general form of the current frustum six plane equations, i.e., ax + by + cz + d = 0, formatted as an object literal having keys:Tree.LEFT
,Tree.RIGHT
,Tree.BOTTOM
,Tree.TOP
,Tree.NEAR
andTree.FAR
, e.g., access the near plane coefficients as:let bounds = bounds() let near = bounds[Tree.NEAR] // near.a, near.b, near.c and near.d
This section includes a range of functions designed for visualizing various graphical elements in 3D space, such as axes, grids, bullseyes, and view frustums. These tools are essential for debugging, illustrating spatial relationships, and enhancing the visual comprehension of 3D scenes. Have a look at the toon shading, blur with focal point, post-effects, and visualizing perspective transformation to NDC examples.
parseGeometry(fn, ...args)
: Captures geometry by runningfn
, which should be passed as first parameter, withargs
, then returns a p5.Geometry object. Appliescolors
fromargs
if specified, or clears them, and computes normals.axes([{ [size = 100], [colors = ['Red', 'Lime', 'DodgerBlue']], [bits = Tree.LABELS | Tree.X | Tree.Y | Tree.Z] }])
: Draws axes with givensize
in world units,colors
, and bitwise mask that may be composed ofTree.X
,Tree._X
,Tree.Y
,Tree._Y
,Tree.Z
,Tree._Z
andTree.LABELS
bits
.grid([{ [size = 100], [subdivisions = 10], [style = Tree.DOTS] }])
: Draws grid with givensize
in world units,subdivisions
anddotted
(Tree.DOTS
) or solid (Tree.SOLID
) lines.cross([{ [mMatrix = this.mMatrix()], [x], [y], [size = 50], [eMatrix], [pMatrix], [vMatrix], [pvMatrix] }])
: Draws a cross atx
,y
screen coordinates with givensize
in pixels. PassmMatrix
to compute (x
,y
) as the screen space projection of the local space origin (defined bymMatrix
).bullsEye([{ [mMatrix = this.mMatrix()], [x], [y], [size = 50], [shape = Tree.CIRCLE], [eMatrix], [pMatrix], [vMatrix], [pvMatrix] }])
: Draws a circled bullseye (useTree.SQUARE
to draw it as a square) atx
,y
screen coordinates with givensize
in pixels. PassmMatrix
to compute (x
,y
) as the screen space projection of the local space origin (defined bymMatrix
).viewFrustum([{ [pg], [bits = Tree.NEAR | Tree.FAR], [viewer = () => this.axes({ size: 50, bits: Tree.X | Tree._X | Tree.Y | Tree._Y | Tree.Z | Tree._Z })], [eMatrix = pg?.eMatrix()], [pMatrix = pg?.pMatrix()], [vMatrix = this.vMatrix()] }])
: Draws a view frustum based on the specified bitwise mask bitsTree.NEAR
,Tree.FAR
,Tree.APEX
,Tree.BODY
, andviewer
callback visual representation. The function determines the view frustum's position, orientation, and viewing volume either from a givenpg
, or directly througheMatrix
andpMatrix
parameters.
Link the p5.treegl.js
library into your HTML file, after you have linked in p5.js. For example:
<!doctype html>
<html>
<head>
<script src="p5.js"></script>
<script src="p5.sound.js"></script>
<script src=https://cdn.jsdelivr.net/gh/VisualComputing/p5.treegl/p5.treegl.js></script>
<script src="sketch.js"></script>
</head>
<body>
</body>
</html>
to include its minified version use:
<script src=https://cdn.jsdelivr.net/gh/VisualComputing/p5.treegl/p5.treegl.min.js></script>
instead.
Clone the repo (git clone https://github.com/VisualComputing/p5.treegl
) and open it with your favorite editor.
Don't forget to check these p5.js references:
Footnotes
-
For an in-depth review, please refer to the post-effects study conducted by Diego Bulla. ↩