Skip to content

Commit 2e8cdaf

Browse files
authored
Merge pull request #7409 from perminder-17/2d-build-filter
FilterRenderer2D for a 2d-Build
2 parents 685ca3c + 80a5c5d commit 2e8cdaf

File tree

15 files changed

+313
-49
lines changed

15 files changed

+313
-49
lines changed

src/core/p5.Renderer2D.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@ import { Graphics } from './p5.Graphics';
55
import { Image } from '../image/p5.Image';
66
import { Element } from '../dom/p5.Element';
77
import { MediaElement } from '../dom/p5.MediaElement';
8+
9+
import FilterRenderer2D from '../image/filterRenderer2D';
10+
811
import { PrimitiveToPath2DConverter } from '../shape/custom_shapes';
912

13+
1014
const styleEmpty = 'rgba(0,0,0,0)';
1115
// const alphaThreshold = 0.00125; // minimum visible
1216

@@ -67,6 +71,9 @@ class Renderer2D extends Renderer {
6771
}
6872
this.scale(this._pixelDensity, this._pixelDensity);
6973

74+
if(!this.filterRenderer){
75+
this.filterRenderer = new FilterRenderer2D(this);
76+
}
7077
// Set and return p5.Element
7178
this.wrappedElt = new Element(this.elt, this._pInst);
7279

src/image/const.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import * as constants from '../core/constants';
2+
export const filterParamDefaults = {
3+
[constants.BLUR]: 3,
4+
[constants.POSTERIZE]: 4,
5+
[constants.THRESHOLD]: 0.5,
6+
};

