From d6e0fb2901375dbfcb6663cff012c76d8460664f Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 2 Dec 2025 11:59:30 -0500 Subject: [PATCH 01/34] Experiment: flatter strands API --- src/strands/strands_api.js | 49 ++++++++++++++++++++++++++++++------ src/strands/strands_node.js | 15 +++++++---- test/unit/webgl/p5.Shader.js | 49 ++++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 12 deletions(-) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index fb88413749..825f4a1728 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -227,7 +227,9 @@ function createHookArguments(strandsContext, parameters){ if(isStructType(param.type.typeName)) { const structTypeInfo = structType(param); const { id, dimension } = build.structInstanceNode(strandsContext, structTypeInfo, param.name, []); - const structNode = createStrandsNode(id, dimension, strandsContext); + const structNode = createStrandsNode(id, dimension, strandsContext).withStructProperties( + structTypeInfo.properties.map(prop => prop.name) + ); for (let i = 0; i < structTypeInfo.properties.length; i++) { const propertyType = structTypeInfo.properties[i]; Object.defineProperty(structNode, propertyType.name, { @@ -327,12 +329,43 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { const { cfg, dag } = strandsContext; for (const hookType of hookTypes) { - const hookImplementation = function(hookUserCallback) { - const entryBlockID = CFG.createBasicBlock(cfg, BlockType.FUNCTION); + const hook = function(hookUserCallback) { + const args = setupHook(); + hook.result = hookUserCallback(...args); + finishHook(); + } + + let entryBlockID; + function setupHook() { + entryBlockID = CFG.createBasicBlock(cfg, BlockType.FUNCTION); CFG.addEdge(cfg, cfg.currentBlock, entryBlockID); CFG.pushBlock(cfg, entryBlockID); const args = createHookArguments(strandsContext, hookType.parameters); - const userReturned = hookUserCallback(...args); + if (args.length === 1 && hookType.parameters[0].type.properties) { + for (const key of args[0].structProperties || []) { + Object.defineProperty(hook, key, { + get() { + return args[0][key]; + }, + set(val) { + args[0][key] = val; + }, + enumerable: true, + }); + } + if (hookType.returnType?.typeName === hookType.parameters[0].type.typeName) { + hook.result = args[0]; + } + } else { + for (let i = 0; i < args.length; i++) { + hook[hookType.parameters[i].name] = args[i]; + } + } + return args; + }; + + function finishHook() { + const userReturned = hook.result; const expectedReturnType = hookType.returnType; let rootNodeID = null; if(isStructType(expectedReturnType.typeName)) { @@ -385,10 +418,12 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { shaderContext: hookInfo?.shaderContext, // 'vertex' or 'fragment' }); CFG.popBlock(cfg); - } + }; + hook.begin = setupHook; + hook.end = finishHook; strandsContext.windowOverrides[hookType.name] = window[hookType.name]; strandsContext.fnOverrides[hookType.name] = fn[hookType.name]; - window[hookType.name] = hookImplementation; - fn[hookType.name] = hookImplementation; + window[hookType.name] = hook; + fn[hookType.name] = hook; } } diff --git a/src/strands/strands_node.js b/src/strands/strands_node.js index 0901355aff..f8ff752eca 100644 --- a/src/strands/strands_node.js +++ b/src/strands/strands_node.js @@ -7,6 +7,7 @@ export class StrandsNode { this.id = id; this.strandsContext = strandsContext; this.dimension = dimension; + this.structProperties = null; // Store original identifier for varying variables const dag = this.strandsContext.dag; @@ -17,6 +18,10 @@ export class StrandsNode { this._originalDimension = nodeData.dimension; } } + withStructProperties(properties) { + this.structProperties = properties; + return this; + } copy() { return createStrandsNode(this.id, this.dimension, this.strandsContext); } @@ -30,8 +35,8 @@ export class StrandsNode { newValueID = value.id; } else { const newVal = primitiveConstructorNode( - this.strandsContext, - { baseType, dimension: this.dimension }, + this.strandsContext, + { baseType, dimension: this.dimension }, value ); newValueID = newVal.id; @@ -85,8 +90,8 @@ export class StrandsNode { newValueID = value.id; } else { const newVal = primitiveConstructorNode( - this.strandsContext, - { baseType, dimension: this.dimension }, + this.strandsContext, + { baseType, dimension: this.dimension }, value ); newValueID = newVal.id; @@ -159,4 +164,4 @@ export function createStrandsNode(id, dimension, strandsContext, onRebind) { new StrandsNode(id, dimension, strandsContext), swizzleTrap(id, dimension, strandsContext, onRebind) ); -} \ No newline at end of file +} diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index c69893019a..ed1c305b66 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -1398,5 +1398,54 @@ suite('p5.Shader', function() { }); } }); + + test('Can use begin/end API for hooks with result', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseFilterShader().modify(() => { + myp5.getColor.begin(); + myp5.getColor.result = [1.0, 0.5, 0.0, 1.0]; + myp5.getColor.end(); + }, { myp5 }); + + // Create a simple scene to filter + myp5.background(0, 0, 255); // Blue background + + // Apply the filter + myp5.filter(testShader); + + // Check that the filter was applied (should be orange) + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 + assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127 + assert.approximately(pixelColor[2], 0, 5); // Blue channel should be 0 + }); + + test.only('Can use begin/end API for hooks modifying inputs', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs.begin(); + debugger + myp5.getPixelInputs.color = [1.0, 0.5, 0.0, 1.0]; + myp5.getPixelInputs.end(); + }, { myp5 }); + + // Create a simple scene to filter + myp5.background(0, 0, 255); // Blue background + + // Draw a fullscreen rectangle + myp5.noStroke(); + myp5.fill('red') + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + console.log(myp5._renderer.canvas.toDataURL()) + + // Check that the filter was applied (should be orange) + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 + assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127 + assert.approximately(pixelColor[2], 0, 5); // Blue channel should be 0 + }); }); }); From 9045761a6e392eea40ad9934c99194d85cf5c782 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 2 Dec 2025 12:25:19 -0500 Subject: [PATCH 02/34] Remove logging --- test/unit/webgl/p5.Shader.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index ed1c305b66..b9d7807c1b 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -1421,7 +1421,7 @@ suite('p5.Shader', function() { assert.approximately(pixelColor[2], 0, 5); // Blue channel should be 0 }); - test.only('Can use begin/end API for hooks modifying inputs', () => { + test('Can use begin/end API for hooks modifying inputs', () => { myp5.createCanvas(50, 50, myp5.WEBGL); const testShader = myp5.baseMaterialShader().modify(() => { @@ -1439,7 +1439,6 @@ suite('p5.Shader', function() { myp5.fill('red') myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); - console.log(myp5._renderer.canvas.toDataURL()) // Check that the filter was applied (should be orange) const pixelColor = myp5.get(25, 25); From 9216e8e5271f3d60e72d8f88c547969bbe8d459f Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Wed, 24 Dec 2025 14:31:20 -0500 Subject: [PATCH 03/34] Add create* and load* methods --- src/webgl/material.js | 83 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/src/webgl/material.js b/src/webgl/material.js index 60f01a3969..c193ad7084 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -11,6 +11,11 @@ import { Shader } from './p5.Shader'; import { request } from '../io/files'; import { Color } from '../color/p5.Color'; +async function urlToStrandsCallback(url) { + const src = await fetch(url).then(res => res.text()); + return new Function(src); +} + function material(p5, fn){ /** * Loads vertex and fragment shaders to create a @@ -562,7 +567,12 @@ function material(p5, fn){ const fragString = await fragSrc.join('\n'); // Create the shader using createFilterShader - const loadedShader = this.createFilterShader(fragString, true); + let loadedShader; + if (fragString.test(/void\s+main/)) { + loadedShader = this.createFilterShader(new Function(fragString)); + } else { + loadedShader = this.createFilterShader(fragString, true); + } if (successCallback) { successCallback(loadedShader); @@ -668,6 +678,9 @@ function material(p5, fn){ * */ fn.createFilterShader = function (fragSrc, skipContextCheck = false) { + if (fragSrc instanceof Function) { + return this.baseFilterShader().modify(fragSrc); + } // p5._validateParameters('createFilterShader', arguments); let defaultVertV1 = ` uniform mat4 uModelViewMatrix; @@ -1507,6 +1520,23 @@ function material(p5, fn){ * * */ + fn.createMaterialShader = function(cb) { + return this.baseMaterialShader().modify(cb); + }; + fn.loadMaterialShader = async function (url, onSuccess, onFail) { + try { + const shader = this.createMaterialShader(await urlToStrandsCallback(url)); + if (onSuccess) { + onSuccess(shader); + } + return shader; + } catch (e) { + console.error(e); + if (onFail) { + onFail(e); + } + } + }; fn.baseMaterialShader = function() { this._assert3d('baseMaterialShader'); return this._renderer.baseMaterialShader(); @@ -1744,6 +1774,23 @@ function material(p5, fn){ * * */ + fn.createNormalShader = function(cb) { + return this.baseNormalShader().modify(cb); + }; + fn.loadNormalShader = async function (url, onSuccess, onFail) { + try { + const shader = this.createNormalShader(await urlToStrandsCallback(url)); + if (onSuccess) { + onSuccess(shader); + } + return shader; + } catch (e) { + console.error(e); + if (onFail) { + onFail(e); + } + } + }; fn.baseNormalShader = function() { this._assert3d('baseNormalShader'); return this._renderer.baseNormalShader(); @@ -1875,6 +1922,23 @@ function material(p5, fn){ * * */ + fn.createColorShader = function(cb) { + return this.baseColorShader().modify(cb); + }; + fn.loadColorShader = async function (url, onSuccess, onFail) { + try { + const shader = this.createColorShader(await urlToStrandsCallback(url)); + if (onSuccess) { + onSuccess(shader); + } + return shader; + } catch (e) { + console.error(e); + if (onFail) { + onFail(e); + } + } + }; fn.baseColorShader = function() { this._assert3d('baseColorShader'); return this._renderer.baseColorShader(); @@ -2123,6 +2187,23 @@ function material(p5, fn){ * * */ + fn.createStrokeShader = function(cb) { + return this.baseStrokeShader().modify(cb); + }; + fn.loadStrokeShader = async function (url, onSuccess, onFail) { + try { + const shader = this.createStrokeShader(await urlToStrandsCallback(url)); + if (onSuccess) { + onSuccess(shader); + } + return shader; + } catch (e) { + console.error(e); + if (onFail) { + onFail(e); + } + } + }; fn.baseStrokeShader = function() { this._assert3d('baseStrokeShader'); return this._renderer.baseStrokeShader(); From f175035d91ff2e52b39cb3626882512b42ed8f4d Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Thu, 25 Dec 2025 16:53:30 -0500 Subject: [PATCH 04/34] Update create/loadFilterShader docs --- src/webgl/material.js | 193 +++++++++++++++++++++++++++--------------- 1 file changed, 123 insertions(+), 70 deletions(-) diff --git a/src/webgl/material.js b/src/webgl/material.js index c193ad7084..2b63ed641d 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -523,7 +523,7 @@ function material(p5, fn){ * Creates and loads a filter shader from an external file. * * @method loadFilterShader - * @param {String} fragFilename path to the fragment shader file + * @param {String} filename path to a p5.strands JavaScript file or a GLSL fragment shader file * @param {Function} [successCallback] callback to be called once the shader is * loaded. Will be passed the * p5.Shader object. @@ -592,90 +592,143 @@ function material(p5, fn){ * Creates a p5.Shader object to be used with the * filter() function. * - * `createFilterShader()` works like - * createShader() but has a default vertex - * shader included. `createFilterShader()` is intended to be used along with - * filter() for filtering the contents of a canvas. - * A filter shader will be applied to the whole canvas instead of just - * p5.Geometry objects. + * The main way to use `createFilterShader` is to pass a function in as a parameter. + * This will let you create a shader using p5.strands. * - * The parameter, `fragSrc`, sets the fragment shader. It’s a string that - * contains the fragment shader program written in - * GLSL. + * In your function, you can call `getColor` with a function + * that will be called for each pixel on the image to determine its final color. You can + * read the color of the current pixel with `getTexture(canvasContent, coord)`. * - * The p5.Shader object that's created has some - * uniforms that can be set: - * - `sampler2D tex0`, which contains the canvas contents as a texture. - * - `vec2 canvasSize`, which is the width and height of the canvas, not including pixel density. - * - `vec2 texelSize`, which is the size of a physical pixel including pixel density. This is calculated as `1.0 / (width * density)` for the pixel width and `1.0 / (height * density)` for the pixel height. + * ```js example + * async function setup() { + * createCanvas(50, 50, WEBGL); + * let img = await loadImage('assets/bricks.jpg'); + * let myFilter = createFilterShader(() => { + * getColor((inputs, canvasContent) => { + * let result = getTexture(canvasContent, inputs.texCoord); + * // Zero out the green and blue channels, leaving red + * result.g = 0; + * result.b = 0; + * return result; + * }); + * }); * - * The p5.Shader that's created also provides - * `varying vec2 vTexCoord`, a coordinate with values between 0 and 1. - * `vTexCoord` describes where on the canvas the pixel will be drawn. + * image(img, -50, -50); + * filter(myFilter); + * describe('Bricks tinted red'); + * } + * ``` * - * For more info about filters and shaders, see Adam Ferriss' repo of shader examples - * or the Introduction to Shaders tutorial. + * You can create *uniforms* if you want to pass data into your filter from the rest of your sketch. + * For example, you could pass in the mouse cursor position and use that to control how much + * you warp the content. If you create a uniform inside the shader using a function like `uniformFloat()`, with + * `uniform` + the type of the data, you can set its value using `setUniform` right before applying the filter. * - * @method createFilterShader - * @param {String} fragSrc source code for the fragment shader. - * @returns {p5.Shader} new shader object created from the fragment shader. + * ```js example + * let img; + * let myFilter; + * async function setup() { + * createCanvas(50, 50, WEBGL); + * let img = await loadImage('assets/bricks.jpg'); + * myFilter = createFilterShader(() => { + * let warpAmount = uniformFloat(); + * getColor((inputs, canvasContent) => { + * let coord = inputs.texCoord; + * coord.y += sin(coord.x * 10) * warpAmount; + * return getTexture(canvasContent, coord); + * }); + * }); + * describe('Warped bricks'); + * } * - * @example - *
- * + * function draw() { + * image(img, -50, -50); + * myFilter.setUniform( + * 'warpAmount', + * map(mouseX, 0, width, 0, 1, true) + * ); + * filter(myFilter); + * } + * ``` + * + * You can also make filters that do not need any content to be drawn first! + * There is a lot you can draw just using, for example, the position of the pixel. + * `inputs.texCoord` has an `x` and a `y` property, each with a number between 0 and 1. + * + * ```js example * function setup() { - * let fragSrc = `precision highp float; - * void main() { - * gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0); - * }`; + * createCanvas(50, 50, WEBGL); + * let myFilter = createFilterShader(() => { + * getColor((inputs) => { + * return [inputs.texCoord.x, inputs.texCoord.y, 0, 1]; + * }); + * }); + * describe('A gradient with red, green, yellow, and black'); + * filter(myFilter); + * } + * ``` * - * createCanvas(100, 100, WEBGL); - * let s = createFilterShader(fragSrc); - * filter(s); - * describe('a yellow canvas'); + * ```js example + * function setup() { + * createCanvas(50, 50, WEBGL); + * let myFilter = createFilterShader(() => { + * getColor((inputs) => { + * return mix( + * [1, 0, 0, 1], // Red + * [0, 0, 1, 1], // Blue + * inputs.texCoord.x // x coordinate, from 0 to 1 + * ); + * }); + * }); + * describe('A gradient from red to blue'); + * filter(myFilter); * } - * - *
+ * ``` * - *
- * - * let img, s; - * async function setup() { - * img = await loadImage('assets/bricks.jpg'); - * let fragSrc = `precision highp float; - * - * // x,y coordinates, given from the vertex shader - * varying vec2 vTexCoord; - * - * // the canvas contents, given from filter() - * uniform sampler2D tex0; - * // other useful information from the canvas - * uniform vec2 texelSize; - * uniform vec2 canvasSize; - * // a custom variable from this sketch - * uniform float darkness; - * - * void main() { - * // get the color at current pixel - * vec4 color = texture2D(tex0, vTexCoord); - * // set the output color - * color.b = 1.0; - * color *= darkness; - * gl_FragColor = vec4(color.rgb, 1.0); - * }`; + * You can also animate your filters over time by passing the time into the shader with `uniformFloat`. * - * createCanvas(100, 100, WEBGL); - * s = createFilterShader(fragSrc); + * ```js example + * let myFilter; + * function setup() { + * createCanvas(50, 50, WEBGL); + * myFilter = createFilterShader(() => { + * let time = uniformFloat(() => millis()); + * getColor((inputs) => { + * return mix( + * [1, 0, 0, 1], // Red + * [0, 0, 1, 1], // Blue + * sin(inputs.texCoord.x*15 + time*0.004)/2+0.5 + * ); + * }); + * }); + * describe('A moving, repeating gradient from red to blue'); * } * * function draw() { - * image(img, -50, -50); - * s.setUniform('darkness', 0.5); - * filter(s); - * describe('a image of bricks tinted dark blue'); + * filter(myFilter); * } - * - *
+ * ``` + * + * Like the `modify()` method on shaders, + * advanced users can also fill in `getColor` using GLSL + * instead of JavaScript. + * Read the reference entry for `modify()` + * for more info. Alternatively, `createFilterShader()` can also be used like + * createShader(), but where you only specify a fragment shader. + * + * For more info about filters and shaders, see Adam Ferriss' repo of shader examples + * or the Introduction to Shaders tutorial. + * + * @method createFilterShader + * @param {Function} callback A function building a p5.strands shader. + */ + /** + * @method createFilterShader + * @param {Object} hooks An object specifying p5.strands hooks in GLSL. + */ + /** + * @method createFilterShader + * @param {String} fragSrc Full GLSL source code for the fragment shader. */ fn.createFilterShader = function (fragSrc, skipContextCheck = false) { if (fragSrc instanceof Function) { From d44c67c72384909eecfa53cf3d3757ca4824dbdd Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Fri, 26 Dec 2025 11:10:39 -0500 Subject: [PATCH 05/34] Do material shaders too --- src/webgl/material.js | 638 +++++++++++++++++++----------------------- 1 file changed, 284 insertions(+), 354 deletions(-) diff --git a/src/webgl/material.js b/src/webgl/material.js index 2b63ed641d..a1a33d0865 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -721,14 +721,17 @@ function material(p5, fn){ * * @method createFilterShader * @param {Function} callback A function building a p5.strands shader. + * @returns {p5.Shader} The material shader */ /** * @method createFilterShader * @param {Object} hooks An object specifying p5.strands hooks in GLSL. + * @returns {p5.Shader} The material shader */ /** * @method createFilterShader * @param {String} fragSrc Full GLSL source code for the fragment shader. + * @returns {p5.Shader} The material shader */ fn.createFilterShader = function (fragSrc, skipContextCheck = false) { if (fragSrc instanceof Function) { @@ -1303,147 +1306,33 @@ function material(p5, fn){ }; /** - * Get the default shader used with lights, materials, - * and textures. - * - * You can call `baseMaterialShader().modify()` - * and change any of the following hooks: - * - * - * - * - * - * - * - * - * - * - * - * - * - *
HookDescription
- * - * `void beforeVertex` - * - * - * - * Called at the start of the vertex shader. - * - *
- * - * `Vertex getObjectInputs` - * - * - * - * Update the vertex data of the model being drawn before any positioning has been applied. It takes in a `Vertex` struct, which includes: - * - `vec3 position`, the position of the vertex - * - `vec3 normal`, the direction facing out of the surface - * - `vec2 texCoord`, the texture coordinates associeted with the vertex - * - `vec4 color`, the per-vertex color - * The struct can be modified and returned. - * - *
- * - * `Vertex getWorldInputs` - * - * - * - * Update the vertex data of the model being drawn after transformations such as `translate()` and `scale()` have been applied, but before the camera has been applied. It takes in a `Vertex` struct like, in the `getObjectInputs` hook above, that can be modified and returned. - * - *
- * - * `Vertex getCameraInputs` - * - * - * - * Update the vertex data of the model being drawn as they appear relative to the camera. It takes in a `Vertex` struct like, in the `getObjectInputs` hook above, that can be modified and returned. - * - *
- * - * `void afterVertex` - * - * + * Create a new shader that can change how fills are drawn. Pass the resulting + * shader into the `shader()` function to apply it + * to any fills you draw. * - * Called at the end of the vertex shader. - * - *
- * - * `void beforeFragment` - * - * - * - * Called at the start of the fragment shader. - * - *
- * - * `Inputs getPixelInputs` - * - * - * - * Update the per-pixel inputs of the material. It takes in an `Inputs` struct, which includes: - * - `vec3 normal`, the direction pointing out of the surface - * - `vec2 texCoord`, a vector where `x` and `y` are between 0 and 1 describing the spot on a texture the pixel is mapped to, as a fraction of the texture size - * - `vec3 ambientLight`, the ambient light color on the vertex - * - `vec4 color`, the base material color of the pixel - * - `vec3 ambientMaterial`, the color of the pixel when affected by ambient light - * - `vec3 specularMaterial`, the color of the pixel when reflecting specular highlights - * - `vec3 emissiveMaterial`, the light color emitted by the pixel - * - `float shininess`, a number representing how sharp specular reflections should be, from 1 to infinity - * - `float metalness`, a number representing how mirrorlike the material should be, between 0 and 1 - * The struct can be modified and returned. - *
- * - * `vec4 combineColors` - * - * - * - * Take in a `ColorComponents` struct containing all the different components of light, and combining them into - * a single final color. The struct contains: - * - `vec3 baseColor`, the base color of the pixel - * - `float opacity`, the opacity between 0 and 1 that it should be drawn at - * - `vec3 ambientColor`, the color of the pixel when affected by ambient light - * - `vec3 specularColor`, the color of the pixel when affected by specular reflections - * - `vec3 diffuse`, the amount of diffused light hitting the pixel - * - `vec3 ambient`, the amount of ambient light hitting the pixel - * - `vec3 specular`, the amount of specular reflection hitting the pixel - * - `vec3 emissive`, the amount of light emitted by the pixel - * - *
- * - * `vec4 getFinalColor` - * - * - * - * Update the final color after mixing. It takes in a `vec4 color` and must return a modified version. - * - *
- * - * `void afterFragment` - * - * + * The main way to use `createMaterialShader` is to pass a function in as a parameter. + * This will let you create a shader using p5.strands. * - * Called at the end of the fragment shader. + * In your function, you can call *hooks* to change part of the shader. In a material + * shader, these are the hooks available: + * - `getObjectInputs`: Update vertices before any positioning has been applied. Your function gets run on every vertex. + * - `getWorldInputs`: Update vertices after transformations have been applied. Your function gets run on every vertex. + * - `getCameraInputs`: Update vertices after transformations have been applied, relative to the camera. Your function gets run on every vertex. + * - `getPixelInputs`: Update property values on pixels on the surface of a shape. Your function gets run on every pixel. + * - `combineColors`: Control how the ambient, diffuse, and specular components of lighting are combined into a single color on the surface of a shape. Your function gets run on every pixel. + * - `getFinalColor`: Update or replace the pixel color on the surface of a shape. Your function gets run on every pixel. * - *
+ * Read the linked reference page for each hook for more information about how to use them. * - * Most of the time, you will need to write your hooks in GLSL ES version 300. If you - * are using WebGL 1 instead of 2, write your hooks in GLSL ES 100 instead. + * One thing you can do with a material shader is animate the positions of vertices + * over time: * - * Call `baseMaterialShader().inspectHooks()` to see all the possible hooks and - * their default implementations. - * - * @method baseMaterialShader - * @beta - * @returns {p5.Shader} The material shader - * - * @example - *
- * + * ```js example * let myShader; * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = baseMaterialShader().modify(() => { + * myShader = createMaterialShader(() => { * let time = uniformFloat(() => millis()); * getWorldInputs((inputs) => { * inputs.position.y += @@ -1461,47 +1350,13 @@ function material(p5, fn){ * fill('red'); * sphere(50); * } - * - *
- * - * @example - *
- * - * let myShader; - * - * function setup() { - * createCanvas(200, 200, WEBGL); - * myShader = baseMaterialShader().modify({ - * declarations: 'vec3 myNormal;', - * 'Inputs getPixelInputs': `(Inputs inputs) { - * myNormal = inputs.normal; - * return inputs; - * }`, - * 'vec4 getFinalColor': `(vec4 color) { - * return mix( - * vec4(1.0, 1.0, 1.0, 1.0), - * color, - * abs(dot(myNormal, vec3(0.0, 0.0, 1.0))) - * ); - * }` - * }); - * } + * ``` * - * function draw() { - * background(255); - * rotateY(millis() * 0.001); - * shader(myShader); - * lights(); - * noStroke(); - * fill('red'); - * torus(30); - * } - * - *
+ * There are also many uses in updating values per pixel. This can be a good + * way to give your sketch texture and detail. For example, instead of having a single + * shininess or metalness value for a whole shape, you could vary it in different spots on its surface: * - * @example - *
- * + * ```js example * let myShader; * let environment; * @@ -1509,7 +1364,7 @@ function material(p5, fn){ * environment = await loadImage('assets/outdoor_spheremap.jpg'); * * createCanvas(200, 200, WEBGL); - * myShader = baseMaterialShader().modify(() => { + * myShader = createMaterialShader(() => { * getPixelInputs((inputs) => { * let factor = sin( * TWO_PI * (inputs.texCoord.x + inputs.texCoord.y) @@ -1532,17 +1387,19 @@ function material(p5, fn){ * specularMaterial(150); * sphere(50); * } - * - *
+ * ``` * - * @example - *
- * + * A technique seen often in games called *bump mapping* is to vary the + * *normal*, which is the orientation of the surface, per pixel to create texture + * rather than using many tightly packed vertices. Sometimes this can come from + * bump images, but it can also be done generatively with math. + * + * ```js * let myShader; * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = baseMaterialShader().modify(() => { + * myShader = createMaterialShader(() => { * getPixelInputs((inputs) => { * inputs.normal.x += 0.2 * sin( * sin(TWO_PI * dot(inputs.texCoord.yx, vec2(10, 25))) @@ -1570,12 +1427,119 @@ function material(p5, fn){ * specularMaterial(255); * sphere(50); * } + * ``` + * + * You can also update the final color directly instead of modifying + * lighting settings. Sometimes in photographs, a light source is placed + * behind the subject to create *rim lighting,* where the edges of the + * subject are lit up. This can be simulated by adding white to the final + * color on parts of the shape that are facing away from the camera. + * + * ```js + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseMaterialShader().modify(() => { + * let myNormal = sharedVec3(); + * getPixelInputs((inputs) => { + * myNormal = inputs.normal; + * return inputs; + * }); + * getFinalColor((color) => { + * return mix( + * [1, 1, 1, 1], + * color, + * abs(dot(myNormal, [0, 0, 1])) + * ); + * }); + * }); + * } + * + * function draw() { + * background(255); + * rotateY(millis() * 0.001); + * shader(myShader); + * lights(); + * noStroke(); + * fill('red'); + * torus(30); + * } + * ``` + * + * Like the `modify()` method on shaders, + * advanced users can also fill in hooks using GLSL + * instead of JavaScript. + * Read the reference entry for `modify()` + * for more info. + * + * @method createMaterialShader + * @beta + * @param {Function} callback A function building a p5.strands shader. + * @returns {p5.Shader} The material shader. + * * *
*/ + /** + * @method createMaterialShader + * @param {Object} hooks An object specifying p5.strands hooks in GLSL. + * @returns {p5.Shader} The material shader. + */ fn.createMaterialShader = function(cb) { return this.baseMaterialShader().modify(cb); }; + + /** + * Loads a new shader that can change how fills are drawn. Pass the resulting + * shader into the `shader()` function to apply it + * to any fills you draw. + * + * Since this function loads data from another file, it returns a `Promise`. + * Use it in an `async function setup`, and `await` its result. + * + * ```js + * let myShader; + * async function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = await loadMaterialShader('myMaterial.js'); + * } + * + * function draw() { + * background(255); + * shader(myShader); + * lights(); + * noStroke(); + * fill('red'); + * sphere(50); + * } + * ``` + * + * Inside your shader file, you can call p5.strands hooks to change parts of the shader. For + * example, you might call `getWorldInputs()` with a callback to change each vertex, or you + * might call `getPixelInputs()` with a callback to change each pixel on the surface of a shape. + * + * ```js + * // myMaterial.js + * let time = uniformFloat(() => millis()); + * getWorldInputs((inputs) => { + * inputs.position.y += + * 20 * sin(time * 0.001 + inputs.position.x * 0.05); + * return inputs; + * }); + * ``` + * + * Read the reference for `createMaterialShader`, + * the version of `loadMaterialShader` that takes in a function instead of a separate file, + * for a full list of hooks you can use and examples for each. + * + * @method loadMaterialShader + * @beta + * @param {String} url The URL of your p5.strands JavaScript file. + * @param {Function} [onSuccess] A callback function to run when loading completes. + * @param {Function} [onFailure] A callback function to run when loading fails. + * @returns {Promise} The material shader. + */ fn.loadMaterialShader = async function (url, onSuccess, onFail) { try { const shader = this.createMaterialShader(await urlToStrandsCallback(url)); @@ -1590,76 +1554,39 @@ function material(p5, fn){ } } }; + + /** + * Returns the default shader used for fills when lights or textures are used. + * + * Calling `createMaterialShader(shaderFunction)` + * is equivalent to calling `baseMaterialShader().modify(shaderFunction)`. + * + * Read the `createMaterialShader` reference or + * call `baseMaterialShader().inspectHooks()` for more information on what you can do with + * the base material shader. + * + * @method baseMaterialShader + * @beta + * @returns {p5.Shader} The base material shader. + */ fn.baseMaterialShader = function() { this._assert3d('baseMaterialShader'); return this._renderer.baseMaterialShader(); }; /** - * Get the base shader for filters. - * - * You can then call `baseFilterShader().modify()` - * and change the following hook: - * - * - * - * - *
HookDescription
- * - * `vec4 getColor` - * - * - * - * Output the final color for the current pixel. It takes in two parameters: - * `FilterInputs inputs`, and `in sampler2D canvasContent`, and must return a color - * as a `vec4`. + * Returns the base shader used for filters. * - * `FilterInputs inputs` is a scruct with the following properties: - * - `vec2 texCoord`, the position on the canvas, with coordinates between 0 and 1. Calling - * `getTexture(canvasContent, texCoord)` returns the original color of the current pixel. - * - `vec2 canvasSize`, the width and height of the sketch. - * - `vec2 texelSize`, the size of one real pixel relative to the size of the whole canvas. - * This is equivalent to `1 / (canvasSize * pixelDensity)`. + * Calling `createFilterShader(shaderFunction)` + * is equivalent to calling `baseFilterShader().modify(shaderFunction)`. * - * `in sampler2D canvasContent` is a texture with the contents of the sketch, pre-filter. Call - * `getTexture(canvasContent, someCoordinate)` to retrieve the color of the sketch at that coordinate, - * with coordinate values between 0 and 1. - * - *
- * - * Most of the time, you will need to write your hooks in GLSL ES version 300. If you - * are using WebGL 1, write your hooks in GLSL ES 100 instead. + * Read the `createFilterShader` reference or + * call `baseFilterShader().inspectHooks()` for more information on what you can do with + * the base filter shader. * * @method baseFilterShader * @beta - * @returns {p5.Shader} The filter shader - * - * @example - *
- * - * let img; - * let myShader; - * - * async function setup() { - * img = await loadImage('assets/bricks.jpg'); - * createCanvas(100, 100, WEBGL); - * myShader = baseFilterShader().modify(() => { - * let time = uniformFloat(() => millis()); - * getColor((inputs, canvasContent) => { - * inputs.texCoord.y += - * 0.02 * sin(time * 0.001 + inputs.texCoord.x * 5); - * return texture(canvasContent, inputs.texCoord); - * }); - * }); - * } - * - * function draw() { - * image(img, -50, -50); - * filter(myShader); - * describe('an image of bricks, distorting over time'); - * } - * - *
+ * @returns {p5.Shader} The base filter shader. */ fn.baseFilterShader = function() { return (this._renderer.filterRenderer || this._renderer) @@ -1667,118 +1594,38 @@ function material(p5, fn){ }; /** - * Get the shader used by `normalMaterial()`. + * Create a new shader that can change how fills are drawn, based on the material used + * when `normalMaterial()` is active. Pass the resulting + * shader into the `shader()` function to apply it to any fills + * you draw. * - * You can call `baseNormalShader().modify()` - * and change any of the following hooks: - * - * - * - * - * - * - * - * - * - * - * - *
HookDescription
- * - * `void beforeVertex` - * - * - * - * Called at the start of the vertex shader. - * - *
- * - * `Vertex getObjectInputs` - * - * - * - * Update the vertex data of the model being drawn before any positioning has been applied. It takes in a `Vertex` struct, which includes: - * - `vec3 position`, the position of the vertex - * - `vec3 normal`, the direction facing out of the surface - * - `vec2 texCoord`, the texture coordinates associeted with the vertex - * - `vec4 color`, the per-vertex color - * The struct can be modified and returned. - * - *
- * - * `Vertex getWorldInputs` - * - * - * - * Update the vertex data of the model being drawn after transformations such as `translate()` and `scale()` have been applied, but before the camera has been applied. It takes in a `Vertex` struct like, in the `getObjectInputs` hook above, that can be modified and returned. - * - *
- * - * `Vertex getCameraInputs` - * - * - * - * Update the vertex data of the model being drawn as they appear relative to the camera. It takes in a `Vertex` struct like, in the `getObjectInputs` hook above, that can be modified and returned. - * - *
- * - * `void afterVertex` - * - * - * - * Called at the end of the vertex shader. - * - *
- * - * `void beforeFragment` - * - * - * - * Called at the start of the fragment shader. - * - *
- * - * `vec4 getFinalColor` - * - * - * - * Update the final color after mixing. It takes in a `vec4 color` and must return a modified version. - * - *
- * - * `void afterFragment` - * - * - * - * Called at the end of the fragment shader. - * - *
+ * The main way to use `createNormalShader` is to pass a function in as a parameter. + * This will let you create a shader using p5.strands. * - * Most of the time, you will need to write your hooks in GLSL ES version 300. If you - * are using WebGL 1 instead of 2, write your hooks in GLSL ES 100 instead. + * In your function, you can call *hooks* to change part of the shader. In a material + * shader, these are the hooks available: + * - `getObjectInputs`: Update vertices before any positioning has been applied. Your function gets run on every vertex. + * - `getWorldInputs`: Update vertices after transformations have been applied. Your function gets run on every vertex. + * - `getCameraInputs`: Update vertices after transformations have been applied, relative to the camera. Your function gets run on every vertex. + * - `getPixelInputs`: Update property values on pixels on the surface of a shape. Your function gets run on every pixel. + * - `getFinalColor`: Update or replace the pixel color on the surface of a shape. Your function gets run on every pixel. * - * Call `baseNormalShader().inspectHooks()` to see all the possible hooks and - * their default implementations. + * Read the linked reference page for each hook for more information about how to use them. * - * @method baseNormalShader - * @beta - * @returns {p5.Shader} The `normalMaterial` shader + * One thing you may want to do is update the position of all the vertices in an object over time: * - * @example - *
- * + * ```js example * let myShader; * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = baseNormalShader().modify({ - * uniforms: { - * 'float time': () => millis() - * }, - * 'Vertex getWorldInputs': `(Vertex inputs) { + * myShader = createNormalShader(() => { + * let time = uniformFloat(() => millis()); + * getWorldInputs((inputs) => { * inputs.position.y += * 20. * sin(time * 0.001 + inputs.position.x * 0.05); * return inputs; - * }` + * }); * }); * } * @@ -1788,31 +1635,31 @@ function material(p5, fn){ * noStroke(); * sphere(50); * } - * - *
+ * ``` * - * @example - *
- * + * You may also want to change the colors used. By default, the x, y, and z values of the orientation + * of the surface are mapped directly to red, green, and blue. But you can pick different colors: + * + * ```js example * let myShader; * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = baseNormalShader().modify({ - * 'Vertex getCameraInputs': `(Vertex inputs) { + * myShader = createNormalShader(() => { + * getCameraInputs((inputs) => { * inputs.normal = abs(inputs.normal); * return inputs; - * }`, - * 'vec4 getFinalColor': `(vec4 color) { + * }); + * getFinalColor((color) => { * // Map the r, g, and b values of the old normal to new colors * // instead of just red, green, and blue: - * vec3 newColor = - * color.r * vec3(89.0, 240.0, 232.0) / 255.0 + - * color.g * vec3(240.0, 237.0, 89.0) / 255.0 + - * color.b * vec3(205.0, 55.0, 222.0) / 255.0; + * let newColor = + * color.r * [89, 240, 232] / 255 + + * color.g * [240, 237, 89] / 255 + + * color.b * [205, 55, 222] / 255; * newColor = newColor / (color.r + color.g + color.b); - * return vec4(newColor, 1.0) * color.a; - * }` + * return [newColor.r, newColor.g, newColor.b, color.a]; + * }); * }); * } * @@ -1824,12 +1671,79 @@ function material(p5, fn){ * rotateY(frameCount * 0.015); * box(100); * } - * - *
+ * ``` + * + * Like the `modify()` method on shaders, + * advanced users can also fill in hooks using GLSL + * instead of JavaScript. + * Read the reference entry for `modify()` + * for more info. + * + * @method createNormalShader + * @beta + * @param {Function} callback A function building a p5.strands shader. + * @returns {p5.Shader} The normal shader. + */ + /** + * @method createNormalShader + * @param {Object} hooks An object specifying p5.strands hooks in GLSL. + * @returns {p5.Shader} The normal shader. */ fn.createNormalShader = function(cb) { return this.baseNormalShader().modify(cb); }; + + /** + * Loads a new shader that can change how fills are drawn, based on the material used + * when `normalMaterial()` is active. Pass the resulting + * shader into the `shader()` function to apply it + * to any fills you draw. + * + * Since this function loads data from another file, it returns a `Promise`. + * Use it in an `async function setup`, and `await` its result. + * + * ```js + * let myShader; + * async function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = await loadNormalShader('myMaterial.js'); + * } + * + * function draw() { + * background(255); + * shader(myShader); + * lights(); + * noStroke(); + * fill('red'); + * sphere(50); + * } + * ``` + * + * Inside your shader file, you can call p5.strands hooks to change parts of the shader. For + * example, you might call `getWorldInputs()` with a callback to change each vertex, or you + * might call `getFinalColor()` with a callback to change the color of each pixel on the surface of a shape. + * + * ```js + * // myMaterial.js + * let time = uniformFloat(() => millis()); + * getWorldInputs((inputs) => { + * inputs.position.y += + * 20 * sin(time * 0.001 + inputs.position.x * 0.05); + * return inputs; + * }); + * ``` + * + * Read the reference for `createNormalShader`, + * the version of `loadNormalShader` that takes in a function instead of a separate file, + * for a full list of hooks you can use and examples for each. + * + * @method loadNormalShader + * @beta + * @param {String} url The URL of your p5.strands JavaScript file. + * @param {Function} [onSuccess] A callback function to run when loading completes. + * @param {Function} [onFailure] A callback function to run when loading fails. + * @returns {Promise} The normal shader. + */ fn.loadNormalShader = async function (url, onSuccess, onFail) { try { const shader = this.createNormalShader(await urlToStrandsCallback(url)); @@ -1844,6 +1758,22 @@ function material(p5, fn){ } } }; + + /** + * Returns the default shader used for fills when + * `normalMaterial()` is activated. + * + * Calling `createNormalShader(shaderFunction)` + * is equivalent to calling `baseNormalShader().modify(shaderFunction)`. + * + * Read the `createNormalShader` reference or + * call `baseNormalShader().inspectHooks()` for more information on what you can do with + * the base normal shader. + * + * @method baseNormalShader + * @beta + * @returns {p5.Shader} The base material shader. + */ fn.baseNormalShader = function() { this._assert3d('baseNormalShader'); return this._renderer.baseNormalShader(); From 154cd4fc3d066b84006dd95d16c0d8706fb71f8a Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 27 Dec 2025 08:22:09 -0500 Subject: [PATCH 06/34] Add stroke shaders --- src/webgl/material.js | 562 +++++++++++++++++++++--------------------- 1 file changed, 275 insertions(+), 287 deletions(-) diff --git a/src/webgl/material.js b/src/webgl/material.js index a1a33d0865..fa8f88aa47 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -153,7 +153,15 @@ function material(p5, fn){ }; /** - * Creates a new p5.Shader object. + * Creates a new p5.Shader object using GLSL. + * + * If you are interested in writing shaders, consider using p5.strands shaders using + * `createMaterialShader`, + * `createStrokeShader`, or + * `createFiltershader`. + * With p5.strands, you can modify existing shaders using JavaScript. With + * `createShader`, shaders are made from scratch, and are written in GLSL. This + * will be most useful for advanced cases, and for authors of add-on libraries. * * Shaders are programs that run on the graphics processing unit (GPU). They * can process many pixels at the same time, making them fast for many @@ -1477,9 +1485,6 @@ function material(p5, fn){ * @beta * @param {Function} callback A function building a p5.strands shader. * @returns {p5.Shader} The material shader. - * - * - * */ /** * @method createMaterialShader @@ -1607,7 +1612,6 @@ function material(p5, fn){ * - `getObjectInputs`: Update vertices before any positioning has been applied. Your function gets run on every vertex. * - `getWorldInputs`: Update vertices after transformations have been applied. Your function gets run on every vertex. * - `getCameraInputs`: Update vertices after transformations have been applied, relative to the camera. Your function gets run on every vertex. - * - `getPixelInputs`: Update property values on pixels on the surface of a shape. Your function gets run on every pixel. * - `getFinalColor`: Update or replace the pixel color on the surface of a shape. Your function gets run on every pixel. * * Read the linked reference page for each hook for more information about how to use them. @@ -1780,118 +1784,37 @@ function material(p5, fn){ }; /** - * Get the shader used when no lights or materials are applied. - * - * You can call `baseColorShader().modify()` - * and change any of the following hooks: - * - * - * - * - * - * - * - * - * - * - * - *
HookDescription
- * - * `void beforeVertex` - * - * - * - * Called at the start of the vertex shader. - * - *
- * - * `Vertex getObjectInputs` - * - * - * - * Update the vertex data of the model being drawn before any positioning has been applied. It takes in a `Vertex` struct, which includes: - * - `vec3 position`, the position of the vertex - * - `vec3 normal`, the direction facing out of the surface - * - `vec2 texCoord`, the texture coordinates associeted with the vertex - * - `vec4 color`, the per-vertex color - * The struct can be modified and returned. - * - *
- * - * `Vertex getWorldInputs` - * - * - * - * Update the vertex data of the model being drawn after transformations such as `translate()` and `scale()` have been applied, but before the camera has been applied. It takes in a `Vertex` struct like, in the `getObjectInputs` hook above, that can be modified and returned. - * - *
- * - * `Vertex getCameraInputs` - * - * - * - * Update the vertex data of the model being drawn as they appear relative to the camera. It takes in a `Vertex` struct like, in the `getObjectInputs` hook above, that can be modified and returned. - * - *
- * - * `void afterVertex` - * - * - * - * Called at the end of the vertex shader. - * - *
- * - * `void beforeFragment` - * - * - * - * Called at the start of the fragment shader. - * - *
- * - * `vec4 getFinalColor` - * - * - * - * Update the final color after mixing. It takes in a `vec4 color` and must return a modified version. - * - *
- * - * `void afterFragment` - * - * - * - * Called at the end of the fragment shader. + * Create a new shader that can change how fills are drawn, based on the default shader + * used when no lights or textures are applied. Pass the resulting + * shader into the `shader()` function to apply it + * to any fills you draw. * - *
+ * The main way to use `createColorShader` is to pass a function in as a parameter. + * This will let you create a shader using p5.strands. * - * Most of the time, you will need to write your hooks in GLSL ES version 300. If you - * are using WebGL 1 instead of 2, write your hooks in GLSL ES 100 instead. + * In your function, you can call *hooks* to change part of the shader. In a material + * shader, these are the hooks available: + * - `getObjectInputs`: Update vertices before any positioning has been applied. Your function gets run on every vertex. + * - `getWorldInputs`: Update vertices after transformations have been applied. Your function gets run on every vertex. + * - `getCameraInputs`: Update vertices after transformations have been applied, relative to the camera. Your function gets run on every vertex. + * - `getFinalColor`: Update or replace the pixel color on the surface of a shape. Your function gets run on every pixel. * - * Call `baseColorShader().inspectHooks()` to see all the possible hooks and - * their default implementations. + * Read the linked reference page for each hook for more information about how to use them. * - * @method baseColorShader - * @beta - * @returns {p5.Shader} The color shader + * One thing you might want to do is modify the position of every vertex over time: * - * @example - *
- * + * ```js example * let myShader; * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = baseColorShader().modify({ - * uniforms: { - * 'float time': () => millis() - * }, - * 'Vertex getWorldInputs': `(Vertex inputs) { + * myShader = createColorShader(() => { + * let time = uniformFloat(() => millis()); + * getWorldInputs((inputs) => { * inputs.position.y += - * 20. * sin(time * 0.001 + inputs.position.x * 0.05); + * 20 * sin(time * 0.001 + inputs.position.x * 0.05); * return inputs; - * }` + * }); * }); * } * @@ -1902,12 +1825,79 @@ function material(p5, fn){ * fill('red'); * circle(0, 0, 50); * } - * - *
+ * ``` + * + * Like the `modify()` method on shaders, + * advanced users can also fill in hooks using GLSL + * instead of JavaScript. + * Read the reference entry for `modify()` + * for more info. + * + * @method createColorShader + * @beta + * @param {Function} callback A function building a p5.strands shader. + * @returns {p5.Shader} The color shader. + */ + /** + * @method createColorShader + * @param {Object} hooks An object specifying p5.strands hooks in GLSL. + * @returns {p5.Shader} The color shader. */ fn.createColorShader = function(cb) { return this.baseColorShader().modify(cb); }; + + /** + * Loads a new shader that can change how fills are drawn, based on the material used + * when no lights or textures are active. Pass the resulting + * shader into the `shader()` function to apply it + * to any fills you draw. + * + * Since this function loads data from another file, it returns a `Promise`. + * Use it in an `async function setup`, and `await` its result. + * + * ```js + * let myShader; + * async function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = await loadColorShader('myMaterial.js'); + * } + * + * function draw() { + * background(255); + * shader(myShader); + * lights(); + * noStroke(); + * fill('red'); + * circle(0, 0, 50); + * } + * ``` + * + * Inside your shader file, you can call p5.strands hooks to change parts of the shader. For + * example, you might call `getWorldInputs()` with a callback to change each vertex, or you + * might call `getFinalColor()` with a callback to change the color of each pixel on the surface of a shape. + * + * ```js + * // myMaterial.js + * let time = uniformFloat(() => millis()); + * getWorldInputs((inputs) => { + * inputs.position.y += + * 20 * sin(time * 0.001 + inputs.position.x * 0.05); + * return inputs; + * }); + * ``` + * + * Read the reference for `createColorShader`, + * the version of `loadColorShader` that takes in a function instead of a separate file, + * for a full list of hooks you can use and examples for each. + * + * @method loadColorShader + * @beta + * @param {String} url The URL of your p5.strands JavaScript file. + * @param {Function} [onSuccess] A callback function to run when loading completes. + * @param {Function} [onFailure] A callback function to run when loading fails. + * @returns {Promise} The color shader. + */ fn.loadColorShader = async function (url, onSuccess, onFail) { try { const shader = this.createColorShader(await urlToStrandsCallback(url)); @@ -1922,149 +1912,64 @@ function material(p5, fn){ } } }; + + /** + * Returns the default shader used for fills when no lights or textures are activate. + * + * Calling `createColorShader(shaderFunction)` + * is equivalent to calling `baseColorShader().modify(shaderFunction)`. + * + * Read the `createColorShader` reference or + * call `baseColorShader().inspectHooks()` for more information on what you can do with + * the base color shader. + * + * @method baseColorShader + * @beta + * @returns {p5.Shader} The base color shader. + */ fn.baseColorShader = function() { this._assert3d('baseColorShader'); return this._renderer.baseColorShader(); }; /** - * Get the shader used when drawing the strokes of shapes. - * - * You can call `baseStrokeShader().modify()` - * and change any of the following hooks: - * - * - * - * - * - * - * - * - * - * - * - * - * - *
HookDescription
- * - * `void beforeVertex` - * - * - * - * Called at the start of the vertex shader. - * - *
- * - * `StrokeVertex getObjectInputs` - * - * - * - * Update the vertex data of the stroke being drawn before any positioning has been applied. It takes in a `StrokeVertex` struct, which includes: - * - `vec3 position`, the position of the vertex - * - `vec3 tangentIn`, the tangent coming in to the vertex - * - `vec3 tangentOut`, the tangent coming out of the vertex. In straight segments, this will be the same as `tangentIn`. In joins, it will be different. In caps, one of the tangents will be 0. - * - `vec4 color`, the per-vertex color - * - `float weight`, the stroke weight - * The struct can be modified and returned. - * - *
- * - * `StrokeVertex getWorldInputs` - * - * - * - * Update the vertex data of the model being drawn after transformations such as `translate()` and `scale()` have been applied, but before the camera has been applied. It takes in a `StrokeVertex` struct like, in the `getObjectInputs` hook above, that can be modified and returned. - * - *
- * - * `StrokeVertex getCameraInputs` - * - * - * - * Update the vertex data of the model being drawn as they appear relative to the camera. It takes in a `StrokeVertex` struct like, in the `getObjectInputs` hook above, that can be modified and returned. - * - *
- * - * `void afterVertex` - * - * - * - * Called at the end of the vertex shader. - * - *
- * - * `void beforeFragment` - * - * - * - * Called at the start of the fragment shader. - * - *
- * - * `Inputs getPixelInputs` - * - * - * - * Update the inputs to the shader. It takes in a struct `Inputs inputs`, which includes: - * - `vec4 color`, the color of the stroke - * - `vec2 tangent`, the direction of the stroke in screen space - * - `vec2 center`, the coordinate of the center of the stroke in screen space p5.js pixels - * - `vec2 position`, the coordinate of the current pixel in screen space p5.js pixels - * - `float strokeWeight`, the thickness of the stroke in p5.js pixels - * - *
- * - * `bool shouldDiscard` - * - * - * - * Caps and joins are made by discarded pixels in the fragment shader to carve away unwanted areas. Use this to change this logic. It takes in a `bool willDiscard` and must return a modified version. - * - *
- * - * `vec4 getFinalColor` - * - * + * Create a new shader that can change how strokes are drawn, based on the default + * shader used for strokes. Pass the resulting shader into the + * `strokeShader()` function to apply it to any + * strokes you draw. * - * Update the final color after mixing. It takes in a `vec4 color` and must return a modified version. - * - *
- * - * `void afterFragment` - * - * - * - * Called at the end of the fragment shader. - * - *
+ * The main way to use `createStrokeShader` is to pass a function in as a parameter. + * This will let you create a shader using p5.strands. * - * Most of the time, you will need to write your hooks in GLSL ES version 300. If you - * are using WebGL 1 instead of 2, write your hooks in GLSL ES 100 instead. + * In your function, you can call *hooks* to change part of the shader. In a material + * shader, these are the hooks available: + * - `getObjectInputs`: Update vertices before any positioning has been applied. Your function gets run on every vertex. + * - `getWorldInputs`: Update vertices after transformations have been applied. Your function gets run on every vertex. + * - `getCameraInputs`: Update vertices after transformations have been applied, relative to the camera. Your function gets run on every vertex. + * - `getPixelInputs`: Update property values on pixels on the surface of a shape. Your function gets run on every pixel. + * - `shouldDiscard`: Decide whether or not a pixel should be drawn, generally used to carve out pieces to make rounded or mitered joins and caps. Your function gets run on every pixel. + * - `getFinalColor`: Update or replace the pixel color on the surface of a shape. Your function gets run on every pixel. * - * Call `baseStrokeShader().inspectHooks()` to see all the possible hooks and - * their default implementations. + * Read the linked reference page for each hook for more information about how to use them. * - * @method baseStrokeShader - * @beta - * @returns {p5.Shader} The stroke shader + * One thing you might want to do is update the color of a stroke per pixel. Here, it is being used + * to create a soft texture: * - * @example - *
- * + * ```js example * let myShader; * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = baseStrokeShader().modify({ - * 'Inputs getPixelInputs': `(Inputs inputs) { - * float opacity = 1.0 - smoothstep( - * 0.0, - * 15.0, + * myShader = createStrokeShader(() => { + * getPixelInputs((inputs) => { + * let opacity = 1 - smoothstep( + * 0, + * 15, * length(inputs.position - inputs.center) * ); - * inputs.color *= opacity; + * inputs.color.a *= opacity; * return inputs; - * }` + * }); * }); * } * @@ -2079,100 +1984,168 @@ function material(p5, fn){ * sin(millis()*0.001 + 1) * height/4 * ); * } - * - *
+ * ``` * - * @example - *
- * + * Rather than using opacity, we could use a form of *dithering* to get a different + * texture. This involves using only fully opaque or transparent pixels. Here, we + * randomly choose which pixels to be transparent: + * + * ```js * let myShader; * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = baseStrokeShader().modify({ - * uniforms: { - * 'float time': () => millis() - * }, - * 'StrokeVertex getWorldInputs': `(StrokeVertex inputs) { - * // Add a somewhat random offset to the weight - * // that varies based on position and time - * float scale = 0.8 + 0.2*sin(10.0 * sin( - * floor(time/250.) + - * inputs.position.x*0.01 + - * inputs.position.y*0.01 - * )); - * inputs.weight *= scale; + * myShader = createStrokeShader(() => { + * getPixelInputs((inputs) => { + * // Replace alpha in the color with dithering by + * // randomly setting pixel colors to 0 based on opacity + * let a = 1; + * if (noise(inputs.position.xy) > inputs.color.a) { + * a = 0; + * } + * inputs.color.a = a; * return inputs; - * }` + * }); * }); * } * * function draw() { * background(255); * strokeShader(myShader); - * myShader.setUniform('time', millis()); * strokeWeight(10); * beginShape(); * for (let i = 0; i <= 50; i++) { - * let r = map(i, 0, 50, 0, width/3); - * let x = r*cos(i*0.2); - * let y = r*sin(i*0.2); - * vertex(x, y); + * stroke( + * 0, + * 255 + * * map(i, 0, 20, 0, 1, true) + * * map(i, 30, 50, 1, 0, true) + * ); + * vertex( + * map(i, 0, 50, -1, 1) * width/3, + * 50 * sin(i/10 + frameCount/100) + * ); * } * endShape(); * } - * - *
+ * ``` * - * @example - *
- * + * You might also want to update some properties per vertex, such as the stroke + * thickness. This lets you create a more varied line: + * + * ```js example * let myShader; * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = baseStrokeShader().modify({ - * 'float random': `(vec2 p) { - * vec3 p3 = fract(vec3(p.xyx) * .1031); - * p3 += dot(p3, p3.yzx + 33.33); - * return fract((p3.x + p3.y) * p3.z); - * }`, - * 'Inputs getPixelInputs': `(Inputs inputs) { - * // Replace alpha in the color with dithering by - * // randomly setting pixel colors to 0 based on opacity - * float a = inputs.color.a; - * inputs.color.a = 1.0; - * inputs.color *= random(inputs.position.xy) > a ? 0.0 : 1.0; + * myShader = createStrokeShader(() => { + * let time = uniformFloat(() => millis()); + * getWorldInputs((inputs) => { + * // Add a somewhat random offset to the weight + * // that varies based on position and time + * let scale = noise( + * inputs.position.x * 0.1, + * inputs.position.y * 0.1, + * time * 0.001 + * ); + * inputs.weight *= scale; * return inputs; - * }` + * }); * }); * } * * function draw() { * background(255); * strokeShader(myShader); + * myShader.setUniform('time', millis()); * strokeWeight(10); * beginShape(); * for (let i = 0; i <= 50; i++) { - * stroke( - * 0, - * 255 - * * map(i, 0, 20, 0, 1, true) - * * map(i, 30, 50, 1, 0, true) - * ); - * vertex( - * map(i, 0, 50, -1, 1) * width/3, - * 50 * sin(i/10 + frameCount/100) - * ); + * let r = map(i, 0, 50, 0, width/3); + * let x = r*cos(i*0.2); + * let y = r*sin(i*0.2); + * vertex(x, y); * } * endShape(); * } - * - *
+ * ``` + * + * Like the `modify()` method on shaders, + * advanced users can also fill in hooks using GLSL + * instead of JavaScript. + * Read the reference entry for `modify()` + * for more info. + * + * @method createStrokeShader + * @beta + * @param {Function} callback A function building a p5.strands shader. + * @returns {p5.Shader} The stroke shader. + */ + /** + * @method createStrokeShader + * @param {Object} hooks An object specifying p5.strands hooks in GLSL. + * @returns {p5.Shader} The stroke shader. */ fn.createStrokeShader = function(cb) { return this.baseStrokeShader().modify(cb); }; + + /** + * Loads a new shader that can change how strokes are drawn. Pass the resulting + * shader into the `strokeShader()` function to apply it + * to any strokes you draw. + * + * Since this function loads data from another file, it returns a `Promise`. + * Use it in an `async function setup`, and `await` its result. + * + * ```js + * let myShader; + * async function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = await loadStrokeShader('myMaterial.js'); + * } + * + * function draw() { + * background(255); + * strokeShader(myShader); + * strokeWeight(30); + * line( + * -width/3, + * sin(millis()*0.001) * height/4, + * width/3, + * sin(millis()*0.001 + 1) * height/4 + * ); + * } + * ``` + * + * Inside your shader file, you can call p5.strands hooks to change parts of the shader. For + * example, you might call `getWorldInputs()` with a callback to change each vertex, or you + * might call `getPixelInputs()` with a callback to change each pixel on the surface of a stroke. + * + * ```js + * // myMaterial.js + * getPixelInputs((inputs) => { + * let opacity = 1 - smoothstep( + * 0, + * 15, + * length(inputs.position - inputs.center) + * ); + * inputs.color.a *= opacity; + * return inputs; + * }); + * ``` + * + * Read the reference for `createStrokeShader`, + * the version of `loadStrokeShader` that takes in a function instead of a separate file, + * for a full list of hooks you can use and examples for each. + * + * @method loadStrokeShader + * @beta + * @param {String} url The URL of your p5.strands JavaScript file. + * @param {Function} [onSuccess] A callback function to run when loading completes. + * @param {Function} [onFailure] A callback function to run when loading fails. + * @returns {Promise} The stroke shader. + */ fn.loadStrokeShader = async function (url, onSuccess, onFail) { try { const shader = this.createStrokeShader(await urlToStrandsCallback(url)); @@ -2187,6 +2160,21 @@ function material(p5, fn){ } } }; + + /** + * Returns the default shader used for strokes. + * + * Calling `createStrokeShader(shaderFunction)` + * is equivalent to calling `baseStrokeShader().modify(shaderFunction)`. + * + * Read the `createStrokeShader` reference or + * call `baseStrokeShader().inspectHooks()` for more information on what you can do with + * the base material shader. + * + * @method baseStrokeShader + * @beta + * @returns {p5.Shader} The base material shader. + */ fn.baseStrokeShader = function() { this._assert3d('baseStrokeShader'); return this._renderer.baseStrokeShader(); From 5d06815cad64c4df58254d2427846d7c946a0167 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 27 Dec 2025 08:22:45 -0500 Subject: [PATCH 07/34] Update FES typeS --- docs/parameterData.json | 82 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/docs/parameterData.json b/docs/parameterData.json index 494f63f022..066db3c23a 100644 --- a/docs/parameterData.json +++ b/docs/parameterData.json @@ -2887,6 +2887,12 @@ }, "createFilterShader": { "overloads": [ + [ + "Function" + ], + [ + "Object" + ], [ "String" ] @@ -2913,6 +2919,25 @@ ] ] }, + "createMaterialShader": { + "overloads": [ + [ + "Function" + ], + [ + "Object" + ] + ] + }, + "loadMaterialShader": { + "overloads": [ + [ + "String", + "Function?", + "Function?" + ] + ] + }, "baseMaterialShader": { "overloads": [ [] @@ -2923,16 +2948,73 @@ [] ] }, + "createNormalShader": { + "overloads": [ + [ + "Function" + ], + [ + "Object" + ] + ] + }, + "loadNormalShader": { + "overloads": [ + [ + "String", + "Function?", + "Function?" + ] + ] + }, "baseNormalShader": { "overloads": [ [] ] }, + "createColorShader": { + "overloads": [ + [ + "Function" + ], + [ + "Object" + ] + ] + }, + "loadColorShader": { + "overloads": [ + [ + "String", + "Function?", + "Function?" + ] + ] + }, "baseColorShader": { "overloads": [ [] ] }, + "createStrokeShader": { + "overloads": [ + [ + "Function" + ], + [ + "Object" + ] + ] + }, + "loadStrokeShader": { + "overloads": [ + [ + "String", + "Function?", + "Function?" + ] + ] + }, "baseStrokeShader": { "overloads": [ [] From 9a64b832f7599cd0b5e4444b17ffe00e5f11182f Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 27 Dec 2025 08:32:01 -0500 Subject: [PATCH 08/34] Update loadFilterShader reference --- src/webgl/material.js | 67 +++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/src/webgl/material.js b/src/webgl/material.js index fa8f88aa47..7d44fe30ee 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -528,7 +528,41 @@ function material(p5, fn){ }; /** - * Creates and loads a filter shader from an external file. + * Loads a new shader from a file that can be applied to the contents of the canvas with + * `filter()`. Pass the resulting shader into `filter()` to apply it. + * + * Since this function loads data from another file, it returns a `Promise`. + * Use it in an `async function setup`, and `await` its result. + * + * ```js + * async function setup() { + * createCanvas(50, 50, WEBGL); + * let img = await loadImage('assets/bricks.jpg'); + * let myFilter = loadFilterShader('myFilter.js'); + * + * image(img, -50, -50); + * filter(myFilter); + * describe('Bricks tinted red'); + * } + * ``` + * + * Inside your shader file, you can call p5.strands hooks to change parts of the shader. For + * a filter shader, call `getColor()` with a callback to change each pixel on the canvas. + * + * ```js + * // myFilter.js + * getColor((inputs, canvasContent) => { + * let result = getTexture(canvasContent, inputs.texCoord); + * // Zero out the green and blue channels, leaving red + * result.g = 0; + * result.b = 0; + * return result; + * }); + * ``` + * + * Read the reference for `createFilterShader`, + * the version of `loadFilterShader` that takes in a function instead of a separate file, + * for more examples. * * @method loadFilterShader * @param {String} filename path to a p5.strands JavaScript file or a GLSL fragment shader file @@ -539,29 +573,6 @@ function material(p5, fn){ * loading the shader. Will be passed the * error event. * @return {Promise} a promise that resolves with a shader object - * - * @example - *
- * - * let myShader; - * - * async function setup() { - * myShader = await loadFilterShader('assets/basic.frag'); - * createCanvas(100, 100, WEBGL); - * noStroke(); - * } - * - * function draw() { - * // shader() sets the active shader with our shader - * shader(myShader); - * - * // rect gives us some geometry on the screen - * rect(-50, -50, width, height); - * } - * - *
- * @alt - * A rectangle with a shader applied to it. */ fn.loadFilterShader = async function ( fragFilename, @@ -1496,7 +1507,7 @@ function material(p5, fn){ }; /** - * Loads a new shader that can change how fills are drawn. Pass the resulting + * Loads a new shader from a file that can change how fills are drawn. Pass the resulting * shader into the `shader()` function to apply it * to any fills you draw. * @@ -1698,7 +1709,7 @@ function material(p5, fn){ }; /** - * Loads a new shader that can change how fills are drawn, based on the material used + * Loads a new shader from a file that can change how fills are drawn, based on the material used * when `normalMaterial()` is active. Pass the resulting * shader into the `shader()` function to apply it * to any fills you draw. @@ -1848,7 +1859,7 @@ function material(p5, fn){ }; /** - * Loads a new shader that can change how fills are drawn, based on the material used + * Loads a new shader from a file that can change how fills are drawn, based on the material used * when no lights or textures are active. Pass the resulting * shader into the `shader()` function to apply it * to any fills you draw. @@ -2091,7 +2102,7 @@ function material(p5, fn){ }; /** - * Loads a new shader that can change how strokes are drawn. Pass the resulting + * Loads a new shader from a file that can change how strokes are drawn. Pass the resulting * shader into the `strokeShader()` function to apply it * to any strokes you draw. * From e4044fff38c1abe5e74531bbba2aeeacae06e3b5 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 27 Dec 2025 08:38:55 -0500 Subject: [PATCH 09/34] Update load* methods to return the callback's value --- src/webgl/material.js | 50 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/src/webgl/material.js b/src/webgl/material.js index 7d44fe30ee..2f8ad0d8e8 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -139,7 +139,7 @@ function material(p5, fn){ loadedShader._fragSrc = (await request(fragFilename, 'text')).data; if (successCallback) { - return successCallback(loadedShader); + return successCallback(loadedShader) || loadedShader; } else { return loadedShader; } @@ -564,6 +564,12 @@ function material(p5, fn){ * the version of `loadFilterShader` that takes in a function instead of a separate file, * for more examples. * + * The second parameter, `successCallback`, is optional. If a function is passed, as in + * `loadFilterShader('myShader.js', onLoaded)`, then the `onLoaded()` function will be called + * once the shader loads. The shader will be passed to `onLoaded()` as its only argument. + * The return value of `handleData()`, if present, will be used as the final return value of + * `loadFilterShader('myShader.js', onLoaded)`. + * * @method loadFilterShader * @param {String} filename path to a p5.strands JavaScript file or a GLSL fragment shader file * @param {Function} [successCallback] callback to be called once the shader is @@ -594,7 +600,7 @@ function material(p5, fn){ } if (successCallback) { - successCallback(loadedShader); + loadedShader = successCallback(loadedShader) || loadedShader; } return loadedShader; @@ -1549,6 +1555,12 @@ function material(p5, fn){ * the version of `loadMaterialShader` that takes in a function instead of a separate file, * for a full list of hooks you can use and examples for each. * + * The second parameter, `successCallback`, is optional. If a function is passed, as in + * `loadMaterialShader('myShader.js', onLoaded)`, then the `onLoaded()` function will be called + * once the shader loads. The shader will be passed to `onLoaded()` as its only argument. + * The return value of `handleData()`, if present, will be used as the final return value of + * `loadMaterialShader('myShader.js', onLoaded)`. + * * @method loadMaterialShader * @beta * @param {String} url The URL of your p5.strands JavaScript file. @@ -1558,9 +1570,9 @@ function material(p5, fn){ */ fn.loadMaterialShader = async function (url, onSuccess, onFail) { try { - const shader = this.createMaterialShader(await urlToStrandsCallback(url)); + let shader = this.createMaterialShader(await urlToStrandsCallback(url)); if (onSuccess) { - onSuccess(shader); + shader = onSuccess(shader) || shader; } return shader; } catch (e) { @@ -1752,6 +1764,12 @@ function material(p5, fn){ * the version of `loadNormalShader` that takes in a function instead of a separate file, * for a full list of hooks you can use and examples for each. * + * The second parameter, `successCallback`, is optional. If a function is passed, as in + * `loadNormalShader('myShader.js', onLoaded)`, then the `onLoaded()` function will be called + * once the shader loads. The shader will be passed to `onLoaded()` as its only argument. + * The return value of `handleData()`, if present, will be used as the final return value of + * `loadNormalShader('myShader.js', onLoaded)`. + * * @method loadNormalShader * @beta * @param {String} url The URL of your p5.strands JavaScript file. @@ -1761,9 +1779,9 @@ function material(p5, fn){ */ fn.loadNormalShader = async function (url, onSuccess, onFail) { try { - const shader = this.createNormalShader(await urlToStrandsCallback(url)); + let shader = this.createNormalShader(await urlToStrandsCallback(url)); if (onSuccess) { - onSuccess(shader); + shader = onSuccess(shader) || shader; } return shader; } catch (e) { @@ -1902,6 +1920,12 @@ function material(p5, fn){ * the version of `loadColorShader` that takes in a function instead of a separate file, * for a full list of hooks you can use and examples for each. * + * The second parameter, `successCallback`, is optional. If a function is passed, as in + * `loadColorShader('myShader.js', onLoaded)`, then the `onLoaded()` function will be called + * once the shader loads. The shader will be passed to `onLoaded()` as its only argument. + * The return value of `handleData()`, if present, will be used as the final return value of + * `loadColorShader('myShader.js', onLoaded)`. + * * @method loadColorShader * @beta * @param {String} url The URL of your p5.strands JavaScript file. @@ -1911,9 +1935,9 @@ function material(p5, fn){ */ fn.loadColorShader = async function (url, onSuccess, onFail) { try { - const shader = this.createColorShader(await urlToStrandsCallback(url)); + let shader = this.createColorShader(await urlToStrandsCallback(url)); if (onSuccess) { - onSuccess(shader); + shader = onSuccess(shader) || shader; } return shader; } catch (e) { @@ -2150,6 +2174,12 @@ function material(p5, fn){ * the version of `loadStrokeShader` that takes in a function instead of a separate file, * for a full list of hooks you can use and examples for each. * + * The second parameter, `successCallback`, is optional. If a function is passed, as in + * `loadStrokeShader('myShader.js', onLoaded)`, then the `onLoaded()` function will be called + * once the shader loads. The shader will be passed to `onLoaded()` as its only argument. + * The return value of `handleData()`, if present, will be used as the final return value of + * `loadStrokeShader('myShader.js', onLoaded)`. + * * @method loadStrokeShader * @beta * @param {String} url The URL of your p5.strands JavaScript file. @@ -2159,9 +2189,9 @@ function material(p5, fn){ */ fn.loadStrokeShader = async function (url, onSuccess, onFail) { try { - const shader = this.createStrokeShader(await urlToStrandsCallback(url)); + let shader = this.createStrokeShader(await urlToStrandsCallback(url)); if (onSuccess) { - onSuccess(shader); + shader = onSuccess(shader) || shader; } return shader; } catch (e) { From c8dab305be21f906f1fbf48ab5a7a2bd3b1fe66a Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 27 Dec 2025 09:25:09 -0500 Subject: [PATCH 10/34] Correct some examples --- src/webgl/material.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/webgl/material.js b/src/webgl/material.js index 2f8ad0d8e8..e3a30d03d8 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -1419,7 +1419,7 @@ function material(p5, fn){ * rather than using many tightly packed vertices. Sometimes this can come from * bump images, but it can also be done generatively with math. * - * ```js + * ```js example * let myShader; * * function setup() { @@ -1460,12 +1460,12 @@ function material(p5, fn){ * subject are lit up. This can be simulated by adding white to the final * color on parts of the shape that are facing away from the camera. * - * ```js + * ```js example * let myShader; * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = baseMaterialShader().modify(() => { + * myShader = createMaterialShader(() => { * let myNormal = sharedVec3(); * getPixelInputs((inputs) => { * myNormal = inputs.normal; @@ -2025,7 +2025,7 @@ function material(p5, fn){ * texture. This involves using only fully opaque or transparent pixels. Here, we * randomly choose which pixels to be transparent: * - * ```js + * ```js example * let myShader; * * function setup() { From 0c0fcb3c16f423a6958c9dcdc27b3f4499588230 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Mon, 29 Dec 2025 15:45:38 -0500 Subject: [PATCH 11/34] Fix typo in filter shader example --- src/webgl/material.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webgl/material.js b/src/webgl/material.js index e3a30d03d8..4aacfa70a8 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -654,7 +654,7 @@ function material(p5, fn){ * let myFilter; * async function setup() { * createCanvas(50, 50, WEBGL); - * let img = await loadImage('assets/bricks.jpg'); + * img = await loadImage('assets/bricks.jpg'); * myFilter = createFilterShader(() => { * let warpAmount = uniformFloat(); * getColor((inputs, canvasContent) => { From d6dfddf0db44e0651a0956562524d28a15ad957f Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Mon, 29 Dec 2025 15:49:46 -0500 Subject: [PATCH 12/34] Add more explanation --- src/webgl/material.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/webgl/material.js b/src/webgl/material.js index 4aacfa70a8..ad1ea96f1e 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -648,6 +648,7 @@ function material(p5, fn){ * For example, you could pass in the mouse cursor position and use that to control how much * you warp the content. If you create a uniform inside the shader using a function like `uniformFloat()`, with * `uniform` + the type of the data, you can set its value using `setUniform` right before applying the filter. + * In the example below, move your mouse across the image to see it update the `warpAmount` uniform: * * ```js example * let img; From 0123f2ec1aa8781acb55eee554c57d8890df1841 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Mon, 29 Dec 2025 16:04:30 -0500 Subject: [PATCH 13/34] Fix angle brackets in inline examples --- utils/shared-helpers.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/shared-helpers.mjs b/utils/shared-helpers.mjs index a4681c473d..e8fb5791bb 100644 --- a/utils/shared-helpers.mjs +++ b/utils/shared-helpers.mjs @@ -36,7 +36,7 @@ export function descriptionString(node, parent) { if (classes.length > 0) { attrs=` class="${classes.join(' ')}"`; } - return `
${node.value}
`; + return `
${node.value.replace(//g, '>')}
`; } else if (node.type === 'inlineCode') { return '' + node.value + ''; } else if (node.type === 'list') { From d124dd6437cda8b8954246b0b1fcef9dc436e16b Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Mon, 29 Dec 2025 16:07:54 -0500 Subject: [PATCH 14/34] Slightly update stroke shader example to look nicer --- src/webgl/material.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/webgl/material.js b/src/webgl/material.js index ad1ea96f1e..3300caf486 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -2079,10 +2079,10 @@ function material(p5, fn){ * getWorldInputs((inputs) => { * // Add a somewhat random offset to the weight * // that varies based on position and time - * let scale = noise( - * inputs.position.x * 0.1, - * inputs.position.y * 0.1, - * time * 0.001 + * let scale = 0.5 + noise( + * inputs.position.x * 0.01, + * inputs.position.y * 0.01, + * time * 0.0005 * ); * inputs.weight *= scale; * return inputs; From 5b950f37fc52545eb658871e4acc4fd6d3a5636a Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Mon, 29 Dec 2025 16:11:55 -0500 Subject: [PATCH 15/34] Move more methods into the p5.strands submodule --- src/strands/p5.strands.js | 2 +- src/webgl/material.js | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index cd0961e8a2..76e32ffe21 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -1,6 +1,6 @@ /** * @module 3D - * @submodule strands + * @submodule p5.strands * @for p5 * @requires core */ diff --git a/src/webgl/material.js b/src/webgl/material.js index 3300caf486..ad8fc18069 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -571,6 +571,7 @@ function material(p5, fn){ * `loadFilterShader('myShader.js', onLoaded)`. * * @method loadFilterShader + * @submodule p5.strands * @param {String} filename path to a p5.strands JavaScript file or a GLSL fragment shader file * @param {Function} [successCallback] callback to be called once the shader is * loaded. Will be passed the @@ -746,6 +747,7 @@ function material(p5, fn){ * or the Introduction to Shaders tutorial. * * @method createFilterShader + * @submodule p5.strands * @param {Function} callback A function building a p5.strands shader. * @returns {p5.Shader} The material shader */ @@ -1500,6 +1502,7 @@ function material(p5, fn){ * for more info. * * @method createMaterialShader + * @submodule p5.strands * @beta * @param {Function} callback A function building a p5.strands shader. * @returns {p5.Shader} The material shader. @@ -1563,6 +1566,7 @@ function material(p5, fn){ * `loadMaterialShader('myShader.js', onLoaded)`. * * @method loadMaterialShader + * @submodule p5.strands * @beta * @param {String} url The URL of your p5.strands JavaScript file. * @param {Function} [onSuccess] A callback function to run when loading completes. @@ -1595,6 +1599,7 @@ function material(p5, fn){ * the base material shader. * * @method baseMaterialShader + * @submodule p5.strands * @beta * @returns {p5.Shader} The base material shader. */ @@ -1614,6 +1619,7 @@ function material(p5, fn){ * the base filter shader. * * @method baseFilterShader + * @submodule p5.strands * @beta * @returns {p5.Shader} The base filter shader. */ @@ -1708,6 +1714,7 @@ function material(p5, fn){ * for more info. * * @method createNormalShader + * @submodule p5.strands * @beta * @param {Function} callback A function building a p5.strands shader. * @returns {p5.Shader} The normal shader. @@ -1772,6 +1779,7 @@ function material(p5, fn){ * `loadNormalShader('myShader.js', onLoaded)`. * * @method loadNormalShader + * @submodule p5.strands * @beta * @param {String} url The URL of your p5.strands JavaScript file. * @param {Function} [onSuccess] A callback function to run when loading completes. @@ -1805,6 +1813,7 @@ function material(p5, fn){ * the base normal shader. * * @method baseNormalShader + * @submodule p5.strands * @beta * @returns {p5.Shader} The base material shader. */ @@ -1864,6 +1873,7 @@ function material(p5, fn){ * for more info. * * @method createColorShader + * @submodule p5.strands * @beta * @param {Function} callback A function building a p5.strands shader. * @returns {p5.Shader} The color shader. @@ -1928,6 +1938,7 @@ function material(p5, fn){ * `loadColorShader('myShader.js', onLoaded)`. * * @method loadColorShader + * @submodule p5.strands * @beta * @param {String} url The URL of your p5.strands JavaScript file. * @param {Function} [onSuccess] A callback function to run when loading completes. @@ -1960,6 +1971,7 @@ function material(p5, fn){ * the base color shader. * * @method baseColorShader + * @submodule p5.strands * @beta * @returns {p5.Shader} The base color shader. */ @@ -2113,6 +2125,7 @@ function material(p5, fn){ * for more info. * * @method createStrokeShader + * @submodule p5.strands * @beta * @param {Function} callback A function building a p5.strands shader. * @returns {p5.Shader} The stroke shader. @@ -2182,6 +2195,7 @@ function material(p5, fn){ * `loadStrokeShader('myShader.js', onLoaded)`. * * @method loadStrokeShader + * @submodule p5.strands * @beta * @param {String} url The URL of your p5.strands JavaScript file. * @param {Function} [onSuccess] A callback function to run when loading completes. @@ -2214,6 +2228,7 @@ function material(p5, fn){ * the base material shader. * * @method baseStrokeShader + * @submodule p5.strands * @beta * @returns {p5.Shader} The base material shader. */ From d9c0e43b3cd78adb01fb230b039560656d5221e1 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Mon, 29 Dec 2025 16:22:38 -0500 Subject: [PATCH 16/34] Allow individual reference items to override the file's module/submodule --- utils/data-processor.mjs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/utils/data-processor.mjs b/utils/data-processor.mjs index 9a99b34452..fbb0b34f76 100644 --- a/utils/data-processor.mjs +++ b/utils/data-processor.mjs @@ -70,6 +70,8 @@ export function processData(rawData, strategy) { const entryForTagValue = entryForTag?.description; const file = entry.context?.file; let { module, submodule, for: forEntry } = fileModuleInfo[file] || {}; + module = entry.tags?.find(tag => tag.title === 'module')?.description || module; + submodule = entry.tags?.find(tag => tag.title === 'submodule')?.description || submodule; let memberof = entry.memberof; if (memberof === 'fn') memberof = 'p5'; if (memberof && memberof !== 'p5' && !memberof.startsWith('p5.')) { @@ -265,8 +267,8 @@ export function processData(rawData, strategy) { }, class: className, static: entry.scope === 'static' && 1, - module, - submodule + module: prevItem?.module ?? module, + submodule: prevItem?.submodule ?? submodule, }; processed.classMethods[className] = processed.classMethods[className] || {}; From 1a9c3339852117e53ef99bd028e59913f7c7bae8 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 30 Dec 2025 10:31:20 -0500 Subject: [PATCH 17/34] Add missing await in example --- src/webgl/material.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webgl/material.js b/src/webgl/material.js index ad8fc18069..e9ae6da295 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -538,7 +538,7 @@ function material(p5, fn){ * async function setup() { * createCanvas(50, 50, WEBGL); * let img = await loadImage('assets/bricks.jpg'); - * let myFilter = loadFilterShader('myFilter.js'); + * let myFilter = await loadFilterShader('myFilter.js'); * * image(img, -50, -50); * filter(myFilter); From 39051d2c50a40d60321db4195c9b83f27b9e2199 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 30 Dec 2025 10:48:31 -0500 Subject: [PATCH 18/34] Use syntax for callbacks --- src/webgl/material.js | 221 +++++++++++++++++++++++++++++------------- 1 file changed, 155 insertions(+), 66 deletions(-) diff --git a/src/webgl/material.js b/src/webgl/material.js index e9ae6da295..3e571bf961 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -156,9 +156,9 @@ function material(p5, fn){ * Creates a new p5.Shader object using GLSL. * * If you are interested in writing shaders, consider using p5.strands shaders using - * `createMaterialShader`, - * `createStrokeShader`, or - * `createFiltershader`. + * `buildMaterialShader`, + * `buildStrokeShader`, or + * `buildFilterShader`. * With p5.strands, you can modify existing shaders using JavaScript. With * `createShader`, shaders are made from scratch, and are written in GLSL. This * will be most useful for advanced cases, and for authors of add-on libraries. @@ -560,7 +560,7 @@ function material(p5, fn){ * }); * ``` * - * Read the reference for `createFilterShader`, + * Read the reference for `buildFilterShader`, * the version of `loadFilterShader` that takes in a function instead of a separate file, * for more examples. * @@ -592,12 +592,12 @@ function material(p5, fn){ const fragSrc = await this.loadStrings(fragFilename); const fragString = await fragSrc.join('\n'); - // Create the shader using createFilterShader + // Test if we've loaded GLSL or not by checking for the existence of `void main` let loadedShader; if (fragString.test(/void\s+main/)) { - loadedShader = this.createFilterShader(new Function(fragString)); - } else { loadedShader = this.createFilterShader(fragString, true); + } else { + loadedShader = this.baseFilterShader().modify(new Function(fragString)); } if (successCallback) { @@ -618,7 +618,7 @@ function material(p5, fn){ * Creates a p5.Shader object to be used with the * filter() function. * - * The main way to use `createFilterShader` is to pass a function in as a parameter. + * The main way to use `buildFilterShader` is to pass a function in as a parameter. * This will let you create a shader using p5.strands. * * In your function, you can call `getColor` with a function @@ -629,7 +629,7 @@ function material(p5, fn){ * async function setup() { * createCanvas(50, 50, WEBGL); * let img = await loadImage('assets/bricks.jpg'); - * let myFilter = createFilterShader(() => { + * let myFilter = buildFilterShader(() => { * getColor((inputs, canvasContent) => { * let result = getTexture(canvasContent, inputs.texCoord); * // Zero out the green and blue channels, leaving red @@ -657,7 +657,7 @@ function material(p5, fn){ * async function setup() { * createCanvas(50, 50, WEBGL); * img = await loadImage('assets/bricks.jpg'); - * myFilter = createFilterShader(() => { + * myFilter = buildFilterShader(() => { * let warpAmount = uniformFloat(); * getColor((inputs, canvasContent) => { * let coord = inputs.texCoord; @@ -685,7 +685,7 @@ function material(p5, fn){ * ```js example * function setup() { * createCanvas(50, 50, WEBGL); - * let myFilter = createFilterShader(() => { + * let myFilter = buildFilterShader(() => { * getColor((inputs) => { * return [inputs.texCoord.x, inputs.texCoord.y, 0, 1]; * }); @@ -698,7 +698,7 @@ function material(p5, fn){ * ```js example * function setup() { * createCanvas(50, 50, WEBGL); - * let myFilter = createFilterShader(() => { + * let myFilter = buildFilterShader(() => { * getColor((inputs) => { * return mix( * [1, 0, 0, 1], // Red @@ -718,7 +718,7 @@ function material(p5, fn){ * let myFilter; * function setup() { * createCanvas(50, 50, WEBGL); - * myFilter = createFilterShader(() => { + * myFilter = buildFilterShader(() => { * let time = uniformFloat(() => millis()); * getColor((inputs) => { * return mix( @@ -740,32 +740,121 @@ function material(p5, fn){ * advanced users can also fill in `getColor` using GLSL * instead of JavaScript. * Read the reference entry for `modify()` - * for more info. Alternatively, `createFilterShader()` can also be used like + * for more info. Alternatively, `buildFilterShader()` can also be used like * createShader(), but where you only specify a fragment shader. * * For more info about filters and shaders, see Adam Ferriss' repo of shader examples * or the Introduction to Shaders tutorial. * - * @method createFilterShader + * @method buildFilterShader * @submodule p5.strands * @param {Function} callback A function building a p5.strands shader. * @returns {p5.Shader} The material shader */ /** - * @method createFilterShader + * @method buildFilterShader * @param {Object} hooks An object specifying p5.strands hooks in GLSL. * @returns {p5.Shader} The material shader */ + fn.buildFilterShader = function (callback) { + return this.baseFilterShader().modify(callback); + } + /** + * Creates a p5.Shader object to be used with the + * filter() function using GLSL. + * + * Since this method requires you to write your shaders in GLSL, it is most suitable + * for advanced use cases. Consider using `buildFilterShader` + * first, as a way to create filters in JavaScript using p5.strands. + * + * `createFilterShader()` works like + * createShader() but has a default vertex + * shader included. `createFilterShader()` is intended to be used along with + * filter() for filtering the contents of a canvas. + * A filter shader will be applied to the whole canvas instead of just + * p5.Geometry objects. + * + * The parameter, `fragSrc`, sets the fragment shader. It’s a string that + * contains the fragment shader program written in + * GLSL. + * + * The p5.Shader object that's created has some + * uniforms that can be set: + * - `sampler2D tex0`, which contains the canvas contents as a texture. + * - `vec2 canvasSize`, which is the width and height of the canvas, not including pixel density. + * - `vec2 texelSize`, which is the size of a physical pixel including pixel density. This is calculated as `1.0 / (width * density)` for the pixel width and `1.0 / (height * density)` for the pixel height. + * + * The p5.Shader that's created also provides + * `varying vec2 vTexCoord`, a coordinate with values between 0 and 1. + * `vTexCoord` describes where on the canvas the pixel will be drawn. + * + * For more info about filters and shaders, see Adam Ferriss' repo of shader examples + * or the Introduction to Shaders tutorial. + * * @method createFilterShader - * @param {String} fragSrc Full GLSL source code for the fragment shader. - * @returns {p5.Shader} The material shader + * @param {String} fragSrc source code for the fragment shader. + * @returns {p5.Shader} new shader object created from the fragment shader. + * + * @example + *
+ * + * function setup() { + * let fragSrc = `precision highp float; + * void main() { + * gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0); + * }`; + * + * createCanvas(100, 100, WEBGL); + * let s = createFilterShader(fragSrc); + * filter(s); + * describe('a yellow canvas'); + * } + * + *
+ * + *
+ * + * let img, s; + * async function setup() { + * img = await loadImage('assets/bricks.jpg'); + * let fragSrc = `precision highp float; + * + * // x,y coordinates, given from the vertex shader + * varying vec2 vTexCoord; + * + * // the canvas contents, given from filter() + * uniform sampler2D tex0; + * // other useful information from the canvas + * uniform vec2 texelSize; + * uniform vec2 canvasSize; + * // a custom variable from this sketch + * uniform float darkness; + * + * void main() { + * // get the color at current pixel + * vec4 color = texture2D(tex0, vTexCoord); + * // set the output color + * color.b = 1.0; + * color *= darkness; + * gl_FragColor = vec4(color.rgb, 1.0); + * }`; + * + * createCanvas(100, 100, WEBGL); + * s = createFilterShader(fragSrc); + * } + * + * function draw() { + * image(img, -50, -50); + * s.setUniform('darkness', 0.5); + * filter(s); + * describe('a image of bricks tinted dark blue'); + * } + * + *
*/ fn.createFilterShader = function (fragSrc, skipContextCheck = false) { - if (fragSrc instanceof Function) { - return this.baseFilterShader().modify(fragSrc); - } - // p5._validateParameters('createFilterShader', arguments); + // p5._validateParameters('buildFilterShader', arguments); let defaultVertV1 = ` uniform mat4 uModelViewMatrix; uniform mat4 uProjectionMatrix; @@ -1338,7 +1427,7 @@ function material(p5, fn){ * shader into the `shader()` function to apply it * to any fills you draw. * - * The main way to use `createMaterialShader` is to pass a function in as a parameter. + * The main way to use `buildMaterialShader` is to pass a function in as a parameter. * This will let you create a shader using p5.strands. * * In your function, you can call *hooks* to change part of the shader. In a material @@ -1360,7 +1449,7 @@ function material(p5, fn){ * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = createMaterialShader(() => { + * myShader = buildMaterialShader(() => { * let time = uniformFloat(() => millis()); * getWorldInputs((inputs) => { * inputs.position.y += @@ -1392,7 +1481,7 @@ function material(p5, fn){ * environment = await loadImage('assets/outdoor_spheremap.jpg'); * * createCanvas(200, 200, WEBGL); - * myShader = createMaterialShader(() => { + * myShader = buildMaterialShader(() => { * getPixelInputs((inputs) => { * let factor = sin( * TWO_PI * (inputs.texCoord.x + inputs.texCoord.y) @@ -1427,7 +1516,7 @@ function material(p5, fn){ * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = createMaterialShader(() => { + * myShader = buildMaterialShader(() => { * getPixelInputs((inputs) => { * inputs.normal.x += 0.2 * sin( * sin(TWO_PI * dot(inputs.texCoord.yx, vec2(10, 25))) @@ -1468,7 +1557,7 @@ function material(p5, fn){ * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = createMaterialShader(() => { + * myShader = buildMaterialShader(() => { * let myNormal = sharedVec3(); * getPixelInputs((inputs) => { * myNormal = inputs.normal; @@ -1501,18 +1590,18 @@ function material(p5, fn){ * Read the reference entry for `modify()` * for more info. * - * @method createMaterialShader + * @method buildMaterialShader * @submodule p5.strands * @beta * @param {Function} callback A function building a p5.strands shader. * @returns {p5.Shader} The material shader. */ /** - * @method createMaterialShader + * @method buildMaterialShader * @param {Object} hooks An object specifying p5.strands hooks in GLSL. * @returns {p5.Shader} The material shader. */ - fn.createMaterialShader = function(cb) { + fn.buildMaterialShader = function(cb) { return this.baseMaterialShader().modify(cb); }; @@ -1555,7 +1644,7 @@ function material(p5, fn){ * }); * ``` * - * Read the reference for `createMaterialShader`, + * Read the reference for `buildMaterialShader`, * the version of `loadMaterialShader` that takes in a function instead of a separate file, * for a full list of hooks you can use and examples for each. * @@ -1575,7 +1664,7 @@ function material(p5, fn){ */ fn.loadMaterialShader = async function (url, onSuccess, onFail) { try { - let shader = this.createMaterialShader(await urlToStrandsCallback(url)); + let shader = this.buildMaterialShader(await urlToStrandsCallback(url)); if (onSuccess) { shader = onSuccess(shader) || shader; } @@ -1591,10 +1680,10 @@ function material(p5, fn){ /** * Returns the default shader used for fills when lights or textures are used. * - * Calling `createMaterialShader(shaderFunction)` + * Calling `buildMaterialShader(shaderFunction)` * is equivalent to calling `baseMaterialShader().modify(shaderFunction)`. * - * Read the `createMaterialShader` reference or + * Read the `buildMaterialShader` reference or * call `baseMaterialShader().inspectHooks()` for more information on what you can do with * the base material shader. * @@ -1611,10 +1700,10 @@ function material(p5, fn){ /** * Returns the base shader used for filters. * - * Calling `createFilterShader(shaderFunction)` + * Calling `buildFilterShader(shaderFunction)` * is equivalent to calling `baseFilterShader().modify(shaderFunction)`. * - * Read the `createFilterShader` reference or + * Read the `buildFilterShader` reference or * call `baseFilterShader().inspectHooks()` for more information on what you can do with * the base filter shader. * @@ -1634,7 +1723,7 @@ function material(p5, fn){ * shader into the `shader()` function to apply it to any fills * you draw. * - * The main way to use `createNormalShader` is to pass a function in as a parameter. + * The main way to use `buildNormalShader` is to pass a function in as a parameter. * This will let you create a shader using p5.strands. * * In your function, you can call *hooks* to change part of the shader. In a material @@ -1653,7 +1742,7 @@ function material(p5, fn){ * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = createNormalShader(() => { + * myShader = buildNormalShader(() => { * let time = uniformFloat(() => millis()); * getWorldInputs((inputs) => { * inputs.position.y += @@ -1679,7 +1768,7 @@ function material(p5, fn){ * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = createNormalShader(() => { + * myShader = buildNormalShader(() => { * getCameraInputs((inputs) => { * inputs.normal = abs(inputs.normal); * return inputs; @@ -1713,18 +1802,18 @@ function material(p5, fn){ * Read the reference entry for `modify()` * for more info. * - * @method createNormalShader + * @method buildNormalShader * @submodule p5.strands * @beta * @param {Function} callback A function building a p5.strands shader. * @returns {p5.Shader} The normal shader. */ /** - * @method createNormalShader + * @method buildNormalShader * @param {Object} hooks An object specifying p5.strands hooks in GLSL. * @returns {p5.Shader} The normal shader. */ - fn.createNormalShader = function(cb) { + fn.buildNormalShader = function(cb) { return this.baseNormalShader().modify(cb); }; @@ -1768,7 +1857,7 @@ function material(p5, fn){ * }); * ``` * - * Read the reference for `createNormalShader`, + * Read the reference for `buildNormalShader`, * the version of `loadNormalShader` that takes in a function instead of a separate file, * for a full list of hooks you can use and examples for each. * @@ -1788,7 +1877,7 @@ function material(p5, fn){ */ fn.loadNormalShader = async function (url, onSuccess, onFail) { try { - let shader = this.createNormalShader(await urlToStrandsCallback(url)); + let shader = this.buildNormalShader(await urlToStrandsCallback(url)); if (onSuccess) { shader = onSuccess(shader) || shader; } @@ -1805,10 +1894,10 @@ function material(p5, fn){ * Returns the default shader used for fills when * `normalMaterial()` is activated. * - * Calling `createNormalShader(shaderFunction)` + * Calling `buildNormalShader(shaderFunction)` * is equivalent to calling `baseNormalShader().modify(shaderFunction)`. * - * Read the `createNormalShader` reference or + * Read the `buildNormalShader` reference or * call `baseNormalShader().inspectHooks()` for more information on what you can do with * the base normal shader. * @@ -1828,7 +1917,7 @@ function material(p5, fn){ * shader into the `shader()` function to apply it * to any fills you draw. * - * The main way to use `createColorShader` is to pass a function in as a parameter. + * The main way to use `buildColorShader` is to pass a function in as a parameter. * This will let you create a shader using p5.strands. * * In your function, you can call *hooks* to change part of the shader. In a material @@ -1847,7 +1936,7 @@ function material(p5, fn){ * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = createColorShader(() => { + * myShader = buildColorShader(() => { * let time = uniformFloat(() => millis()); * getWorldInputs((inputs) => { * inputs.position.y += @@ -1872,18 +1961,18 @@ function material(p5, fn){ * Read the reference entry for `modify()` * for more info. * - * @method createColorShader + * @method buildColorShader * @submodule p5.strands * @beta * @param {Function} callback A function building a p5.strands shader. * @returns {p5.Shader} The color shader. */ /** - * @method createColorShader + * @method buildColorShader * @param {Object} hooks An object specifying p5.strands hooks in GLSL. * @returns {p5.Shader} The color shader. */ - fn.createColorShader = function(cb) { + fn.buildColorShader = function(cb) { return this.baseColorShader().modify(cb); }; @@ -1927,7 +2016,7 @@ function material(p5, fn){ * }); * ``` * - * Read the reference for `createColorShader`, + * Read the reference for `buildColorShader`, * the version of `loadColorShader` that takes in a function instead of a separate file, * for a full list of hooks you can use and examples for each. * @@ -1947,7 +2036,7 @@ function material(p5, fn){ */ fn.loadColorShader = async function (url, onSuccess, onFail) { try { - let shader = this.createColorShader(await urlToStrandsCallback(url)); + let shader = this.buildColorShader(await urlToStrandsCallback(url)); if (onSuccess) { shader = onSuccess(shader) || shader; } @@ -1963,10 +2052,10 @@ function material(p5, fn){ /** * Returns the default shader used for fills when no lights or textures are activate. * - * Calling `createColorShader(shaderFunction)` + * Calling `buildColorShader(shaderFunction)` * is equivalent to calling `baseColorShader().modify(shaderFunction)`. * - * Read the `createColorShader` reference or + * Read the `buildColorShader` reference or * call `baseColorShader().inspectHooks()` for more information on what you can do with * the base color shader. * @@ -1986,7 +2075,7 @@ function material(p5, fn){ * `strokeShader()` function to apply it to any * strokes you draw. * - * The main way to use `createStrokeShader` is to pass a function in as a parameter. + * The main way to use `buildStrokeShader` is to pass a function in as a parameter. * This will let you create a shader using p5.strands. * * In your function, you can call *hooks* to change part of the shader. In a material @@ -2008,7 +2097,7 @@ function material(p5, fn){ * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = createStrokeShader(() => { + * myShader = buildStrokeShader(() => { * getPixelInputs((inputs) => { * let opacity = 1 - smoothstep( * 0, @@ -2043,7 +2132,7 @@ function material(p5, fn){ * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = createStrokeShader(() => { + * myShader = buildStrokeShader(() => { * getPixelInputs((inputs) => { * // Replace alpha in the color with dithering by * // randomly setting pixel colors to 0 based on opacity @@ -2086,7 +2175,7 @@ function material(p5, fn){ * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = createStrokeShader(() => { + * myShader = buildStrokeShader(() => { * let time = uniformFloat(() => millis()); * getWorldInputs((inputs) => { * // Add a somewhat random offset to the weight @@ -2124,18 +2213,18 @@ function material(p5, fn){ * Read the reference entry for `modify()` * for more info. * - * @method createStrokeShader + * @method buildStrokeShader * @submodule p5.strands * @beta * @param {Function} callback A function building a p5.strands shader. * @returns {p5.Shader} The stroke shader. */ /** - * @method createStrokeShader + * @method buildStrokeShader * @param {Object} hooks An object specifying p5.strands hooks in GLSL. * @returns {p5.Shader} The stroke shader. */ - fn.createStrokeShader = function(cb) { + fn.buildStrokeShader = function(cb) { return this.baseStrokeShader().modify(cb); }; @@ -2184,7 +2273,7 @@ function material(p5, fn){ * }); * ``` * - * Read the reference for `createStrokeShader`, + * Read the reference for `buildStrokeShader`, * the version of `loadStrokeShader` that takes in a function instead of a separate file, * for a full list of hooks you can use and examples for each. * @@ -2204,7 +2293,7 @@ function material(p5, fn){ */ fn.loadStrokeShader = async function (url, onSuccess, onFail) { try { - let shader = this.createStrokeShader(await urlToStrandsCallback(url)); + let shader = this.buildStrokeShader(await urlToStrandsCallback(url)); if (onSuccess) { shader = onSuccess(shader) || shader; } @@ -2220,10 +2309,10 @@ function material(p5, fn){ /** * Returns the default shader used for strokes. * - * Calling `createStrokeShader(shaderFunction)` + * Calling `buildStrokeShader(shaderFunction)` * is equivalent to calling `baseStrokeShader().modify(shaderFunction)`. * - * Read the `createStrokeShader` reference or + * Read the `buildStrokeShader` reference or * call `baseStrokeShader().inspectHooks()` for more information on what you can do with * the base material shader. * From c83f4a77a83aa98d838812be573e61ad5aecc70d Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 30 Dec 2025 11:01:23 -0500 Subject: [PATCH 19/34] Fix missing beta tags --- docs/parameterData.json | 16 ++++++++++------ utils/convert.mjs | 2 +- utils/data-processor.mjs | 4 +++- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/parameterData.json b/docs/parameterData.json index 066db3c23a..21a9ad801d 100644 --- a/docs/parameterData.json +++ b/docs/parameterData.json @@ -2885,14 +2885,18 @@ ] ] }, - "createFilterShader": { + "buildFilterShader": { "overloads": [ [ "Function" ], [ "Object" - ], + ] + ] + }, + "createFilterShader": { + "overloads": [ [ "String" ] @@ -2919,7 +2923,7 @@ ] ] }, - "createMaterialShader": { + "buildMaterialShader": { "overloads": [ [ "Function" @@ -2948,7 +2952,7 @@ [] ] }, - "createNormalShader": { + "buildNormalShader": { "overloads": [ [ "Function" @@ -2972,7 +2976,7 @@ [] ] }, - "createColorShader": { + "buildColorShader": { "overloads": [ [ "Function" @@ -2996,7 +3000,7 @@ [] ] }, - "createStrokeShader": { + "buildStrokeShader": { "overloads": [ [ "Function" diff --git a/utils/convert.mjs b/utils/convert.mjs index 8a28b89697..9e874259ce 100644 --- a/utils/convert.mjs +++ b/utils/convert.mjs @@ -169,7 +169,7 @@ function cleanUpClassItems(data) { function buildParamDocs(docs) { let newClassItems = {}; // the fields we need—note that `name` and `class` are needed at this step because it's used to group classitems together. They will be removed later in cleanUpClassItems. - let allowed = new Set(['name', 'class', 'params', 'overloads']); + let allowed = new Set(['name', 'class', 'params', 'overloads', 'beta']); for (let classitem of docs.classitems) { // If `classitem` doesn't have overloads, then it's not a function—skip processing in this case diff --git a/utils/data-processor.mjs b/utils/data-processor.mjs index fbb0b34f76..175bbecec5 100644 --- a/utils/data-processor.mjs +++ b/utils/data-processor.mjs @@ -150,7 +150,8 @@ export function processData(rawData, strategy) { alt: getAlt(entry), module, submodule, - class: forEntry || 'p5' + class: forEntry || 'p5', + beta: entry.tags?.some(t => t.title === 'beta') || undefined, }; processed.classitems.push(item); @@ -269,6 +270,7 @@ export function processData(rawData, strategy) { static: entry.scope === 'static' && 1, module: prevItem?.module ?? module, submodule: prevItem?.submodule ?? submodule, + beta: prevItem?.beta || entry.tags?.some(t => t.title === 'beta') || undefined, }; processed.classMethods[className] = processed.classMethods[className] || {}; From 9775510a281999cab50013a9d7dfd3a0ac66322d Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 30 Dec 2025 11:35:51 -0500 Subject: [PATCH 20/34] Update shader() and createShader() docs --- src/webgl/material.js | 365 +++++++++++++++++------------------------- 1 file changed, 143 insertions(+), 222 deletions(-) diff --git a/src/webgl/material.js b/src/webgl/material.js index 3e571bf961..f4685ee932 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -165,13 +165,11 @@ function material(p5, fn){ * * Shaders are programs that run on the graphics processing unit (GPU). They * can process many pixels at the same time, making them fast for many - * graphics tasks. They’re written in a language called - * GLSL - * and run along with the rest of the code in a sketch. + * graphics tasks. * * Once the p5.Shader object is created, it can be * used with the shader() function, as in - * `shader(myShader)`. A shader program consists of two parts, a vertex shader + * `shader(myShader)`. A GLSL shader program consists of two parts, a vertex shader * and a fragment shader. The vertex shader affects where 3D geometry is drawn * on the screen and the fragment shader affects color. * @@ -181,62 +179,11 @@ function material(p5, fn){ * The second parameter, `fragSrc`, sets the fragment shader. It’s a string * that contains the fragment shader program written in GLSL. * - * A shader can optionally describe *hooks,* which are functions in GLSL that - * users may choose to provide to customize the behavior of the shader using the - * `modify()` method of `p5.Shader`. These are added by - * describing the hooks in a third parameter, `options`, and referencing the hooks in - * your `vertSrc` or `fragSrc`. Hooks for the vertex or fragment shader are described under - * the `vertex` and `fragment` keys of `options`. Each one is an object. where each key is - * the type and name of a hook function, and each value is a string with the - * parameter list and default implementation of the hook. For example, to let users - * optionally run code at the start of the vertex shader, the options object could - * include: - * - * ```js - * { - * vertex: { - * 'void beforeVertex': '() {}' - * } - * } - * ``` - * - * Then, in your vertex shader source, you can run a hook by calling a function - * with the same name prefixed by `HOOK_`. If you want to check if the default - * hook has been replaced, maybe to avoid extra overhead, you can check if the - * same name prefixed by `AUGMENTED_HOOK_` has been defined: - * - * ```glsl - * void main() { - * // In most cases, just calling the hook is fine: - * HOOK_beforeVertex(); - * - * // Alternatively, for more efficiency: - * #ifdef AUGMENTED_HOOK_beforeVertex - * HOOK_beforeVertex(); - * #endif - * - * // Add the rest of your shader code here! - * } - * ``` - * - * Note: Only filter shaders can be used in 2D mode. All shaders can be used - * in WebGL mode. - * - * @method createShader - * @param {String} vertSrc source code for the vertex shader. - * @param {String} fragSrc source code for the fragment shader. - * @param {Object} [options] An optional object describing how this shader can - * be augmented with hooks. It can include: - * @param {Object} [options.vertex] An object describing the available vertex shader hooks. - * @param {Object} [options.fragment] An object describing the available frament shader hooks. - * @returns {p5.Shader} new shader object created from the - * vertex and fragment shaders. - * - * @example - *
- * - * // Note: A "uniform" is a global variable within a shader program. + * Here is a simple example with a simple vertex shader that applies whatevre + * transformations have been set, and a simple fragment shader that ignores + * all material settings and just outputs yellow: * + * ```js example * // Create a string with the vertex shader program. * // The vertex shader is called for each vertex. * let vertSrc = ` @@ -282,93 +229,14 @@ function material(p5, fn){ * * describe('A yellow square.'); * } - * - *
- * - *
- * - * // Note: A "uniform" is a global variable within a shader program. - * - * // Create a string with the vertex shader program. - * // The vertex shader is called for each vertex. - * let vertSrc = ` - * precision highp float; - * uniform mat4 uModelViewMatrix; - * uniform mat4 uProjectionMatrix; - * attribute vec3 aPosition; - * attribute vec2 aTexCoord; - * varying vec2 vTexCoord; - * - * void main() { - * vTexCoord = aTexCoord; - * vec4 positionVec4 = vec4(aPosition, 1.0); - * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; - * } - * `; - * - * // Create a string with the fragment shader program. - * // The fragment shader is called for each pixel. - * let fragSrc = ` - * precision highp float; - * uniform vec2 p; - * uniform float r; - * const int numIterations = 500; - * varying vec2 vTexCoord; - * - * void main() { - * vec2 c = p + gl_FragCoord.xy * r; - * vec2 z = c; - * float n = 0.0; - * - * for (int i = numIterations; i > 0; i--) { - * if (z.x * z.x + z.y * z.y > 4.0) { - * n = float(i) / float(numIterations); - * break; - * } - * z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c; - * } - * - * gl_FragColor = vec4( - * 0.5 - cos(n * 17.0) / 2.0, - * 0.5 - cos(n * 13.0) / 2.0, - * 0.5 - cos(n * 23.0) / 2.0, - * 1.0 - * ); - * } - * `; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Shader object. - * let mandelbrot = createShader(vertSrc, fragSrc); - * - * // Compile and apply the p5.Shader object. - * shader(mandelbrot); - * - * // Set the shader uniform p to an array. - * // p is the center point of the Mandelbrot image. - * mandelbrot.setUniform('p', [-0.74364388703, 0.13182590421]); - * - * // Set the shader uniform r to 0.005. - * // r is the size of the image in Mandelbrot-space. - * mandelbrot.setUniform('r', 0.005); - * - * // Style the drawing surface. - * noStroke(); - * - * // Add a plane as a drawing surface. - * plane(100, 100); - * - * describe('A black fractal image on a magenta background.'); - * } - * - *
+ * ``` * - *
- * - * // Note: A "uniform" is a global variable within a shader program. + * Fragment shaders are often the fastest way to dynamically create per-pixel textures. + * Here is an example of a fractal being drawn in the fragment shader. It also creates custom + * *uniform* variables in the shader, which can be set from your main sketch code. By passing + * the time in as a uniform, we can animate the fractal in the shader. * + * ```js example * // Create a string with the vertex shader program. * // The vertex shader is called for each vertex. * let vertSrc = ` @@ -444,17 +312,57 @@ function material(p5, fn){ * let radius = 0.005 * (sin(frameCount * 0.01) + 1); * mandelbrot.setUniform('r', radius); * - * // Style the drawing surface. - * noStroke(); - * * // Add a plane as a drawing surface. + * noStroke(); * plane(100, 100); * } - * - *
+ * ``` * - *
- * + * A shader can optionally describe *hooks,* which are functions in GLSL that + * users may choose to provide to customize the behavior of the shader using the + * `modify()` method of `p5.Shader`. Users can + * write their modifications using p5.strands, without needing to learn GLSL. + * + * These are added by + * describing the hooks in a third parameter, `options`, and referencing the hooks in + * your `vertSrc` or `fragSrc`. Hooks for the vertex or fragment shader are described under + * the `vertex` and `fragment` keys of `options`. Each one is an object. where each key is + * the type and name of a hook function, and each value is a string with the + * parameter list and default implementation of the hook. For example, to let users + * optionally run code at the start of the vertex shader, the options object could + * include: + * + * ```js + * { + * vertex: { + * 'void beforeVertex': '() {}' + * } + * } + * ``` + * + * Then, in your vertex shader source, you can run a hook by calling a function + * with the same name prefixed by `HOOK_`. If you want to check if the default + * hook has been replaced, maybe to avoid extra overhead, you can check if the + * same name prefixed by `AUGMENTED_HOOK_` has been defined: + * + * ```glsl + * void main() { + * // In most cases, just calling the hook is fine: + * HOOK_beforeVertex(); + * + * // Alternatively, for more efficiency: + * #ifdef AUGMENTED_HOOK_beforeVertex + * HOOK_beforeVertex(); + * #endif + * + * // Add the rest of your shader code here! + * } + * ``` + * + * Then, a user of your shader can modify it with p5.strands. Here is what + * that looks like when we put everything together: + * + * ```js example * // A shader with hooks. * let myShader; * @@ -487,9 +395,10 @@ function material(p5, fn){ * `; * * function setup() { - * createCanvas(50, 50, WEBGL); + * createCanvas(100, 100, WEBGL); * - * // Create a shader with hooks + * // Create a shader with hooks. By default, this hook returns + * // the initial value. * myShader = createShader(vertSrc, fragSrc, { * fragment: { * 'vec4 getColor': '(vec4 color) { return color; }' @@ -497,10 +406,12 @@ function material(p5, fn){ * }); * * // Make a version of the shader with a hook overridden - * modifiedShader = myShader.modify({ - * 'vec4 getColor': `(vec4 color) { - * return vec4(0., 0., 1., 1.); - * }` + * modifiedShader = myShader.modify(() => { + * // Create new uniforms and override the getColor hook + * let t = uniformFloat(() => millis() / 1000); + * getColor(() => { + * return [0, 0.5 + 0.5 * sin(t), 1, 1]; + * }); * }); * } * @@ -510,17 +421,29 @@ function material(p5, fn){ * push(); * shader(myShader); * translate(-width/3, 0); - * sphere(10); + * sphere(20); * pop(); * * push(); * shader(modifiedShader); * translate(width/3, 0); - * sphere(10); + * sphere(20); * pop(); * } - * - *
+ * ```js + * + * Note: Only filter shaders can be used in 2D mode. All shaders can be used + * in WebGL mode. + * + * @method createShader + * @param {String} vertSrc source code for the vertex shader. + * @param {String} fragSrc source code for the fragment shader. + * @param {Object} [options] An optional object describing how this shader can + * be augmented with hooks. It can include: + * @param {Object} [options.vertex] An object describing the available vertex shader hooks. + * @param {Object} [options.fragment] An object describing the available frament shader hooks. + * @returns {p5.Shader} new shader object created from the + * vertex and fragment shaders. */ fn.createShader = function (vertSrc, fragSrc, options) { // p5._validateParameters('createShader', arguments); @@ -747,6 +670,7 @@ function material(p5, fn){ * or the Introduction to Shaders tutorial. * * @method buildFilterShader + * @beta * @submodule p5.strands * @param {Function} callback A function building a p5.strands shader. * @returns {p5.Shader} The material shader @@ -912,12 +836,16 @@ function material(p5, fn){ * * Shaders are programs that run on the graphics processing unit (GPU). They * can process many pixels or vertices at the same time, making them fast for - * many graphics tasks. They’re written in a language called - * GLSL - * and run along with the rest of the code in a sketch. - * p5.Shader objects can be created using the - * createShader() and - * loadShader() functions. + * many graphics tasks. + * + * You can make new shaders using p5.strands with the + * `buildMaterialShader`, + * `buildColorShader`, and + * `buildNormalShader` functions. You can also use + * `buildFilterShader` alongside + * `filter`, and + * `buildStrokeShader` alongside + * `stroke`. * * The parameter, `s`, is the p5.Shader object to * apply. For example, calling `shader(myShader)` applies `myShader` to @@ -925,34 +853,49 @@ function material(p5, fn){ * but does not affect the outlines (strokes) or any images drawn using the `image()` function. * The source code from a p5.Shader object's * fragment and vertex shaders will be compiled the first time it's passed to - * `shader()`. See - * MDN - * for more information about compiling shaders. + * `shader()`. * * Calling resetShader() restores a sketch’s * default shaders. * * Note: Shaders can only be used in WebGL mode. * - *
- *

+ * ```js example + * let myShader; * - * If you want to apply shaders to strokes or images, use the following methods: - * - strokeShader() : Applies a shader to the stroke (outline) of shapes, allowing independent control over the stroke rendering using shaders. - * - imageShader() : Applies a shader to images or textures, controlling how the shader modifies their appearance during rendering. + * function setup() { + * createCanvas(200, 200, WEBGL); * - *

- *
+ * myShader = createMaterialShader(() => { + * let time = uniformFloat(() => millis() / 1000); + * getFinalColor(() => { + * let r = 0.2 + 0.5 * abs(sin(time + 0)); + * let g = 0.2 + 0.5 * abs(sin(time + 1)); + * let b = 0.2 + 0.5 * abs(sin(time + 2)); + * return [r, g, b, 1]; + * }); + * }); * + * noStroke(); + * describe('A square with dynamically changing colors on a beige background.'); + * } * - * @method shader - * @chainable - * @param {p5.Shader} s p5.Shader object - * to apply. + * function draw() { + * background(245, 245, 220); + * shader(myShader); * - * @example - *
- * + * rectMode(CENTER); + * rect(0, 0, 50, 50); + * } + * ``` + * + * For advanced usage, shaders can be written in a language called + * GLSL. + * p5.Shader objects can be created in this way using the + * createShader() and + * loadShader() functions. + * + * ```js * let fillShader; * * let vertSrc = ` @@ -992,20 +935,16 @@ function material(p5, fn){ * * function draw() { * background(20, 20, 40); - * let lightDir = [0.5, 0.5, -1.0]; + * let lightDir = [0.5, 0.5, 1.0]; * fillShader.setUniform('uLightDir', lightDir); * shader(fillShader); * rotateY(frameCount * 0.02); * rotateX(frameCount * 0.02); - * //lights(); * torus(25, 10, 30, 30); * } - * - *
+ * ``` * - * @example - *
- * + * ```js example * let fillShader; * * let vertSrc = ` @@ -1048,43 +987,25 @@ function material(p5, fn){ * let fillColor = [map(mouseX, 0, width, 0, 1), * map(mouseY, 0, height, 0, 1), 0.5]; * fillShader.setUniform('uFillColor', fillColor); - * plane(100, 100); + * plane(width, height); * } - * - *
- * - * @example - *
- * - * let myShader; + * ``` * - * function setup() { - * createCanvas(200, 200, WEBGL); + *
+ *

* - * myShader = baseMaterialShader().modify({ - * declarations: 'uniform float time;', - * 'vec4 getFinalColor': `(vec4 color) { - * float r = 0.2 + 0.5 * abs(sin(time + 0.0)); - * float g = 0.2 + 0.5 * abs(sin(time + 1.0)); - * float b = 0.2 + 0.5 * abs(sin(time + 2.0)); - * color.rgb = vec3(r, g, b); - * return color; - * }` - * }); + * If you want to apply shaders to strokes or images, use the following methods: + * - strokeShader() : Applies a shader to the stroke (outline) of shapes, allowing independent control over the stroke rendering using shaders. + * - imageShader() : Applies a shader to images or textures, controlling how the shader modifies their appearance during rendering. * - * noStroke(); - * describe('A 3D cube with dynamically changing colors on a beige background.'); - * } + *

+ *
* - * function draw() { - * background(245, 245, 220); - * shader(myShader); - * myShader.setUniform('time', millis() / 1000.0); * - * box(50); - * } - *
- *
+ * @method shader + * @chainable + * @param {p5.Shader} s p5.Shader object + * to apply. * */ fn.shader = function (s) { From 967b14b47a761e5efd4f56f4bc2ae482238c522e Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 30 Dec 2025 11:37:37 -0500 Subject: [PATCH 21/34] Fix typo --- src/webgl/material.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webgl/material.js b/src/webgl/material.js index f4685ee932..46a6a832fb 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -430,7 +430,7 @@ function material(p5, fn){ * sphere(20); * pop(); * } - * ```js + * ``` * * Note: Only filter shaders can be used in 2D mode. All shaders can be used * in WebGL mode. From c485f5ca02f51788c3e54580069701ff5558c94b Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 30 Dec 2025 11:42:32 -0500 Subject: [PATCH 22/34] Remove shouldDiscard for now, since it's private --- src/webgl/material.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/webgl/material.js b/src/webgl/material.js index 46a6a832fb..e0f43cc387 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -2005,7 +2005,6 @@ function material(p5, fn){ * - `getWorldInputs`: Update vertices after transformations have been applied. Your function gets run on every vertex. * - `getCameraInputs`: Update vertices after transformations have been applied, relative to the camera. Your function gets run on every vertex. * - `getPixelInputs`: Update property values on pixels on the surface of a shape. Your function gets run on every pixel. - * - `shouldDiscard`: Decide whether or not a pixel should be drawn, generally used to carve out pieces to make rounded or mitered joins and caps. Your function gets run on every pixel. * - `getFinalColor`: Update or replace the pixel color on the surface of a shape. Your function gets run on every pixel. * * Read the linked reference page for each hook for more information about how to use them. From ecca5bd511332e0266725351e9c496e108fe94c5 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 30 Dec 2025 11:52:19 -0500 Subject: [PATCH 23/34] Fix regex preventing loading from working --- src/strands/strands_transpiler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index be1d75a739..cf8c5c5d7c 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -1058,7 +1058,7 @@ const ASTCallbacks = { recursive(ast, { varyings: {} }, postOrderControlFlowTransform); const transpiledSource = escodegen.generate(ast); const scopeKeys = Object.keys(scope); - const match = /\(?\s*(?:function)?\s*\(([^)]*)\)\s*(?:=>)?\s*{((?:.|\n)*)}\s*;?\s*\)?/ + const match = /\(?\s*(?:function)?\s*(?:\w+\s*)?\(([^)]*)\)\s*(?:=>)?\s*{((?:.|\n)*)}\s*;?\s*\)?/ .exec(transpiledSource); if (!match) { console.log(transpiledSource); From 66587192065afe4d2150f5842219d46c6a66f65a Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 30 Dec 2025 12:13:10 -0500 Subject: [PATCH 24/34] Add visual tests for load* --- src/strands/strands_api.js | 5 +++ src/webgl/material.js | 2 +- test/unit/assets/testFilter.js | 6 ++++ test/unit/assets/testMaterial.js | 9 +++++ test/unit/visual/cases/webgl.js | 31 ++++++++++++++++++ .../loadFilterShader/000.png | Bin 0 -> 248 bytes .../loadFilterShader/metadata.json | 3 ++ .../loadMaterialShader/000.png | Bin 0 -> 394 bytes .../loadMaterialShader/metadata.json | 3 ++ 9 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 test/unit/assets/testFilter.js create mode 100644 test/unit/assets/testMaterial.js create mode 100644 test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadFilterShader/000.png create mode 100644 test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadFilterShader/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadMaterialShader/000.png create mode 100644 test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadMaterialShader/metadata.json diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 6ae744954c..956843b4eb 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -500,4 +500,9 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { window[hookType.name] = hook; fn[hookType.name] = hook; } + + // TODO: remove this if we find a better way to make loaded strands shaders + // run in what looks like global mode + strandsContext.windowOverrides.getTexture = window.getTexture; + window.getTexture = fn.getTexture.bind(strandsContext.p5); } diff --git a/src/webgl/material.js b/src/webgl/material.js index e0f43cc387..2c8ef4003e 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -517,7 +517,7 @@ function material(p5, fn){ // Test if we've loaded GLSL or not by checking for the existence of `void main` let loadedShader; - if (fragString.test(/void\s+main/)) { + if (/void\s+main/.exec(fragString)) { loadedShader = this.createFilterShader(fragString, true); } else { loadedShader = this.baseFilterShader().modify(new Function(fragString)); diff --git a/test/unit/assets/testFilter.js b/test/unit/assets/testFilter.js new file mode 100644 index 0000000000..d0aead583b --- /dev/null +++ b/test/unit/assets/testFilter.js @@ -0,0 +1,6 @@ +// Test filter shader that inverts red and green channels +getColor((inputs, canvasContent) => { + const originalColor = getTexture(canvasContent, inputs.texCoord); + // Swap red and green channels, keep blue and alpha + return [originalColor.g, originalColor.r, originalColor.b, originalColor.a]; +}); \ No newline at end of file diff --git a/test/unit/assets/testMaterial.js b/test/unit/assets/testMaterial.js new file mode 100644 index 0000000000..7c0f32e6b9 --- /dev/null +++ b/test/unit/assets/testMaterial.js @@ -0,0 +1,9 @@ +// Test material shader that creates a gradient based on position +getPixelInputs((inputs) => { + // Create a color gradient based on texture coordinates + const red = inputs.texCoord.x; + const green = inputs.texCoord.y; + const blue = 0.5; + inputs.color = [red, green, blue, 1.0]; + return inputs; +}); \ No newline at end of file diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index c2157b3d18..767b2ca467 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -489,6 +489,37 @@ visualSuite('WebGL', function() { p5.image(img, -p5.width / 2, -p5.height / 2, p5.width, p5.height); screenshot(); }); + + visualTest('loadMaterialShader', async (p5, screenshot) => { + p5.createCanvas(50, 50, p5.WEBGL); + const materialShader = await p5.loadMaterialShader('/unit/assets/testMaterial.js'); + + p5.noStroke(); + p5.shader(materialShader); + p5.plane(p5.width, p5.height); + screenshot(); + }); + + visualTest('loadFilterShader', async (p5, screenshot) => { + p5.createCanvas(50, 50, p5.WEBGL); + + // Create a scene to filter (red and green stripes) + p5.background(255); + p5.noStroke(); + for (let i = 0; i < 5; i++) { + if (i % 2 === 0) { + p5.fill(255, 0, 0); // Red + } else { + p5.fill(0, 255, 0); // Green + } + p5.rect(-p5.width/2 + i * 10, -p5.height/2, 10, p5.height); + } + + // Apply the filter shader (should swap red and green channels) + const filterShader = await p5.loadFilterShader('/unit/assets/testFilter.js'); + p5.filter(filterShader); + screenshot(); + }); }); visualSuite('Strokes', function() { diff --git a/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadFilterShader/000.png b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadFilterShader/000.png new file mode 100644 index 0000000000000000000000000000000000000000..56ad16c11eaeb401875295cc44f27825cb571618 GIT binary patch literal 248 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nETwVp1HAr*{oCOL8)G2mf#{`dd+ zlzXKcd|SO{#8wGLEHb|P`pUobwZWBHxm(wjufMhZx~hHLlYAw^CP86FJ6+)$g}QFX zUih?27&gjYPfF=ISJEMf5EM*UEaIh5{Arq*OQm@o~daNG=5O_*VIi8=-&VTnOWC50vT)| bhch#XMWirZ*K861iZFP(`njxgN@xNAmTFkE literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadFilterShader/metadata.json b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadFilterShader/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadFilterShader/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadMaterialShader/000.png b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadMaterialShader/000.png new file mode 100644 index 0000000000000000000000000000000000000000..2112a108f958d8be76b9f1a3696001555ea6a3b8 GIT binary patch literal 394 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2o;fFxq>%IEGX(zPYLB&+I7B_VBEk z01sR9!2|;d5eUV@)(YgQumHsZBv7PGO-+4omH&5`oMyH2 Date: Tue, 30 Dec 2025 17:47:41 -0500 Subject: [PATCH 25/34] Run load* method shaders in global mode --- src/strands/p5.strands.js | 35 ++++++++++++++++++++++++++++++++++- src/strands/strands_api.js | 5 ----- src/webgl/material.js | 24 +++++++++++++++++++----- 3 files changed, 53 insertions(+), 11 deletions(-) diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 76e32ffe21..90f9da6b7b 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -22,6 +22,11 @@ import { } from "./strands_api"; function strands(p5, fn) { + // Whether or not strands callbacks should be forced to be executed in global mode. + // This is turned on while loading shaders from files, when there is not a feasible + // way to pass context in. + fn._runStrandsInGlobalMode = false; + ////////////////////////////////////////////// // Global Runtime ////////////////////////////////////////////// @@ -68,6 +73,30 @@ function strands(p5, fn) { initStrandsContext(strandsContext); initGlobalStrandsAPI(p5, fn, strandsContext); + function withTempGlobalMode(pInst, callback) { + if (pInst._isGlobal) return callback(); + + const prev = {}; + for (const key of Object.getOwnPropertyNames(fn)) { + const descriptor = Object.getOwnPropertyDescriptor( + fn, + key + ); + if (descriptor && !descriptor.get && typeof fn[key] === 'function') { + prev[key] = window[key]; + window[key] = fn[key].bind(pInst); + } + } + + try { + callback(); + } finally { + for (const key in prev) { + window[key] = prev[key]; + } + } + } + ////////////////////////////////////////////// // Entry Point ////////////////////////////////////////////// @@ -111,7 +140,11 @@ function strands(p5, fn) { BlockType.GLOBAL, ); pushBlock(strandsContext.cfg, globalScope); - strandsCallback(); + if (strandsContext.renderer?._pInst?._runStrandsInGlobalMode) { + withTempGlobalMode(strandsContext.renderer._pInst, strandsCallback); + } else { + strandsCallback(); + } popBlock(strandsContext.cfg); // 3. Generate shader code hooks object from the IR diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 956843b4eb..6ae744954c 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -500,9 +500,4 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { window[hookType.name] = hook; fn[hookType.name] = hook; } - - // TODO: remove this if we find a better way to make loaded strands shaders - // run in what looks like global mode - strandsContext.windowOverrides.getTexture = window.getTexture; - window.getTexture = fn.getTexture.bind(strandsContext.p5); } diff --git a/src/webgl/material.js b/src/webgl/material.js index 2c8ef4003e..c7b978f92f 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -16,6 +16,16 @@ async function urlToStrandsCallback(url) { return new Function(src); } +function withGlobalStrands(p5, cb) { + const prevGlobalStrands = p5._runStrandsInGlobalMode; + p5._runStrandsInGlobalMode = true; + try { + return cb(); + } finally { + p5._runStrandsInGlobalMode = prevGlobalStrands; + } +} + function material(p5, fn){ /** * Loads vertex and fragment shaders to create a @@ -520,7 +530,7 @@ function material(p5, fn){ if (/void\s+main/.exec(fragString)) { loadedShader = this.createFilterShader(fragString, true); } else { - loadedShader = this.baseFilterShader().modify(new Function(fragString)); + loadedShader = withGlobalStrands(this, () => this.baseFilterShader().modify(new Function(fragString))); } if (successCallback) { @@ -1585,7 +1595,8 @@ function material(p5, fn){ */ fn.loadMaterialShader = async function (url, onSuccess, onFail) { try { - let shader = this.buildMaterialShader(await urlToStrandsCallback(url)); + const cb = await urlToStrandsCallback(url); + let shader = withGlobalStrands(this, () => this.buildMaterialShader(cb)); if (onSuccess) { shader = onSuccess(shader) || shader; } @@ -1798,7 +1809,8 @@ function material(p5, fn){ */ fn.loadNormalShader = async function (url, onSuccess, onFail) { try { - let shader = this.buildNormalShader(await urlToStrandsCallback(url)); + const cb = await urlToStrandsCallback(url); + let shader = this.withGlobalStrands(this, () => this.buildNormalShader(cb)); if (onSuccess) { shader = onSuccess(shader) || shader; } @@ -1957,7 +1969,8 @@ function material(p5, fn){ */ fn.loadColorShader = async function (url, onSuccess, onFail) { try { - let shader = this.buildColorShader(await urlToStrandsCallback(url)); + const cb = await urlToStrandsCallback(url) + let shader = withGlobalStrands(this, () => this.buildColorShader(cb)); if (onSuccess) { shader = onSuccess(shader) || shader; } @@ -2213,7 +2226,8 @@ function material(p5, fn){ */ fn.loadStrokeShader = async function (url, onSuccess, onFail) { try { - let shader = this.buildStrokeShader(await urlToStrandsCallback(url)); + const cb = await urlToStrandsCallback(url); + let shader = withGlobalStrands(this, () => this.buildStrokeShader(cb)); if (onSuccess) { shader = onSuccess(shader) || shader; } From 87c0eac230eaeae5aba7bb29b2658d41ae3bc384 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Thu, 1 Jan 2026 14:54:11 -0500 Subject: [PATCH 26/34] Allow .set() on hooks to return results --- src/strands/strands_api.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 6ae744954c..01a9c41032 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -402,10 +402,16 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { for (const hookType of hookTypes) { const hook = function(hookUserCallback) { const args = setupHook(); - hook.result = hookUserCallback(...args); + hook._result = hookUserCallback(...args); finishHook(); } + // In the flat strands API, this is how result-returning hooks + // are used + hook.set = function(result) { + hook._result = result; + }; + let entryBlockID; function setupHook() { entryBlockID = CFG.createBasicBlock(cfg, BlockType.FUNCTION); @@ -425,7 +431,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { }); } if (hookType.returnType?.typeName === hookType.parameters[0].type.typeName) { - hook.result = args[0]; + hook._result = args[0]; } } else { for (let i = 0; i < args.length; i++) { @@ -436,7 +442,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { }; function finishHook() { - const userReturned = hook.result; + const userReturned = hook._result; const expectedReturnType = hookType.returnType; let rootNodeID = null; if(isStructType(expectedReturnType)) { From e1817f497fa672e99214b80e9785ff77cd9a7900 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Thu, 1 Jan 2026 15:11:01 -0500 Subject: [PATCH 27/34] Add manual hook aliases --- src/strands/strands_api.js | 26 ++++++++++++++++++++++---- src/webgl/p5.RendererGL.js | 17 ++++++++++------- src/webgl/p5.Shader.js | 3 +++ src/webgpu/p5.RendererWebGPU.js | 3 +++ 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 01a9c41032..84eb4ca650 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -501,9 +501,27 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { }; hook.begin = setupHook; hook.end = finishHook; - strandsContext.windowOverrides[hookType.name] = window[hookType.name]; - strandsContext.fnOverrides[hookType.name] = fn[hookType.name]; - window[hookType.name] = hook; - fn[hookType.name] = hook; + + const aliases = [hookType.name]; + if (strandsContext.baseShader?.hooks?.hookAliases?.[hookType.name]) { + aliases.push(...strandsContext.baseShader.hooks.hookAliases[hookType.name]); + } + + // If the hook has a name like getPixelInputs, create an alias without + // the get* prefix, like pixelInputs + const nameMatch = /^get([A-Z0-9]\w*)$/.exec(hookType.name); + if (nameMatch) { + const unprefixedName = nameMatch[1][0].toLowerCase() + nameMatch[1].slice(1); + if (!(unprefixedName in fn)) { + aliases.push(unprefixedName); + } + } + + for (const name of aliases) { + strandsContext.windowOverrides[name] = window[name]; + strandsContext.fnOverrides[name] = fn[name]; + window[name] = hook; + fn[name] = hook; + } } } diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index a7037d3759..da75ebe2e1 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -855,13 +855,16 @@ class RendererGL extends Renderer3D { this._webGL2CompatibilityPrefix("frag", "highp") + defaultShaders.filterBaseFrag, { - vertex: {}, - fragment: { - "vec4 getColor": `(FilterInputs inputs, in sampler2D canvasContent) { - return getTexture(canvasContent, inputs.texCoord); - }`, - }, - } + vertex: {}, + fragment: { + "vec4 getColor": `(FilterInputs inputs, in sampler2D canvasContent) { + return getTexture(canvasContent, inputs.texCoord); + }`, + }, + hookAliases: { + 'getColor': ['filterColor'], + }, + } ); } return this._baseFilterShader; diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 5ea36874d0..98556410fb 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -40,6 +40,8 @@ class Shader { vertex: options.vertex || {}, fragment: options.fragment || {}, + hookAliases: options.hookAliases || {}, + // Stores whether or not the hook implementation has been modified // from the default. This is supplied automatically by calling // yourShader.modify(...). @@ -398,6 +400,7 @@ class Shader { fragment: Object.assign({}, this.hooks.fragment, newHooks.fragment || {}), vertex: Object.assign({}, this.hooks.vertex, newHooks.vertex || {}), helpers: Object.assign({}, this.hooks.helpers, newHooks.helpers || {}), + hookAliases: Object.assign({}, this.hooks.hookAliases, newHooks.hookAliases || {}), modified: { vertex: modifiedVertex, fragment: modifiedFragment diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 8ed51bd96a..802d1a09cd 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -2537,6 +2537,9 @@ function rendererWebGPU(p5, fn) { return textureSample(tex, tex_sampler, inputs.texCoord); }`, }, + hookAliases: { + 'getColor': ['filterColor'], + }, } ); } From e428541c663713d562c423fd59218f220a40b7dd Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Thu, 1 Jan 2026 15:15:01 -0500 Subject: [PATCH 28/34] Let you still access struct properties when there are other non-struct args --- src/strands/strands_api.js | 39 +++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 84eb4ca650..0b290ffd2d 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -418,23 +418,28 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { CFG.addEdge(cfg, cfg.currentBlock, entryBlockID); CFG.pushBlock(cfg, entryBlockID); const args = createHookArguments(strandsContext, hookType.parameters); - if (args.length === 1 && hookType.parameters[0].type.properties) { - for (const key of args[0].structProperties || []) { - Object.defineProperty(hook, key, { - get() { - return args[0][key]; - }, - set(val) { - args[0][key] = val; - }, - enumerable: true, - }); - } - if (hookType.returnType?.typeName === hookType.parameters[0].type.typeName) { - hook._result = args[0]; - } - } else { - for (let i = 0; i < args.length; i++) { + const numStructArgs = hookType.parameters.filter(param => param.type.properties); + let argIdx = -1; + if (numStructArgs === 1) { + argIdx = hookType.parameters.findIndex(param => param.type.properties); + } + for (let i = 0; i < args.length; i++) { + if (i === argIdx) { + for (const key of args[argIdx].structProperties || []) { + Object.defineProperty(hook, key, { + get() { + return args[argIdx][key]; + }, + set(val) { + args[argIdx][key] = val; + }, + enumerable: true, + }); + } + if (hookType.returnType?.typeName === hookType.parameters[argIdx].type.typeName) { + hook._result = args[argIdx]; + } + } else { hook[hookType.parameters[i].name] = args[i]; } } From 55e65accab56d6506da4d0e2ff83db06d097ad1b Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Thu, 1 Jan 2026 15:29:55 -0500 Subject: [PATCH 29/34] Update tests --- src/strands/strands_api.js | 6 +-- test/unit/webgl/p5.Shader.js | 86 ++++++++++++++++++++++++++++++++---- 2 files changed, 81 insertions(+), 11 deletions(-) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 0b290ffd2d..b7ac7899aa 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -418,7 +418,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { CFG.addEdge(cfg, cfg.currentBlock, entryBlockID); CFG.pushBlock(cfg, entryBlockID); const args = createHookArguments(strandsContext, hookType.parameters); - const numStructArgs = hookType.parameters.filter(param => param.type.properties); + const numStructArgs = hookType.parameters.filter(param => param.type.properties).length; let argIdx = -1; if (numStructArgs === 1) { argIdx = hookType.parameters.findIndex(param => param.type.properties); @@ -437,7 +437,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { }); } if (hookType.returnType?.typeName === hookType.parameters[argIdx].type.typeName) { - hook._result = args[argIdx]; + hook.set(args[argIdx]); } } else { hook[hookType.parameters[i].name] = args[i]; @@ -517,7 +517,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { const nameMatch = /^get([A-Z0-9]\w*)$/.exec(hookType.name); if (nameMatch) { const unprefixedName = nameMatch[1][0].toLowerCase() + nameMatch[1].slice(1); - if (!(unprefixedName in fn)) { + if (!fn[unprefixedName]) { aliases.push(unprefixedName); } } diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index 2c59a24ef8..ca5d39aa61 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -1636,7 +1636,7 @@ suite('p5.Shader', function() { const testShader = myp5.baseFilterShader().modify(() => { myp5.getColor.begin(); - myp5.getColor.result = [1.0, 0.5, 0.0, 1.0]; + myp5.getColor.set([1.0, 0.5, 0.0, 1.0]); myp5.getColor.end(); }, { myp5 }); @@ -1648,9 +1648,31 @@ suite('p5.Shader', function() { // Check that the filter was applied (should be orange) const pixelColor = myp5.get(25, 25); - assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 - assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127 - assert.approximately(pixelColor[2], 0, 5); // Blue channel should be 0 + assert.approximately(pixelColor[0], 255, 5); + assert.approximately(pixelColor[1], 127, 5); + assert.approximately(pixelColor[2], 0, 5); + }); + + test('Can use begin/end API for hooks with hook alias', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseFilterShader().modify(() => { + myp5.filterColor.begin(); + myp5.filterColor.set([1.0, 0.5, 0.0, 1.0]); + myp5.filterColor.end(); + }, { myp5 }); + + // Create a simple scene to filter + myp5.background(0, 0, 255); // Blue background + + // Apply the filter + myp5.filter(testShader); + + // Check that the filter was applied (should be orange) + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 255, 5); + assert.approximately(pixelColor[1], 127, 5); + assert.approximately(pixelColor[2], 0, 5); }); test('Can use begin/end API for hooks modifying inputs', () => { @@ -1658,7 +1680,6 @@ suite('p5.Shader', function() { const testShader = myp5.baseMaterialShader().modify(() => { myp5.getPixelInputs.begin(); - debugger myp5.getPixelInputs.color = [1.0, 0.5, 0.0, 1.0]; myp5.getPixelInputs.end(); }, { myp5 }); @@ -1674,9 +1695,58 @@ suite('p5.Shader', function() { // Check that the filter was applied (should be orange) const pixelColor = myp5.get(25, 25); - assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 - assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127 - assert.approximately(pixelColor[2], 0, 5); // Blue channel should be 0 + assert.approximately(pixelColor[0], 255, 5); + assert.approximately(pixelColor[1], 127, 5); + assert.approximately(pixelColor[2], 0, 5); + }); + + test('Can use begin/end API for hooks with struct access', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseFilterShader().modify(() => { + myp5.filterColor.begin(); + let c = myp5.getTexture(myp5.filterColor.canvasContent, myp5.filterColor.texCoord); + c.r = 1; + myp5.filterColor.set(c); + myp5.filterColor.end(); + }, { myp5 }); + + // Create a simple scene to filter + myp5.background(0, 0, 255); // Blue background + + // Apply the filter + myp5.filter(testShader); + + // Check that the filter was applied (should be magenta) + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 255, 5); + assert.approximately(pixelColor[1], 0, 5); + assert.approximately(pixelColor[2], 255, 5); + }); + + test('Can use begin/end API for hooks with get* prefix removed', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.pixelInputs.begin(); + myp5.pixelInputs.color = [1.0, 0.5, 0.0, 1.0]; + myp5.pixelInputs.end(); + }, { myp5 }); + + // Create a simple scene to filter + myp5.background(0, 0, 255); // Blue background + + // Draw a fullscreen rectangle + myp5.noStroke(); + myp5.fill('red') + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Check that the filter was applied (should be orange) + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 255, 5); + assert.approximately(pixelColor[1], 127, 5); + assert.approximately(pixelColor[2], 0, 5); }); }); }); From e4ed3225a11be74feea92d55e43f648faf415682 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 4 Jan 2026 12:43:39 -0500 Subject: [PATCH 30/34] Update examples to use named functions --- src/webgl/material.js | 527 ++++++++++++++++++++++-------------------- 1 file changed, 281 insertions(+), 246 deletions(-) diff --git a/src/webgl/material.js b/src/webgl/material.js index c7b978f92f..af0003156e 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -5,14 +5,14 @@ * @requires core */ -import * as constants from '../core/constants'; -import { Renderer3D } from '../core/p5.Renderer3D'; -import { Shader } from './p5.Shader'; -import { request } from '../io/files'; -import { Color } from '../color/p5.Color'; +import * as constants from "../core/constants"; +import { Renderer3D } from "../core/p5.Renderer3D"; +import { Shader } from "./p5.Shader"; +import { request } from "../io/files"; +import { Color } from "../color/p5.Color"; async function urlToStrandsCallback(url) { - const src = await fetch(url).then(res => res.text()); + const src = await fetch(url).then((res) => res.text()); return new Function(src); } @@ -26,7 +26,7 @@ function withGlobalStrands(p5, cb) { } } -function material(p5, fn){ +function material(p5, fn) { /** * Loads vertex and fragment shaders to create a * p5.Shader object. @@ -138,22 +138,22 @@ function material(p5, fn){ vertFilename, fragFilename, successCallback, - failureCallback + failureCallback, ) { // p5._validateParameters('loadShader', arguments); const loadedShader = new Shader(); try { - loadedShader._vertSrc = (await request(vertFilename, 'text')).data; - loadedShader._fragSrc = (await request(fragFilename, 'text')).data; + loadedShader._vertSrc = (await request(vertFilename, "text")).data; + loadedShader._fragSrc = (await request(fragFilename, "text")).data; if (successCallback) { return successCallback(loadedShader) || loadedShader; } else { return loadedShader; } - } catch(err) { + } catch (err) { if (failureCallback) { return failureCallback(err); } else { @@ -517,20 +517,22 @@ function material(p5, fn){ fn.loadFilterShader = async function ( fragFilename, successCallback, - failureCallback + failureCallback, ) { // p5._validateParameters('loadFilterShader', arguments); try { // Load the fragment shader const fragSrc = await this.loadStrings(fragFilename); - const fragString = await fragSrc.join('\n'); + const fragString = await fragSrc.join("\n"); // Test if we've loaded GLSL or not by checking for the existence of `void main` let loadedShader; if (/void\s+main/.exec(fragString)) { loadedShader = this.createFilterShader(fragString, true); } else { - loadedShader = withGlobalStrands(this, () => this.baseFilterShader().modify(new Function(fragString))); + loadedShader = withGlobalStrands(this, () => + this.baseFilterShader().modify(new Function(fragString)), + ); } if (successCallback) { @@ -562,20 +564,22 @@ function material(p5, fn){ * async function setup() { * createCanvas(50, 50, WEBGL); * let img = await loadImage('assets/bricks.jpg'); - * let myFilter = buildFilterShader(() => { - * getColor((inputs, canvasContent) => { - * let result = getTexture(canvasContent, inputs.texCoord); - * // Zero out the green and blue channels, leaving red - * result.g = 0; - * result.b = 0; - * return result; - * }); - * }); + * let myFilter = buildFilterShader(tintShader); * * image(img, -50, -50); * filter(myFilter); * describe('Bricks tinted red'); * } + * + * function tintShader() { + * getColor((inputs, canvasContent) => { + * let result = getTexture(canvasContent, inputs.texCoord); + * // Zero out the green and blue channels, leaving red + * result.g = 0; + * result.b = 0; + * return result; + * }); + * } * ``` * * You can create *uniforms* if you want to pass data into your filter from the rest of your sketch. @@ -590,17 +594,19 @@ function material(p5, fn){ * async function setup() { * createCanvas(50, 50, WEBGL); * img = await loadImage('assets/bricks.jpg'); - * myFilter = buildFilterShader(() => { - * let warpAmount = uniformFloat(); - * getColor((inputs, canvasContent) => { - * let coord = inputs.texCoord; - * coord.y += sin(coord.x * 10) * warpAmount; - * return getTexture(canvasContent, coord); - * }); - * }); + * myFilter = buildFilterShader(warpShader); * describe('Warped bricks'); * } * + * function warpShader() { + * let warpAmount = uniformFloat(); + * getColor((inputs, canvasContent) => { + * let coord = inputs.texCoord; + * coord.y += sin(coord.x * 10) * warpAmount; + * return getTexture(canvasContent, coord); + * }); + * } + * * function draw() { * image(img, -50, -50); * myFilter.setUniform( @@ -618,31 +624,35 @@ function material(p5, fn){ * ```js example * function setup() { * createCanvas(50, 50, WEBGL); - * let myFilter = buildFilterShader(() => { - * getColor((inputs) => { - * return [inputs.texCoord.x, inputs.texCoord.y, 0, 1]; - * }); - * }); + * let myFilter = buildFilterShader(gradient); * describe('A gradient with red, green, yellow, and black'); * filter(myFilter); * } + * + * function gradient() { + * getColor((inputs) => { + * return [inputs.texCoord.x, inputs.texCoord.y, 0, 1]; + * }); + * } * ``` * * ```js example * function setup() { * createCanvas(50, 50, WEBGL); - * let myFilter = buildFilterShader(() => { - * getColor((inputs) => { - * return mix( - * [1, 0, 0, 1], // Red - * [0, 0, 1, 1], // Blue - * inputs.texCoord.x // x coordinate, from 0 to 1 - * ); - * }); - * }); + * let myFilter = buildFilterShader(gradient); * describe('A gradient from red to blue'); * filter(myFilter); * } + * + * function gradient() { + * getColor((inputs) => { + * return mix( + * [1, 0, 0, 1], // Red + * [0, 0, 1, 1], // Blue + * inputs.texCoord.x // x coordinate, from 0 to 1 + * ); + * }); + * } * ``` * * You can also animate your filters over time by passing the time into the shader with `uniformFloat`. @@ -651,19 +661,21 @@ function material(p5, fn){ * let myFilter; * function setup() { * createCanvas(50, 50, WEBGL); - * myFilter = buildFilterShader(() => { - * let time = uniformFloat(() => millis()); - * getColor((inputs) => { - * return mix( - * [1, 0, 0, 1], // Red - * [0, 0, 1, 1], // Blue - * sin(inputs.texCoord.x*15 + time*0.004)/2+0.5 - * ); - * }); - * }); + * myFilter = buildFilterShader(gradient); * describe('A moving, repeating gradient from red to blue'); * } * + * function gradient() { + * let time = uniformFloat(() => millis()); + * getColor((inputs) => { + * return mix( + * [1, 0, 0, 1], // Red + * [0, 0, 1, 1], // Blue + * sin(inputs.texCoord.x*15 + time*0.004)/2+0.5 + * ); + * }); + * } + * * function draw() { * filter(myFilter); * } @@ -692,7 +704,7 @@ function material(p5, fn){ */ fn.buildFilterShader = function (callback) { return this.baseFilterShader().modify(callback); - } + }; /** * Creates a p5.Shader object to be used with the @@ -829,7 +841,9 @@ function material(p5, fn){ gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; } `; - let vertSrc = fragSrc.includes('#version 300 es') ? defaultVertV2 : defaultVertV1; + let vertSrc = fragSrc.includes("#version 300 es") + ? defaultVertV2 + : defaultVertV1; const shader = new Shader(this._renderer, vertSrc, fragSrc); if (!skipContextCheck) { if (this._renderer.GL) { @@ -875,21 +889,21 @@ function material(p5, fn){ * * function setup() { * createCanvas(200, 200, WEBGL); - * - * myShader = createMaterialShader(() => { - * let time = uniformFloat(() => millis() / 1000); - * getFinalColor(() => { - * let r = 0.2 + 0.5 * abs(sin(time + 0)); - * let g = 0.2 + 0.5 * abs(sin(time + 1)); - * let b = 0.2 + 0.5 * abs(sin(time + 2)); - * return [r, g, b, 1]; - * }); - * }); - * + * myShader = buildMaterialShader(material); * noStroke(); * describe('A square with dynamically changing colors on a beige background.'); * } * + * function material() { + * let time = uniformFloat(() => millis() / 1000); + * getFinalColor(() => { + * let r = 0.2 + 0.5 * abs(sin(time + 0)); + * let g = 0.2 + 0.5 * abs(sin(time + 1)); + * let b = 0.2 + 0.5 * abs(sin(time + 2)); + * return [r, g, b, 1]; + * }); + * } + * * function draw() { * background(245, 245, 220); * shader(myShader); @@ -1019,7 +1033,7 @@ function material(p5, fn){ * */ fn.shader = function (s) { - this._assert3d('shader'); + this._assert3d("shader"); // p5._validateParameters('shader', arguments); this._renderer.shader(s); @@ -1192,7 +1206,7 @@ function material(p5, fn){ * */ fn.strokeShader = function (s) { - this._assert3d('strokeShader'); + this._assert3d("strokeShader"); // p5._validateParameters('strokeShader', arguments); this._renderer.strokeShader(s); @@ -1345,7 +1359,7 @@ function material(p5, fn){ * */ fn.imageShader = function (s) { - this._assert3d('imageShader'); + this._assert3d("imageShader"); // p5._validateParameters('imageShader', arguments); this._renderer.imageShader(s); @@ -1380,13 +1394,15 @@ function material(p5, fn){ * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = buildMaterialShader(() => { - * let time = uniformFloat(() => millis()); - * getWorldInputs((inputs) => { - * inputs.position.y += - * 20 * sin(time * 0.001 + inputs.position.x * 0.05); - * return inputs; - * }); + * myShader = buildMaterialShader(material); + * } + * + * function material() { + * let time = uniformFloat(() => millis()); + * getWorldInputs((inputs) => { + * inputs.position.y += + * 20 * sin(time * 0.001 + inputs.position.x * 0.05); + * return inputs; * }); * } * @@ -1412,16 +1428,18 @@ function material(p5, fn){ * environment = await loadImage('assets/outdoor_spheremap.jpg'); * * createCanvas(200, 200, WEBGL); - * myShader = buildMaterialShader(() => { - * getPixelInputs((inputs) => { - * let factor = sin( - * TWO_PI * (inputs.texCoord.x + inputs.texCoord.y) - * ); - * inputs.shininess = mix(1, 100, factor); - * inputs.metalness = factor; - * return inputs; - * }) - * }); + * myShader = buildMaterialShader(material); + * } + * + * function material() { + * getPixelInputs((inputs) => { + * let factor = sin( + * TWO_PI * (inputs.texCoord.x + inputs.texCoord.y) + * ); + * inputs.shininess = mix(1, 100, factor); + * inputs.metalness = factor; + * return inputs; + * }) * } * * function draw() { @@ -1447,17 +1465,19 @@ function material(p5, fn){ * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = buildMaterialShader(() => { - * getPixelInputs((inputs) => { - * inputs.normal.x += 0.2 * sin( - * sin(TWO_PI * dot(inputs.texCoord.yx, vec2(10, 25))) - * ); - * inputs.normal.y += 0.2 * sin( - * sin(TWO_PI * dot(inputs.texCoord, vec2(10, 25))) - * ); - * inputs.normal = normalize(inputs.normal); - * return inputs; - * }); + * myShader = buildMaterialShader(material); + * } + * + * function material() { + * getPixelInputs((inputs) => { + * inputs.normal.x += 0.2 * sin( + * sin(TWO_PI * dot(inputs.texCoord.yx, vec2(10, 25))) + * ); + * inputs.normal.y += 0.2 * sin( + * sin(TWO_PI * dot(inputs.texCoord, vec2(10, 25))) + * ); + * inputs.normal = normalize(inputs.normal); + * return inputs; * }); * } * @@ -1488,19 +1508,21 @@ function material(p5, fn){ * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = buildMaterialShader(() => { - * let myNormal = sharedVec3(); - * getPixelInputs((inputs) => { - * myNormal = inputs.normal; - * return inputs; - * }); - * getFinalColor((color) => { - * return mix( - * [1, 1, 1, 1], - * color, - * abs(dot(myNormal, [0, 0, 1])) - * ); - * }); + * myShader = buildMaterialShader(material); + * } + * + * function material() { + * let myNormal = sharedVec3(); + * getPixelInputs((inputs) => { + * myNormal = inputs.normal; + * return inputs; + * }); + * getFinalColor((color) => { + * return mix( + * [1, 1, 1, 1], + * color, + * abs(dot(myNormal, [0, 0, 1])) + * ); * }); * } * @@ -1532,7 +1554,7 @@ function material(p5, fn){ * @param {Object} hooks An object specifying p5.strands hooks in GLSL. * @returns {p5.Shader} The material shader. */ - fn.buildMaterialShader = function(cb) { + fn.buildMaterialShader = function (cb) { return this.baseMaterialShader().modify(cb); }; @@ -1624,8 +1646,8 @@ function material(p5, fn){ * @beta * @returns {p5.Shader} The base material shader. */ - fn.baseMaterialShader = function() { - this._assert3d('baseMaterialShader'); + fn.baseMaterialShader = function () { + this._assert3d("baseMaterialShader"); return this._renderer.baseMaterialShader(); }; @@ -1644,9 +1666,8 @@ function material(p5, fn){ * @beta * @returns {p5.Shader} The base filter shader. */ - fn.baseFilterShader = function() { - return (this._renderer.filterRenderer || this._renderer) - .baseFilterShader(); + fn.baseFilterShader = function () { + return (this._renderer.filterRenderer || this._renderer).baseFilterShader(); }; /** @@ -1658,7 +1679,7 @@ function material(p5, fn){ * The main way to use `buildNormalShader` is to pass a function in as a parameter. * This will let you create a shader using p5.strands. * - * In your function, you can call *hooks* to change part of the shader. In a material + * In your function, you can call *hooks* to change part of the shader. In a material * shader, these are the hooks available: * - `getObjectInputs`: Update vertices before any positioning has been applied. Your function gets run on every vertex. * - `getWorldInputs`: Update vertices after transformations have been applied. Your function gets run on every vertex. @@ -1674,13 +1695,15 @@ function material(p5, fn){ * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = buildNormalShader(() => { - * let time = uniformFloat(() => millis()); - * getWorldInputs((inputs) => { - * inputs.position.y += - * 20. * sin(time * 0.001 + inputs.position.x * 0.05); - * return inputs; - * }); + * myShader = buildNormalShader(material); + * } + * + * function material() { + * let time = uniformFloat(() => millis()); + * getWorldInputs((inputs) => { + * inputs.position.y += + * 20. * sin(time * 0.001 + inputs.position.x * 0.05); + * return inputs; * }); * } * @@ -1700,21 +1723,23 @@ function material(p5, fn){ * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = buildNormalShader(() => { - * getCameraInputs((inputs) => { - * inputs.normal = abs(inputs.normal); - * return inputs; - * }); - * getFinalColor((color) => { - * // Map the r, g, and b values of the old normal to new colors - * // instead of just red, green, and blue: - * let newColor = - * color.r * [89, 240, 232] / 255 + - * color.g * [240, 237, 89] / 255 + - * color.b * [205, 55, 222] / 255; - * newColor = newColor / (color.r + color.g + color.b); - * return [newColor.r, newColor.g, newColor.b, color.a]; - * }); + * myShader = buildNormalShader(material); + * } + * + * function material() { + * getCameraInputs((inputs) => { + * inputs.normal = abs(inputs.normal); + * return inputs; + * }); + * getFinalColor((color) => { + * // Map the r, g, and b values of the old normal to new colors + * // instead of just red, green, and blue: + * let newColor = + * color.r * [89, 240, 232] / 255 + + * color.g * [240, 237, 89] / 255 + + * color.b * [205, 55, 222] / 255; + * newColor = newColor / (color.r + color.g + color.b); + * return [newColor.r, newColor.g, newColor.b, color.a]; * }); * } * @@ -1745,7 +1770,7 @@ function material(p5, fn){ * @param {Object} hooks An object specifying p5.strands hooks in GLSL. * @returns {p5.Shader} The normal shader. */ - fn.buildNormalShader = function(cb) { + fn.buildNormalShader = function (cb) { return this.baseNormalShader().modify(cb); }; @@ -1810,7 +1835,9 @@ function material(p5, fn){ fn.loadNormalShader = async function (url, onSuccess, onFail) { try { const cb = await urlToStrandsCallback(url); - let shader = this.withGlobalStrands(this, () => this.buildNormalShader(cb)); + let shader = this.withGlobalStrands(this, () => + this.buildNormalShader(cb), + ); if (onSuccess) { shader = onSuccess(shader) || shader; } @@ -1839,8 +1866,8 @@ function material(p5, fn){ * @beta * @returns {p5.Shader} The base material shader. */ - fn.baseNormalShader = function() { - this._assert3d('baseNormalShader'); + fn.baseNormalShader = function () { + this._assert3d("baseNormalShader"); return this._renderer.baseNormalShader(); }; @@ -1869,13 +1896,15 @@ function material(p5, fn){ * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = buildColorShader(() => { - * let time = uniformFloat(() => millis()); - * getWorldInputs((inputs) => { - * inputs.position.y += - * 20 * sin(time * 0.001 + inputs.position.x * 0.05); - * return inputs; - * }); + * myShader = buildColorShader(material); + * } + * + * function material() { + * let time = uniformFloat(() => millis()); + * getWorldInputs((inputs) => { + * inputs.position.y += + * 20 * sin(time * 0.001 + inputs.position.x * 0.05); + * return inputs; * }); * } * @@ -1905,7 +1934,7 @@ function material(p5, fn){ * @param {Object} hooks An object specifying p5.strands hooks in GLSL. * @returns {p5.Shader} The color shader. */ - fn.buildColorShader = function(cb) { + fn.buildColorShader = function (cb) { return this.baseColorShader().modify(cb); }; @@ -1969,7 +1998,7 @@ function material(p5, fn){ */ fn.loadColorShader = async function (url, onSuccess, onFail) { try { - const cb = await urlToStrandsCallback(url) + const cb = await urlToStrandsCallback(url); let shader = withGlobalStrands(this, () => this.buildColorShader(cb)); if (onSuccess) { shader = onSuccess(shader) || shader; @@ -1998,8 +2027,8 @@ function material(p5, fn){ * @beta * @returns {p5.Shader} The base color shader. */ - fn.baseColorShader = function() { - this._assert3d('baseColorShader'); + fn.baseColorShader = function () { + this._assert3d("baseColorShader"); return this._renderer.baseColorShader(); }; @@ -2030,16 +2059,18 @@ function material(p5, fn){ * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = buildStrokeShader(() => { - * getPixelInputs((inputs) => { - * let opacity = 1 - smoothstep( - * 0, - * 15, - * length(inputs.position - inputs.center) - * ); - * inputs.color.a *= opacity; - * return inputs; - * }); + * myShader = buildStrokeShader(material); + * } + * + * function material() { + * getPixelInputs((inputs) => { + * let opacity = 1 - smoothstep( + * 0, + * 15, + * length(inputs.position - inputs.center) + * ); + * inputs.color.a *= opacity; + * return inputs; * }); * } * @@ -2065,17 +2096,19 @@ function material(p5, fn){ * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = buildStrokeShader(() => { - * getPixelInputs((inputs) => { - * // Replace alpha in the color with dithering by - * // randomly setting pixel colors to 0 based on opacity - * let a = 1; - * if (noise(inputs.position.xy) > inputs.color.a) { - * a = 0; - * } - * inputs.color.a = a; - * return inputs; - * }); + * myShader = buildStrokeShader(material); + * } + * + * function material() { + * getPixelInputs((inputs) => { + * // Replace alpha in the color with dithering by + * // randomly setting pixel colors to 0 based on opacity + * let a = 1; + * if (noise(inputs.position.xy) > inputs.color.a) { + * a = 0; + * } + * inputs.color.a = a; + * return inputs; * }); * } * @@ -2108,19 +2141,21 @@ function material(p5, fn){ * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = buildStrokeShader(() => { - * let time = uniformFloat(() => millis()); - * getWorldInputs((inputs) => { - * // Add a somewhat random offset to the weight - * // that varies based on position and time - * let scale = 0.5 + noise( - * inputs.position.x * 0.01, - * inputs.position.y * 0.01, - * time * 0.0005 - * ); - * inputs.weight *= scale; - * return inputs; - * }); + * myShader = buildStrokeShader(material); + * } + * + * function material() { + * let time = uniformFloat(() => millis()); + * getWorldInputs((inputs) => { + * // Add a somewhat random offset to the weight + * // that varies based on position and time + * let scale = 0.5 + noise( + * inputs.position.x * 0.01, + * inputs.position.y * 0.01, + * time * 0.0005 + * ); + * inputs.weight *= scale; + * return inputs; * }); * } * @@ -2157,7 +2192,7 @@ function material(p5, fn){ * @param {Object} hooks An object specifying p5.strands hooks in GLSL. * @returns {p5.Shader} The stroke shader. */ - fn.buildStrokeShader = function(cb) { + fn.buildStrokeShader = function (cb) { return this.baseStrokeShader().modify(cb); }; @@ -2255,8 +2290,8 @@ function material(p5, fn){ * @beta * @returns {p5.Shader} The base material shader. */ - fn.baseStrokeShader = function() { - this._assert3d('baseStrokeShader'); + fn.baseStrokeShader = function () { + this._assert3d("baseStrokeShader"); return this._renderer.baseStrokeShader(); }; @@ -2516,7 +2551,7 @@ function material(p5, fn){ * */ fn.texture = function (tex) { - this._assert3d('texture'); + this._assert3d("texture"); // p5._validateParameters('texture', arguments); // NOTE: make generic or remove need for @@ -2697,10 +2732,10 @@ function material(p5, fn){ fn.textureMode = function (mode) { if (mode !== constants.IMAGE && mode !== constants.NORMAL) { console.warn( - `You tried to set ${mode} textureMode only supports IMAGE & NORMAL ` + `You tried to set ${mode} textureMode only supports IMAGE & NORMAL `, ); } else { - this._renderer.states.setValue('textureMode', mode); + this._renderer.states.setValue("textureMode", mode); } }; @@ -2971,8 +3006,8 @@ function material(p5, fn){ * */ fn.textureWrap = function (wrapX, wrapY = wrapX) { - this._renderer.states.setValue('textureWrapX', wrapX); - this._renderer.states.setValue('textureWrapY', wrapY); + this._renderer.states.setValue("textureWrapX", wrapX); + this._renderer.states.setValue("textureWrapY", wrapY); for (const texture of this._renderer.textures.values()) { texture.setWrapMode(wrapX, wrapY); @@ -3018,7 +3053,7 @@ function material(p5, fn){ * */ fn.normalMaterial = function (...args) { - this._assert3d('normalMaterial'); + this._assert3d("normalMaterial"); // p5._validateParameters('normalMaterial', args); this._renderer.normalMaterial(...args); @@ -3245,16 +3280,16 @@ function material(p5, fn){ * @chainable */ fn.ambientMaterial = function (v1, v2, v3) { - this._assert3d('ambientMaterial'); + this._assert3d("ambientMaterial"); // p5._validateParameters('ambientMaterial', arguments); const color = fn.color.apply(this, arguments); - this._renderer.states.setValue('_hasSetAmbient', true); - this._renderer.states.setValue('curAmbientColor', color._array); - this._renderer.states.setValue('_useNormalMaterial', false); - this._renderer.states.setValue('enableLighting', true); + this._renderer.states.setValue("_hasSetAmbient", true); + this._renderer.states.setValue("curAmbientColor", color._array); + this._renderer.states.setValue("_useNormalMaterial", false); + this._renderer.states.setValue("enableLighting", true); if (!this._renderer.states.fillColor) { - this._renderer.states.setValue('fillColor', new Color([1, 1, 1])); + this._renderer.states.setValue("fillColor", new Color([1, 1, 1])); } return this; }; @@ -3343,14 +3378,14 @@ function material(p5, fn){ * @chainable */ fn.emissiveMaterial = function (v1, v2, v3, a) { - this._assert3d('emissiveMaterial'); + this._assert3d("emissiveMaterial"); // p5._validateParameters('emissiveMaterial', arguments); const color = fn.color.apply(this, arguments); - this._renderer.states.setValue('curEmissiveColor', color._array); - this._renderer.states.setValue('_useEmissiveMaterial', true); - this._renderer.states.setValue('_useNormalMaterial', false); - this._renderer.states.setValue('enableLighting', true); + this._renderer.states.setValue("curEmissiveColor", color._array); + this._renderer.states.setValue("_useEmissiveMaterial", true); + this._renderer.states.setValue("_useNormalMaterial", false); + this._renderer.states.setValue("enableLighting", true); return this; }; @@ -3598,14 +3633,14 @@ function material(p5, fn){ * @chainable */ fn.specularMaterial = function (v1, v2, v3, alpha) { - this._assert3d('specularMaterial'); + this._assert3d("specularMaterial"); // p5._validateParameters('specularMaterial', arguments); const color = fn.color.apply(this, arguments); - this._renderer.states.setValue('curSpecularColor', color._array); - this._renderer.states.setValue('_useSpecularMaterial', true); - this._renderer.states.setValue('_useNormalMaterial', false); - this._renderer.states.setValue('enableLighting', true); + this._renderer.states.setValue("curSpecularColor", color._array); + this._renderer.states.setValue("_useSpecularMaterial", true); + this._renderer.states.setValue("_useNormalMaterial", false); + this._renderer.states.setValue("enableLighting", true); return this; }; @@ -3671,7 +3706,7 @@ function material(p5, fn){ * */ fn.shininess = function (shine) { - this._assert3d('shininess'); + this._assert3d("shininess"); // p5._validateParameters('shininess', arguments); this._renderer.shininess(shine); @@ -3787,54 +3822,54 @@ function material(p5, fn){ * */ fn.metalness = function (metallic) { - this._assert3d('metalness'); + this._assert3d("metalness"); this._renderer.metalness(metallic); return this; }; - Renderer3D.prototype.shader = function(s) { + Renderer3D.prototype.shader = function (s) { // Always set the shader as a fill shader - this.states.setValue('userFillShader', s); - this.states.setValue('_useNormalMaterial', false); + this.states.setValue("userFillShader", s); + this.states.setValue("_useNormalMaterial", false); s.ensureCompiledOnContext(this); s.setDefaultUniforms(); }; - Renderer3D.prototype.strokeShader = function(s) { - this.states.setValue('userStrokeShader', s); + Renderer3D.prototype.strokeShader = function (s) { + this.states.setValue("userStrokeShader", s); s.ensureCompiledOnContext(this); s.setDefaultUniforms(); }; - Renderer3D.prototype.imageShader = function(s) { - this.states.setValue('userImageShader', s); + Renderer3D.prototype.imageShader = function (s) { + this.states.setValue("userImageShader", s); s.ensureCompiledOnContext(this); s.setDefaultUniforms(); }; - Renderer3D.prototype.resetShader = function() { - this.states.setValue('userFillShader', null); - this.states.setValue('userStrokeShader', null); - this.states.setValue('userImageShader', null); + Renderer3D.prototype.resetShader = function () { + this.states.setValue("userFillShader", null); + this.states.setValue("userStrokeShader", null); + this.states.setValue("userImageShader", null); }; - Renderer3D.prototype.texture = function(tex) { - this.states.setValue('drawMode', constants.TEXTURE); - this.states.setValue('_useNormalMaterial', false); - this.states.setValue('_tex', tex); - this.states.setValue('fillColor', new Color([1, 1, 1])); + Renderer3D.prototype.texture = function (tex) { + this.states.setValue("drawMode", constants.TEXTURE); + this.states.setValue("_useNormalMaterial", false); + this.states.setValue("_tex", tex); + this.states.setValue("fillColor", new Color([1, 1, 1])); }; - Renderer3D.prototype.normalMaterial = function(...args) { - this.states.setValue('drawMode', constants.FILL); - this.states.setValue('_useSpecularMaterial', false); - this.states.setValue('_useEmissiveMaterial', false); - this.states.setValue('_useNormalMaterial', true); - this.states.setValue('curFillColor', [1, 1, 1, 1]); - this.states.setValue('fillColor', new Color([1, 1, 1])); - this.states.setValue('strokeColor', null); + Renderer3D.prototype.normalMaterial = function (...args) { + this.states.setValue("drawMode", constants.FILL); + this.states.setValue("_useSpecularMaterial", false); + this.states.setValue("_useEmissiveMaterial", false); + this.states.setValue("_useNormalMaterial", true); + this.states.setValue("curFillColor", [1, 1, 1, 1]); + this.states.setValue("fillColor", new Color([1, 1, 1])); + this.states.setValue("strokeColor", null); }; // Renderer3D.prototype.ambientMaterial = function(v1, v2, v3) { @@ -3846,21 +3881,21 @@ function material(p5, fn){ // Renderer3D.prototype.specularMaterial = function(v1, v2, v3, alpha) { // } - Renderer3D.prototype.shininess = function(shine) { + Renderer3D.prototype.shininess = function (shine) { if (shine < 1) { shine = 1; } - this.states.setValue('_useShininess', shine); + this.states.setValue("_useShininess", shine); }; - Renderer3D.prototype.metalness = function(metallic) { + Renderer3D.prototype.metalness = function (metallic) { const metalMix = 1 - Math.exp(-metallic / 100); - this.states.setValue('_useMetalness', metalMix); + this.states.setValue("_useMetalness", metalMix); }; } export default material; -if(typeof p5 !== 'undefined'){ +if (typeof p5 !== "undefined") { loading(p5, p5.prototype); } From 7e5a966b3911c1a06ea8510cd996157b32509ba9 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 4 Jan 2026 14:50:36 -0500 Subject: [PATCH 31/34] Update reference to use the flat API --- src/strands/p5.strands.js | 277 ++++++++++++++------------- src/webgl/material.js | 391 +++++++++++++++++++------------------- 2 files changed, 340 insertions(+), 328 deletions(-) diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 90f9da6b7b..d83bda878f 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -171,24 +171,21 @@ if (typeof p5 !== "undefined") { /* ------------------------------------------------------------- */ /** - * @method getWorldInputs + * @property {Object} worldInputs * @description - * Registers a callback to modify the world-space properties of each vertex in a shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to customize vertex positions, normals, texture coordinates, and colors before rendering. "World space" refers to the coordinate system of the 3D scene, before any camera or projection transformations are applied. + * A shader hook block that modifies the world-space properties of each vertex in a shader. This hook can be used inside `buildColorShader()` and similar shader `modify()` calls to customize vertex positions, normals, texture coordinates, and colors before rendering. Modifications happen between the `.begin()` and `.end()` methods of the hook. "World space" refers to the coordinate system of the 3D scene, before any camera or projection transformations are applied. * - * The callback receives a vertex object with the following properties: + * `worldInputs` has the following properties: * - `position`: a three-component vector representing the original position of the vertex. * - `normal`: a three-component vector representing the direction the surface is facing. * - `texCoord`: a two-component vector representing the texture coordinates. * - `color`: a four-component vector representing the color of the vertex (red, green, blue, alpha). * * This hook is available in: - * - baseMaterialShader() - * - baseNormalShader() - * - baseColorShader() - * - baseStrokeShader() - * - * @param {Function} callback - * A callback function which receives a vertex object containing position (vec3), normal (vec3), texCoord (vec2), and color (vec4) properties. The function should return the modified vertex object. + * - `buildMaterialShader()` + * - `buildNormalShader()` + * - `buildColorShader()` + * - `buildStrokeShader()` * * @example *
@@ -196,20 +193,23 @@ if (typeof p5 !== "undefined") { * let myShader; * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = baseMaterialShader().modify(() => { - * let t = uniformFloat(() => millis()); - * getWorldInputs(inputs => { - * // Move the vertex up and down in a wave in world space - * // In world space, moving the object (e.g., with translate()) will affect these coordinates - * // The sphere is ~50 units tall here, so 20 gives a noticeable wave - * inputs.position.y += 20 * sin(t * 0.001 + inputs.position.x * 0.05); - * return inputs; - * }); - * }); + * myShader = buildMaterialShader(material); + * } + * + * function material() { + * let t = uniformFloat(); + * worldInputs.begin(); + * // Move the vertex up and down in a wave in world space + * // In world space, moving the object (e.g., with translate()) will affect these coordinates + * // The sphere is ~50 units tall here, so 20 gives a noticeable wave + * worldInputs.position.y += 20 * sin(t * 0.001 + worldInputs.position.x * 0.05); + * worldInputs.end(); * } + * * function draw() { * background(255); * shader(myShader); + * myShader.setUniform('t', millis()); * lights(); * noStroke(); * fill('red'); @@ -220,9 +220,11 @@ if (typeof p5 !== "undefined") { */ /** - * @method combineColors + * @property {Object} combineColors * @description - * Registers a callback to customize how color components are combined in the fragment shader. This hook can be used inside baseMaterialShader().modify() and similar shader modify() calls to control the final color output of a material. The callback receives an object with the following properties: + * A shader hook block that modifies how color components are combined in the fragment shader. This hook can be used inside `buildMaterialShader()` and similar shader `modify()` calls to control the final color output of a material. Modifications happen between the `.begin()` and `.end()` methods of the hook. + * + * `combineColors` has the following properties: * * - `baseColor`: a three-component vector representing the base color (red, green, blue). * - `diffuse`: a single number representing the diffuse reflection. @@ -233,13 +235,10 @@ if (typeof p5 !== "undefined") { * - `emissive`: a three-component vector representing the emissive color. * - `opacity`: a single number representing the opacity. * - * The callback should return a vector with four components (red, green, blue, alpha) for the final color. + * Call `.set()` on the hook with a vector with four components (red, green, blue, alpha) for the final color. * * This hook is available in: - * - baseMaterialShader() - * - * @param {Function} callback - * A callback function which receives the object described above and returns a vector with four components for the final color. + * - `buildMaterialShader()` * * @example *
@@ -247,20 +246,23 @@ if (typeof p5 !== "undefined") { * let myShader; * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = baseMaterialShader().modify(() => { - * combineColors(components => { - * // Custom color combination: add a green tint using vector properties - * return [ - * components.baseColor * components.diffuse + - * components.ambientColor * components.ambient + - * components.specularColor * components.specular + - * components.emissive + - * [0, 0.2, 0], // Green tint for visibility - * components.opacity - * ]; - * }); - * }); + * myShader = buildMaterialShader(material); * } + * + * function material() { + * combineColors.begin(); + * // Custom color combination: add a green tint using vector properties + * combineColors.set([ + * combineColors.baseColor * combineColors.diffuse + + * combineColors.ambientColor * combineColors.ambient + + * combineColors.specularColor * combineColors.specular + + * combineColors.emissive + + * [0, 0.2, 0], // Green tint + * combineColors.opacity + * ]); + * combineColors.end(); + * } + * * function draw() { * background(255); * shader(myShader); @@ -354,13 +356,13 @@ if (typeof p5 !== "undefined") { */ /** - * @method getPixelInputs + * @property {Object} pixelInputs * @description - * Registers a callback to modify the properties of each fragment (pixel) before the final color is calculated in the fragment shader. This hook can be used inside baseMaterialShader().modify() and similar shader modify() calls to adjust per-pixel data before lighting/mixing. + * A shader hook block that modifies the properties of each pixel before the final color is calculated. This hook can be used inside `buildMaterialShader()` and similar shader `modify()` calls to adjust per-pixel data before lighting is applied. Modifications happen between the `.begin()` and `.end()` methods of the hook. * - * The callback receives an `Inputs` object. Available fields depend on the shader: + * The properties of `pixelInputs` depend on the shader: * - * - In baseMaterialShader(): + * - In `buildMaterialShader()`: * - `normal`: a three-component vector representing the surface normal. * - `texCoord`: a two-component vector representing the texture coordinates (u, v). * - `ambientLight`: a three-component vector representing the ambient light color. @@ -371,21 +373,16 @@ if (typeof p5 !== "undefined") { * - `shininess`: a number controlling specular highlights. * - `metalness`: a number controlling the metalness factor. * - * - In baseStrokeShader(): + * - In `buildStrokeShader()`: * - `color`: a four-component vector representing the stroke color (red, green, blue, alpha). * - `tangent`: a two-component vector representing the stroke tangent. * - `center`: a two-component vector representing the cap/join center. * - `position`: a two-component vector representing the current fragment position. * - `strokeWeight`: a number representing the stroke weight in pixels. * - * Return the modified object to update the fragment. - * * This hook is available in: - * - baseMaterialShader() - * - baseStrokeShader() - * - * @param {Function} callback - * A callback function which receives the fragment inputs object and should return it after making any changes. + * - `buildMaterialShader()` + * - `buildStrokeShader()` * * @example *
@@ -393,18 +390,22 @@ if (typeof p5 !== "undefined") { * let myShader; * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = baseMaterialShader().modify(() => { - * let t = uniformFloat(() => millis()); - * getPixelInputs(inputs => { - * // Animate alpha (transparency) based on x position - * inputs.color.a = 0.5 + 0.5 * sin(inputs.texCoord.x * 10.0 + t * 0.002); - * return inputs; - * }); - * }); + * myShader = buildMaterialShader(material); * } + * + * function material() { + * let t = uniformFloat(); + * pixelInputs.begin(); + * // Animate alpha (transparency) based on x position + * pixelInputs.color.a = 0.5 + 0.5 * + * sin(pixelInputs.texCoord.x * 10.0 + t * 0.002); + * pixelInputs.end(); + * } + * * function draw() { * background(240); * shader(myShader); + * myShader.setUniform('t', millis()); * lights(); * noStroke(); * fill('purple'); @@ -450,20 +451,20 @@ if (typeof p5 !== "undefined") { */ /** - * @method getFinalColor + * @property finalColor * @description - * Registers a callback to change the final color of each pixel after all lighting and mixing is done in the fragment shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to adjust the color before it appears on the screen. The callback receives a four component vector representing red, green, blue, and alpha. + * A shader hook block that modifies the final color of each pixel after all lighting is applied. This hook can be used inside `buildMaterialShader()` and similar shader `modify()` calls to adjust the color before it appears on the screen. Modifications happen between the `.begin()` and `.end()` methods of the hook. * - * Return a new color array to change the output color. + * `finalColor` has the following properties: + * - `color`: a four-component vector representing the pixel color (red, green, blue, alpha). * - * This hook is available in: - * - baseColorShader() - * - baseMaterialShader() - * - baseNormalShader() - * - baseStrokeShader() + * Call `.set()` on the hook with a vector with four components (red, green, blue, alpha) to update the final color. * - * @param {Function} callback - * A callback function which receives the color array and should return a color array. + * This hook is available in: + * - `buildColorShader()` + * - `buildMaterialShader()` + * - `buildNormalShader()` + * - `buildStrokeShader()` * * @example *
@@ -471,14 +472,18 @@ if (typeof p5 !== "undefined") { * let myShader; * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = baseColorShader().modify(() => { - * getFinalColor(color => { - * // Add a blue tint to the output color - * color.b += 0.4; - * return color; - * }); - * }); + * myShader = buildMaterialShader(material); * } + * + * function material() { + * finalColor.begin(); + * let color = finalColor.color; + * // Add a blue tint to the output color + * color.b += 0.4; + * finalColor.set(color); + * finalColor.end(); + * } + * * function draw() { * background(230); * shader(myShader); @@ -535,22 +540,20 @@ if (typeof p5 !== "undefined") { */ /** - * @method getColor + * @property {Object} filterColor * @description - * Registers a callback to set the final color for each pixel in a filter shader. This hook can be used inside baseFilterShader().modify() and similar shader modify() calls to control the output color for each pixel. The callback receives the following arguments: - * - `inputs`: an object with the following properties: - * - `texCoord`: a two-component vector representing the texture coordinates (u, v). - * - `canvasSize`: a two-component vector representing the canvas size in pixels (width, height). - * - `texelSize`: a two-component vector representing the size of a single texel in texture space. + * A shader hook block that sets the color for each pixel in a filter shader. This hook can be used inside `buildFilterShader()` to control the output color for each pixel. + * + * `filterColor` has the following properties: + * - `texCoord`: a two-component vector representing the texture coordinates (u, v). + * - `canvasSize`: a two-component vector representing the canvas size in pixels (width, height). + * - `texelSize`: a two-component vector representing the size of a single texel in texture space. * - `canvasContent`: a texture containing the sketch's contents before the filter is applied. * - * Return a four-component vector `[r, g, b, a]` for the pixel. + * Call `.set()` on the hook with a vector with four components (red, green, blue, alpha) to update the final color. * * This hook is available in: - * - baseFilterShader() - * - * @param {Function} callback - * A callback function which receives the inputs object and canvasContent, and should return a color array. + * - `buildFilterShader()` * * @example *
@@ -558,14 +561,20 @@ if (typeof p5 !== "undefined") { * let myShader; * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = baseFilterShader().modify(() => { - * getColor((inputs, canvasContent) => { - * // Warp the texture coordinates for a wavy effect - * let warped = [inputs.texCoord.x, inputs.texCoord.y + 0.1 * sin(inputs.texCoord.x * 10.0)]; - * return getTexture(canvasContent, warped); - * }); - * }); + * myShader = buildFilterShader(warp); + * } + * + * function warp() { + * filterColor.begin(); + * // Warp the texture coordinates for a wavy effect + * let warped = [ + * filterColor.texCoord.x, + * filterColor.texCoord.y + 0.1 * sin(filterColor.texCoord.x * 10) + * ]; + * filterColor.set(getTexture(canvasContent, warped)); + * filterColor.end(); * } + * * function draw() { * background(180); * // Draw something to the canvas @@ -578,25 +587,21 @@ if (typeof p5 !== "undefined") { */ /** - * @method getObjectInputs + * @property {Object} objectInputs * @description - * Registers a callback to modify the properties of each vertex before any transformations are applied in the vertex shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to move, color, or otherwise modify the raw model data. The callback receives an object with the following properties: + * A shader hook block to modify the properties of each vertex before any transformations are applied. This hook can be used inside `buildMaterialShader()` and similar shader `modify()` calls to customize vertex positions, normals, texture coordinates, and colors before rendering. Modifications happen between the `.begin()` and `.end()` methods of the hook. "Object space" refers to the coordinate system of the 3D scene before any transformations, cameras, or projection transformations are applied. * + * `objectInputs` has the following properties: * - `position`: a three-component vector representing the original position of the vertex. * - `normal`: a three-component vector representing the direction the surface is facing. * - `texCoord`: a two-component vector representing the texture coordinates. * - `color`: a four-component vector representing the color of the vertex (red, green, blue, alpha). * - * Return the modified object to update the vertex. - * * This hook is available in: - * - baseColorShader() - * - baseMaterialShader() - * - baseNormalShader() - * - baseStrokeShader() - * - * @param {Function} callback - * A callback function which receives the vertex object and should return it after making any changes. + * - `buildColorShader()` + * - `buildMaterialShader()` + * - `buildNormalShader()` + * - `buildStrokeShader()` * * @example *
@@ -604,18 +609,21 @@ if (typeof p5 !== "undefined") { * let myShader; * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = baseColorShader().modify(() => { - * let t = uniformFloat(() => millis()); - * getObjectInputs(inputs => { - * // Create a sine wave along the x axis in object space - * inputs.position.y += sin(t * 0.001 + inputs.position.x); - * return inputs; - * }); - * }); + * myShader = buildMaterialShader(material); + * } + * + * function material() { + * let t = uniformFloat(); + * objectInputs.begin(); + * // Create a sine wave along the object + * objectInputs.position.y += sin(t * 0.001 + objectInputs.position.x); + * objectInputs.end(); * } + * * function draw() { * background(220); * shader(myShader); + * myShader.setUniform('t', millis()); * noStroke(); * fill('orange'); * sphere(50); @@ -625,25 +633,21 @@ if (typeof p5 !== "undefined") { */ /** - * @method getCameraInputs + * @property {Object} cameraInputs * @description - * Registers a callback to adjust vertex properties after the model has been transformed by the camera, but before projection, in the vertex shader. This hook can be used inside baseColorShader().modify() and similar shader modify() calls to create effects that depend on the camera's view. The callback receives an object with the following properties: + * A shader hook block that adjusts vertex properties from the perspective of the camera. This hook can be used inside `buildMaterialShader()` and similar shader `modify()` calls to customize vertex positions, normals, texture coordinates, and colors before rendering. "Camera space" refers to the coordinate system of the 3D scene after transformations have been applied, seen relative to the camera. * + * `cameraInputs` has the following properties: * - `position`: a three-component vector representing the position after camera transformation. * - `normal`: a three-component vector representing the normal after camera transformation. * - `texCoord`: a two-component vector representing the texture coordinates. * - `color`: a four-component vector representing the color of the vertex (red, green, blue, alpha). * - * Return the modified object to update the vertex. - * * This hook is available in: - * - baseColorShader() - * - baseMaterialShader() - * - baseNormalShader() - * - baseStrokeShader() - * - * @param {Function} callback - * A callback function which receives the vertex object and should return it after making any changes. + * - `buildColorShader()` + * - `buildMaterialShader()` + * - `buildNormalShader()` + * - `buildStrokeShader()` * * @example *
@@ -651,20 +655,23 @@ if (typeof p5 !== "undefined") { * let myShader; * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = baseColorShader().modify(() => { - * getCameraInputs(inputs => { - * // Move vertices in camera space based on their x position - * let t = uniformFloat(() => millis()); - * inputs.position.y += 30 * sin(inputs.position.x * 0.05 + t * 0.001); - * // Tint all vertices blue - * inputs.color.b = 1; - * return inputs; - * }); - * }); + * myShader = baseColorShader(material); * } + * + * function material() { + * let t = uniformFloat(); + * cameraInputs.begin(); + * // Move vertices in camera space based on their x position + * cameraInputs.position.y += 30 * sin(cameraInputs.position.x * 0.05 + t * 0.001); + * // Tint all vertices blue + * cameraInputs.color.b = 1; + * cameraInputs.end(); + * } + * * function draw() { * background(200); * shader(myShader); + * myShader.setUniform('t', millis()); * noStroke(); * fill('red'); * sphere(50); diff --git a/src/webgl/material.js b/src/webgl/material.js index af0003156e..533d696399 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -479,18 +479,21 @@ function material(p5, fn) { * } * ``` * - * Inside your shader file, you can call p5.strands hooks to change parts of the shader. For - * a filter shader, call `getColor()` with a callback to change each pixel on the canvas. + * Inside your shader file, you can use p5.strands hooks to change parts of the shader. For + * a filter shader, use `filterColor` to change each pixel on the canvas. * * ```js * // myFilter.js - * getColor((inputs, canvasContent) => { - * let result = getTexture(canvasContent, inputs.texCoord); - * // Zero out the green and blue channels, leaving red - * result.g = 0; - * result.b = 0; - * return result; - * }); + * filterColor.begin(); + * let result = getTexture( + * filterColor.canvasContent, + * filterColor.texCoord + * ); + * // Zero out the green and blue channels, leaving red + * result.g = 0; + * result.b = 0; + * filterColor.set(result); + * filterColor.end(); * ``` * * Read the reference for `buildFilterShader`, @@ -556,7 +559,7 @@ function material(p5, fn) { * The main way to use `buildFilterShader` is to pass a function in as a parameter. * This will let you create a shader using p5.strands. * - * In your function, you can call `getColor` with a function + * In your function, you can use `filterColor` with a function * that will be called for each pixel on the image to determine its final color. You can * read the color of the current pixel with `getTexture(canvasContent, coord)`. * @@ -572,13 +575,16 @@ function material(p5, fn) { * } * * function tintShader() { - * getColor((inputs, canvasContent) => { - * let result = getTexture(canvasContent, inputs.texCoord); - * // Zero out the green and blue channels, leaving red - * result.g = 0; - * result.b = 0; - * return result; - * }); + * filterColor.begin(); + * let result = getTexture( + * filterColor.canvasContent, + * filterColor.texCoord + * ); + * // Zero out the green and blue channels, leaving red + * result.g = 0; + * result.b = 0; + * filterColor.set(result); + * filterColor.end(); * } * ``` * @@ -600,11 +606,13 @@ function material(p5, fn) { * * function warpShader() { * let warpAmount = uniformFloat(); - * getColor((inputs, canvasContent) => { - * let coord = inputs.texCoord; - * coord.y += sin(coord.x * 10) * warpAmount; - * return getTexture(canvasContent, coord); - * }); + * filterColor.begin(); + * let coord = filterColor.texCoord; + * coord.y += sin(coord.x * 10) * warpAmount; + * filterColor.set( + * getTexture(filterColor.canvasContent, coord) + * ); + * filterColor.end(); * } * * function draw() { @@ -630,9 +638,9 @@ function material(p5, fn) { * } * * function gradient() { - * getColor((inputs) => { - * return [inputs.texCoord.x, inputs.texCoord.y, 0, 1]; - * }); + * filterColor.begin(); + * filterColor.set([filterColor.texCoord.x, filterColor.texCoord.y, 0, 1]); + * filterColor.end(); * } * ``` * @@ -645,13 +653,13 @@ function material(p5, fn) { * } * * function gradient() { - * getColor((inputs) => { - * return mix( - * [1, 0, 0, 1], // Red - * [0, 0, 1, 1], // Blue - * inputs.texCoord.x // x coordinate, from 0 to 1 - * ); - * }); + * filterColor.begin(); + * filterColor.set(mix( + * [1, 0, 0, 1], // Red + * [0, 0, 1, 1], // Blue + * filterColor.texCoord.x // x coordinate, from 0 to 1 + * )); + * filterColor.end(); * } * ``` * @@ -666,23 +674,24 @@ function material(p5, fn) { * } * * function gradient() { - * let time = uniformFloat(() => millis()); - * getColor((inputs) => { - * return mix( - * [1, 0, 0, 1], // Red - * [0, 0, 1, 1], // Blue - * sin(inputs.texCoord.x*15 + time*0.004)/2+0.5 - * ); - * }); + * let time = uniformFloat(); + * filterColor.begin(); + * filterColor.set(mix( + * [1, 0, 0, 1], // Red + * [0, 0, 1, 1], // Blue + * sin(filterColor.texCoord.x*15 + time*0.004)/2+0.5 + * )); + * filterColor.end(); * } * * function draw() { + * myFilter.setUniform('time', millis()); * filter(myFilter); * } * ``` * * Like the `modify()` method on shaders, - * advanced users can also fill in `getColor` using GLSL + * advanced users can also fill in `filterColor` using GLSL * instead of JavaScript. * Read the reference entry for `modify()` * for more info. Alternatively, `buildFilterShader()` can also be used like @@ -895,17 +904,18 @@ function material(p5, fn) { * } * * function material() { - * let time = uniformFloat(() => millis() / 1000); - * getFinalColor(() => { - * let r = 0.2 + 0.5 * abs(sin(time + 0)); - * let g = 0.2 + 0.5 * abs(sin(time + 1)); - * let b = 0.2 + 0.5 * abs(sin(time + 2)); - * return [r, g, b, 1]; - * }); + * let time = uniformFloat(); + * finalColor.begin(); + * let r = 0.2 + 0.5 * abs(sin(time + 0)); + * let g = 0.2 + 0.5 * abs(sin(time + 1)); + * let b = 0.2 + 0.5 * abs(sin(time + 2)); + * finalColor.set([r, g, b, 1]); + * finalColor.end(); * } * * function draw() { * background(245, 245, 220); + * myShader.setUniform('time', millis() / 1000); * shader(myShader); * * rectMode(CENTER); @@ -1377,12 +1387,12 @@ function material(p5, fn) { * * In your function, you can call *hooks* to change part of the shader. In a material * shader, these are the hooks available: - * - `getObjectInputs`: Update vertices before any positioning has been applied. Your function gets run on every vertex. - * - `getWorldInputs`: Update vertices after transformations have been applied. Your function gets run on every vertex. - * - `getCameraInputs`: Update vertices after transformations have been applied, relative to the camera. Your function gets run on every vertex. - * - `getPixelInputs`: Update property values on pixels on the surface of a shape. Your function gets run on every pixel. + * - `objectInputs`: Update vertices before any positioning has been applied. Your function gets run on every vertex. + * - `worldInputs`: Update vertices after transformations have been applied. Your function gets run on every vertex. + * - `cameraInputs`: Update vertices after transformations have been applied, relative to the camera. Your function gets run on every vertex. + * - `pixelInputs`: Update property values on pixels on the surface of a shape. Your function gets run on every pixel. * - `combineColors`: Control how the ambient, diffuse, and specular components of lighting are combined into a single color on the surface of a shape. Your function gets run on every pixel. - * - `getFinalColor`: Update or replace the pixel color on the surface of a shape. Your function gets run on every pixel. + * - `finalColor`: Update or replace the pixel color on the surface of a shape. Your function gets run on every pixel. * * Read the linked reference page for each hook for more information about how to use them. * @@ -1398,17 +1408,17 @@ function material(p5, fn) { * } * * function material() { - * let time = uniformFloat(() => millis()); - * getWorldInputs((inputs) => { - * inputs.position.y += - * 20 * sin(time * 0.001 + inputs.position.x * 0.05); - * return inputs; - * }); + * let time = uniformFloat(); + * worldInputs.begin(); + * worldInputs.position.y += + * 20 * sin(time * 0.001 + worldInputs.position.x * 0.05); + * worldInputs.end(); * } * * function draw() { * background(255); * shader(myShader); + * myShader.setUniform('time', millis()); * lights(); * noStroke(); * fill('red'); @@ -1432,14 +1442,13 @@ function material(p5, fn) { * } * * function material() { - * getPixelInputs((inputs) => { - * let factor = sin( - * TWO_PI * (inputs.texCoord.x + inputs.texCoord.y) - * ); - * inputs.shininess = mix(1, 100, factor); - * inputs.metalness = factor; - * return inputs; - * }) + * pixelInputs.begin(); + * let factor = sin( + * TWO_PI * (pixelInputs.texCoord.x + pixelInputs.texCoord.y) + * ); + * pixelInputs.shininess = mix(1, 100, factor); + * pixelInputs.metalness = factor; + * pixelInputs.end(); * } * * function draw() { @@ -1469,16 +1478,15 @@ function material(p5, fn) { * } * * function material() { - * getPixelInputs((inputs) => { - * inputs.normal.x += 0.2 * sin( - * sin(TWO_PI * dot(inputs.texCoord.yx, vec2(10, 25))) - * ); - * inputs.normal.y += 0.2 * sin( - * sin(TWO_PI * dot(inputs.texCoord, vec2(10, 25))) - * ); - * inputs.normal = normalize(inputs.normal); - * return inputs; - * }); + * pixelInputs.begin(); + * pixelInputs.normal.x += 0.2 * sin( + * sin(TWO_PI * dot(pixelInputs.texCoord.yx, vec2(10, 25))) + * ); + * pixelInputs.normal.y += 0.2 * sin( + * sin(TWO_PI * dot(pixelInputs.texCoord, vec2(10, 25))) + * ); + * pixelInputs.normal = normalize(pixelInputs.normal); + * pixelpixelInputs.end(); * } * * function draw() { @@ -1513,17 +1521,18 @@ function material(p5, fn) { * * function material() { * let myNormal = sharedVec3(); - * getPixelInputs((inputs) => { - * myNormal = inputs.normal; - * return inputs; - * }); - * getFinalColor((color) => { - * return mix( - * [1, 1, 1, 1], - * color, - * abs(dot(myNormal, [0, 0, 1])) - * ); - * }); + * + * pixelInputs.begin(); + * myNormal = pixelInputs.normal; + * pixelInputs.end(); + * + * finalColor.begin(); + * finalColor.set(mix( + * [1, 1, 1, 1], + * finalColor.color, + * abs(dot(myNormal, [0, 0, 1])) + * )); + * finalColor.end(); * } * * function draw() { @@ -1576,6 +1585,7 @@ function material(p5, fn) { * function draw() { * background(255); * shader(myShader); + * myShader.setUniform('time', millis()); * lights(); * noStroke(); * fill('red'); @@ -1584,17 +1594,16 @@ function material(p5, fn) { * ``` * * Inside your shader file, you can call p5.strands hooks to change parts of the shader. For - * example, you might call `getWorldInputs()` with a callback to change each vertex, or you - * might call `getPixelInputs()` with a callback to change each pixel on the surface of a shape. + * example, you might use the `worldInputs` hook to change each vertex, or you + * might use the `pixelInputs` hook to change each pixel on the surface of a shape. * * ```js * // myMaterial.js - * let time = uniformFloat(() => millis()); - * getWorldInputs((inputs) => { - * inputs.position.y += - * 20 * sin(time * 0.001 + inputs.position.x * 0.05); - * return inputs; - * }); + * let time = uniformFloat(); + * worldInputs.begin(); + * worldInputs.position.y += + * 20 * sin(time * 0.001 + worldInputs.position.x * 0.05); + * worldInputs.end(); * ``` * * Read the reference for `buildMaterialShader`, @@ -1681,10 +1690,10 @@ function material(p5, fn) { * * In your function, you can call *hooks* to change part of the shader. In a material * shader, these are the hooks available: - * - `getObjectInputs`: Update vertices before any positioning has been applied. Your function gets run on every vertex. - * - `getWorldInputs`: Update vertices after transformations have been applied. Your function gets run on every vertex. - * - `getCameraInputs`: Update vertices after transformations have been applied, relative to the camera. Your function gets run on every vertex. - * - `getFinalColor`: Update or replace the pixel color on the surface of a shape. Your function gets run on every pixel. + * - `objectInputs`: Update vertices before any positioning has been applied. Your function gets run on every vertex. + * - `worldInputs`: Update vertices after transformations have been applied. Your function gets run on every vertex. + * - `cameraInputs`: Update vertices after transformations have been applied, relative to the camera. Your function gets run on every vertex. + * - `finalColor`: Update or replace the pixel color on the surface of a shape. Your function gets run on every pixel. * * Read the linked reference page for each hook for more information about how to use them. * @@ -1699,17 +1708,17 @@ function material(p5, fn) { * } * * function material() { - * let time = uniformFloat(() => millis()); - * getWorldInputs((inputs) => { - * inputs.position.y += - * 20. * sin(time * 0.001 + inputs.position.x * 0.05); - * return inputs; - * }); + * let time = uniformFloat(); + * worldInputs.begin(); + * worldInputs.position.y += + * 20. * sin(time * 0.001 + worldInputs.position.x * 0.05); + * worldInputs.end(); * } * * function draw() { * background(255); * shader(myShader); + * myShader.setUniform('time', millis()); * noStroke(); * sphere(50); * } @@ -1727,20 +1736,20 @@ function material(p5, fn) { * } * * function material() { - * getCameraInputs((inputs) => { - * inputs.normal = abs(inputs.normal); - * return inputs; - * }); - * getFinalColor((color) => { - * // Map the r, g, and b values of the old normal to new colors - * // instead of just red, green, and blue: - * let newColor = - * color.r * [89, 240, 232] / 255 + - * color.g * [240, 237, 89] / 255 + - * color.b * [205, 55, 222] / 255; - * newColor = newColor / (color.r + color.g + color.b); - * return [newColor.r, newColor.g, newColor.b, color.a]; - * }); + * cameraInputs.begin(); + * cameraInputs.normal = abs(cameraInputs.normal); + * cameraInputs.end(); + * + * finalColor.begin(); + * // Map the r, g, and b values of the old normal to new colors + * // instead of just red, green, and blue: + * let newColor = + * finalColor.color.r * [89, 240, 232] / 255 + + * finalColor.color.g * [240, 237, 89] / 255 + + * finalColor.color.b * [205, 55, 222] / 255; + * newColor = newColor / (color.r + color.g + color.b); + * finalColor.set([newColor.r, newColor.g, newColor.b, color.a]); + * finalColor.end(); * } * * function draw() { @@ -1793,6 +1802,7 @@ function material(p5, fn) { * function draw() { * background(255); * shader(myShader); + * myShader.setUniform('time', millis()); * lights(); * noStroke(); * fill('red'); @@ -1801,17 +1811,16 @@ function material(p5, fn) { * ``` * * Inside your shader file, you can call p5.strands hooks to change parts of the shader. For - * example, you might call `getWorldInputs()` with a callback to change each vertex, or you - * might call `getFinalColor()` with a callback to change the color of each pixel on the surface of a shape. + * example, you might use the `worldInputs` hook to change each vertex, or you + * might use the `finalColor` hook to change the color of each pixel on the surface of a shape. * * ```js * // myMaterial.js - * let time = uniformFloat(() => millis()); - * getWorldInputs((inputs) => { - * inputs.position.y += - * 20 * sin(time * 0.001 + inputs.position.x * 0.05); - * return inputs; - * }); + * let time = uniformFloat(); + * worldInputs.begin(); + * worldInputs.position.y += + * 20 * sin(time * 0.001 + worldInputs.position.x * 0.05); + * worldInputs.end(); * ``` * * Read the reference for `buildNormalShader`, @@ -1882,10 +1891,10 @@ function material(p5, fn) { * * In your function, you can call *hooks* to change part of the shader. In a material * shader, these are the hooks available: - * - `getObjectInputs`: Update vertices before any positioning has been applied. Your function gets run on every vertex. - * - `getWorldInputs`: Update vertices after transformations have been applied. Your function gets run on every vertex. - * - `getCameraInputs`: Update vertices after transformations have been applied, relative to the camera. Your function gets run on every vertex. - * - `getFinalColor`: Update or replace the pixel color on the surface of a shape. Your function gets run on every pixel. + * - `objectInputs`: Update vertices before any positioning has been applied. Your function gets run on every vertex. + * - `worldInputs`: Update vertices after transformations have been applied. Your function gets run on every vertex. + * - `cameraInputs`: Update vertices after transformations have been applied, relative to the camera. Your function gets run on every vertex. + * - `finalColor`: Update or replace the pixel color on the surface of a shape. Your function gets run on every pixel. * * Read the linked reference page for each hook for more information about how to use them. * @@ -1900,17 +1909,17 @@ function material(p5, fn) { * } * * function material() { - * let time = uniformFloat(() => millis()); - * getWorldInputs((inputs) => { - * inputs.position.y += - * 20 * sin(time * 0.001 + inputs.position.x * 0.05); - * return inputs; - * }); + * let time = uniformFloat(); + * worldInputs.begin(); + * worldInputs.position.y += + * 20 * sin(time * 0.001 + worldInputs.position.x * 0.05); + * worldInputs.end(); * } * * function draw() { * background(255); * shader(myShader); + * myShader.setUniform('time', millis()); * noStroke(); * fill('red'); * circle(0, 0, 50); @@ -1957,6 +1966,7 @@ function material(p5, fn) { * function draw() { * background(255); * shader(myShader); + * myShader.setUniform('time', millis()); * lights(); * noStroke(); * fill('red'); @@ -1965,17 +1975,16 @@ function material(p5, fn) { * ``` * * Inside your shader file, you can call p5.strands hooks to change parts of the shader. For - * example, you might call `getWorldInputs()` with a callback to change each vertex, or you - * might call `getFinalColor()` with a callback to change the color of each pixel on the surface of a shape. + * example, you might use the `worldInputs` hook to change each vertex, or you + * might use the `finalColor` hook to change the color of each pixel on the surface of a shape. * * ```js * // myMaterial.js - * let time = uniformFloat(() => millis()); - * getWorldInputs((inputs) => { - * inputs.position.y += - * 20 * sin(time * 0.001 + inputs.position.x * 0.05); - * return inputs; - * }); + * let time = uniformFloat(); + * worldInputs.begin(); + * worldInputs.position.y += + * 20 * sin(time * 0.001 + worldInputs.position.x * 0.05); + * worldInputs.end(); * ``` * * Read the reference for `buildColorShader`, @@ -2043,11 +2052,11 @@ function material(p5, fn) { * * In your function, you can call *hooks* to change part of the shader. In a material * shader, these are the hooks available: - * - `getObjectInputs`: Update vertices before any positioning has been applied. Your function gets run on every vertex. - * - `getWorldInputs`: Update vertices after transformations have been applied. Your function gets run on every vertex. - * - `getCameraInputs`: Update vertices after transformations have been applied, relative to the camera. Your function gets run on every vertex. - * - `getPixelInputs`: Update property values on pixels on the surface of a shape. Your function gets run on every pixel. - * - `getFinalColor`: Update or replace the pixel color on the surface of a shape. Your function gets run on every pixel. + * - `objectInputs`: Update vertices before any positioning has been applied. Your function gets run on every vertex. + * - `worldInputs`: Update vertices after transformations have been applied. Your function gets run on every vertex. + * - `cameraInputs`: Update vertices after transformations have been applied, relative to the camera. Your function gets run on every vertex. + * - `pixelInputs`: Update property values on pixels on the surface of a shape. Your function gets run on every pixel. + * - `finalColor`: Update or replace the pixel color on the surface of a shape. Your function gets run on every pixel. * * Read the linked reference page for each hook for more information about how to use them. * @@ -2063,15 +2072,14 @@ function material(p5, fn) { * } * * function material() { - * getPixelInputs((inputs) => { - * let opacity = 1 - smoothstep( - * 0, - * 15, - * length(inputs.position - inputs.center) - * ); - * inputs.color.a *= opacity; - * return inputs; - * }); + * pixelInputs.begin(); + * let opacity = 1 - smoothstep( + * 0, + * 15, + * length(pixelInputs.position - pixelInputs.center) + * ); + * pixelInputs.color.a *= opacity; + * pixelInputs.end(); * } * * function draw() { @@ -2100,16 +2108,15 @@ function material(p5, fn) { * } * * function material() { - * getPixelInputs((inputs) => { - * // Replace alpha in the color with dithering by - * // randomly setting pixel colors to 0 based on opacity - * let a = 1; - * if (noise(inputs.position.xy) > inputs.color.a) { - * a = 0; - * } - * inputs.color.a = a; - * return inputs; - * }); + * pixelInputs.begin(); + * // Replace alpha in the color with dithering by + * // randomly setting pixel colors to 0 based on opacity + * let a = 1; + * if (noise(pixelInputs.position.xy) > pixelInputs.color.a) { + * a = 0; + * } + * pixelInputs.color.a = a; + * pixelInputs.end(); * } * * function draw() { @@ -2145,18 +2152,17 @@ function material(p5, fn) { * } * * function material() { - * let time = uniformFloat(() => millis()); - * getWorldInputs((inputs) => { - * // Add a somewhat random offset to the weight - * // that varies based on position and time - * let scale = 0.5 + noise( - * inputs.position.x * 0.01, - * inputs.position.y * 0.01, - * time * 0.0005 - * ); - * inputs.weight *= scale; - * return inputs; - * }); + * let time = uniformFloat(); + * worldInputs.begin(); + * // Add a somewhat random offset to the weight + * // that varies based on position and time + * let scale = 0.5 + noise( + * worldInputs.position.x * 0.01, + * worldInputs.position.y * 0.01, + * time * 0.0005 + * ); + * worldInputs.weight *= scale; + * worldInputs.end(); * } * * function draw() { @@ -2225,20 +2231,19 @@ function material(p5, fn) { * ``` * * Inside your shader file, you can call p5.strands hooks to change parts of the shader. For - * example, you might call `getWorldInputs()` with a callback to change each vertex, or you - * might call `getPixelInputs()` with a callback to change each pixel on the surface of a stroke. + * example, you might use the `worldInputs` hook to change each vertex, or you + * might use the `pixelInputs` hook to change each pixel on the surface of a stroke. * * ```js * // myMaterial.js - * getPixelInputs((inputs) => { - * let opacity = 1 - smoothstep( - * 0, - * 15, - * length(inputs.position - inputs.center) - * ); - * inputs.color.a *= opacity; - * return inputs; - * }); + * pixelInputs.begin(); + * let opacity = 1 - smoothstep( + * 0, + * 15, + * length(pixelInputs.position - pixelInputs.center) + * ); + * pixelInputs.color.a *= opacity; + * pixelInputs.end(); * ``` * * Read the reference for `buildStrokeShader`, From e88e67116b40b3e6f68be0d764afc80466b58436 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 4 Jan 2026 14:59:17 -0500 Subject: [PATCH 32/34] Fix typos in examples --- src/strands/p5.strands.js | 2 +- src/webgl/material.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index d83bda878f..b020df6ed8 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -655,7 +655,7 @@ if (typeof p5 !== "undefined") { * let myShader; * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = baseColorShader(material); + * myShader = buildMaterialShader(material); * } * * function material() { diff --git a/src/webgl/material.js b/src/webgl/material.js index 533d696399..4cd813c188 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -1486,7 +1486,7 @@ function material(p5, fn) { * sin(TWO_PI * dot(pixelInputs.texCoord, vec2(10, 25))) * ); * pixelInputs.normal = normalize(pixelInputs.normal); - * pixelpixelInputs.end(); + * pixelInputs.end(); * } * * function draw() { @@ -1747,8 +1747,8 @@ function material(p5, fn) { * finalColor.color.r * [89, 240, 232] / 255 + * finalColor.color.g * [240, 237, 89] / 255 + * finalColor.color.b * [205, 55, 222] / 255; - * newColor = newColor / (color.r + color.g + color.b); - * finalColor.set([newColor.r, newColor.g, newColor.b, color.a]); + * newColor = newColor / (newColor.r + newColor.g + newColor.b); + * finalColor.set([newColor.r, newColor.g, newColor.b, finalColor.color.a]); * finalColor.end(); * } * From 654c9cdd714747ea1fbc50b62dbfb764dc79af9b Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 4 Jan 2026 15:04:15 -0500 Subject: [PATCH 33/34] Add ts types for callback functions again --- docs/parameterData.json | 7 ------- src/strands/p5.strands.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/docs/parameterData.json b/docs/parameterData.json index 21a9ad801d..3911862da2 100644 --- a/docs/parameterData.json +++ b/docs/parameterData.json @@ -2033,13 +2033,6 @@ ] ] }, - "combineColors": { - "overloads": [ - [ - "Function" - ] - ] - }, "getPixelInputs": { "overloads": [ [ diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index b020df6ed8..7d212c332d 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -679,3 +679,33 @@ if (typeof p5 !== "undefined") { * *
*/ + +/** + * @method getWorldInputs + * @param {Function} callback + */ + +/** + * @method getPixelInputs + * @param {Function} callback + */ + +/** + * @method getFinalColor + * @param {Function} callback + */ + +/** + * @method getColor + * @param {Function} callback + */ + +/** + * @method getObjectInputs + * @param {Function} callback + */ + +/** + * @method getCameraInputs + * @param {Function} callback + */ From d921d420b2ee5cfc83b06071bd732d5bdd47c810 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 4 Jan 2026 15:05:58 -0500 Subject: [PATCH 34/34] Fix denominator of normal material example --- src/webgl/material.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webgl/material.js b/src/webgl/material.js index 4cd813c188..36fc3e6ddc 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -1747,7 +1747,7 @@ function material(p5, fn) { * finalColor.color.r * [89, 240, 232] / 255 + * finalColor.color.g * [240, 237, 89] / 255 + * finalColor.color.b * [205, 55, 222] / 255; - * newColor = newColor / (newColor.r + newColor.g + newColor.b); + * newColor = newColor / (finalColor.color.r + finalColor.color.g + finalColor.color.b); * finalColor.set([newColor.r, newColor.g, newColor.b, finalColor.color.a]); * finalColor.end(); * }