diff --git a/src/image/filterRenderer2D.js b/src/image/filterRenderer2D.js index 3848b6ed76..243652d5f8 100644 --- a/src/image/filterRenderer2D.js +++ b/src/image/filterRenderer2D.js @@ -14,6 +14,11 @@ import filterThresholdFrag from '../webgl/shaders/filters/threshold.frag'; import filterShaderVert from '../webgl/shaders/filters/default.vert'; import { filterParamDefaults } from "./const"; + +import filterBaseFrag from "../webgl/shaders/filters/base.frag"; +import filterBaseVert from "../webgl/shaders/filters/base.vert"; +import webgl2CompatibilityShader from "../webgl/shaders/webgl2Compatibility.glsl"; + class FilterRenderer2D { /** * Creates a new FilterRenderer2D instance. @@ -27,7 +32,12 @@ class FilterRenderer2D { this.canvas.height = pInst.height; // Initialize the WebGL context - this.gl = this.canvas.getContext('webgl'); + let webglVersion = constants.WEBGL2; + this.gl = this.canvas.getContext('webgl2'); + if (!this.gl) { + webglVersion = constants.WEBGL; + this.gl = this.canvas.getContext('webgl'); + } if (!this.gl) { console.error("WebGL not supported, cannot apply filter."); return; @@ -38,7 +48,7 @@ class FilterRenderer2D { registerEnabled: new Set(), _curShader: null, _emptyTexture: null, - webglVersion: 'WEBGL', + webglVersion, states: { textureWrapX: this.gl.CLAMP_TO_EDGE, textureWrapY: this.gl.CLAMP_TO_EDGE, @@ -54,6 +64,8 @@ class FilterRenderer2D { }, }; + this._baseFilterShader = undefined; + // Store the fragment shader sources this.filterShaderSources = { [constants.BLUR]: filterBlurFrag, @@ -90,6 +102,45 @@ class FilterRenderer2D { this._bindBufferData(this.texcoordBuffer, this.gl.ARRAY_BUFFER, this.texcoords); } + _webGL2CompatibilityPrefix(shaderType, floatPrecision) { + let code = ""; + if (this._renderer.webglVersion === constants.WEBGL2) { + code += "#version 300 es\n#define WEBGL2\n"; + } + if (shaderType === "vert") { + code += "#define VERTEX_SHADER\n"; + } else if (shaderType === "frag") { + code += "#define FRAGMENT_SHADER\n"; + } + if (floatPrecision) { + code += `precision ${floatPrecision} float;\n`; + } + return code; + } + + baseFilterShader() { + if (!this._baseFilterShader) { + this._baseFilterShader = new Shader( + this._renderer, + this._webGL2CompatibilityPrefix("vert", "highp") + + webgl2CompatibilityShader + + filterBaseVert, + this._webGL2CompatibilityPrefix("frag", "highp") + + webgl2CompatibilityShader + + filterBaseFrag, + { + vertex: {}, + fragment: { + "vec4 getColor": `(FilterInputs inputs, in sampler2D canvasContent) { + return getTexture(canvasContent, inputs.texCoord); + }`, + }, + } + ); + } + return this._baseFilterShader; + } + /** * Set the current filter operation and parameter. If a customShader is provided, * that overrides the operation-based shader. diff --git a/src/webgl/material.js b/src/webgl/material.js index cfb27738b8..3e171ccc4b 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -1520,6 +1520,82 @@ function material(p5, fn){ 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. + * + *
+ * + * 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. + * + * @method baseFilterShader + * @beta + * @returns {p5.Shader} The filter shader + * + * @example + *
+ * + * let img; + * let myShader; + * + * async function setup() { + * img = await loadImage('assets/bricks.jpg'); + * createCanvas(100, 100, WEBGL); + * myShader = baseFilterShader().modify({ + * uniforms: { + * 'float time': () => millis() + * }, + * 'vec4 getColor': `( + * FilterInputs inputs, + * in sampler2D canvasContent + * ) { + * inputs.texCoord.y += + * 0.01 * sin(time * 0.001 + inputs.position.x * 5.0); + * return getTexture(canvasContent, inputs.texCoord); + * }` + * }); + * } + * + * function draw() { + * image(img, -50, -50); + * filter(myShader); + * describe('an image of bricks, distorting over time'); + * } + * + *
+ */ + fn.baseFilterShader = function() { + return (this._renderer.filterRenderer || this._renderer) + .baseFilterShader(); + }; + /** * Get the shader used by `normalMaterial()`. * diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 8e2f1e4317..20e2bb4d15 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -16,6 +16,7 @@ import { ShapeBuilder } from "./ShapeBuilder"; import { GeometryBufferCache } from "./GeometryBufferCache"; import { filterParamDefaults } from "../image/const"; +import filterBaseVert from "./shaders/filters/base.vert"; import lightingShader from "./shaders/lighting.glsl"; import webgl2CompatibilityShader from "./shaders/webgl2Compatibility.glsl"; import normalVert from "./shaders/normal.vert"; @@ -36,6 +37,7 @@ import imageLightVert from "./shaders/imageLight.vert"; import imageLightDiffusedFrag from "./shaders/imageLightDiffused.frag"; import imageLightSpecularFrag from "./shaders/imageLightSpecular.frag"; +import filterBaseFrag from "./shaders/filters/base.frag"; import filterGrayFrag from "./shaders/filters/gray.frag"; import filterErodeFrag from "./shaders/filters/erode.frag"; import filterDilateFrag from "./shaders/filters/dilate.frag"; @@ -87,6 +89,8 @@ const defaultShaders = { imageLightVert, imageLightDiffusedFrag, imageLightSpecularFrag, + filterBaseVert, + filterBaseFrag, }; let sphereMapping = defaultShaders.sphereMappingFrag; for (const key in defaultShaders) { @@ -302,6 +306,7 @@ class RendererGL extends Renderer { this.specularShader = undefined; this.sphereMapping = undefined; this.diffusedShader = undefined; + this._baseFilterShader = undefined; this._defaultLightShader = undefined; this._defaultImmediateModeShader = undefined; this._defaultNormalShader = undefined; @@ -2087,6 +2092,27 @@ class RendererGL extends Renderer { return this._defaultFontShader; } + baseFilterShader() { + if (!this._baseFilterShader) { + this._baseFilterShader = new Shader( + this, + this._webGL2CompatibilityPrefix("vert", "highp") + + defaultShaders.filterBaseVert, + this._webGL2CompatibilityPrefix("frag", "highp") + + defaultShaders.filterBaseFrag, + { + vertex: {}, + fragment: { + "vec4 getColor": `(FilterInputs inputs, in sampler2D canvasContent) { + return getTexture(canvasContent, inputs.texCoord); + }`, + }, + } + ); + } + return this._baseFilterShader; + } + _webGL2CompatibilityPrefix(shaderType, floatPrecision) { let code = ""; if (this.webglVersion === constants.WEBGL2) { diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index ff623eff25..0d45dd679f 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -52,6 +52,79 @@ class Shader { }; } + hookTypes(hookName) { + let fullSrc = this._vertSrc; + let body = this.hooks.vertex[hookName]; + if (!body) { + body = this.hooks.fragment[hookName]; + fullSrc = this._fragSrc; + } + if (!body) { + throw new Error(`Can't find hook ${hookName}!`); + } + const nameParts = hookName.split(/\s+/g); + const functionName = nameParts.pop(); + const returnType = nameParts.pop(); + const returnQualifiers = [...nameParts]; + + const parameterMatch = /\(([^\)]*)\)/.exec(body); + if (!parameterMatch) { + throw new Error(`Couldn't find function parameters in hook body:\n${body}`); + } + + const structProperties = structName => { + const structDefMatch = new RegExp(`struct\\s+${structName}\\s*\{([^\}]*)\}`).exec(fullSrc); + if (!structDefMatch) return undefined; + + const properties = []; + for (const defSrc of structDefMatch[1].split(';')) { + // E.g. `int var1, var2;` or `MyStruct prop;` + const parts = defSrc.trim().split(/\s+|,/g); + const typeName = parts.shift(); + const names = [...parts]; + const typeProperties = structProperties(typeName); + for (const name of names) { + properties.push({ + name, + type: { + typeName, + qualifiers: [], + properties: typeProperties, + }, + }); + } + } + return properties; + }; + + const parameters = parameterMatch[1].split(',').map(paramString => { + // e.g. `int prop` or `in sampler2D prop` or `const float prop` + const parts = paramString.trim().split(/\s+/g); + const name = parts.pop(); + const typeName = parts.pop(); + const qualifiers = [...parts]; + const properties = structProperties(typeName); + return { + name, + type: { + typeName, + qualifiers, + properties, + } + } + }); + + return { + name: functionName, + returnType: { + typeName: returnType, + qualifiers: returnQualifiers, + properties: structProperties(returnType) + }, + parameters + }; + } + shaderSrc(src, shaderType) { const main = 'void main'; let [preMain, postMain] = src.split(main); diff --git a/src/webgl/shaders/filters/base.frag b/src/webgl/shaders/filters/base.frag new file mode 100644 index 0000000000..316d0f75bd --- /dev/null +++ b/src/webgl/shaders/filters/base.frag @@ -0,0 +1,22 @@ +precision highp float; + +uniform sampler2D tex0; +uniform vec2 canvasSize; +uniform vec2 texelSize; + +IN vec2 vTexCoord; + +struct FilterInputs { + vec2 texCoord; + vec2 canvasSize; + vec2 texelSize; +}; + +void main(void) { + FilterInputs inputs; + inputs.texCoord = vTexCoord; + inputs.canvasSize = canvasSize; + inputs.texelSize = texelSize; + OUT_COLOR = HOOK_getColor(inputs, tex0); + OUT_COLOR.rgb *= outColor.a; +} diff --git a/src/webgl/shaders/filters/base.vert b/src/webgl/shaders/filters/base.vert new file mode 100644 index 0000000000..69ed4581ae --- /dev/null +++ b/src/webgl/shaders/filters/base.vert @@ -0,0 +1,19 @@ +precision highp int; + +uniform mat4 uModelViewMatrix; +uniform mat4 uProjectionMatrix; + +IN vec3 aPosition; +IN vec2 aTexCoord; +OUT vec2 vTexCoord; + +void main() { + // transferring texcoords for the frag shader + vTexCoord = aTexCoord; + + // copy position with a fourth coordinate for projection (1.0 is normal) + vec4 positionVec4 = vec4(aPosition, 1.0); + + // project to 3D space + gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; +} diff --git a/src/webgl/shaders/webgl2Compatibility.glsl b/src/webgl/shaders/webgl2Compatibility.glsl index 9aed0ef22c..9664e05a52 100644 --- a/src/webgl/shaders/webgl2Compatibility.glsl +++ b/src/webgl/shaders/webgl2Compatibility.glsl @@ -24,3 +24,11 @@ out vec4 outColor; #endif #endif + +#ifdef FRAGMENT_SHADER +vec4 getTexture(in sampler2D content, vec2 coord) { + vec4 color = TEXTURE(content, coord); + color.rgb /= color.a; + return color; +} +#endif diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index bf112aab2a..de50a71f99 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -71,6 +71,31 @@ visualSuite('WebGL', function() { } ); + for (const mode of ['webgl', '2d']) { + visualSuite(`In ${mode} mode`, function() { + visualTest('It can use filter shader hooks', function(p5, screenshot) { + p5.createCanvas(50, 50, mode === 'webgl' ? p5.WEBGL : p5.P2D); + + const s = p5.baseFilterShader().modify({ + 'vec4 getColor': `(FilterInputs inputs, in sampler2D content) { + vec4 c = getTexture(content, inputs.texCoord); + float avg = (c.r + c.g + c.b) / 3.0; + return vec4(avg, avg, avg, c.a); + }` + }); + + if (mode === 'webgl') p5.translate(-p5.width/2, -p5.height/2); + p5.background(255); + p5.fill('red'); + p5.noStroke(); + p5.circle(15, 15, 20); + p5.circle(30, 30, 20); + p5.filter(s); + screenshot(); + }); + }); + } + for (const mode of ['webgl', '2d']) { visualSuite(`In ${mode} mode`, function() { visualTest('It can combine multiple filter passes', function(p5, screenshot) { diff --git a/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can use filter shader hooks/000.png b/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can use filter shader hooks/000.png new file mode 100644 index 0000000000..78e625579c Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can use filter shader hooks/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can use filter shader hooks/metadata.json b/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can use filter shader hooks/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can use filter shader hooks/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can use filter shader hooks/000.png b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can use filter shader hooks/000.png new file mode 100644 index 0000000000..8ecd5c2bc4 Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can use filter shader hooks/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can use filter shader hooks/metadata.json b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can use filter shader hooks/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can use filter shader hooks/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 86e79b4615..567cf8acee 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -334,4 +334,61 @@ suite('p5.Shader', function() { }); }); }); + + suite('hookTypes', function() { + test('Produces expected types on baseFilterShader()', function() { + const types = myp5.baseFilterShader().hookTypes('vec4 getColor'); + assert.deepEqual(types, { + name: 'getColor', + returnType: { + typeName: 'vec4', + qualifiers: [], + properties: undefined, + }, + parameters: [ + { + name: 'inputs', + type: { + typeName: 'FilterInputs', + qualifiers: [], + properties: [ + { + name: 'texCoord', + type: { + typeName: 'vec2', + qualifiers: [], + properties: undefined, + } + }, + { + name: 'canvasSize', + type: { + typeName: 'vec2', + qualifiers: [], + properties: undefined, + } + }, + { + name: 'texelSize', + type: { + typeName: 'vec2', + qualifiers: [], + properties: undefined, + } + }, + ], + } + }, + { + name: 'canvasContent', + type: { + typeName: 'sampler2D', + qualifiers: ['in'], + properties: undefined, + } + } + ] + }); + }); + }); });