src/image/filterRenderer2D.js

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import { Shader } from "../webgl/p5.Shader";
2+
import { Texture } from "../webgl/p5.Texture";
3+
import { Image } from "./p5.Image";
4+
import * as constants from '../core/constants';
5+
6+
import filterGrayFrag from '../webgl/shaders/filters/gray.frag';
7+
import filterErodeFrag from '../webgl/shaders/filters/erode.frag';
8+
import filterDilateFrag from '../webgl/shaders/filters/dilate.frag';
9+
import filterBlurFrag from '../webgl/shaders/filters/blur.frag';
10+
import filterPosterizeFrag from '../webgl/shaders/filters/posterize.frag';
11+
import filterOpaqueFrag from '../webgl/shaders/filters/opaque.frag';
12+
import filterInvertFrag from '../webgl/shaders/filters/invert.frag';
13+
import filterThresholdFrag from '../webgl/shaders/filters/threshold.frag';
14+
import filterShaderVert from '../webgl/shaders/filters/default.vert';
15+
import { filterParamDefaults } from "./const";
16+
17+
class FilterRenderer2D {
18+
/**
19+
* Creates a new FilterRenderer2D instance.
20+
* @param {p5} pInst - The p5.js instance.
21+
*/
22+
constructor(pInst) {
23+
this.pInst = pInst;
24+
// Create a canvas for applying WebGL-based filters
25+
this.canvas = document.createElement('canvas');
26+
this.canvas.width = pInst.width;
27+
this.canvas.height = pInst.height;
28+
29+
// Initialize the WebGL context
30+
this.gl = this.canvas.getContext('webgl');
31+
if (!this.gl) {
32+
console.error("WebGL not supported, cannot apply filter.");
33+
return;
34+
}
35+
// Minimal renderer object required by p5.Shader and p5.Texture
36+
this._renderer = {
37+
GL: this.gl,
38+
registerEnabled: new Set(),
39+
_curShader: null,
40+
_emptyTexture: null,
41+
webglVersion: 'WEBGL',
42+
states: {
43+
textureWrapX: this.gl.CLAMP_TO_EDGE,
44+
textureWrapY: this.gl.CLAMP_TO_EDGE,
45+
},
46+
_arraysEqual: (a, b) => JSON.stringify(a) === JSON.stringify(b),
47+
_getEmptyTexture: () => {
48+
if (!this._emptyTexture) {
49+
const im = new Image(1, 1);
50+
im.set(0, 0, 255);
51+
this._emptyTexture = new Texture(this._renderer, im);
52+
}
53+
return this._emptyTexture;
54+
},
55+
};
56+
57+
// Store the fragment shader sources
58+
this.filterShaderSources = {
59+
[constants.BLUR]: filterBlurFrag,
60+
[constants.INVERT]: filterInvertFrag,
61+
[constants.THRESHOLD]: filterThresholdFrag,
62+
[constants.ERODE]: filterErodeFrag,
63+
[constants.GRAY]: filterGrayFrag,
64+
[constants.DILATE]: filterDilateFrag,
65+
[constants.POSTERIZE]: filterPosterizeFrag,
66+
[constants.OPAQUE]: filterOpaqueFrag,
67+
};
68+
69+
// Store initialized shaders for each operation
70+
this.filterShaders = {};
71+
72+
// These will be set by setOperation
73+
this.operation = null;
74+
this.filterParameter = 1;
75+
this.customShader = null;
76+
this._shader = null;
77+
78+
// Create buffers once
79+
this.vertexBuffer = this.gl.createBuffer();
80+
this.texcoordBuffer = this.gl.createBuffer();
81+
82+
// Set up the vertices and texture coordinates for a full-screen quad
83+
this.vertices = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);
84+
this.texcoords = new Float32Array([0, 1, 1, 1, 0, 0, 1, 0]);
85+
86+
// Upload vertex data once
87+
this._bindBufferData(this.vertexBuffer, this.gl.ARRAY_BUFFER, this.vertices);
88+
89+
// Upload texcoord data once
90+
this._bindBufferData(this.texcoordBuffer, this.gl.ARRAY_BUFFER, this.texcoords);
91+
}
92+
93+
/**
94+
* Set the current filter operation and parameter. If a customShader is provided,
95+
* that overrides the operation-based shader.
96+
* @param {string} operation - The filter operation type (e.g., constants.BLUR).
97+
* @param {number} filterParameter - The strength of the filter.
98+
* @param {p5.Shader} customShader - Optional custom shader.
99+
*/
100+
setOperation(operation, filterParameter, customShader = null) {
101+
this.operation = operation;
102+
this.filterParameter = filterParameter;
103+
104+
let useDefaultParam = operation in filterParamDefaults && filterParameter === undefined;
105+
if (useDefaultParam) {
106+
this.filterParameter = filterParamDefaults[operation];
107+
}
108+
109+
this.customShader = customShader;
110+
this._initializeShader();
111+
}
112+
113+
/**
114+
* Initializes or retrieves the shader program for the current operation.
115+
* If a customShader is provided, that is used.
116+
* Otherwise, returns a cached shader if available, or creates a new one, caches it, and sets it as current.
117+
*/
118+
_initializeShader() {
119+
if (this.customShader) {
120+
this._shader = this.customShader;
121+
return;
122+
}
123+
124+
if (!this.operation) {
125+
console.error("No operation set for FilterRenderer2D, cannot initialize shader.");
126+
return;
127+
}
128+
129+
// If we already have a compiled shader for this operation, reuse it
130+
if (this.filterShaders[this.operation]) {
131+
this._shader = this.filterShaders[this.operation];
132+
return;
133+
}
134+
135+
const fragShaderSrc = this.filterShaderSources[this.operation];
136+
if (!fragShaderSrc) {
137+
console.error("No shader available for this operation:", this.operation);
138+
return;
139+
}
140+
141+
// Create and store the new shader
142+
const newShader = new Shader(this._renderer, filterShaderVert, fragShaderSrc);
143+
this.filterShaders[this.operation] = newShader;
144+
this._shader = newShader;
145+
}
146+
147+
/**
148+
* Binds a buffer to the drawing context
149+
* when passed more than two arguments it also updates or initializes
150+
* the data associated with the buffer
151+
*/
152+
_bindBufferData(buffer, target, values) {
153+
const gl = this.gl;
154+
gl.bindBuffer(target, buffer);
155+
gl.bufferData(target, values, gl.STATIC_DRAW);
156+
}
157+
158+
get canvasTexture() {
159+
if (!this._canvasTexture) {
160+
this._canvasTexture = new Texture(this._renderer, this.pInst.wrappedElt);
161+
}
162+
return this._canvasTexture;
163+
}
164+
165+
/**
166+
* Prepares and runs the full-screen quad draw call.
167+
*/
168+
_renderPass() {
169+
const gl = this.gl;
170+
this._shader.bindShader();
171+
const pixelDensity = this.pInst.pixelDensity ? this.pInst.pixelDensity() : 1;
172+
173+
const texelSize = [
174+
1 / (this.pInst.width * pixelDensity),
175+
1 / (this.pInst.height * pixelDensity)
176+
];
177+
178+
const canvasTexture = this.canvasTexture;
179+
180+
// Set uniforms for the shader
181+
this._shader.setUniform('tex0', canvasTexture);
182+
this._shader.setUniform('texelSize', texelSize);
183+
this._shader.setUniform('canvasSize', [this.pInst.width, this.pInst.height]);
184+
this._shader.setUniform('radius', Math.max(1, this.filterParameter));
185+
this._shader.setUniform('filterParameter', this.filterParameter);
186+
187+
this.pInst.states.rectMode = constants.CORNER;
188+
this.pInst.states.imageMode = constants.CORNER;
189+
this.pInst.blendMode(constants.BLEND);
190+
this.pInst.resetMatrix();
191+
192+
193+
const identityMatrix = [1, 0, 0, 0,
194+
0, 1, 0, 0,
195+
0, 0, 1, 0,
196+
0, 0, 0, 1];
197+
this._shader.setUniform('uModelViewMatrix', identityMatrix);
198+
this._shader.setUniform('uProjectionMatrix', identityMatrix);
199+
200+
// Bind and enable vertex attributes
201+
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
202+
this._shader.enableAttrib(this._shader.attributes.aPosition, 2);
203+
204+
gl.bindBuffer(gl.ARRAY_BUFFER, this.texcoordBuffer);
205+
this._shader.enableAttrib(this._shader.attributes.aTexCoord, 2);
206+
207+
this._shader.bindTextures();
208+
this._shader.disableRemainingAttributes();
209+
210+
// Draw the quad
211+
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
212+
// Unbind the shader
213+
this._shader.unbindShader();
214+
}
215+
216+
/**
217+
* Applies the current filter operation. If the filter requires multiple passes (e.g. blur),
218+
* it handles those internally. Make sure setOperation() has been called before applyFilter().
219+
*/
220+
applyFilter() {
221+
if (!this._shader) {
222+
console.error("Cannot apply filter: shader not initialized.");
223+
return;
224+
}
225+
this.pInst.push();
226+
this.pInst.resetMatrix();
227+
// For blur, we typically do two passes: one horizontal, one vertical.
228+
if (this.operation === constants.BLUR && !this.customShader) {
229+
// Horizontal pass
230+
this._shader.setUniform('direction', [1, 0]);
231+
this._renderPass();
232+
233+
// Draw the result onto itself
234+
this.pInst.clear();
235+
this.pInst.drawingContext.drawImage(this.canvas, 0, 0, this.pInst.width, this.pInst.height);
236+
237+
// Vertical pass
238+
this._shader.setUniform('direction', [0, 1]);
239+
this._renderPass();
240+
241+
this.pInst.clear();
242+
this.pInst.drawingContext.drawImage(this.canvas, 0, 0, this.pInst.width, this.pInst.height);
243+
} else {
244+
// Single-pass filters
245+
246+
this._renderPass();
247+
this.pInst.clear();
248+
// con
249+
this.pInst.blendMode(constants.BLEND);
250+
251+
252+
this.pInst.drawingContext.drawImage(this.canvas, 0, 0, this.pInst.width, this.pInst.height);
253+
}
254+
this.pInst.pop();
255+
}
256+
}
257+
258+
export default FilterRenderer2D;

