diff --git a/docs/parameterData.json b/docs/parameterData.json index 494f63f022..3911862da2 100644 --- a/docs/parameterData.json +++ b/docs/parameterData.json @@ -2033,13 +2033,6 @@ ] ] }, - "combineColors": { - "overloads": [ - [ - "Function" - ] - ] - }, "getPixelInputs": { "overloads": [ [ @@ -2885,6 +2878,16 @@ ] ] }, + "buildFilterShader": { + "overloads": [ + [ + "Function" + ], + [ + "Object" + ] + ] + }, "createFilterShader": { "overloads": [ [ @@ -2913,6 +2916,25 @@ ] ] }, + "buildMaterialShader": { + "overloads": [ + [ + "Function" + ], + [ + "Object" + ] + ] + }, + "loadMaterialShader": { + "overloads": [ + [ + "String", + "Function?", + "Function?" + ] + ] + }, "baseMaterialShader": { "overloads": [ [] @@ -2923,16 +2945,73 @@ [] ] }, + "buildNormalShader": { + "overloads": [ + [ + "Function" + ], + [ + "Object" + ] + ] + }, + "loadNormalShader": { + "overloads": [ + [ + "String", + "Function?", + "Function?" + ] + ] + }, "baseNormalShader": { "overloads": [ [] ] }, + "buildColorShader": { + "overloads": [ + [ + "Function" + ], + [ + "Object" + ] + ] + }, + "loadColorShader": { + "overloads": [ + [ + "String", + "Function?", + "Function?" + ] + ] + }, "baseColorShader": { "overloads": [ [] ] }, + "buildStrokeShader": { + "overloads": [ + [ + "Function" + ], + [ + "Object" + ] + ] + }, + "loadStrokeShader": { + "overloads": [ + [ + "String", + "Function?", + "Function?" + ] + ] + }, "baseStrokeShader": { "overloads": [ [] diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index cd0961e8a2..7d212c332d 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 */ @@ -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 @@ -138,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 *
@@ -163,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'); @@ -187,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. @@ -200,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 *
@@ -214,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); @@ -321,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. @@ -338,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 *
@@ -360,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'); @@ -417,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 *
@@ -438,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); @@ -502,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 *
@@ -525,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 @@ -545,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 *
@@ -571,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); @@ -592,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 *
@@ -618,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 = buildMaterialShader(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); @@ -639,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 + */ diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 05ae0ec5ea..b7ac7899aa 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,54 @@ 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(); + } + + // 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); CFG.addEdge(cfg, cfg.currentBlock, entryBlockID); CFG.pushBlock(cfg, entryBlockID); const args = createHookArguments(strandsContext, hookType.parameters); - const userReturned = hookUserCallback(...args); + const numStructArgs = hookType.parameters.filter(param => param.type.properties).length; + 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.set(args[argIdx]); + } + } else { + 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 +503,30 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { shaderContext: hookInfo?.shaderContext, // 'vertex' or 'fragment' }); CFG.popBlock(cfg); + }; + hook.begin = setupHook; + hook.end = finishHook; + + 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 (!fn[unprefixedName]) { + aliases.push(unprefixedName); + } + } + + for (const name of aliases) { + strandsContext.windowOverrides[name] = window[name]; + strandsContext.fnOverrides[name] = fn[name]; + window[name] = hook; + fn[name] = hook; } - strandsContext.windowOverrides[hookType.name] = window[hookType.name]; - strandsContext.fnOverrides[hookType.name] = fn[hookType.name]; - window[hookType.name] = hookImplementation; - fn[hookType.name] = hookImplementation; } } 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/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); diff --git a/src/webgl/material.js b/src/webgl/material.js index 60f01a3969..36fc3e6ddc 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -5,13 +5,28 @@ * @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()); + 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){ +function material(p5, fn) { /** * Loads vertex and fragment shaders to create a * p5.Shader object. @@ -123,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); + return successCallback(loadedShader) || loadedShader; } else { return loadedShader; } - } catch(err) { + } catch (err) { if (failureCallback) { return failureCallback(err); } else { @@ -148,17 +163,23 @@ 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 + * `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. * * 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. * @@ -168,62 +189,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 = ` @@ -269,93 +239,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 = ` @@ -431,17 +322,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; * @@ -474,9 +405,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; }' @@ -484,10 +416,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]; + * }); * }); * } * @@ -497,17 +431,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(); * } - * - *
+ * ``` + * + * 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); @@ -515,10 +461,54 @@ 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 = await loadFilterShader('myFilter.js'); + * + * image(img, -50, -50); + * filter(myFilter); + * describe('Bricks tinted red'); + * } + * ``` + * + * 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 + * 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`, + * 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 + * @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 * p5.Shader object. @@ -526,46 +516,30 @@ 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, 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"); - // Create the shader using createFilterShader - const loadedShader = this.createFilterShader(fragString, true); + // 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)), + ); + } if (successCallback) { - successCallback(loadedShader); + loadedShader = successCallback(loadedShader) || loadedShader; } return loadedShader; @@ -582,14 +556,181 @@ 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 `buildFilterShader` 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 + * 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)`. + * + * ```js example + * async function setup() { + * createCanvas(50, 50, WEBGL); + * let img = await loadImage('assets/bricks.jpg'); + * let myFilter = buildFilterShader(tintShader); + * + * image(img, -50, -50); + * filter(myFilter); + * describe('Bricks tinted red'); + * } + * + * function tintShader() { + * 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(); + * } + * ``` + * + * 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. + * In the example below, move your mouse across the image to see it update the `warpAmount` uniform: + * + * ```js example + * let img; + * let myFilter; + * async function setup() { + * createCanvas(50, 50, WEBGL); + * img = await loadImage('assets/bricks.jpg'); + * myFilter = buildFilterShader(warpShader); + * describe('Warped bricks'); + * } + * + * function warpShader() { + * let warpAmount = uniformFloat(); + * filterColor.begin(); + * let coord = filterColor.texCoord; + * coord.y += sin(coord.x * 10) * warpAmount; + * filterColor.set( + * getTexture(filterColor.canvasContent, coord) + * ); + * filterColor.end(); + * } + * + * 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() { + * createCanvas(50, 50, WEBGL); + * let myFilter = buildFilterShader(gradient); + * describe('A gradient with red, green, yellow, and black'); + * filter(myFilter); + * } + * + * function gradient() { + * filterColor.begin(); + * filterColor.set([filterColor.texCoord.x, filterColor.texCoord.y, 0, 1]); + * filterColor.end(); + * } + * ``` + * + * ```js example + * function setup() { + * createCanvas(50, 50, WEBGL); + * let myFilter = buildFilterShader(gradient); + * describe('A gradient from red to blue'); + * filter(myFilter); + * } + * + * function gradient() { + * 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(); + * } + * ``` + * + * You can also animate your filters over time by passing the time into the shader with `uniformFloat`. + * + * ```js example + * let myFilter; + * function setup() { + * createCanvas(50, 50, WEBGL); + * myFilter = buildFilterShader(gradient); + * describe('A moving, repeating gradient from red to blue'); + * } + * + * function gradient() { + * 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 `filterColor` using GLSL + * instead of JavaScript. + * Read the reference entry for `modify()` + * 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 buildFilterShader + * @beta + * @submodule p5.strands + * @param {Function} callback A function building a p5.strands shader. + * @returns {p5.Shader} The material shader + */ + /** + * @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. * @@ -668,7 +809,7 @@ function material(p5, fn){ *
*/ fn.createFilterShader = function (fragSrc, skipContextCheck = false) { - // p5._validateParameters('createFilterShader', arguments); + // p5._validateParameters('buildFilterShader', arguments); let defaultVertV1 = ` uniform mat4 uModelViewMatrix; uniform mat4 uProjectionMatrix; @@ -709,7 +850,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) { @@ -726,12 +869,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 @@ -739,34 +886,50 @@ 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 = buildMaterialShader(material); + * noStroke(); + * describe('A square with dynamically changing colors on a beige background.'); + * } * - *

