-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
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
FilterRenderer2D for a 2d-Build #7409
Changes from 2 commits
d71efff
7b5b889
ff99903
9aee0d0
e714125
a5b0973
6978551
d45c50f
64adf22
9755b1d
3a1df0a
5db9a7f
beaff23
8a27e79
9f340fe
61754a0
e4e020e
80a5c5d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
import { Shader } from "../webgl/p5.Shader"; | ||
import { Texture } from "../webgl/p5.Texture"; | ||
import { Image } from "./p5.Image"; | ||
import * as constants from '../core/constants'; | ||
|
||
import filterGrayFrag from '../webgl/shaders/filters/gray.frag'; | ||
import filterErodeFrag from '../webgl/shaders/filters/erode.frag'; | ||
import filterDilateFrag from '../webgl/shaders/filters/dilate.frag'; | ||
import filterBlurFrag from '../webgl/shaders/filters/blur.frag'; | ||
import filterPosterizeFrag from '../webgl/shaders/filters/posterize.frag'; | ||
import filterOpaqueFrag from '../webgl/shaders/filters/opaque.frag'; | ||
import filterInvertFrag from '../webgl/shaders/filters/invert.frag'; | ||
import filterThresholdFrag from '../webgl/shaders/filters/threshold.frag'; | ||
import filterShaderVert from '../webgl/shaders/filters/default.vert'; | ||
|
||
class FilterRenderer2D { | ||
/** | ||
* Creates a new FilterRenderer2D instance. | ||
* @param {p5} pInst - The p5.js instance. | ||
* @param {string} operation - The filter operation type (e.g., constants.BLUR). | ||
* @param {string} filterParameter - The strength of applying filter. | ||
*/ | ||
constructor(pInst, operation, filterParameter) { | ||
this.pInst = pInst; | ||
this.filterParameter = filterParameter; | ||
this.operation = operation; | ||
|
||
// Create a canvas for applying WebGL-based filters | ||
this.canvas = document.createElement('canvas'); | ||
this.canvas.width = pInst.width; | ||
this.canvas.height = pInst.height; | ||
|
||
// Initialize the WebGL context | ||
this.gl = this.canvas.getContext('webgl'); | ||
if (!this.gl) { | ||
console.error("WebGL not supported, cannot apply filter."); | ||
return; | ||
} | ||
|
||
// Minimal renderer object required by p5.Shader and p5.Texture | ||
this._renderer = { | ||
GL: this.gl, | ||
registerEnabled: new Set(), | ||
_curShader: null, | ||
_emptyTexture: null, | ||
webglVersion: 'WEBGL', | ||
states: { | ||
textureWrapX: this.gl.CLAMP_TO_EDGE, | ||
textureWrapY: this.gl.CLAMP_TO_EDGE, | ||
}, | ||
_arraysEqual: (a, b) => JSON.stringify(a) === JSON.stringify(b), | ||
_getEmptyTexture: () => { | ||
if (!this._emptyTexture) { | ||
const im = new Image(1, 1); | ||
im.set(0, 0, 255); | ||
this._emptyTexture = new Texture(this._renderer, im); | ||
} | ||
return this._emptyTexture; | ||
}, | ||
}; | ||
|
||
// Fragment shaders mapped to filter operations | ||
this.filterShaders = { | ||
[constants.BLUR]: filterBlurFrag, | ||
[constants.INVERT]: filterInvertFrag, | ||
[constants.THRESHOLD]: filterThresholdFrag, | ||
[constants.ERODE]: filterErodeFrag, | ||
[constants.GRAY]: filterGrayFrag, | ||
[constants.DILATE]: filterDilateFrag, | ||
[constants.POSTERIZE]: filterPosterizeFrag, | ||
[constants.OPAQUE]: filterOpaqueFrag, | ||
}; | ||
|
||
this._shader = null; | ||
this._initializeShader(); | ||
} | ||
|
||
/** | ||
* Initializes the shader program if it hasn't been already. | ||
*/ | ||
_initializeShader() { | ||
if (this._shader) return; // Already initialized | ||
|
||
const fragShaderSrc = this.filterShaders[this.operation]; | ||
if (!fragShaderSrc) { | ||
console.error("No shader available for this operation:", this.operation); | ||
return; | ||
} | ||
|
||
this._shader = new Shader(this._renderer, filterShaderVert, fragShaderSrc); | ||
} | ||
|
||
/** | ||
* Binds a buffer to the drawing context | ||
* when passed more than two arguments it also updates or initializes | ||
* the data associated with the buffer | ||
*/ | ||
_bindBufferData(buffer, target, values, type, usage) { | ||
const gl = this.gl; | ||
gl.bindBuffer(target, buffer); | ||
let data = values instanceof (type || Float32Array) ? values : new (type || Float32Array)(values); | ||
gl.bufferData(target, data, usage || gl.STATIC_DRAW); | ||
} | ||
|
||
/** | ||
* Prepares and runs the full-screen quad draw call. | ||
*/ | ||
_renderPass() { | ||
const gl = this.gl; | ||
this._shader.bindShader(); | ||
|
||
const pixelDensity = this.pInst._renderer.pixelDensity ? this.pInst._renderer.pixelDensity() : 1; | ||
|
||
const texelSize = [ | ||
1 / (this.pInst.width * pixelDensity), | ||
1 / (this.pInst.height * pixelDensity) | ||
]; | ||
|
||
const canvasTexture = new Texture(this._renderer, this.pInst._renderer.wrappedElt); | ||
|
||
// Set uniforms for the shader | ||
this._shader.setUniform('tex0', canvasTexture); | ||
this._shader.setUniform('texelSize', texelSize); | ||
this._shader.setUniform('canvasSize', [this.pInst.width, this.pInst.height]); | ||
this._shader.setUniform('radius', Math.max(1, this.filterParameter)); | ||
|
||
// Identity matrices for projection/model-view (unsure) | ||
|
||
// TODO: FIX IT | ||
const identityMatrix = [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we are dealing with 2D models here, do you think making the matrices identity works well? I tested them and results look good to me. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that makes sense! |
||
this._shader.setUniform('uModelViewMatrix', identityMatrix); | ||
this._shader.setUniform('uProjectionMatrix', identityMatrix); | ||
|
||
// Set up the vertices and texture coordinates for a full-screen quad | ||
const vertices = [-1, -1, 1, -1, -1, 1, 1, 1]; | ||
const texcoords = [0, 1, 1, 1, 0, 0, 1, 0]; | ||
|
||
// Create and bind buffers | ||
const vertexBuffer = gl.createBuffer(); | ||
perminder-17 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
this._bindBufferData(vertexBuffer, gl.ARRAY_BUFFER, vertices, Float32Array, gl.STATIC_DRAW); | ||
this._shader.enableAttrib(this._shader.attributes.aPosition, 2); | ||
|
||
const texcoordBuffer = gl.createBuffer(); | ||
this._bindBufferData(texcoordBuffer, gl.ARRAY_BUFFER, texcoords, Float32Array, gl.STATIC_DRAW); | ||
this._shader.enableAttrib(this._shader.attributes.aTexCoord, 2); | ||
|
||
// Draw the quad | ||
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); | ||
|
||
// Unbind the shader and texture | ||
this._shader.unbindShader(); | ||
} | ||
|
||
/** | ||
* Applies the filter operation. If the filter requires multiple passes (e.g. blur), | ||
* it handles those internally. | ||
*/ | ||
applyFilter() { | ||
if (!this._shader) { | ||
console.error("Cannot apply filter: shader not initialized."); | ||
return; | ||
} | ||
|
||
// For blur, we typically do two passes: one horizontal, one vertical. | ||
if (this.operation === constants.BLUR) { | ||
// Horizontal pass | ||
this._renderPass(); | ||
this._shader.setUniform('direction', [0,1]); | ||
|
||
// Draw the result onto itself | ||
this.pInst.clear(); | ||
this.pInst.drawingContext.drawImage(this.canvas, 0, 0, this.pInst.width, this.pInst.height); | ||
|
||
// Vertical pass | ||
this._renderPass(); | ||
this._shader.setUniform('direction', [1,0]); | ||
|
||
this.pInst.clear(); | ||
this.pInst.drawingContext.drawImage(this.canvas, 0, 0, this.pInst.width, this.pInst.height); | ||
} else { | ||
// Single-pass filters | ||
this._renderPass(); | ||
this.pInst.clear(); | ||
this.pInst.drawingContext.drawImage(this.canvas, 0, 0, this.pInst.width, this.pInst.height); | ||
} | ||
} | ||
} | ||
|
||
export default FilterRenderer2D; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ | |
*/ | ||
|
||
import Filters from './filters'; | ||
import FilterRenderer2D from './filterRenderer2D'; | ||
|
||
function pixels(p5, fn){ | ||
/** | ||
|
@@ -752,34 +753,12 @@ function pixels(p5, fn){ | |
|
||
// when this is P2D renderer, create/use hidden webgl renderer | ||
else { | ||
const filterGraphicsLayer = this.getFilterGraphicsLayer(); | ||
// copy p2d canvas contents to secondary webgl renderer | ||
// dest | ||
filterGraphicsLayer.copy( | ||
// src | ||
this._renderer, | ||
// src coods | ||
0, 0, this.width, this.height, | ||
// dest coords | ||
-this.width/2, -this.height/2, this.width, this.height | ||
); | ||
//clearing the main canvas | ||
this._renderer.clear(); | ||
|
||
this._renderer.resetMatrix(); | ||
// filter it with shaders | ||
filterGraphicsLayer.filter(...args); | ||
|
||
// copy secondary webgl renderer back to original p2d canvas | ||
this.copy( | ||
// src | ||
filterGraphicsLayer._renderer, | ||
// src coods | ||
0, 0, this.width, this.height, | ||
// dest coords | ||
0, 0, this.width, this.height | ||
); | ||
filterGraphicsLayer.clear(); // prevent feedback effects on p2d canvas | ||
|
||
if (!this.filterRenderer) { | ||
this.filterRenderer = new FilterRenderer2D(this, operation, value); | ||
} | ||
|
||
this.filterRenderer.applyFilter(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If a different filter is rendered (e.g. a blur then a threshold) I think we'll need to update the addressee used here. Might need to add an update method, and maybe also cache the shaders by their type instead of just having one cached There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And I guess if a custom filter shader is passed in, we don't need to cache that, since the user holds on to the shader instance themself in that case |
||
} | ||
}; | ||
|
||
|
@@ -1181,4 +1160,4 @@ export default pixels; | |
|
||
if(typeof p5 !== 'undefined'){ | ||
pixels(p5, p5.prototype); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like once we set
this._shader
, it will never update. I think that will cause this scenario to apply the first filter twice instead of doing each one once:I think maybe we should structure this in a way where we have
this.filterShaderSources
(storing the fragment shaders) andthis.filterShaders
(storing the initializedp5.Shader
, if it exists). So in_initializeShader
, we could just return the custom shader if a custom shader has been provided, and if not, return the storedthis.filterShaders[this.operation]
if it already exists, and if not, initialize a shader and store it inthis.filterShaders
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be instead of having separate filter renderers per shader as you have it right now. The browser has a pretty low limit for the number of active WebGL contexts it can maintain (it's something like 6, and lower on phones), so we'd want to have just one filter renderer per 2D context, and have it have multiple shaders.