src/image/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ import image from './image.js';
22
import loadingDisplaying from './loading_displaying.js';
33
import p5image from './p5.Image.js';
44
import pixels from './pixels.js';
5+
import shader from '../webgl/p5.Shader.js';
6+
import texture from '../webgl/p5.Texture.js';
57

68
export default function(p5){
79
p5.registerAddon(image);
810
p5.registerAddon(loadingDisplaying);
911
p5.registerAddon(p5image);
1012
p5.registerAddon(pixels);
13+
p5.registerAddon(shader);
14+
p5.registerAddon(texture);
1115
}

src/image/pixels.js

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -752,34 +752,14 @@ function pixels(p5, fn){
752752

753753
// when this is P2D renderer, create/use hidden webgl renderer
754754
else {
755-
const filterGraphicsLayer = this.getFilterGraphicsLayer();
756-
// copy p2d canvas contents to secondary webgl renderer
757-
// dest
758-
filterGraphicsLayer.copy(
759-
// src
760-
this._renderer,
761-
// src coods
762-
0, 0, this.width, this.height,
763-
// dest coords
764-
-this.width/2, -this.height/2, this.width, this.height
765-
);
766-
//clearing the main canvas
767-
this._renderer.clear();
768755

769-
this._renderer.resetMatrix();
770-
// filter it with shaders
771-
filterGraphicsLayer.filter(...args);
756+
if (shader) {
757+
this._renderer.filterRenderer.setOperation(operation, value, shader);
758+
} else {
759+
this._renderer.filterRenderer.setOperation(operation, value);
760+
}
772761

773-
// copy secondary webgl renderer back to original p2d canvas
774-
this.copy(
775-
// src
776-
filterGraphicsLayer._renderer,
777-
// src coods
778-
0, 0, this.width, this.height,
779-
// dest coords
780-
0, 0, this.width, this.height
781-
);
782-
filterGraphicsLayer.clear(); // prevent feedback effects on p2d canvas
762+
this._renderer.filterRenderer.applyFilter();
783763
}
784764
};
785765