- *
+ * function material() { + * 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); + * rect(0, 0, 50, 50); + * } + * ``` * - * @method shader - * @chainable - * @param {p5.Shader} s p5.Shader object - * to apply. + * 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. * - * @example - *
- * + * ```js * let fillShader; * * let vertSrc = ` @@ -806,20 +969,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 = ` @@ -862,47 +1021,29 @@ 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) { - this._assert3d('shader'); + this._assert3d("shader"); // p5._validateParameters('shader', arguments); this._renderer.shader(s); @@ -1075,7 +1216,7 @@ function material(p5, fn){ *
*/ fn.strokeShader = function (s) { - this._assert3d('strokeShader'); + this._assert3d("strokeShader"); // p5._validateParameters('strokeShader', arguments); this._renderer.strokeShader(s); @@ -1228,7 +1369,7 @@ function material(p5, fn){ *
*/ fn.imageShader = function (s) { - this._assert3d('imageShader'); + this._assert3d("imageShader"); // p5._validateParameters('imageShader', arguments); this._renderer.imageShader(s); @@ -1237,205 +1378,59 @@ 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. + * 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. * - *
+ * 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. * - * `Vertex getWorldInputs` + * In your function, you can call *hooks* to change part of the shader. In a material + * shader, these are the hooks available: + * - `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. + * - `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. * - * 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. - * - *
- * - * `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 + * One thing you can do with a material shader is animate the positions of vertices + * over time: * - * @example - *
- * + * ```js example * let myShader; * * function setup() { * createCanvas(200, 200, WEBGL); - * myShader = baseMaterialShader().modify(() => { - * let time = uniformFloat(() => millis()); - * getWorldInputs((inputs) => { - * inputs.position.y += - * 20 * sin(time * 0.001 + inputs.position.x * 0.05); - * return inputs; - * }); - * }); - * } - * - * function draw() { - * background(255); - * shader(myShader); - * lights(); - * noStroke(); - * fill('red'); - * sphere(50); + * myShader = buildMaterialShader(material); * } - * - *
- * - * @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 material() { + * let time = uniformFloat(); + * worldInputs.begin(); + * worldInputs.position.y += + * 20 * sin(time * 0.001 + worldInputs.position.x * 0.05); + * worldInputs.end(); * } * * function draw() { * background(255); - * rotateY(millis() * 0.001); * shader(myShader); + * myShader.setUniform('time', millis()); * lights(); * noStroke(); * fill('red'); - * torus(30); + * sphere(50); * } - * - *
+ * ``` * - * @example - *
- * + * 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: + * + * ```js example * let myShader; * let environment; * @@ -1443,16 +1438,17 @@ function material(p5, fn){ * environment = await loadImage('assets/outdoor_spheremap.jpg'); * * createCanvas(200, 200, WEBGL); - * myShader = baseMaterialShader().modify(() => { - * 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() { + * 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() { @@ -1466,28 +1462,31 @@ 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(() => { - * 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() { + * 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); + * pixelInputs.end(); * } * * function draw() { @@ -1504,233 +1503,253 @@ function material(p5, fn){ * specularMaterial(255); * sphere(50); * } - * - *
- */ - 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`. - * - * `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)`. + * ``` * - * `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. + * 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; * - * 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. + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = buildMaterialShader(material); + * } * - * @method baseFilterShader - * @beta - * @returns {p5.Shader} The filter shader + * function material() { + * let myNormal = sharedVec3(); * - * @example - *
- * - * let img; - * let myShader; + * pixelInputs.begin(); + * myNormal = pixelInputs.normal; + * pixelInputs.end(); * - * 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); - * }); - * }); + * finalColor.begin(); + * finalColor.set(mix( + * [1, 1, 1, 1], + * finalColor.color, + * abs(dot(myNormal, [0, 0, 1])) + * )); + * finalColor.end(); * } * * function draw() { - * image(img, -50, -50); - * filter(myShader); - * describe('an image of bricks, distorting over time'); + * 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 buildMaterialShader + * @submodule p5.strands + * @beta + * @param {Function} callback A function building a p5.strands shader. + * @returns {p5.Shader} The material shader. + */ + /** + * @method buildMaterialShader + * @param {Object} hooks An object specifying p5.strands hooks in GLSL. + * @returns {p5.Shader} The material shader. */ - fn.baseFilterShader = function() { - return (this._renderer.filterRenderer || this._renderer) - .baseFilterShader(); + fn.buildMaterialShader = function (cb) { + return this.baseMaterialShader().modify(cb); }; /** - * 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` - * - * + * 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. * - * 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. + * Since this function loads data from another file, it returns a `Promise`. + * Use it in an `async function setup`, and `await` its result. * - *
- * - * `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. + * ```js + * let myShader; + * async function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = await loadMaterialShader('myMaterial.js'); + * } * - *
+ * function draw() { + * background(255); + * shader(myShader); + * myShader.setUniform('time', millis()); + * lights(); + * noStroke(); + * fill('red'); + * sphere(50); + * } + * ``` * - * `void beforeFragment` + * Inside your shader file, you can call p5.strands hooks to change parts of the shader. For + * 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(); + * worldInputs.begin(); + * worldInputs.position.y += + * 20 * sin(time * 0.001 + worldInputs.position.x * 0.05); + * worldInputs.end(); + * ``` * - * Called at the start of the fragment shader. + * 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. * - *
+ * 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)`. * - * `vec4 getFinalColor` + * @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. + * @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 cb = await urlToStrandsCallback(url); + let shader = withGlobalStrands(this, () => this.buildMaterialShader(cb)); + 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. * - * + * Calling `buildMaterialShader(shaderFunction)` + * is equivalent to calling `baseMaterialShader().modify(shaderFunction)`. * - * Update the final color after mixing. It takes in a `vec4 color` and must return a modified version. + * Read the `buildMaterialShader` reference or + * call `baseMaterialShader().inspectHooks()` for more information on what you can do with + * the base material shader. * - *
+ * @method baseMaterialShader + * @submodule p5.strands + * @beta + * @returns {p5.Shader} The base material shader. + */ + fn.baseMaterialShader = function () { + this._assert3d("baseMaterialShader"); + return this._renderer.baseMaterialShader(); + }; + + /** + * Returns the base shader used for filters. * - * `void afterFragment` + * Calling `buildFilterShader(shaderFunction)` + * is equivalent to calling `baseFilterShader().modify(shaderFunction)`. * - * + * Read the `buildFilterShader` reference or + * call `baseFilterShader().inspectHooks()` for more information on what you can do with + * the base filter shader. * - * Called at the end of the fragment shader. + * @method baseFilterShader + * @submodule p5.strands + * @beta + * @returns {p5.Shader} The base filter shader. + */ + fn.baseFilterShader = function () { + return (this._renderer.filterRenderer || this._renderer).baseFilterShader(); + }; + + /** + * 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. * - *
+ * 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. * - * 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: + * - `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. * - * 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) { - * inputs.position.y += - * 20. * sin(time * 0.001 + inputs.position.x * 0.05); - * return inputs; - * }` - * }); + * myShader = buildNormalShader(material); + * } + * + * function material() { + * 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); * } - * - *
+ * ``` * - * @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) { - * inputs.normal = abs(inputs.normal); - * return inputs; - * }`, - * 'vec4 getFinalColor': `(vec4 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; - * newColor = newColor / (color.r + color.g + color.b); - * return vec4(newColor, 1.0) * color.a; - * }` - * }); + * myShader = buildNormalShader(material); + * } + * + * function material() { + * 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 / (finalColor.color.r + finalColor.color.g + finalColor.color.b); + * finalColor.set([newColor.r, newColor.g, newColor.b, finalColor.color.a]); + * finalColor.end(); * } * * function draw() { @@ -1741,284 +1760,326 @@ 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 buildNormalShader + * @submodule p5.strands + * @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 buildNormalShader + * @param {Object} hooks An object specifying p5.strands hooks in GLSL. + * @returns {p5.Shader} The normal shader. + */ + fn.buildNormalShader = 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` - * - * - * - * 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. + * 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. * - *
+ * 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); + * myShader.setUniform('time', millis()); + * 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 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. * - * `vec4 getFinalColor` + * ```js + * // myMaterial.js + * 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`, + * 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 + * @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. + * @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 cb = await urlToStrandsCallback(url); + let shader = this.withGlobalStrands(this, () => + this.buildNormalShader(cb), + ); + 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 `buildNormalShader(shaderFunction)` + * is equivalent to calling `baseNormalShader().modify(shaderFunction)`. * - * + * Read the `buildNormalShader` 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 + * @submodule p5.strands + * @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 `buildColorShader` 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: + * - `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. * - * 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) { - * inputs.position.y += - * 20. * sin(time * 0.001 + inputs.position.x * 0.05); - * return inputs; - * }` - * }); + * myShader = buildColorShader(material); + * } + * + * function material() { + * 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); * } - * - *
+ * ``` + * + * 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 buildColorShader + * @submodule p5.strands + * @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 buildColorShader + * @param {Object} hooks An object specifying p5.strands hooks in GLSL. + * @returns {p5.Shader} The color shader. + */ + fn.buildColorShader = 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. - * - *
- * - * `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. + * 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. * - * `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); + * myShader.setUniform('time', millis()); + * 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 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. * - * `vec4 getFinalColor` + * ```js + * // myMaterial.js + * 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`, + * 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 + * @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. + * @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 cb = await urlToStrandsCallback(url); + let shader = withGlobalStrands(this, () => this.buildColorShader(cb)); + 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 `buildColorShader(shaderFunction)` + * is equivalent to calling `baseColorShader().modify(shaderFunction)`. * - * + * Read the `buildColorShader` 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 + * @submodule p5.strands + * @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 `buildStrokeShader` 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: + * - `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. * - * 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, - * length(inputs.position - inputs.center) - * ); - * inputs.color *= opacity; - * return inputs; - * }` - * }); + * myShader = buildStrokeShader(material); + * } + * + * function material() { + * pixelInputs.begin(); + * let opacity = 1 - smoothstep( + * 0, + * 15, + * length(pixelInputs.position - pixelInputs.center) + * ); + * pixelInputs.color.a *= opacity; + * pixelInputs.end(); * } * * function draw() { @@ -2032,99 +2093,210 @@ 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; - * return inputs; - * }` - * }); + * myShader = buildStrokeShader(material); + * } + * + * function material() { + * 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() { * 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; - * return inputs; - * }` - * }); + * myShader = buildStrokeShader(material); + * } + * + * function material() { + * 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() { * 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 buildStrokeShader + * @submodule p5.strands + * @beta + * @param {Function} callback A function building a p5.strands shader. + * @returns {p5.Shader} The stroke shader. + */ + /** + * @method buildStrokeShader + * @param {Object} hooks An object specifying p5.strands hooks in GLSL. + * @returns {p5.Shader} The stroke shader. + */ + fn.buildStrokeShader = 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 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 + * pixelInputs.begin(); + * let opacity = 1 - smoothstep( + * 0, + * 15, + * length(pixelInputs.position - pixelInputs.center) + * ); + * pixelInputs.color.a *= opacity; + * pixelInputs.end(); + * ``` + * + * 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. + * + * 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 + * @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. + * @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 cb = await urlToStrandsCallback(url); + let shader = withGlobalStrands(this, () => this.buildStrokeShader(cb)); + 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 `buildStrokeShader(shaderFunction)` + * is equivalent to calling `baseStrokeShader().modify(shaderFunction)`. + * + * Read the `buildStrokeShader` reference or + * call `baseStrokeShader().inspectHooks()` for more information on what you can do with + * the base material shader. + * + * @method baseStrokeShader + * @submodule p5.strands + * @beta + * @returns {p5.Shader} The base material shader. */ - fn.baseStrokeShader = function() { - this._assert3d('baseStrokeShader'); + fn.baseStrokeShader = function () { + this._assert3d("baseStrokeShader"); return this._renderer.baseStrokeShader(); }; @@ -2384,7 +2556,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 @@ -2565,10 +2737,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); } }; @@ -2839,8 +3011,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); @@ -2886,7 +3058,7 @@ function material(p5, fn){ *
*/ fn.normalMaterial = function (...args) { - this._assert3d('normalMaterial'); + this._assert3d("normalMaterial"); // p5._validateParameters('normalMaterial', args); this._renderer.normalMaterial(...args); @@ -3113,16 +3285,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; }; @@ -3211,14 +3383,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; }; @@ -3466,14 +3638,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; }; @@ -3539,7 +3711,7 @@ function material(p5, fn){ * */ fn.shininess = function (shine) { - this._assert3d('shininess'); + this._assert3d("shininess"); // p5._validateParameters('shininess', arguments); this._renderer.shininess(shine); @@ -3655,54 +3827,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) { @@ -3714,21 +3886,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); } 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'], + }, } ); } 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 0000000000..56ad16c11e Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadFilterShader/000.png differ 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 0000000000..2112a108f9 Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadMaterialShader/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadMaterialShader/metadata.json b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadMaterialShader/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/loadMaterialShader/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index 42c0f11b95..ca5d39aa61 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -1630,5 +1630,123 @@ 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.set([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); + 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', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs.begin(); + 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); + 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); + }); }); }); 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 9a99b34452..175bbecec5 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.')) { @@ -148,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); @@ -265,8 +268,9 @@ export function processData(rawData, strategy) { }, class: className, static: entry.scope === 'static' && 1, - module, - submodule + 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] || {}; 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') {