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": [ [] diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 05ae0ec5ea..6ae744954c 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -291,7 +291,9 @@ function createHookArguments(strandsContext, parameters){ if(isStructType(param.type)) { 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, { @@ -398,12 +400,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)) { @@ -459,10 +492,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 7d69b4438f..a181ff608c 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; this.isStrandsNode = true; // Store original identifier for varying variables @@ -20,6 +21,10 @@ export class StrandsNode { this._originalDimension = nodeData.dimension; } } + withStructProperties(properties) { + this.structProperties = properties; + return this; + } copy() { return createStrandsNode(this.id, this.dimension, this.strandsContext); } diff --git a/src/webgl/material.js b/src/webgl/material.js index 60f01a3969..e3a30d03d8 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 @@ -134,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; } @@ -148,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 @@ -515,10 +528,50 @@ 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. + * + * 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} 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. @@ -526,29 +579,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, @@ -562,10 +592,15 @@ 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); + loadedShader = successCallback(loadedShader) || loadedShader; } return loadedShader; @@ -582,92 +617,151 @@ 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. + * @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) { + return this.baseFilterShader().modify(fragSrc); + } // p5._validateParameters('createFilterShader', arguments); let defaultVertV1 = ` uniform mat4 uModelViewMatrix; @@ -1237,147 +1331,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` - * - * + * 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. * - * 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. + * 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. * - *
+ * 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. * - * `void afterVertex` + * Read the linked reference page for each hook for more information about how to use them. * - * + * One thing you can do with a material shader is animate the positions of vertices + * over time: * - * 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` - * - * - * - * Called at the end of the fragment shader. - * - *
- * - * 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. - * - * 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 += @@ -1395,47 +1375,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; * @@ -1443,7 +1389,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) @@ -1466,17 +1412,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 example * 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))) @@ -1504,79 +1452,169 @@ 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 example + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = createMaterialShader(() => { + * 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. */ - fn.baseMaterialShader = function() { - this._assert3d('baseMaterialShader'); - return this._renderer.baseMaterialShader(); + /** + * @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); }; /** - * Get the base shader for filters. + * 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. * - * You can then call `baseFilterShader().modify()` - * and change the following hook: + * Since this function loads data from another file, it returns a `Promise`. + * Use it in an `async function setup`, and `await` its result. * - * - * - * - *
HookDescription
+ * ```js + * let myShader; + * async function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = await loadMaterialShader('myMaterial.js'); + * } * - * `vec4 getColor` + * 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. * - * 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`. + * ```js + * // myMaterial.js + * let time = uniformFloat(() => millis()); + * getWorldInputs((inputs) => { + * inputs.position.y += + * 20 * sin(time * 0.001 + inputs.position.x * 0.05); + * return inputs; + * }); + * ``` * - * `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)`. + * 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. * - * `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. + * 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. + * @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 { + let shader = this.createMaterialShader(await urlToStrandsCallback(url)); + if (onSuccess) { + shader = onSuccess(shader) || shader; + } + return shader; + } catch (e) { + console.error(e); + if (onFail) { + onFail(e); + } + } + }; + + /** + * Returns the default shader used for fills when lights or textures are used. * - * 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. + * Calling `createMaterialShader(shaderFunction)` + * is equivalent to calling `baseMaterialShader().modify(shaderFunction)`. * - * @method baseFilterShader + * 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 filter shader + * @returns {p5.Shader} The base material shader. + */ + fn.baseMaterialShader = function() { + this._assert3d('baseMaterialShader'); + return this._renderer.baseMaterialShader(); + }; + + /** + * Returns the base shader used for filters. * - * @example - *
- * - * let img; - * let myShader; + * Calling `createFilterShader(shaderFunction)` + * is equivalent to calling `baseFilterShader().modify(shaderFunction)`. * - * 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); - * }); - * }); - * } + * Read the `createFilterShader` reference or + * call `baseFilterShader().inspectHooks()` for more information on what you can do with + * the base filter shader. * - * function draw() { - * image(img, -50, -50); - * filter(myShader); - * describe('an image of bricks, distorting over time'); - * } - * - *
+ * @method baseFilterShader + * @beta + * @returns {p5.Shader} The base filter shader. */ fn.baseFilterShader = function() { return (this._renderer.filterRenderer || this._renderer) @@ -1584,118 +1622,37 @@ function material(p5, fn){ }; /** - * Get the shader used by `normalMaterial()`. - * - * 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. - * - *
+ * 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. * - * `Vertex getWorldInputs` + * 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. * - * + * 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. * - * 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. + * Read the linked reference page for each hook for more information about how to use them. * - *
+ * One thing you may want to do is update the position of all the vertices in an object over time: * - * `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. - * - *
- * - * 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. - * - * Call `baseNormalShader().inspectHooks()` to see all the possible hooks and - * their default implementations. - * - * @method baseNormalShader - * @beta - * @returns {p5.Shader} The `normalMaterial` shader - * - * @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; - * }` + * }); * }); * } * @@ -1705,31 +1662,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]; + * }); * }); * } * @@ -1741,127 +1698,152 @@ 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. */ - fn.baseNormalShader = function() { - this._assert3d('baseNormalShader'); - return this._renderer.baseNormalShader(); + /** + * @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); }; /** - * 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` - * - * + * 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. * - * 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. + * Since this function loads data from another file, it returns a `Promise`. + * Use it in an `async function setup`, and `await` its result. * - *
- * - * `void afterVertex` - * - * - * - * Called at the end of the vertex shader. - * - *
- * - * `void beforeFragment` - * - * + * ```js + * let myShader; + * async function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = await loadNormalShader('myMaterial.js'); + * } * - * Called at the start of the fragment shader. + * 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. * - * `vec4 getFinalColor` + * ```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. * - * Update the final color after mixing. It takes in a `vec4 color` and must return a modified version. + * 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. + * @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 { + let shader = this.createNormalShader(await urlToStrandsCallback(url)); + if (onSuccess) { + shader = onSuccess(shader) || shader; + } + return shader; + } catch (e) { + console.error(e); + if (onFail) { + onFail(e); + } + } + }; + + /** + * Returns the default shader used for fills when + * `normalMaterial()` is activated. * - * `void afterFragment` + * 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. * - * Called at the end of the fragment shader. + * @method baseNormalShader + * @beta + * @returns {p5.Shader} The base material shader. + */ + fn.baseNormalShader = function() { + this._assert3d('baseNormalShader'); + return this._renderer.baseNormalShader(); + }; + + /** + * 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; - * }` + * }); * }); * } * @@ -1872,152 +1854,157 @@ 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. */ - fn.baseColorShader = function() { - this._assert3d('baseColorShader'); - return this._renderer.baseColorShader(); + /** + * @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); }; /** - * 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. + * 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. * - *
+ * Since this function loads data from another file, it returns a `Promise`. + * Use it in an `async function setup`, and `await` its result. * - * `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` - * - * + * ```js + * let myShader; + * async function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = await loadColorShader('myMaterial.js'); + * } * - * 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. + * 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. * - * `vec4 getFinalColor` + * ```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. * - * Update the final color after mixing. It takes in a `vec4 color` and must return a modified version. + * 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. + * @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 { + let shader = this.createColorShader(await urlToStrandsCallback(url)); + if (onSuccess) { + shader = onSuccess(shader) || shader; + } + return shader; + } catch (e) { + console.error(e); + if (onFail) { + onFail(e); + } + } + }; + + /** + * Returns the default shader used for fills when no lights or textures are activate. * - * `void afterFragment` + * 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. * - * Called at the end of the fragment shader. + * @method baseColorShader + * @beta + * @returns {p5.Shader} The base color shader. + */ + fn.baseColorShader = function() { + this._assert3d('baseColorShader'); + return this._renderer.baseColorShader(); + }; + + /** + * 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. * - *
+ * 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; - * }` + * }); * }); * } * @@ -2032,96 +2019,202 @@ 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 example * 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 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. + * + * 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. + * + * 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. + * @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 { + let shader = this.createStrokeShader(await urlToStrandsCallback(url)); + if (onSuccess) { + shader = onSuccess(shader) || shader; + } + return shader; + } catch (e) { + console.error(e); + if (onFail) { + onFail(e); + } + } + }; + + /** + * 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'); diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index e9645b8426..d8ac8dd3dc 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -1462,5 +1462,53 @@ 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('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); + + // 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 + }); }); });