diff --git a/src/webgl/material.js b/src/webgl/material.js index 54bebc9ba1..6694bd3b01 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -191,12 +191,54 @@ p5.prototype.loadShader = function ( * 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: + * - `vertex`: An object describing the available vertex shader hooks. + * - `fragment`: An object describing the available frament shader hooks. * @returns {p5.Shader} new shader object created from the * vertex and fragment shaders. * @@ -420,10 +462,79 @@ p5.prototype.loadShader = function ( * } * * + * + *
+ * + * // A shader with hooks. + * let myShader; + * + * // A shader with modified hooks. + * let modifiedShader; + * + * // 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; + * + * void main() { + * vec4 positionVec4 = vec4(aPosition, 1.0); + * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; + * } + * `; + * + * // Create a fragment shader that uses a hook. + * let fragSrc = ` + * precision highp float; + * void main() { + * // Let users override the color + * gl_FragColor = HOOK_getColor(vec4(1., 0., 0., 1.)); + * } + * `; + * + * function setup() { + * createCanvas(50, 50, WEBGL); + * + * // Create a shader with hooks + * myShader = createShader(vertSrc, fragSrc, { + * fragment: { + * 'vec4 getColor': '(vec4 color) { return color; }' + * } + * }); + * + * // Make a version of the shader with a hook overridden + * modifiedShader = myShader.modify({ + * 'vec4 getColor': `(vec4 color) { + * return vec4(0., 0., 1., 1.); + * }` + * }); + * } + * + * function draw() { + * noStroke(); + * + * push(); + * shader(myShader); + * translate(-width/3, 0); + * sphere(10); + * pop(); + * + * push(); + * shader(modifiedShader); + * translate(width/3, 0); + * sphere(10); + * pop(); + * } + * + *
*/ -p5.prototype.createShader = function (vertSrc, fragSrc) { +p5.prototype.createShader = function (vertSrc, fragSrc, options) { p5._validateParameters('createShader', arguments); - return new p5.Shader(this._renderer, vertSrc, fragSrc); + return new p5.Shader(this._renderer, vertSrc, fragSrc, options); }; /** @@ -767,9 +878,753 @@ p5.prototype.shader = function (s) { this._renderer._useNormalMaterial = false; } + s.setDefaultUniforms(); + return this; }; +/** + * Get the default shader used with lights, materials, + * and textures. + * + * You can call `materialShader().modify()` + * and change any of the following hooks: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
HookDescription
+ * + * `void beforeVertex` + * + * + * + * Called at the start of the vertex shader. + * + *
+ * + * `vec3 getLocalPosition` + * + * + * + * Update the position of vertices before transforms are applied. It takes in `vec3 position` and must return a modified version. + * + *
+ * + * `vec3 getWorldPosition` + * + * + * + * Update the position of vertices after transforms are applied. It takes in `vec3 position` and pust return a modified version. + * + *
+ * + * `vec3 getLocalNormal` + * + * + * + * Update the normal before transforms are applied. It takes in `vec3 normal` and must return a modified version. + * + *
+ * + * `vec3 getWorldNormal` + * + * + * + * Update the normal after transforms are applied. It takes in `vec3 normal` and must return a modified version. + * + *
+ * + * `vec2 getUV` + * + * + * + * Update the texture coordinates. It takes in `vec2 uv` and must return a modified version. + * + *
+ * + * `vec4 getVertexColor` + * + * + * + * Update the color of each vertex. It takes in a `vec4 color` and must return a modified version. + * + *
+ * + * `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 `materialShader().inspectHooks()` to see all the possible hooks and + * their default implementations. + * + * @method materialShader + * @beta + * @returns {p5.Shader} The material shader + * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = materialShader().modify({ + * uniforms: { + * 'float time': () => millis() + * }, + * 'vec3 getWorldPosition': `(vec3 pos) { + * pos.y += 20.0 * sin(time * 0.001 + pos.x * 0.05); + * return pos; + * }` + * }); + * } + * + * function draw() { + * background(255); + * shader(myShader); + * lights(); + * noStroke(); + * fill('red'); + * sphere(50); + * } + * + *
+ * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = materialShader().modify({ + * declarations: 'vec3 myNormal;', + * 'Inputs getPixelInputs': `(Inputs inputs) { + * myNormal = inputs.normal; + * return inputs; + * }`, + * 'vec4 getFinalColor': `(vec4 color) { + * return mix( + * vec4(1.0, 1.0, 1.0, 1.0), + * color, + * abs(dot(myNormal, vec3(0.0, 0.0, 1.0))) + * ); + * }` + * }); + * } + * + * function draw() { + * background(255); + * rotateY(millis() * 0.001); + * shader(myShader); + * lights(); + * noStroke(); + * fill('red'); + * torus(30); + * } + * + *
+ * + * @example + *
+ * + * let myShader; + * let environment; + * + * function preload() { + * environment = loadImage('assets/outdoor_spheremap.jpg'); + * } + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = materialShader().modify({ + * 'Inputs getPixelInputs': `(Inputs inputs) { + * float factor = + * sin( + * inputs.texCoord.x * ${TWO_PI} + + * inputs.texCoord.y * ${TWO_PI} + * ) * 0.4 + 0.5; + * inputs.shininess = mix(1., 100., factor); + * inputs.metalness = factor; + * return inputs; + * }` + * }); + * } + * + * function draw() { + * panorama(environment); + * ambientLight(100); + * imageLight(environment); + * rotateY(millis() * 0.001); + * shader(myShader); + * noStroke(); + * fill(255); + * specularMaterial(150); + * sphere(50); + * } + * + *
+ * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = materialShader().modify({ + * 'Inputs getPixelInputs': `(Inputs inputs) { + * vec3 newNormal = inputs.normal; + * // Simple bump mapping: adjust the normal based on position + * newNormal.x += 0.2 * sin( + * sin( + * inputs.texCoord.y * ${TWO_PI} * 10.0 + + * inputs.texCoord.x * ${TWO_PI} * 25.0 + * ) + * ); + * newNormal.y += 0.2 * sin( + * sin( + * inputs.texCoord.x * ${TWO_PI} * 10.0 + + * inputs.texCoord.y * ${TWO_PI} * 25.0 + * ) + * ); + * inputs.normal = normalize(newNormal); + * return inputs; + * }` + * }); + * } + * + * function draw() { + * background(255); + * shader(myShader); + * ambientLight(150); + * pointLight( + * 255, 255, 255, + * 100*cos(frameCount*0.04), -50, 100*sin(frameCount*0.04) + * ); + * noStroke(); + * fill('red'); + * shininess(200); + * specularMaterial(255); + * sphere(50); + * } + * + *
+ */ +p5.prototype.materialShader = function() { + this._assert3d('materialShader'); + return this._renderer.materialShader(); +}; + +/** + * Get the shader used by `normalMaterial()`. + * + * You can call `normalShader().modify()` + * and change any of the following hooks: + * + * Hook | Description + * -----|------------ + * `void beforeVertex` | Called at the start of the vertex shader. + * `vec3 getLocalPosition` | Update the position of vertices before transforms are applied. It takes in `vec3 position` and must return a modified version. + * `vec3 getWorldPosition` | Update the position of vertices after transforms are applied. It takes in `vec3 position` and pust return a modified version. + * `vec3 getLocalNormal` | Update the normal before transforms are applied. It takes in `vec3 normal` and must return a modified version. + * `vec3 getWorldNormal` | Update the normal after transforms are applied. It takes in `vec3 normal` and must return a modified version. + * `vec2 getUV` | Update the texture coordinates. It takes in `vec2 uv` and must return a modified version. + * `vec4 getVertexColor` | Update the color of each vertex. It takes in a `vec4 color` and must return a modified version. + * `void afterVertex` | Called at the end of the vertex shader. + * `void beforeFragment` | Called at the start of the fragment shader. + * `vec4 getFinalColor` | Update the final color after mixing. It takes in a `vec4 color` and must return a modified version. + * `void afterFragment` | Called at the end of the fragment shader. + * + * Most of the time, you will need to write your hooks in GLSL ES version 300. If you + * are using WebGL 1 instead of 2, write your hooks in GLSL ES 100 instead. + * + * Call `normalShader().inspectHooks()` to see all the possible hooks and + * their default implementations. + * + * @method normalShader + * @beta + * @returns {p5.Shader} The `normalMaterial` shader + * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = normalShader().modify({ + * uniforms: { + * 'float time': () => millis() + * }, + * 'vec3 getWorldPosition': `(vec3 pos) { + * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); + * return pos; + * }` + * }); + * } + * + * function draw() { + * background(255); + * shader(myShader); + * noStroke(); + * sphere(50); + * } + * + *
+ * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = normalShader().modify({ + * 'vec3 getWorldNormal': '(vec3 normal) { return abs(normal); }', + * '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; + * }` + * }); + * } + * + * function draw() { + * background(255); + * shader(myShader); + * noStroke(); + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.015); + * box(100); + * } + * + *
+ */ +p5.prototype.normalShader = function() { + this._assert3d('materialShader'); + return this._renderer.normalShader(); +}; + +/** + * Get the shader used when no lights or materials are applied. + * + * You can call `colorShader().modify()` + * and change any of the following hooks: + * + * Hook | Description + * -------|------------- + * `void beforeVertex` | Called at the start of the vertex shader. + * `vec3 getLocalPosition` | Update the position of vertices before transforms are applied. It takes in `vec3 position` and must return a modified version. + * `vec3 getWorldPosition` | Update the position of vertices after transforms are applied. It takes in `vec3 position` and pust return a modified version. + * `vec3 getLocalNormal` | Update the normal before transforms are applied. It takes in `vec3 normal` and must return a modified version. + * `vec3 getWorldNormal` | Update the normal after transforms are applied. It takes in `vec3 normal` and must return a modified version. + * `vec2 getUV` | Update the texture coordinates. It takes in `vec2 uv` and must return a modified version. + * `vec4 getVertexColor` | Update the color of each vertex. It takes in a `vec4 color` and must return a modified version. + * `void afterVertex` | Called at the end of the vertex shader. + * `void beforeFragment` | Called at the start of the fragment shader. + * `vec4 getFinalColor` | Update the final color after mixing. It takes in a `vec4 color` and must return a modified version. + * `void afterFragment` | Called at the end of the fragment shader. + * + * Most of the time, you will need to write your hooks in GLSL ES version 300. If you + * are using WebGL 1 instead of 2, write your hooks in GLSL ES 100 instead. + * + * Call `colorShader().inspectHooks()` to see all the possible hooks and + * their default implementations. + * + * @method colorShader + * @beta + * @returns {p5.Shader} The color shader + * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = colorShader().modify({ + * uniforms: { + * 'float time': () => millis() + * }, + * 'vec3 getWorldPosition': `(vec3 pos) { + * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); + * return pos; + * }` + * }); + * } + * + * function draw() { + * background(255); + * shader(myShader); + * noStroke(); + * fill('red'); + * circle(0, 0, 50); + * } + * + *
+ */ +p5.prototype.colorShader = function() { + this._assert3d('colorShader'); + return this._renderer.colorShader(); +}; + +/** + * Get the shader used when drawing the strokes of shapes. + * + * You can call `strokeShader().modify()` + * and change any of the following hooks: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
HookDescription
+ * + * `void beforeVertex` + * + * + * + * Called at the start of the vertex shader. + * + *
+ * + * `vec3 getLocalPosition` + * + * + * + * Update the position of vertices before transforms are applied. It takes in `vec3 position` and must return a modified version. + * + *
+ * + * `vec3 getWorldPosition` + * + * + * + * Update the position of vertices after transforms are applied. It takes in `vec3 position` and pust return a modified version. + * + *
+ * + * `float getStrokeWeight` + * + * + * + * Update the stroke weight. It takes in `float weight` and pust return a modified version. + * + *
+ * + * `vec2 getLineCenter` + * + * + * + * Update the center of the line. It takes in `vec2 center` and must return a modified version. + * + *
+ * + * `vec2 getLinePosition` + * + * + * + * Update the position of each vertex on the edge of the line. It takes in `vec2 position` and must return a modified version. + * + *
+ * + * `vec4 getVertexColor` + * + * + * + * Update the color of each vertex. It takes in a `vec4 color` and must return a modified version. + * + *
+ * + * `void afterVertex` + * + * + * + * Called at the end of the vertex shader. + * + *
+ * + * `void beforeFragment` + * + * + * + * Called at the start of the fragment shader. + * + *
+ * + * `Inputs getPixelInputs` + * + * + * + * Update the inputs to the shader. It takes in a struct `Inputs inputs`, which includes: + * - `vec4 color`, the color of the stroke + * - `vec2 tangent`, the direction of the stroke in screen space + * - `vec2 center`, the coordinate of the center of the stroke in screen space p5.js pixels + * - `vec2 position`, the coordinate of the current pixel in screen space p5.js pixels + * - `float strokeWeight`, the thickness of the stroke in p5.js pixels + * + *
+ * + * `bool shouldDiscard` + * + * + * + * Caps and joins are made by discarded pixels in the fragment shader to carve away unwanted areas. Use this to change this logic. It takes in a `bool willDiscard` and must return a modified version. + * + *
+ * + * `vec4 getFinalColor` + * + * + * + * 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 `strokeShader().inspectHooks()` to see all the possible hooks and + * their default implementations. + * + * @method strokeShader + * @beta + * @returns {p5.Shader} The stroke shader + * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = strokeShader().modify({ + * 'Inputs getPixelInputs': `(Inputs inputs) { + * float opacity = 1.0 - smoothstep( + * 0.0, + * 15.0, + * length(inputs.position - inputs.center) + * ); + * inputs.color *= opacity; + * return inputs; + * }` + * }); + * } + * + * function draw() { + * background(255); + * shader(myShader); + * strokeWeight(30); + * line( + * -width/3, + * sin(millis()*0.001) * height/4, + * width/3, + * sin(millis()*0.001 + 1) * height/4 + * ); + * } + * + *
+ * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = strokeShader().modify({ + * uniforms: { + * 'float time': () => millis() + * }, + * declarations: 'vec3 myPosition;', + * 'vec3 getWorldPosition': `(vec3 pos) { + * myPosition = pos; + * return pos; + * }`, + * 'float getStrokeWeight': `(float w) { + * // 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.) + + * myPosition.x*0.01 + + * myPosition.y*0.01 + * )); + * return w * scale; + * }` + * }); + * } + * + * function draw() { + * background(255); + * shader(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); + * } + * endShape(); + * } + * + *
+ * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = strokeShader().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; + * }` + * }); + * } + * + * function draw() { + * background(255); + * shader(myShader); + * 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) + * ); + * } + * endShape(); + * } + * + *
+ */ +p5.prototype.strokeShader = function() { + this._assert3d('strokeShader'); + return this._renderer.strokeShader(); +}; + /** * Restores the default shaders. * diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 69145241a8..f712787233 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -1714,7 +1714,7 @@ p5.RendererGL = class RendererGL extends Renderer { sphereMapping ); } - this.uNMatrix.inverseTranspose(this.uMVMatrix); + this.uNMatrix.inverseTranspose(this.uViewMatrix); this.uNMatrix.invert3x3(this.uNMatrix); this.sphereMapping.setUniform('uFovY', this._curCamera.cameraFOV); this.sphereMapping.setUniform('uAspect', this._curCamera.aspectRatio); @@ -1785,6 +1785,15 @@ p5.RendererGL = class RendererGL extends Renderer { return this._getImmediateLineShader(); } + materialShader() { + if (!this._pInst._glAttributes.perPixelLighting) { + throw new Error( + 'The material shader does not support hooks without perPixelLighting. Try turning it back on.' + ); + } + return this._getLightShader(); + } + _getLightShader() { if (!this._defaultLightShader) { if (this._pInst._glAttributes.perPixelLighting) { @@ -1793,7 +1802,34 @@ p5.RendererGL = class RendererGL extends Renderer { this._webGL2CompatibilityPrefix('vert', 'highp') + defaultShaders.phongVert, this._webGL2CompatibilityPrefix('frag', 'highp') + - defaultShaders.phongFrag + defaultShaders.phongFrag, + { + vertex: { + 'void beforeVertex': '() {}', + 'vec3 getLocalPosition': '(vec3 position) { return position; }', + 'vec3 getWorldPosition': '(vec3 position) { return position; }', + 'vec3 getLocalNormal': '(vec3 normal) { return normal; }', + 'vec3 getWorldNormal': '(vec3 normal) { return normal; }', + 'vec2 getUV': '(vec2 uv) { return uv; }', + 'vec4 getVertexColor': '(vec4 color) { return color; }', + 'void afterVertex': '() {}' + }, + fragment: { + 'void beforeFragment': '() {}', + 'Inputs getPixelInputs': '(Inputs inputs) { return inputs; }', + 'vec4 combineColors': `(ColorComponents components) { + vec4 color = vec4(0.); + color.rgb += components.diffuse * components.baseColor; + color.rgb += components.ambient * components.ambientColor; + color.rgb += components.specular * components.specularColor; + color.rgb += components.emissive; + color.a = components.opacity; + return color; + }`, + 'vec4 getFinalColor': '(vec4 color) { return color; }', + 'void afterFragment': '() {}' + } + } ); } else { this._defaultLightShader = new p5.Shader( @@ -1823,6 +1859,10 @@ p5.RendererGL = class RendererGL extends Renderer { return this._defaultImmediateModeShader; } + normalShader() { + return this._getNormalShader(); + } + _getNormalShader() { if (!this._defaultNormalShader) { this._defaultNormalShader = new p5.Shader( @@ -1830,13 +1870,34 @@ p5.RendererGL = class RendererGL extends Renderer { this._webGL2CompatibilityPrefix('vert', 'mediump') + defaultShaders.normalVert, this._webGL2CompatibilityPrefix('frag', 'mediump') + - defaultShaders.normalFrag + defaultShaders.normalFrag, + { + vertex: { + 'void beforeVertex': '() {}', + 'vec3 getLocalPosition': '(vec3 position) { return position; }', + 'vec3 getWorldPosition': '(vec3 position) { return position; }', + 'vec3 getLocalNormal': '(vec3 normal) { return normal; }', + 'vec3 getWorldNormal': '(vec3 normal) { return normal; }', + 'vec2 getUV': '(vec2 uv) { return uv; }', + 'vec4 getVertexColor': '(vec4 color) { return color; }', + 'void afterVertex': '() {}' + }, + fragment: { + 'void beforeFragment': '() {}', + 'vec4 getFinalColor': '(vec4 color) { return color; }', + 'void afterFragment': '() {}' + } + } ); } return this._defaultNormalShader; } + colorShader() { + return this._getColorShader(); + } + _getColorShader() { if (!this._defaultColorShader) { this._defaultColorShader = new p5.Shader( @@ -1844,13 +1905,58 @@ p5.RendererGL = class RendererGL extends Renderer { this._webGL2CompatibilityPrefix('vert', 'mediump') + defaultShaders.normalVert, this._webGL2CompatibilityPrefix('frag', 'mediump') + - defaultShaders.basicFrag + defaultShaders.basicFrag, + { + vertex: { + 'void beforeVertex': '() {}', + 'vec3 getLocalPosition': '(vec3 position) { return position; }', + 'vec3 getWorldPosition': '(vec3 position) { return position; }', + 'vec3 getLocalNormal': '(vec3 normal) { return normal; }', + 'vec3 getWorldNormal': '(vec3 normal) { return normal; }', + 'vec2 getUV': '(vec2 uv) { return uv; }', + 'vec4 getVertexColor': '(vec4 color) { return color; }', + 'void afterVertex': '() {}' + }, + fragment: { + 'void beforeFragment': '() {}', + 'vec4 getFinalColor': '(vec4 color) { return color; }', + 'void afterFragment': '() {}' + } + } ); } return this._defaultColorShader; } + /** + * TODO(dave): un-private this when there is a way to actually override the + * shader used for points + * + * Get the shader used when drawing points with `point()`. + * + * You can call `pointShader().modify()` + * and change any of the following hooks: + * - `void beforeVertex`: Called at the start of the vertex shader. + * - `vec3 getLocalPosition`: Update the position of vertices before transforms are applied. It takes in `vec3 position` and must return a modified version. + * - `vec3 getWorldPosition`: Update the position of vertices after transforms are applied. It takes in `vec3 position` and pust return a modified version. + * - `float getPointSize`: Update the size of the point. It takes in `float size` and must return a modified version. + * - `void afterVertex`: Called at the end of the vertex shader. + * - `void beforeFragment`: Called at the start of the fragment shader. + * - `bool shouldDiscard`: Points are drawn inside a square, with the corners discarded in the fragment shader to create a circle. Use this to change this logic. It takes in a `bool willDiscard` and must return a modified version. + * - `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. + * + * Call `pointShader().inspectHooks()` to see all the possible hooks and + * their default implementations. + * + * @returns {p5.Shader} The `point()` shader + * @private() + */ + pointShader() { + return this._getPointShader(); + } + _getPointShader() { if (!this._defaultPointShader) { this._defaultPointShader = new p5.Shader( @@ -1858,12 +1964,31 @@ p5.RendererGL = class RendererGL extends Renderer { this._webGL2CompatibilityPrefix('vert', 'mediump') + defaultShaders.pointVert, this._webGL2CompatibilityPrefix('frag', 'mediump') + - defaultShaders.pointFrag + defaultShaders.pointFrag, + { + vertex: { + 'void beforeVertex': '() {}', + 'vec3 getLocalPosition': '(vec3 position) { return position; }', + 'vec3 getWorldPosition': '(vec3 position) { return position; }', + 'float getPointSize': '(float size) { return size; }', + 'void afterVertex': '() {}' + }, + fragment: { + 'void beforeFragment': '() {}', + 'vec4 getFinalColor': '(vec4 color) { return color; }', + 'bool shouldDiscard': '(bool outside) { return outside; }', + 'void afterFragment': '() {}' + } + } ); } return this._defaultPointShader; } + strokeShader() { + return this._getLineShader(); + } + _getLineShader() { if (!this._defaultLineShader) { this._defaultLineShader = new p5.Shader( @@ -1871,7 +1996,26 @@ p5.RendererGL = class RendererGL extends Renderer { this._webGL2CompatibilityPrefix('vert', 'mediump') + defaultShaders.lineVert, this._webGL2CompatibilityPrefix('frag', 'mediump') + - defaultShaders.lineFrag + defaultShaders.lineFrag, + { + vertex: { + 'void beforeVertex': '() {}', + 'vec3 getLocalPosition': '(vec3 position) { return position; }', + 'vec3 getWorldPosition': '(vec3 position) { return position; }', + 'float getStrokeWeight': '(float weight) { return weight; }', + 'vec2 getLineCenter': '(vec2 center) { return center; }', + 'vec2 getLinePosition': '(vec2 position) { return position; }', + 'vec4 getVertexColor': '(vec4 color) { return color; }', + 'void afterVertex': '() {}' + }, + fragment: { + 'void beforeFragment': '() {}', + 'Inputs getPixelInputs': '(Inputs inputs) { return inputs; }', + 'vec4 getFinalColor': '(vec4 color) { return color; }', + 'bool shouldDiscard': '(bool outside) { return outside; }', + 'void afterFragment': '() {}' + } + } ); } @@ -2088,7 +2232,7 @@ p5.RendererGL = class RendererGL extends Renderer { fillShader.setUniform('uSpecular', this._useSpecularMaterial); fillShader.setUniform('uEmissive', this._useEmissiveMaterial); fillShader.setUniform('uShininess', this._useShininess); - fillShader.setUniform('metallic', this._useMetalness); + fillShader.setUniform('uMetallic', this._useMetalness); this._setImageLightUniforms(fillShader); @@ -2161,14 +2305,7 @@ p5.RendererGL = class RendererGL extends Renderer { let diffusedLight = this.getDiffusedTexture(this.activeImageLight); shader.setUniform('environmentMapDiffused', diffusedLight); let specularLight = this.getSpecularTexture(this.activeImageLight); - // In p5js the range of shininess is >= 1, - // Therefore roughness range will be ([0,1]*8)*20 or [0, 160] - // The factor of 8 is because currently the getSpecularTexture - // only calculated 8 different levels of roughness - // The factor of 20 is just to spread up this range so that, - // [1, max] of shininess is converted to [0,160] of roughness - let roughness = 20 / this._useShininess; - shader.setUniform('levelOfDetail', roughness * 8); + shader.setUniform('environmentMapSpecular', specularLight); } } diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 8333c92ac5..1ea14400d9 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -24,6 +24,32 @@ import p5 from '../core/main'; * created, it can be used with the shader() * function, as in `shader(myShader)`. * + * A shader can optionally describe *hooks,* which are functions in GLSL that + * users may choose to provide to customize the behavior of the shader. For the + * vertex or the fragment shader, users can pass in 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_`: + * + * ```glsl + * void main() { + * HOOK_beforeVertex(); + * // Add the rest ofy our shader code here! + * } + * ``` + * * Note: createShader(), * createFilterShader(), and * loadShader() are the recommended ways to @@ -33,6 +59,10 @@ import p5 from '../core/main'; * @param {p5.RendererGL} renderer WebGL context for this shader. * @param {String} vertSrc source code for the vertex shader program. * @param {String} fragSrc source code for the fragment shader program. + * @param {Object} [options] An optional object describing how this shader can + * be augmented with hooks. It can include: + * - `vertex`: An object describing the available vertex shader hooks. + * - `fragment`: An object describing the available frament shader hooks. * * @example *
@@ -122,7 +152,7 @@ import p5 from '../core/main'; *
*/ p5.Shader = class Shader { - constructor(renderer, vertSrc, fragSrc) { + constructor(renderer, vertSrc, fragSrc, options = {}) { // TODO: adapt this to not take ids, but rather, // to take the source for a vertex and fragment shader // to enable custom shaders at some later date @@ -138,6 +168,314 @@ p5.Shader = class Shader { this.uniforms = {}; this._bound = false; this.samplers = []; + this.hooks = { + // These should be passed in by `.modify()` instead of being manually + // passed in. + + // Stores uniforms + default values. + uniforms: options.uniforms || {}, + + // Stores custom uniform + helper declarations as a string. + declarations: options.declarations, + + // Stores helper functions to prepend to shaders. + helpers: options.helpers || {}, + + // Stores the hook implementations + vertex: options.vertex || {}, + fragment: options.fragment || {}, + + // Stores whether or not the hook implementation has been modified + // from the default. This is supplied automatically by calling + // yourShader.modify(...). + modified: { + vertex: (options.modified && options.modified.vertex) || {}, + fragment: (options.modified && options.modified.fragment) || {} + } + }; + } + + shaderSrc(src, shaderType) { + const main = 'void main'; + const [preMain, postMain] = src.split(main); + + let hooks = ''; + for (const key in this.hooks.uniforms) { + hooks += `uniform ${key};\n`; + } + if (this.hooks.declarations) { + hooks += this.hooks.declarations + '\n'; + } + if (this.hooks[shaderType].declarations) { + hooks += this.hooks[shaderType].declarations + '\n'; + } + for (const hookDef in this.hooks.helpers) { + hooks += `${hookDef}${this.hooks.helpers[hookDef]}\n`; + } + for (const hookDef in this.hooks[shaderType]) { + if (hookDef === 'declarations') continue; + const [hookType, hookName] = hookDef.split(' '); + + // Add a #define so that if the shader wants to use preprocessor directives to + // optimize away the extra function calls in main, it can do so + if (this.hooks.modified[shaderType][hookDef]) { + hooks += '#define AUGMENTED_HOOK_' + hookName + '\n'; + } + + hooks += + hookType + ' HOOK_' + hookName + this.hooks[shaderType][hookDef] + '\n'; + } + + return preMain + hooks + main + postMain; + } + + /** + * Shaders are written in GLSL, but + * there are different versions of GLSL that it might be written in. + * + * Calling this method on a `p5.Shader` will return the GLSL version it uses, either `100 es` or `300 es`. + * WebGL 1 shaders will only use `100 es`, and WebGL 2 shaders may use either. + * + * @returns {String} The GLSL version used by the shader. + */ + version() { + const match = /#version (.+)$/.exec(this.vertSrc()); + if (match) { + return match[1]; + } else { + return '100 es'; + } + } + + vertSrc() { + return this.shaderSrc(this._vertSrc, 'vertex'); + } + + fragSrc() { + return this.shaderSrc(this._fragSrc, 'fragment'); + } + + /** + * Logs the hooks available in this shader, and their current implementation. + * + * Each shader may let you override bits of its behavior. Each bit is called + * a *hook.* A hook is either for the *vertex* shader, if it affects the + * position of vertices, or in the *fragment* shader, if it affects the pixel + * color. This method logs those values to the console, letting you know what + * you are able to use in a call to + * `modify()`. + * + * For example, this shader will produce the following output: + * + * ```js + * myShader = materialShader().modify({ + * declarations: 'uniform float time;', + * 'vec3 getWorldPosition': `(vec3 pos) { + * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); + * return pos; + * }` + * }); + * myShader.inspectHooks(); + * ``` + * + * ``` + * ==== Vertex shader hooks: ==== + * void beforeVertex() {} + * vec3 getLocalPosition(vec3 position) { return position; } + * [MODIFIED] vec3 getWorldPosition(vec3 pos) { + * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); + * return pos; + * } + * vec3 getLocalNormal(vec3 normal) { return normal; } + * vec3 getWorldNormal(vec3 normal) { return normal; } + * vec2 getUV(vec2 uv) { return uv; } + * vec4 getVertexColor(vec4 color) { return color; } + * void afterVertex() {} + * + * ==== Fragment shader hooks: ==== + * void beforeFragment() {} + * Inputs getPixelInputs(Inputs inputs) { return inputs; } + * vec4 combineColors(ColorComponents components) { + * vec4 color = vec4(0.); + * color.rgb += components.diffuse * components.baseColor; + * color.rgb += components.ambient * components.ambientColor; + * color.rgb += components.specular * components.specularColor; + * color.rgb += components.emissive; + * color.a = components.opacity; + * return color; + * } + * vec4 getFinalColor(vec4 color) { return color; } + * void afterFragment() {} + * ``` + * + * @beta + */ + inspectHooks() { + console.log('==== Vertex shader hooks: ===='); + for (const key in this.hooks.vertex) { + console.log( + (this.hooks.modified.vertex[key] ? '[MODIFIED] ' : '') + + key + + this.hooks.vertex[key] + ); + } + console.log(''); + console.log('==== Fragment shader hooks: ===='); + for (const key in this.hooks.fragment) { + console.log( + (this.hooks.modified.fragment[key] ? '[MODIFIED] ' : '') + + key + + this.hooks.fragment[key] + ); + } + console.log(''); + console.log('==== Helper functions: ===='); + for (const key in this.hooks.helpers) { + console.log( + key + + this.hooks.helpers[key] + ); + } + } + + /** + * Returns a new shader, based on the original, but with custom snippets + * of shader code replacing default behaviour. + * + * Each shader may let you override bits of its behavior. Each bit is called + * a *hook.* A hook is either for the *vertex* shader, if it affects the + * position of vertices, or in the *fragment* shader, if it affects the pixel + * color. You can inspect the different hooks available by calling + * `yourShader.inspectHooks()`. You can + * also read the reference for the default material, normal material, color, line, and point shaders to + * see what hooks they have available. + * + * `modify()` takes one parameter, `hooks`, an object with the hooks you want + * to override. Each key of the `hooks` object is the name + * of a hook, and the value is a string with the GLSL code for your hook. + * + * If you supply functions that aren't existing hooks, they will get added at the start of + * the shader as helper functions so that you can use them in your hooks. + * + * To add new uniforms to your shader, you can pass in a `uniforms` object containing + * the type and name of the uniform as the key, and a default value or function returning + * a default value as its value. These will be automatically set when the shader is set + * with `shader(yourShader)`. + * + * You can also add a `declarations` key, where the value is a GLSL string declaring + * custom uniform variables, globals, and functions shared + * between hooks. To add declarations just in a vertex or fragment shader, add + * `vertexDeclarations` and `fragmentDeclarations` keys. + * + * @beta + * @param {Object} [hooks] The hooks in the shader to replace. + * @returns {p5.Shader} + * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = materialShader().modify({ + * uniforms: { + * 'float time': () => millis() + * }, + * 'vec3 getWorldPosition': `(vec3 pos) { + * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); + * return pos; + * }` + * }); + * } + * + * function draw() { + * background(255); + * shader(myShader); + * lights(); + * noStroke(); + * fill('red'); + * sphere(50); + * } + * + *
+ * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = materialShader().modify({ + * // Manually specifying a uniform + * declarations: 'uniform float time;', + * 'vec3 getWorldPosition': `(vec3 pos) { + * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); + * return pos; + * }` + * }); + * } + * + * function draw() { + * background(255); + * shader(myShader); + * myShader.setUniform('time', millis()); + * lights(); + * noStroke(); + * fill('red'); + * sphere(50); + * } + * + *
+ */ + modify(hooks) { + p5._validateParameters('p5.Shader.modify', arguments); + const newHooks = { + vertex: {}, + fragment: {}, + helpers: {} + }; + for (const key in hooks) { + if (key === 'declarations') continue; + if (key === 'uniforms') continue; + if (key === 'vertexDeclarations') { + newHooks.vertex.declarations = + (newHooks.vertex.declarations || '') + '\n' + hooks[key]; + } else if (key === 'fragmentDeclarations') { + newHooks.fragment.declarations = + (newHooks.fragment.declarations || '') + '\n' + hooks[key]; + } else if (this.hooks.vertex[key]) { + newHooks.vertex[key] = hooks[key]; + } else if (this.hooks.fragment[key]) { + newHooks.fragment[key] = hooks[key]; + } else { + newHooks.helpers[key] = hooks[key]; + } + } + const modifiedVertex = Object.assign({}, this.hooks.modified.vertex); + const modifiedFragment = Object.assign({}, this.hooks.modified.fragment); + for (const key in newHooks.vertex || {}) { + if (key === 'declarations') continue; + modifiedVertex[key] = true; + } + for (const key in newHooks.fragment || {}) { + if (key === 'declarations') continue; + modifiedFragment[key] = true; + } + + return new p5.Shader(this._renderer, this._vertSrc, this._fragSrc, { + declarations: + (this.hooks.declarations || '') + '\n' + (hooks.declarations || ''), + uniforms: Object.assign({}, this.hooks.uniforms, hooks.uniforms || {}), + fragment: Object.assign({}, this.hooks.fragment, newHooks.fragment || {}), + vertex: Object.assign({}, this.hooks.vertex, newHooks.vertex || {}), + helpers: Object.assign({}, this.hooks.helpers, newHooks.helpers || {}), + modified: { + vertex: modifiedVertex, + fragment: modifiedFragment + } + }); } /** @@ -162,29 +500,35 @@ p5.Shader = class Shader { // 3. linking the vertex and fragment shaders this._vertShader = gl.createShader(gl.VERTEX_SHADER); //load in our default vertex shader - gl.shaderSource(this._vertShader, this._vertSrc); + gl.shaderSource(this._vertShader, this.vertSrc()); gl.compileShader(this._vertShader); // if our vertex shader failed compilation? if (!gl.getShaderParameter(this._vertShader, gl.COMPILE_STATUS)) { - p5._friendlyError( - `Yikes! An error occurred compiling the vertex shader:${gl.getShaderInfoLog( - this._vertShader - )}` - ); + const glError = gl.getShaderInfoLog(this._vertShader); + if (typeof IS_MINIFIED !== 'undefined') { + console.error(glError); + } else { + p5._friendlyError( + `Yikes! An error occurred compiling the vertex shader:${glError}` + ); + } return null; } this._fragShader = gl.createShader(gl.FRAGMENT_SHADER); //load in our material frag shader - gl.shaderSource(this._fragShader, this._fragSrc); + gl.shaderSource(this._fragShader, this.fragSrc()); gl.compileShader(this._fragShader); // if our frag shader failed compilation? if (!gl.getShaderParameter(this._fragShader, gl.COMPILE_STATUS)) { - p5._friendlyError( - `Darn! An error occurred compiling the fragment shader:${gl.getShaderInfoLog( - this._fragShader - )}` - ); + const glError = gl.getShaderInfoLog(this._fragShader); + if (typeof IS_MINIFIED !== 'undefined') { + console.error(glError); + } else { + p5._friendlyError( + `Darn! An error occurred compiling the fragment shader:${glError}` + ); + } return null; } @@ -206,6 +550,26 @@ p5.Shader = class Shader { return this; } + /** + * @private + */ + setDefaultUniforms() { + for (const key in this.hooks.uniforms) { + const [, name] = key.split(' '); + const initializer = this.hooks.uniforms[key]; + let value; + if (initializer instanceof Function) { + value = initializer(); + } else { + value = initializer; + } + + if (value !== undefined && value !== null) { + this.setUniform(name, value); + } + } + } + /** * Copies the shader from one drawing context to another. * @@ -580,7 +944,10 @@ p5.Shader = class Shader { modelViewProjectionMatrix.mult(projectionMatrix); if (this.isStrokeShader()) { - this.setUniform('uPerspective', this._renderer._curCamera.useLinePerspective ? 1 : 0); + this.setUniform( + 'uPerspective', + this._renderer._curCamera.useLinePerspective ? 1 : 0 + ); } this.setUniform('uViewMatrix', viewMatrix.mat4); this.setUniform('uProjectionMatrix', projectionMatrix.mat4); @@ -973,18 +1340,18 @@ p5.Shader = class Shader { isLightShader() { return [ - this.attributes.aNormal , - this.uniforms.uUseLighting , - this.uniforms.uAmbientLightCount , - this.uniforms.uDirectionalLightCount , - this.uniforms.uPointLightCount , - this.uniforms.uAmbientColor , - this.uniforms.uDirectionalDiffuseColors , - this.uniforms.uDirectionalSpecularColors , - this.uniforms.uPointLightLocation , - this.uniforms.uPointLightDiffuseColors , - this.uniforms.uPointLightSpecularColors , - this.uniforms.uLightingDirection , + this.attributes.aNormal, + this.uniforms.uUseLighting, + this.uniforms.uAmbientLightCount, + this.uniforms.uDirectionalLightCount, + this.uniforms.uPointLightCount, + this.uniforms.uAmbientColor, + this.uniforms.uDirectionalDiffuseColors, + this.uniforms.uDirectionalSpecularColors, + this.uniforms.uPointLightLocation, + this.uniforms.uPointLightDiffuseColors, + this.uniforms.uPointLightSpecularColors, + this.uniforms.uLightingDirection, this.uniforms.uSpecular ].some(x => x !== undefined); } diff --git a/src/webgl/shaders/basic.frag b/src/webgl/shaders/basic.frag index 11b14ea09c..e583955d36 100644 --- a/src/webgl/shaders/basic.frag +++ b/src/webgl/shaders/basic.frag @@ -1,4 +1,6 @@ IN vec4 vColor; void main(void) { - OUT_COLOR = vec4(vColor.rgb, 1.) * vColor.a; + HOOK_beforeFragment(); + OUT_COLOR = HOOK_getFinalColor(vec4(vColor.rgb, 1.) * vColor.a); + HOOK_afterFragment(); } diff --git a/src/webgl/shaders/lighting.glsl b/src/webgl/shaders/lighting.glsl index f95dd92733..b66ac083d1 100644 --- a/src/webgl/shaders/lighting.glsl +++ b/src/webgl/shaders/lighting.glsl @@ -30,7 +30,7 @@ uniform vec3 uSpotLightDirection[5]; uniform bool uSpecular; uniform float uShininess; -uniform float metallic; +uniform float uMetallic; uniform float uConstantAttenuation; uniform float uLinearAttenuation; @@ -43,8 +43,6 @@ uniform bool uUseImageLight; uniform sampler2D environmentMapDiffused; // texture for use in calculateImageSpecular uniform sampler2D environmentMapSpecular; -// roughness for use in calculateImageSpecular -uniform float levelOfDetail; const float specularFactor = 2.0; const float diffuseFactor = 0.73; @@ -68,7 +66,7 @@ float _lambertDiffuse(vec3 lightDirection, vec3 surfaceNormal) { return max(0.0, dot(-lightDirection, surfaceNormal)); } -LightResult _light(vec3 viewDirection, vec3 normal, vec3 lightVector) { +LightResult _light(vec3 viewDirection, vec3 normal, vec3 lightVector, float shininess, float metallic) { vec3 lightDir = normalize(lightVector); @@ -77,7 +75,7 @@ LightResult _light(vec3 viewDirection, vec3 normal, vec3 lightVector) { float specularIntensity = mix(1.0, 0.4, metallic); float diffuseIntensity = mix(1.0, 0.1, metallic); if (uSpecular) - lr.specular = (_phongSpecular(lightDir, viewDirection, normal, uShininess)) * specularIntensity; + lr.specular = (_phongSpecular(lightDir, viewDirection, normal, shininess)) * specularIntensity; lr.diffuse = _lambertDiffuse(lightDir, normal) * diffuseIntensity; return lr; } @@ -109,7 +107,7 @@ vec2 mapTextureToNormal( vec3 v ){ } -vec3 calculateImageDiffuse( vec3 vNormal, vec3 vViewPosition ){ +vec3 calculateImageDiffuse(vec3 vNormal, vec3 vViewPosition, float metallic){ // make 2 seperate builds vec3 worldCameraPosition = vec3(0.0, 0.0, 0.0); // hardcoded world camera position vec3 worldNormal = normalize(vNormal * uCameraRotation); @@ -120,14 +118,21 @@ vec3 calculateImageDiffuse( vec3 vNormal, vec3 vViewPosition ){ return mix(smoothstep(vec3(0.0), vec3(1.0), texture.xyz), vec3(0.0), metallic); } -vec3 calculateImageSpecular( vec3 vNormal, vec3 vViewPosition ){ +vec3 calculateImageSpecular(vec3 vNormal, vec3 vViewPosition, float shininess, float metallic){ vec3 worldCameraPosition = vec3(0.0, 0.0, 0.0); vec3 worldNormal = normalize(vNormal); vec3 lightDirection = normalize( vViewPosition - worldCameraPosition ); vec3 R = reflect(lightDirection, worldNormal) * uCameraRotation; vec2 newTexCoor = mapTextureToNormal( R ); #ifdef WEBGL2 - vec4 outColor = textureLod(environmentMapSpecular, newTexCoor, levelOfDetail); + // In p5js the range of shininess is >= 1, + // Therefore roughness range will be ([0,1]*8)*20 or [0, 160] + // The factor of 8 is because currently the getSpecularTexture + // only calculated 8 different levels of roughness + // The factor of 20 is just to spread up this range so that, + // [1, max] of shininess is converted to [0,160] of roughness + float roughness = 20. / shininess; + vec4 outColor = textureLod(environmentMapSpecular, newTexCoor, roughness * 8.); #else vec4 outColor = TEXTURE(environmentMapSpecular, newTexCoor); #endif @@ -143,6 +148,8 @@ vec3 calculateImageSpecular( vec3 vNormal, vec3 vViewPosition ){ void totalLight( vec3 modelPosition, vec3 normal, + float shininess, + float metallic, out vec3 totalDiffuse, out vec3 totalSpecular ) { @@ -163,7 +170,7 @@ void totalLight( vec3 lightVector = (uViewMatrix * vec4(uLightingDirection[j], 0.0)).xyz; vec3 lightColor = uDirectionalDiffuseColors[j]; vec3 specularColor = uDirectionalSpecularColors[j]; - LightResult result = _light(viewDirection, normal, lightVector); + LightResult result = _light(viewDirection, normal, lightVector, shininess, metallic); totalDiffuse += result.diffuse * lightColor; totalSpecular += result.specular * lightColor * specularColor; } @@ -177,7 +184,7 @@ void totalLight( vec3 lightColor = lightFalloff * uPointLightDiffuseColors[j]; vec3 specularColor = lightFalloff * uPointLightSpecularColors[j]; - LightResult result = _light(viewDirection, normal, lightVector); + LightResult result = _light(viewDirection, normal, lightVector, shininess, metallic); totalDiffuse += result.diffuse * lightColor; totalSpecular += result.specular * lightColor * specularColor; } @@ -203,7 +210,7 @@ void totalLight( vec3 lightColor = uSpotLightDiffuseColors[j]; vec3 specularColor = uSpotLightSpecularColors[j]; - LightResult result = _light(viewDirection, normal, lightVector); + LightResult result = _light(viewDirection, normal, lightVector, shininess, metallic); totalDiffuse += result.diffuse * lightColor * lightFalloff; totalSpecular += result.specular * lightColor * specularColor * lightFalloff; @@ -211,8 +218,8 @@ void totalLight( } if( uUseImageLight ){ - totalDiffuse += calculateImageDiffuse(normal, modelPosition); - totalSpecular += calculateImageSpecular(normal, modelPosition); + totalDiffuse += calculateImageDiffuse(normal, modelPosition, metallic); + totalSpecular += calculateImageSpecular(normal, modelPosition, shininess, metallic); } totalDiffuse *= diffuseFactor; diff --git a/src/webgl/shaders/line.frag b/src/webgl/shaders/line.frag index 42c24edcff..f4b5a5c40b 100644 --- a/src/webgl/shaders/line.frag +++ b/src/webgl/shaders/line.frag @@ -3,12 +3,12 @@ precision mediump int; uniform vec4 uMaterialColor; uniform int uStrokeCap; uniform int uStrokeJoin; -uniform float uStrokeWeight; IN vec4 vColor; IN vec2 vTangent; IN vec2 vCenter; IN vec2 vPosition; +IN float vStrokeWeight; IN float vMaxDist; IN float vCap; IN float vJoin; @@ -18,33 +18,56 @@ float distSquared(vec2 a, vec2 b) { return dot(aToB, aToB); } +struct Inputs { + vec4 color; + vec2 tangent; + vec2 center; + vec2 position; + float strokeWeight; +}; + void main() { + HOOK_beforeFragment(); + + Inputs inputs; + inputs.color = vColor; + inputs.tangent = vTangent; + inputs.center = vCenter; + inputs.position = vPosition; + inputs.strokeWeight = vStrokeWeight; + inputs = HOOK_getPixelInputs(inputs); + if (vCap > 0.) { if ( uStrokeCap == STROKE_CAP_ROUND && - distSquared(vPosition, vCenter) > uStrokeWeight * uStrokeWeight * 0.25 + HOOK_shouldDiscard(distSquared(inputs.position, inputs.center) > inputs.strokeWeight * inputs.strokeWeight * 0.25) ) { discard; } else if ( uStrokeCap == STROKE_CAP_SQUARE && - dot(vPosition - vCenter, vTangent) > 0. + HOOK_shouldDiscard(dot(inputs.position - inputs.center, inputs.tangent) > 0.) ) { discard; - } // Use full area for PROJECT + } else if (HOOK_shouldDiscard(false)) { + discard; + } } else if (vJoin > 0.) { if ( uStrokeJoin == STROKE_JOIN_ROUND && - distSquared(vPosition, vCenter) > uStrokeWeight * uStrokeWeight * 0.25 + HOOK_shouldDiscard(distSquared(inputs.position, inputs.center) > inputs.strokeWeight * inputs.strokeWeight * 0.25) ) { discard; } else if (uStrokeJoin == STROKE_JOIN_BEVEL) { - vec2 normal = vec2(-vTangent.y, vTangent.x); - if (abs(dot(vPosition - vCenter, normal)) > vMaxDist) { + vec2 normal = vec2(-inputs.tangent.y, inputs.tangent.x); + if (HOOK_shouldDiscard(abs(dot(inputs.position - inputs.center, normal)) > vMaxDist)) { discard; } - } // Use full area for MITER + } else if (HOOK_shouldDiscard(false)) { + discard; + } } - OUT_COLOR = vec4(vColor.rgb, 1.) * vColor.a; + OUT_COLOR = HOOK_getFinalColor(vec4(inputs.color.rgb, 1.) * inputs.color.a); + HOOK_afterFragment(); } diff --git a/src/webgl/shaders/line.vert b/src/webgl/shaders/line.vert index e9fc624bfc..6758cfa54a 100644 --- a/src/webgl/shaders/line.vert +++ b/src/webgl/shaders/line.vert @@ -44,6 +44,7 @@ OUT vec2 vPosition; OUT float vMaxDist; OUT float vCap; OUT float vJoin; +OUT float vStrokeWeight; vec2 lineIntersection(vec2 aPoint, vec2 aDir, vec2 bPoint, vec2 bDir) { // Rotate and translate so a starts at the origin and goes out to the right @@ -64,6 +65,7 @@ vec2 lineIntersection(vec2 aPoint, vec2 aDir, vec2 bPoint, vec2 bDir) { } void main() { + HOOK_beforeVertex(); // Caps have one of either the in or out tangent set to 0 vCap = (aTangentIn == vec3(0.)) != (aTangentOut == (vec3(0.))) ? 1. : 0.; @@ -75,9 +77,12 @@ void main() { aTangentIn != aTangentOut ) ? 1. : 0.; - vec4 posp = uModelViewMatrix * aPosition; - vec4 posqIn = uModelViewMatrix * (aPosition + vec4(aTangentIn, 0)); - vec4 posqOut = uModelViewMatrix * (aPosition + vec4(aTangentOut, 0)); + vec4 localPosition = vec4(HOOK_getLocalPosition(aPosition.xyz), 1.); + vec4 posp = vec4(HOOK_getWorldPosition((uModelViewMatrix * localPosition).xyz), 1.); + vec4 posqIn = posp + uModelViewMatrix * vec4(aTangentIn, 0); + vec4 posqOut = posp + uModelViewMatrix * vec4(aTangentOut, 0); + float strokeWeight = HOOK_getStrokeWeight(uStrokeWeight); + vStrokeWeight = strokeWeight; float facingCamera = pow( // The word space tangent's z value is 0 if it's facing the camera @@ -101,7 +106,7 @@ void main() { vec4 p = uProjectionMatrix * posp; vec4 qIn = uProjectionMatrix * posqIn; vec4 qOut = uProjectionMatrix * posqOut; - vCenter = p.xy; + vCenter = HOOK_getLineCenter(p.xy); // formula to convert from clip space (range -1..1) to screen space (range 0..[width or height]) // screen_p = (p.xy/p.w + <1,1>) * 0.5 * uViewport.zw @@ -166,9 +171,9 @@ void main() { // find where the lines intersect to find the elbow of the join vec2 c = (posp.xy/posp.w + vec2(1.,1.)) * 0.5 * uViewport.zw; vec2 intersection = lineIntersection( - c + (side * normalIn * uStrokeWeight / 2.), + c + (side * normalIn * strokeWeight / 2.), tangentIn, - c + (side * normalOut * uStrokeWeight / 2.), + c + (side * normalOut * strokeWeight / 2.), tangentOut ); offset = (intersection - c); @@ -178,21 +183,21 @@ void main() { // the magnitude to avoid lines going across the whole screen when this // happens. float mag = length(offset); - float maxMag = 3. * uStrokeWeight; + float maxMag = 3. * strokeWeight; if (mag > maxMag) { offset *= maxMag / mag; } } else if (sideEnum == 1.) { - offset = side * normalIn * uStrokeWeight / 2.; + offset = side * normalIn * strokeWeight / 2.; } else if (sideEnum == 3.) { - offset = side * normalOut * uStrokeWeight / 2.; + offset = side * normalOut * strokeWeight / 2.; } } if (uStrokeJoin == STROKE_JOIN_BEVEL) { vec2 avgNormal = vec2(-vTangent.y, vTangent.x); - vMaxDist = abs(dot(avgNormal, normalIn * uStrokeWeight / 2.)); + vMaxDist = abs(dot(avgNormal, normalIn * strokeWeight / 2.)); } else { - vMaxDist = uStrokeWeight / 2.; + vMaxDist = strokeWeight / 2.; } } else { vec2 tangent = aTangentIn == vec3(0.) ? tangentOut : tangentIn; @@ -204,13 +209,14 @@ void main() { // extends out from the line float tangentOffset = abs(aSide) - 1.; offset = (normal * normalOffset + tangent * tangentOffset) * - uStrokeWeight * 0.5; - vMaxDist = uStrokeWeight / 2.; + strokeWeight * 0.5; + vMaxDist = strokeWeight / 2.; } - vPosition = vCenter + offset; + vPosition = HOOK_getLinePosition(vCenter + offset); gl_Position.xy = p.xy + offset.xy * curPerspScale; gl_Position.zw = p.zw; - vColor = (uUseLineColor ? aVertexColor : uMaterialColor); + vColor = HOOK_getVertexColor(uUseLineColor ? aVertexColor : uMaterialColor); + HOOK_afterVertex(); } diff --git a/src/webgl/shaders/normal.frag b/src/webgl/shaders/normal.frag index 6b0e370158..0cb362265a 100644 --- a/src/webgl/shaders/normal.frag +++ b/src/webgl/shaders/normal.frag @@ -1,4 +1,6 @@ IN vec3 vVertexNormal; void main(void) { - OUT_COLOR = vec4(vVertexNormal, 1.0); + HOOK_beforeFragment(); + OUT_COLOR = HOOK_getFinalColor(vec4(vVertexNormal, 1.0)); + HOOK_afterFragment(); } diff --git a/src/webgl/shaders/normal.vert b/src/webgl/shaders/normal.vert index 818d6b74ce..63922714b4 100644 --- a/src/webgl/shaders/normal.vert +++ b/src/webgl/shaders/normal.vert @@ -15,9 +15,15 @@ OUT highp vec2 vVertTexCoord; OUT vec4 vColor; void main(void) { - vec4 positionVec4 = vec4(aPosition, 1.0); - gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; - vVertexNormal = normalize(vec3( uNormalMatrix * aNormal )); - vVertTexCoord = aTexCoord; - vColor = ((uUseVertexColor && aVertexColor.x >= 0.0) ? aVertexColor : uMaterialColor); + HOOK_beforeVertex(); + vec4 positionVec4 = vec4(HOOK_getWorldPosition( + (uModelViewMatrix * vec4(HOOK_getLocalPosition(aPosition), 1.0)).xyz + ), 1.); + + gl_Position = uProjectionMatrix * positionVec4; + + vVertexNormal = HOOK_getWorldNormal(normalize(uNormalMatrix * HOOK_getLocalNormal(aNormal))); + vVertTexCoord = HOOK_getUV(aTexCoord); + vColor = HOOK_getVertexColor((uUseVertexColor && aVertexColor.x >= 0.0) ? aVertexColor : uMaterialColor); + HOOK_afterVertex(); } diff --git a/src/webgl/shaders/phong.frag b/src/webgl/shaders/phong.frag index c22531087d..e141f62f1f 100644 --- a/src/webgl/shaders/phong.frag +++ b/src/webgl/shaders/phong.frag @@ -16,26 +16,68 @@ IN vec3 vViewPosition; IN vec3 vAmbientColor; IN vec4 vColor; +struct ColorComponents { + vec3 baseColor; + float opacity; + vec3 ambientColor; + vec3 specularColor; + vec3 diffuse; + vec3 ambient; + vec3 specular; + vec3 emissive; +}; + +struct Inputs { + vec3 normal; + vec2 texCoord; + vec3 ambientLight; + vec3 ambientMaterial; + vec3 specularMaterial; + vec3 emissiveMaterial; + vec4 color; + float shininess; + float metalness; +}; + void main(void) { + HOOK_beforeFragment(); + + Inputs inputs; + inputs.normal = normalize(vNormal); + inputs.texCoord = vTexCoord; + inputs.ambientLight = vAmbientColor; + inputs.color = isTexture + // Textures come in with premultiplied alpha. To apply tint and still have + // premultiplied alpha output, we need to multiply the RGB channels by the + // tint RGB, and all channels by the tint alpha. + ? TEXTURE(uSampler, vTexCoord) * vec4(uTint.rgb/255., 1.) * (uTint.a/255.) + // Colors come in with unmultiplied alpha, so we need to multiply the RGB + // channels by alpha to convert it to premultiplied alpha. + : vec4(vColor.rgb * vColor.a, vColor.a); + inputs.shininess = uShininess; + inputs.metalness = uMetallic; + inputs.ambientMaterial = uHasSetAmbient ? uAmbientMatColor.rgb : inputs.color.rgb; + inputs.specularMaterial = uSpecularMatColor.rgb; + inputs.emissiveMaterial = uEmissiveMatColor.rgb; + inputs = HOOK_getPixelInputs(inputs); vec3 diffuse; vec3 specular; - totalLight(vViewPosition, normalize(vNormal), diffuse, specular); + totalLight(vViewPosition, inputs.normal, inputs.shininess, inputs.metalness, diffuse, specular); // Calculating final color as result of all lights (plus emissive term). - vec4 baseColor = isTexture - // Textures come in with premultiplied alpha. To apply tint and still have - // premultiplied alpha output, we need to multiply the RGB channels by the - // tint RGB, and all channels by the tint alpha. - ? TEXTURE(uSampler, vTexCoord) * vec4(uTint.rgb/255., 1.) * (uTint.a/255.) - // Colors come in with unmultiplied alpha, so we need to multiply the RGB - // channels by alpha to convert it to premultiplied alpha. - : vec4(vColor.rgb * vColor.a, vColor.a); - OUT_COLOR = vec4(diffuse * baseColor.rgb + - vAmbientColor * ( - uHasSetAmbient ? uAmbientMatColor.rgb : baseColor.rgb - ) + - specular * uSpecularMatColor.rgb + - uEmissiveMatColor.rgb, baseColor.a); + vec2 texCoord = inputs.texCoord; + vec4 baseColor = inputs.color; + ColorComponents c; + c.opacity = baseColor.a; + c.baseColor = baseColor.rgb; + c.ambientColor = inputs.ambientMaterial; + c.specularColor = inputs.specularMaterial; + c.diffuse = diffuse; + c.ambient = inputs.ambientLight; + c.specular = specular; + c.emissive = inputs.emissiveMaterial; + OUT_COLOR = HOOK_getFinalColor(HOOK_combineColors(c)); + HOOK_afterFragment(); } diff --git a/src/webgl/shaders/phong.vert b/src/webgl/shaders/phong.vert index 29e939395d..0576ccd304 100644 --- a/src/webgl/shaders/phong.vert +++ b/src/webgl/shaders/phong.vert @@ -22,15 +22,17 @@ OUT vec3 vAmbientColor; OUT vec4 vColor; void main(void) { - - vec4 viewModelPosition = uModelViewMatrix * vec4(aPosition, 1.0); + HOOK_beforeVertex(); + vec4 viewModelPosition = vec4(HOOK_getWorldPosition( + (uModelViewMatrix * vec4(HOOK_getLocalPosition(aPosition), 1.0)).xyz + ), 1.); // Pass varyings to fragment shader vViewPosition = viewModelPosition.xyz; gl_Position = uProjectionMatrix * viewModelPosition; - vNormal = uNormalMatrix * aNormal; - vTexCoord = aTexCoord; + vNormal = HOOK_getWorldNormal(uNormalMatrix * HOOK_getLocalNormal(aNormal)); + vTexCoord = HOOK_getUV(aTexCoord); // TODO: this should be a uniform vAmbientColor = vec3(0.0); @@ -40,5 +42,6 @@ void main(void) { } } - vColor = ((uUseVertexColor && aVertexColor.x >= 0.0) ? aVertexColor : uMaterialColor); + vColor = HOOK_getVertexColor(((uUseVertexColor && aVertexColor.x >= 0.0) ? aVertexColor : uMaterialColor)); + HOOK_afterVertex(); } diff --git a/src/webgl/shaders/point.frag b/src/webgl/shaders/point.frag index 5185794d37..d87cbf0c61 100644 --- a/src/webgl/shaders/point.frag +++ b/src/webgl/shaders/point.frag @@ -3,6 +3,7 @@ uniform vec4 uMaterialColor; IN float vStrokeWeight; void main(){ + HOOK_beforeFragment(); float mask = 0.0; // make a circular mask using the gl_PointCoord (goes from 0 - 1 on a point) @@ -19,9 +20,10 @@ void main(){ // throw away the borders of the mask // otherwise we get weird alpha blending issues - if(mask > 0.98){ + if(HOOK_shouldDiscard(mask > 0.98)){ discard; } - OUT_COLOR = vec4(uMaterialColor.rgb, 1.) * uMaterialColor.a; + OUT_COLOR = HOOK_getFinalColor(vec4(uMaterialColor.rgb, 1.) * uMaterialColor.a); + HOOK_afterFragment(); } diff --git a/src/webgl/shaders/point.vert b/src/webgl/shaders/point.vert index 9df67d1588..6eeb741a64 100644 --- a/src/webgl/shaders/point.vert +++ b/src/webgl/shaders/point.vert @@ -3,9 +3,17 @@ uniform float uPointSize; OUT float vStrokeWeight; uniform mat4 uModelViewMatrix; uniform mat4 uProjectionMatrix; + void main() { - vec4 positionVec4 = vec4(aPosition, 1.0); - gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; - gl_PointSize = uPointSize; - vStrokeWeight = uPointSize; + HOOK_beforeVertex(); + vec4 viewModelPosition = vec4(HOOK_getWorldPosition( + (uModelViewMatrix * vec4(HOOK_getLocalPosition(aPosition), 1.0)).xyz + ), 1.); + gl_Position = uProjectionMatrix * viewModelPosition; + + float pointSize = HOOK_getPointSize(uPointSize); + + gl_PointSize = pointSize; + vStrokeWeight = pointSize; + HOOK_afterVertex(); } diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index f952a60da7..aac2f85241 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -305,5 +305,83 @@ suite('p5.Shader', function() { myp5.shader(s); assert.isFalse(s.isStrokeShader()); }); + + suite('Hooks', function() { + let myShader; + + beforeEach(function() { + myShader = myp5.createShader( + ` + precision highp float; + + attribute vec3 aPosition; + attribute vec2 aTexCoord; + attribute vec4 aVertexColor; + + uniform mat4 uModelViewMatrix; + uniform mat4 uProjectionMatrix; + + varying vec2 vTexCoord; + varying vec4 vVertexColor; + + void main() { + // Apply the camera transform + vec4 viewModelPosition = + uModelViewMatrix * + vec4(aPosition, 1.0); + + // Tell WebGL where the vertex goes + gl_Position = + uProjectionMatrix * + viewModelPosition; + + // Pass along data to the fragment shader + vTexCoord = aTexCoord; + vVertexColor = aVertexColor; + } + `, + ` + precision highp float; + + varying vec2 vTexCoord; + varying vec4 vVertexColor; + + void main() { + // Tell WebGL what color to make the pixel + gl_FragColor = HOOK_getVertexColor(vVertexColor); + } + `, + { + fragment: { + 'vec4 getVertexColor': '(vec4 color) { return color; }' + } + } + ); + }); + + test('available hooks show up in inspectHooks()', function() { + const logs = []; + const myLog = (...data) => logs.push(data.join(', ')); + const oldLog = console.log; + console.log = myLog; + myShader.inspectHooks(); + console.log = oldLog; + expect(logs.join('\n')).to.match(/vec4 getVertexColor/); + }); + + test('unfilled hooks do not have an AUGMENTED_HOOK define', function() { + const modified = myShader.modify({}); + expect(modified.fragSrc()).not.to.match(/#define AUGMENTED_HOOK_getVertexColor/); + }); + + test('filled hooks do have an AUGMENTED_HOOK define', function() { + const modified = myShader.modify({ + 'vec4 getVertexColor': `(vec4 c) { + return vec4(1., 0., 0., 1.); + }` + }); + expect(modified.fragSrc()).to.match(/#define AUGMENTED_HOOK_getVertexColor/); + }); + }); }); });