src/webgl/material.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -651,7 +651,7 @@ function material(p5, fn){
651651
if (this._renderer.GL) {
652652
shader.ensureCompiledOnContext(this._renderer);
653653
} else {
654-
shader.ensureCompiledOnContext(this._renderer.getFilterGraphicsLayer()._renderer);
654+
shader.ensureCompiledOnContext(this);
655655
}
656656
return shader;
657657
};

src/webgl/p5.RendererGL.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Graphics } from "../core/p5.Graphics";
1414
import { Element } from "../dom/p5.Element";
1515
import { ShapeBuilder } from "./ShapeBuilder";
1616
import { GeometryBufferCache } from "./GeometryBufferCache";
17+
import { filterParamDefaults } from '../image/const';
1718

1819
import lightingShader from "./shaders/lighting.glsl";
1920
import webgl2CompatibilityShader from "./shaders/webgl2Compatibility.glsl";
@@ -1150,13 +1151,8 @@ class RendererGL extends Renderer {
11501151
let operation = undefined;
11511152
if (typeof args[0] === "string") {
11521153
operation = args[0];
1153-
let defaults = {
1154-
[constants.BLUR]: 3,
1155-
[constants.POSTERIZE]: 4,
1156-
[constants.THRESHOLD]: 0.5,
1157-
};
1158-
let useDefaultParam = operation in defaults && args[1] === undefined;
1159-
filterParameter = useDefaultParam ? defaults[operation] : args[1];
1154+
let useDefaultParam = operation in filterParamDefaults && args[1] === undefined;
1155+
filterParameter = useDefaultParam ? filterParamDefaults[operation] : args[1];
11601156

11611157
// Create and store shader for constants once on initial filter call.
11621158
// Need to store multiple in case user calls different filters,

src/webgl/p5.Shader.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -625,11 +625,12 @@ class Shader {
625625
'The shader being run is attached to a different context. Do you need to copy it to this context first with .copyToContext()?'
626626
);
627627
} else if (this._glProgram === 0) {
628-
this._renderer = context;
628+
this._renderer = context?._renderer?.filterRenderer?._renderer || context;
629629
this.init();
630630
}
631631
}
632-
632+
633+
633634
/**
634635
* Queries the active attributes for this shader and loads
635636
* their names and locations into the attributes array.

0 commit comments

Comments
 (0)