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 2 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
189 changes: 189 additions & 0 deletions src/image/filterRenderer2D.js
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
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.


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];
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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;
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);
}
37 changes: 8 additions & 29 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,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();
Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Contributor

Choose a reason for hiding this comment

The 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

}
};

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

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