Skip to content
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
merged 18 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
207 changes: 207 additions & 0 deletions src/image/filterRenderer2D.js
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
Copy link
Contributor

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:

function draw() {
  filter(BLUR)
  filter(THRESHOLD)
}

I think maybe we should structure this in a way where we have this.filterShaderSources (storing the fragment shaders) and this.filterShaders (storing the initialized p5.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 stored this.filterShaders[this.operation] if it already exists, and if not, initialize a shader and store it in this.filterShaders.

Copy link
Contributor

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.


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;
4 changes: 4 additions & 0 deletions src/image/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import image from './image.js';
import loadingDisplaying from './loading_displaying.js';
import p5image from './p5.Image.js';
import pixels from './pixels.js';
import shader from '../webgl/p5.Shader.js';
import texture from '../webgl/p5.Texture.js';

export default function(p5){
p5.registerAddon(image);
p5.registerAddon(loadingDisplaying);
p5.registerAddon(p5image);
p5.registerAddon(pixels);
p5.registerAddon(shader);
p5.registerAddon(texture);
}
49 changes: 21 additions & 28 deletions src/image/pixels.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import Filters from './filters';
import FilterRenderer2D from './filterRenderer2D';

function pixels(p5, fn){
/**
Expand Down Expand Up @@ -752,34 +753,26 @@ 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);
if (shader) {
const customFilterRenderer = new FilterRenderer2D(this, operation, value, shader);
customFilterRenderer.applyFilter();
} else {
if (!this._filterRenderers) {
this._filterRenderers = {};
}

// 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
// Check if we have a cached renderer for this operation
if (!this._filterRenderers[operation]) {
// If no cached renderer for this filter, create and cache it
this._filterRenderers[operation] = new FilterRenderer2D(this, operation, value);
} else {
// If we already have a renderer for this operation and value is different, update it.
this._filterRenderers[operation].updateFilterParameter(value);
}
// Use the currently requested operation's renderer
this.filterRenderer = this._filterRenderers[operation];
this.filterRenderer.applyFilter();
}
}
};

Expand Down Expand Up @@ -1181,4 +1174,4 @@ export default pixels;

if(typeof p5 !== 'undefined'){
pixels(p5, p5.prototype);
}
}
Loading