-
-
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
Merged
davepagurek
merged 18 commits into
processing:dev-2.0
from
perminder-17:2d-build-filter
Dec 17, 2024
Merged
Changes from 4 commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
d71efff
2dFilterRenderer still a work in progress PR, needs to do some todo t…
perminder-17 7b5b889
code cleanups and minor fixes
perminder-17 ff99903
cache buffers
perminder-17 9aee0d0
fixes
perminder-17 e714125
suggestions-fixes
perminder-17 a5b0973
minor-changes testing still left
perminder-17 6978551
minor-fixes
perminder-17 d45c50f
minor-fixes
perminder-17 64adf22
adding filterParamter
perminder-17 9755b1d
for-testing
perminder-17 3a1df0a
removing-filterGraphicsLayer-tests
perminder-17 5db9a7f
revert-index.html
perminder-17 beaff23
some-minor-test-fixes
perminder-17 8a27e79
for tests
perminder-17 9f340fe
fixing-webgl-modes
perminder-17 61754a0
Merge branch 'dev-2.0' into 2d-build-filter
perminder-17 e4e020e
Handle default parameters
davepagurek 80a5c5d
Add visual tests
davepagurek File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,207 @@ | ||
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. | ||
* @param {p5.Shader} customShader - Optional custom shader; if provided, ignore operation-based loading. | ||
*/ | ||
constructor(pInst, operation, filterParameter, customShader) { | ||
this.pInst = pInst; | ||
this.filterParameter = filterParameter; | ||
this.operation = operation; | ||
this.customShader = customShader; | ||
|
||
|
||
// 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(); | ||
|
||
// Create buffers once | ||
this.vertexBuffer = this.gl.createBuffer(); | ||
this.texcoordBuffer = this.gl.createBuffer(); | ||
|
||
// Set up the vertices and texture coordinates for a full-screen quad | ||
this.vertices = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]); | ||
this.texcoords = new Float32Array([0, 1, 1, 1, 0, 0, 1, 0]); | ||
|
||
// Upload vertex data once | ||
this._bindBufferData(this.vertexBuffer, this.gl.ARRAY_BUFFER, this.vertices); | ||
|
||
// Upload texcoord data once | ||
this._bindBufferData(this.texcoordBuffer, this.gl.ARRAY_BUFFER, this.texcoords); | ||
|
||
} | ||
|
||
updateFilterParameter(newFilterParameter) { | ||
// Operation is the same, just update parameter if changed | ||
this.filterParameter = newFilterParameter; | ||
} | ||
|
||
/** | ||
* Initializes the shader program if it hasn't been already. | ||
*/ | ||
_initializeShader() { | ||
if (this._shader) return; // Already initialized | ||
|
||
if (this.customShader) { | ||
this._shader = this.customShader; | ||
return; | ||
} | ||
|
||
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) { | ||
const gl = this.gl; | ||
gl.bindBuffer(target, buffer); | ||
gl.bufferData(target, values, 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)); | ||
|
||
const identityMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; | ||
this._shader.setUniform('uModelViewMatrix', identityMatrix); | ||
this._shader.setUniform('uProjectionMatrix', identityMatrix); | ||
|
||
// Bind and enable vertex attributes | ||
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); | ||
this._shader.enableAttrib(this._shader.attributes.aPosition, 2); | ||
|
||
gl.bindBuffer(gl.ARRAY_BUFFER, this.texcoordBuffer); | ||
this._shader.enableAttrib(this._shader.attributes.aTexCoord, 2); | ||
|
||
// Draw the quad | ||
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); | ||
|
||
// Unbind the shader | ||
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 && !this.customShader) { | ||
// Horizontal pass | ||
this._shader.setUniform('direction', [1, 0]); | ||
this._renderPass(); | ||
|
||
// 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._shader.setUniform('direction', [0, 1]); | ||
this._renderPass(); | ||
|
||
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.