Skip to content

Commit af60d1b

Browse files
committed
Add filter shader hooks
1 parent 63746a6 commit af60d1b

File tree

11 files changed

+235
-2
lines changed

11 files changed

+235
-2
lines changed

src/image/filterRenderer2D.js

+53-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ import filterThresholdFrag from '../webgl/shaders/filters/threshold.frag';
1414
import filterShaderVert from '../webgl/shaders/filters/default.vert';
1515
import { filterParamDefaults } from "./const";
1616

17+
18+
import filterBaseFrag from "../webgl/shaders/filters/base.frag";
19+
import filterBaseVert from "../webgl/shaders/filters/base.vert";
20+
import webgl2CompatibilityShader from "../webgl/shaders/webgl2Compatibility.glsl";
21+
1722
class FilterRenderer2D {
1823
/**
1924
* Creates a new FilterRenderer2D instance.
@@ -27,7 +32,12 @@ class FilterRenderer2D {
2732
this.canvas.height = pInst.height;
2833

2934
// Initialize the WebGL context
30-
this.gl = this.canvas.getContext('webgl');
35+
let webglVersion = constants.WEBGL2;
36+
this.gl = this.canvas.getContext('webgl2');
37+
if (!this.gl) {
38+
webglVersion = constants.WEBGL;
39+
this.gl = this.canvas.getContext('webgl');
40+
}
3141
if (!this.gl) {
3242
console.error("WebGL not supported, cannot apply filter.");
3343
return;
@@ -38,7 +48,7 @@ class FilterRenderer2D {
3848
registerEnabled: new Set(),
3949
_curShader: null,
4050
_emptyTexture: null,
41-
webglVersion: 'WEBGL',
51+
webglVersion,
4252
states: {
4353
textureWrapX: this.gl.CLAMP_TO_EDGE,
4454
textureWrapY: this.gl.CLAMP_TO_EDGE,
@@ -54,6 +64,8 @@ class FilterRenderer2D {
5464
},
5565
};
5666

67+
this._baseFilterShader = undefined;
68+
5769
// Store the fragment shader sources
5870
this.filterShaderSources = {
5971
[constants.BLUR]: filterBlurFrag,
@@ -90,6 +102,45 @@ class FilterRenderer2D {
90102
this._bindBufferData(this.texcoordBuffer, this.gl.ARRAY_BUFFER, this.texcoords);
91103
}
92104

105+
_webGL2CompatibilityPrefix(shaderType, floatPrecision) {
106+
let code = "";
107+
if (this._renderer.webglVersion === constants.WEBGL2) {
108+
code += "#version 300 es\n#define WEBGL2\n";
109+
}
110+
if (shaderType === "vert") {
111+
code += "#define VERTEX_SHADER\n";
112+
} else if (shaderType === "frag") {
113+
code += "#define FRAGMENT_SHADER\n";
114+
}
115+
if (floatPrecision) {
116+
code += `precision ${floatPrecision} float;\n`;
117+
}
118+
return code;
119+
}
120+
121+
baseFilterShader() {
122+
if (!this._baseFilterShader) {
123+
this._baseFilterShader = new Shader(
124+
this._renderer,
125+
this._webGL2CompatibilityPrefix("vert", "highp") +
126+
webgl2CompatibilityShader +
127+
filterBaseVert,
128+
this._webGL2CompatibilityPrefix("frag", "highp") +
129+
webgl2CompatibilityShader +
130+
filterBaseFrag,
131+
{
132+
vertex: {},
133+
fragment: {
134+
"vec4 getColor": `(FilterInputs inputs, in sampler2D content) {
135+
return getTexture(content, inputs.texCoord);
136+
}`,
137+
},
138+
}
139+
);
140+
}
141+
return this._baseFilterShader;
142+
}
143+
93144
/**
94145
* Set the current filter operation and parameter. If a customShader is provided,
95146
* that overrides the operation-based shader.

src/webgl/material.js

+76
Original file line numberDiff line numberDiff line change
@@ -1520,6 +1520,82 @@ function material(p5, fn){
15201520
return this._renderer.baseMaterialShader();
15211521
};
15221522

1523+
/**
1524+
* Get the base shader for filters.
1525+
*
1526+
* You can then call <a href="#/p5.Shader/modify">`baseFilterShader().modify()`</a>
1527+
* and change the following hook:
1528+
*
1529+
* <table>
1530+
* <tr><th>Hook</th><th>Description</th></tr>
1531+
* <tr><td>
1532+
*
1533+
* `vec4 getColor`
1534+
*
1535+
* </td><td>
1536+
*
1537+
* Output the final color for the current pixel. It takes in two parameters:
1538+
* `FilterInputs inputs`, and `in sampler2D content`, and must return a color
1539+
* as a `vec4`.
1540+
*
1541+
* `FilterInputs inputs` is a scruct with the following properties:
1542+
* - `vec2 texCoord`, the position on the canvas, with coordinates between 0 and 1. Calling
1543+
* `getTexture(content, texCoord)` returns the original color of the current pixel.
1544+
* - `vec2 canvasSize`, the width and height of the sketch.
1545+
* - `vec2 texelSize`, the size of one real pixel relative to the size of the whole canvas.
1546+
* This is equivalent to `1 / (canvasSize * pixelDensity)`.
1547+
*
1548+
* `in sampler2D content` is a texture with the contents of the sketch, pre-filter. Call
1549+
* `getTexture(content, someCoordinate)` to retrieve the color of the sketch at that coordinate,
1550+
* with coordinate values between 0 and 1.
1551+
*
1552+
* </td></tr>
1553+
* </table>
1554+
*
1555+
* Most of the time, you will need to write your hooks in GLSL ES version 300. If you
1556+
* are using WebGL 1, write your hooks in GLSL ES 100 instead.
1557+
*
1558+
* @method baseFilterShader
1559+
* @beta
1560+
* @returns {p5.Shader} The filter shader
1561+
*
1562+
* @example
1563+
* <div modernizr='webgl'>
1564+
* <code>
1565+
* let img;
1566+
* let myShader;
1567+
*
1568+
* async function setup() {
1569+
* img = await loadImage('assets/bricks.jpg');
1570+
* createCanvas(100, 100, WEBGL);
1571+
* myShader = baseFilterShader().modify({
1572+
* uniforms: {
1573+
* 'float time': () => millis()
1574+
* },
1575+
* 'vec4 getColor': `(
1576+
* FilterInputs inputs,
1577+
* in sampler2D content
1578+
* ) {
1579+
* inputs.texCoord.y +=
1580+
* 0.01 * sin(time * 0.001 + inputs.position.x * 5.0);
1581+
* return getTexture(content, inputs.texCoord);
1582+
* }`
1583+
* });
1584+
* }
1585+
*
1586+
* function draw() {
1587+
* image(img, -50, -50);
1588+
* filter(myShader);
1589+
* describe('an image of bricks, distorting over time');
1590+
* }
1591+
* </code>
1592+
* </div>
1593+
*/
1594+
fn.baseFilterShader = function() {
1595+
return (this._renderer.filterRenderer || this._renderer)
1596+
.baseFilterShader();
1597+
};
1598+
15231599
/**
15241600
* Get the shader used by <a href="#/p5/normalMaterial">`normalMaterial()`</a>.
15251601
*

src/webgl/p5.RendererGL.js

+26
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { ShapeBuilder } from "./ShapeBuilder";
1616
import { GeometryBufferCache } from "./GeometryBufferCache";
1717
import { filterParamDefaults } from "../image/const";
1818

19+
import filterBaseVert from "./shaders/filters/base.vert";
1920
import lightingShader from "./shaders/lighting.glsl";
2021
import webgl2CompatibilityShader from "./shaders/webgl2Compatibility.glsl";
2122
import normalVert from "./shaders/normal.vert";
@@ -36,6 +37,7 @@ import imageLightVert from "./shaders/imageLight.vert";
3637
import imageLightDiffusedFrag from "./shaders/imageLightDiffused.frag";
3738
import imageLightSpecularFrag from "./shaders/imageLightSpecular.frag";
3839

40+
import filterBaseFrag from "./shaders/filters/base.frag";
3941
import filterGrayFrag from "./shaders/filters/gray.frag";
4042
import filterErodeFrag from "./shaders/filters/erode.frag";
4143
import filterDilateFrag from "./shaders/filters/dilate.frag";
@@ -87,6 +89,8 @@ const defaultShaders = {
8789
imageLightVert,
8890
imageLightDiffusedFrag,
8991
imageLightSpecularFrag,
92+
filterBaseVert,
93+
filterBaseFrag,
9094
};
9195
let sphereMapping = defaultShaders.sphereMappingFrag;
9296
for (const key in defaultShaders) {
@@ -302,6 +306,7 @@ class RendererGL extends Renderer {
302306
this.specularShader = undefined;
303307
this.sphereMapping = undefined;
304308
this.diffusedShader = undefined;
309+
this._baseFilterShader = undefined;
305310
this._defaultLightShader = undefined;
306311
this._defaultImmediateModeShader = undefined;
307312
this._defaultNormalShader = undefined;
@@ -2087,6 +2092,27 @@ class RendererGL extends Renderer {
20872092
return this._defaultFontShader;
20882093
}
20892094

2095+
baseFilterShader() {
2096+
if (!this._baseFilterShader) {
2097+
this._baseFilterShader = new Shader(
2098+
this,
2099+
this._webGL2CompatibilityPrefix("vert", "highp") +
2100+
defaultShaders.filterBaseVert,
2101+
this._webGL2CompatibilityPrefix("frag", "highp") +
2102+
defaultShaders.filterBaseFrag,
2103+
{
2104+
vertex: {},
2105+
fragment: {
2106+
"vec4 getColor": `(FilterInputs inputs, in sampler2D content) {
2107+
return getTexture(content, inputs.texCoord);
2108+
}`,
2109+
},
2110+
}
2111+
);
2112+
}
2113+
return this._baseFilterShader;
2114+
}
2115+
20902116
_webGL2CompatibilityPrefix(shaderType, floatPrecision) {
20912117
let code = "";
20922118
if (this.webglVersion === constants.WEBGL2) {

src/webgl/shaders/filters/base.frag

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
precision highp float;
2+
3+
uniform sampler2D tex0;
4+
uniform vec2 canvasSize;
5+
uniform vec2 texelSize;
6+
7+
IN vec2 vTexCoord;
8+
9+
struct FilterInputs {
10+
vec2 texCoord;
11+
vec2 canvasSize;
12+
vec2 texelSize;
13+
};
14+
15+
void main(void) {
16+
FilterInputs inputs;
17+
inputs.texCoord = vTexCoord;
18+
inputs.canvasSize = canvasSize;
19+
inputs.texelSize = texelSize;
20+
OUT_COLOR = HOOK_getColor(inputs, tex0);
21+
OUT_COLOR.rgb *= outColor.a;
22+
}

src/webgl/shaders/filters/base.vert

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
precision highp int;
2+
3+
uniform mat4 uModelViewMatrix;
4+
uniform mat4 uProjectionMatrix;
5+
6+
IN vec3 aPosition;
7+
IN vec2 aTexCoord;
8+
OUT vec2 vTexCoord;
9+
10+
void main() {
11+
// transferring texcoords for the frag shader
12+
vTexCoord = aTexCoord;
13+
14+
// copy position with a fourth coordinate for projection (1.0 is normal)
15+
vec4 positionVec4 = vec4(aPosition, 1.0);
16+
17+
// project to 3D space
18+
gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
19+
}

src/webgl/shaders/webgl2Compatibility.glsl

+8
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,11 @@ out vec4 outColor;
2424
#endif
2525

2626
#endif
27+
28+
#ifdef FRAGMENT_SHADER
29+
vec4 getTexture(in sampler2D content, vec2 coord) {
30+
vec4 color = TEXTURE(content, coord);
31+
color.rgb /= color.a;
32+
return color;
33+
}
34+
#endif

test/unit/visual/cases/webgl.js

+25
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,31 @@ visualSuite('WebGL', function() {
7171
}
7272
);
7373

74+
for (const mode of ['webgl', '2d']) {
75+
visualSuite(`In ${mode} mode`, function() {
76+
visualTest('It can use filter shader hooks', function(p5, screenshot) {
77+
p5.createCanvas(50, 50, mode === 'webgl' ? p5.WEBGL : p5.P2D);
78+
79+
const s = p5.baseFilterShader().modify({
80+
'vec4 getColor': `(FilterInputs inputs, in sampler2D content) {
81+
vec4 c = getTexture(content, inputs.texCoord);
82+
float avg = (c.r + c.g + c.b) / 3.0;
83+
return vec4(avg, avg, avg, c.a);
84+
}`
85+
});
86+
87+
if (mode === 'webgl') p5.translate(-p5.width/2, -p5.height/2);
88+
p5.background(255);
89+
p5.fill('red');
90+
p5.noStroke();
91+
p5.circle(15, 15, 20);
92+
p5.circle(30, 30, 20);
93+
p5.filter(s);
94+
screenshot();
95+
});
96+
});
97+
}
98+
7499
for (const mode of ['webgl', '2d']) {
75100
visualSuite(`In ${mode} mode`, function() {
76101
visualTest('It can combine multiple filter passes', function(p5, screenshot) {
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"numScreenshots": 1
3+
}
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"numScreenshots": 1
3+
}

0 commit comments

Comments
 (0)