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:
+ *
+ *
+ * Hook | Description |
+ *
+ *
+ * `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,
+ }
+ }
+ ]
+ });
+ });
+ });
});