Skip to content

Add filter shader hooks #7582

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 53 additions & 2 deletions src/image/filterRenderer2D.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
Expand All @@ -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,
Expand All @@ -54,6 +64,8 @@ class FilterRenderer2D {
},
};

this._baseFilterShader = undefined;

// Store the fragment shader sources
this.filterShaderSources = {
[constants.BLUR]: filterBlurFrag,
Expand Down Expand Up @@ -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.
Expand Down
76 changes: 76 additions & 0 deletions src/webgl/material.js
Original file line number Diff line number Diff line change
Expand Up @@ -1520,6 +1520,82 @@ function material(p5, fn){
return this._renderer.baseMaterialShader();
};

/**
* Get the base shader for filters.
*
* You can then call <a href="#/p5.Shader/modify">`baseFilterShader().modify()`</a>
* and change the following hook:
*
* <table>
* <tr><th>Hook</th><th>Description</th></tr>
* <tr><td>
*
* `vec4 getColor`
*
* </td><td>
*
* 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.
*
* </td></tr>
* </table>
*
* 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
* <div modernizr='webgl'>
* <code>
* 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');
* }
* </code>
* </div>
*/
fn.baseFilterShader = function() {
return (this._renderer.filterRenderer || this._renderer)
.baseFilterShader();
};

/**
* Get the shader used by <a href="#/p5/normalMaterial">`normalMaterial()`</a>.
*
Expand Down
26 changes: 26 additions & 0 deletions src/webgl/p5.RendererGL.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -87,6 +89,8 @@ const defaultShaders = {
imageLightVert,
imageLightDiffusedFrag,
imageLightSpecularFrag,
filterBaseVert,
filterBaseFrag,
};
let sphereMapping = defaultShaders.sphereMappingFrag;
for (const key in defaultShaders) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
73 changes: 73 additions & 0 deletions src/webgl/p5.Shader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
22 changes: 22 additions & 0 deletions src/webgl/shaders/filters/base.frag
Original file line number Diff line number Diff line change
@@ -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;
}
19 changes: 19 additions & 0 deletions src/webgl/shaders/filters/base.vert
Original file line number Diff line number Diff line change
@@ -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;
}
8 changes: 8 additions & 0 deletions src/webgl/shaders/webgl2Compatibility.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading