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 !== matrixNote 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.Shaderinstance. It builds auniformsUIuser interface usinguniformsUIConfigand, if akeyis given, it binds the shader to thiskeyfor potential use as a Post-effect. Note thatreadShadershould be called within p5 preload. If both callbacks are provided, the first is used assuccessCallbackand 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 auniformsUIuser interface withuniformsUIConfigand, if akeyis provided, binds the shader to this key for potential use as a Post-effect. Note thatmakeShadershould be called within p5 setup.parseShader(fragSrc, [vertSrc], [matrices = Tree.NONE], [uniformsUIConfig], [key], [successCallback], [failureCallback]): A high-level dispatcher function that determines whether to callreadShaderormakeShaderbased on the input parameters and should be called within p5 preload or p5 setup, accordingly. If both callbacks are provided, the first is used assuccessCallbackand the second asfailureCallback.
Vertex shader generation observations
- The
matricesparameter uses the following mask bit fieldsTree.vMatrix,Tree.pMatrix,Tree.mvMatrix,Tree.pmvMatrix,Tree.mMatrixandTree.NONEwhich is the default, to determine how vertices are projected onto NDC, according to the following rules:Mask bit fields gl_PositionTree.NONEaPositionTree.pmvMatrixuModelViewProjectionMatrix * aPositionTree.mvMatrix|Tree.pMatrixuProjectionMatrix * uModelViewMatrix * aPositionTree.pMatrix|Tree.vMatrix|Tree.mMatrixuProjectionMatrix * uViewMatrix * uModelMatrix * aPositionTree.vMatrix|Tree.pMatrixuProjectionMatrix * uViewMatrix * aPositionTree.pMatrix|Tree.mMatrixuProjectionMatrix * uModelMatrix * aPositionTree.mvMatrixuModelViewMatrix * aPositionTree.vMatrix|Tree.mMatrixuViewMatrix * uModelMatrix * aPositionTree.pMatrixuProjectionMatrix * aPositionTree.vMatrixuViewMatrix * aPositionTree.mMatrixuModelMatrix * aPosition - The fragment shader's
varyingsvariables are parsed to determine which and how vertex attributes should be interpolated from the vertex shader, following these naming conventions:type name space vec4color4color vec2texcoords2texture vec2position2local vec3position3local vec4position4eye vec3normal3eye
Examples:
-
Example 1:
parseShader(fragSrc)WEBGL2(GLSL ES 3.00)fragSrc, with novaryingsandhighpprecision:// 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)WEBGL2fragSrcdefiningnormal3andposition4varyings, andmediumpprecision:// 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
floatorintdeclaration 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
speedwith 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
vec4uniform. The comment can specify the default color using a CSS color name.Example:
uniform vec4 color; // 'magenta'
This creates a color picker for
colorwith a default value of magenta. -
Checkboxes: For
booluniforms, 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
isActivethat is checked by default.
These functions manipulate the uniformsUI:
parseUniformsUI(shader, [{ [x = 0], [y = 0], [offset = 0], [width = 120], [color] }]): Parsesshaderuniform variable comments into theshader.uniformsUImap. It automatically callsconfigUniformsUIwith the provideduniformsUIConfigobject. This function should be invoked on custom shaders created with loadShader or createShader, whilereadShaderandmakeShaderalready call it internally.configUniformsUI(shader, [{ [x = 0], [y = 0], [offset = 0], [width = 120], [color] }]): Configures the layout and appearance of theshader.uniformsUIelements based on the provided parameters:xandy: 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.uniformsUIelements associated with theshader's uniforms. It attaches necessary event listeners to update the shader uniforms based on user interactions.hideUniformsUI(shader): Hides theshader.uniformsUIelements and removes the event listeners, stopping any further updates to theshaderuniforms from ui interactions.resetUniformsUI(shader): Hides and resets theshader.uniformsUIwhich should be restored with a call toparseUniformsUI(shader, configUniformsUI).setUniformsUI(shader): Iterates over theuniformsUImap and sets the shader's uniforms based on the current values of the corresponding UI elements. This method should be called within thedrawloop to ensure the shader uniforms are continuously updated. Note thatapplyShaderautomatically 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] }])appliesshaderto the specifiedtarget(which can be the current context, a p5.Framebuffer or a p5.Graphics), emits theshaderuniformsUI(callingshader.setUniformsUI()) and theuniformsobject (formatted as{ uniform_1_name: value_1, ..., uniform_n_name: value_n }), renders geometry by executingscene(options)(defaults to an overlayingquadif not specified), and returns thetargetfor 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. Theuniformsparam maps shaderkeysto their respective uniform values, formatted as{ uniform_1_name: value_1, ..., uniform_n_name: value_n }, provided that asampler2D uniform blendervariable is declared in each shader effect as a common fbo layer. Theflipboolean 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 specifiedoptionsto each shader in theeffectsarray. IfcreateBlenderis not called,applyEffectsautomatically generates a blender layer for each shader, utilizing default options.removeBlender(effects): Removes the individual fbo layers associated with each shader in theeffectsarray, 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, thep5object and p5.RendererGL instances. Note thatpointerXshould always be the first parameter andpointerYthe second.resolution()which is the same as:return [this.pixelDensity() * this.width, this.pixelDensity() * this.height]. Available to both, thep5object 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 theaandbmatrices.
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 top5and p5.RendererGL instances, this method is also available to p5.Camera objects.vMatrix(): Returns the view matrix (the inverse ofeMatrix()). In addition top5and 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 thepvMatrixinverse.lMatrix([{[from = iMatrix()], [to = this.eMatrix()]}]): Returns the 4x4 matrix that transforms locations (points) from matrixfromto matrixto.dMatrix([{[from = iMatrix()], [to = this.eMatrix()]}]): Returns the 3x3 matrix (only rotational part is needed) that transforms directions (vectors) from matrixfromto matrixto. ThenMatrixbelow 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,mMatrixandmvMatrixdefault 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:truefor orthographic andfalsefor perspective.
parsePosition(vector = Tree.ORIGIN, [{[from = Tree.EYE], [to = Tree.WORLD], [pMatrix], [vMatrix], [eMatrix], [pvMatrix], [pvInvMatrix]}]): transforms locations (points) from matrixfromto matrixto.parseDirection(vector = Tree._k, [{[from = Tree.EYE], [to = Tree.WORLD], [vMatrix], [eMatrix], [pMatrix]}]): transforms directions (vectors) from matrixfromto 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.
fromandtomay also be specified as either:Tree.WORLD,Tree.EYE,Tree.SCREEN,Tree.NDCorTree.MODEL.- When no matrix params (
eMatrix,pMatrix,...) are passed the renderer current values are used instead. - The default
parsePositioncall (i.e.,parsePosition(Tree.ORIGIN, {from: Tree.EYE, to: Tree.WORLD)) returns the camera world position. - Note that the default
parseDirectioncall (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 ofnpixels 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]}]): ReturnstrueifpointerX,pointerYlies within the screen space circle centered at (x,y) and havingsizediameter. PassmMatrixto compute (x,y) as the screen space projection of the local space origin (defined bymMatrix), havingsizeas its bounding sphere diameter. UseTree.SQUAREto use a squared shape instead of a circled one. Note thatpointerXshould 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.NEARandTree.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. Appliescolorsfromargsif 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 givensizein world units,colors, and bitwise mask that may be composed ofTree.X,Tree._X,Tree.Y,Tree._Y,Tree.Z,Tree._ZandTree.LABELSbits.grid([{ [size = 100], [subdivisions = 10], [style = Tree.DOTS] }]): Draws grid with givensizein world units,subdivisionsanddotted(Tree.DOTS) or solid (Tree.SOLID) lines.cross([{ [mMatrix = this.mMatrix()], [x], [y], [size = 50], [eMatrix], [pMatrix], [vMatrix], [pvMatrix] }]): Draws a cross atx,yscreen coordinates with givensizein pixels. PassmMatrixto 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.SQUAREto draw it as a square) atx,yscreen coordinates with givensizein pixels. PassmMatrixto 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, andviewercallback visual representation. The function determines the view frustum's position, orientation, and viewing volume either from a givenpg, or directly througheMatrixandpMatrixparameters.
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. ↩
