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 all 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
7 changes: 7 additions & 0 deletions src/core/p5.Renderer2D.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ import { Graphics } from './p5.Graphics';
import { Image } from '../image/p5.Image';
import { Element } from '../dom/p5.Element';
import { MediaElement } from '../dom/p5.MediaElement';

import FilterRenderer2D from '../image/filterRenderer2D';

import { PrimitiveToPath2DConverter } from '../shape/custom_shapes';


const styleEmpty = 'rgba(0,0,0,0)';
// const alphaThreshold = 0.00125; // minimum visible

Expand Down Expand Up @@ -67,6 +71,9 @@ class Renderer2D extends Renderer {
}
this.scale(this._pixelDensity, this._pixelDensity);

if(!this.filterRenderer){
this.filterRenderer = new FilterRenderer2D(this);
Copy link
Contributor Author

@perminder-17 perminder-17 Dec 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw @davepagurek, my custom filterShader works for 2d mode now but I need to add my filterRenderer here as well and in pixel.js as well

      if (!this.filterRenderer) {
        this.filterRenderer = new FilterRenderer2D(this);
        this._renderer.filterRenderer = this.filterRenderer;
      }

any idea why this works only when I have initialized filterRenderer in both the places and by removing any of them, it stops to work?

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 this represents different things in both cases: in this file, it's a p5.Renderer2D, but in the pixels.js file, it's aninstance of p5. In that file, to access the one on the renderer, I think you need to check for this._renderer.filterRenderer.

}
// Set and return p5.Element
this.wrappedElt = new Element(this.elt, this._pInst);

Expand Down
6 changes: 6 additions & 0 deletions src/image/const.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as constants from '../core/constants';
export const filterParamDefaults = {
[constants.BLUR]: 3,
[constants.POSTERIZE]: 4,
[constants.THRESHOLD]: 0.5,
};
258 changes: 258 additions & 0 deletions src/image/filterRenderer2D.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
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';
import { filterParamDefaults } from "./const";

class FilterRenderer2D {
/**
* Creates a new FilterRenderer2D instance.
* @param {p5} pInst - The p5.js instance.
*/
constructor(pInst) {
this.pInst = pInst;
// 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;
},
};

// Store the fragment shader sources
this.filterShaderSources = {
[constants.BLUR]: filterBlurFrag,
[constants.INVERT]: filterInvertFrag,
[constants.THRESHOLD]: filterThresholdFrag,
[constants.ERODE]: filterErodeFrag,
[constants.GRAY]: filterGrayFrag,
[constants.DILATE]: filterDilateFrag,
[constants.POSTERIZE]: filterPosterizeFrag,
[constants.OPAQUE]: filterOpaqueFrag,
};

// Store initialized shaders for each operation
this.filterShaders = {};

// These will be set by setOperation
this.operation = null;
this.filterParameter = 1;
this.customShader = null;
this._shader = null;

// 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);
}

/**
* Set the current filter operation and parameter. If a customShader is provided,
* that overrides the operation-based shader.
* @param {string} operation - The filter operation type (e.g., constants.BLUR).
* @param {number} filterParameter - The strength of the filter.
* @param {p5.Shader} customShader - Optional custom shader.
*/
setOperation(operation, filterParameter, customShader = null) {
this.operation = operation;
this.filterParameter = filterParameter;

let useDefaultParam = operation in filterParamDefaults && filterParameter === undefined;
if (useDefaultParam) {
this.filterParameter = filterParamDefaults[operation];
}

this.customShader = customShader;
this._initializeShader();
}

/**
* Initializes or retrieves the shader program for the current operation.
* If a customShader is provided, that is used.
* Otherwise, returns a cached shader if available, or creates a new one, caches it, and sets it as current.
*/
_initializeShader() {
if (this.customShader) {
this._shader = this.customShader;
return;
}

if (!this.operation) {
console.error("No operation set for FilterRenderer2D, cannot initialize shader.");
return;
}

// If we already have a compiled shader for this operation, reuse it
if (this.filterShaders[this.operation]) {
this._shader = this.filterShaders[this.operation];
return;
}

const fragShaderSrc = this.filterShaderSources[this.operation];
if (!fragShaderSrc) {
console.error("No shader available for this operation:", this.operation);
return;
}

// Create and store the new shader
const newShader = new Shader(this._renderer, filterShaderVert, fragShaderSrc);
this.filterShaders[this.operation] = newShader;
this._shader = newShader;
}

/**
* 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);
}

get canvasTexture() {
if (!this._canvasTexture) {
this._canvasTexture = new Texture(this._renderer, this.pInst.wrappedElt);
}
return this._canvasTexture;
}

/**
* Prepares and runs the full-screen quad draw call.
*/
_renderPass() {
const gl = this.gl;
this._shader.bindShader();
const pixelDensity = this.pInst.pixelDensity ? this.pInst.pixelDensity() : 1;

const texelSize = [
1 / (this.pInst.width * pixelDensity),
1 / (this.pInst.height * pixelDensity)
];

const canvasTexture = this.canvasTexture;

// 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));
this._shader.setUniform('filterParameter', this.filterParameter);

this.pInst.states.rectMode = constants.CORNER;
this.pInst.states.imageMode = constants.CORNER;
this.pInst.blendMode(constants.BLEND);
this.pInst.resetMatrix();


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);

this._shader.bindTextures();
this._shader.disableRemainingAttributes();

// Draw the quad
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
// Unbind the shader
this._shader.unbindShader();
}

/**
* Applies the current filter operation. If the filter requires multiple passes (e.g. blur),
* it handles those internally. Make sure setOperation() has been called before applyFilter().
*/
applyFilter() {
if (!this._shader) {
console.error("Cannot apply filter: shader not initialized.");
return;
}
this.pInst.push();
this.pInst.resetMatrix();
// 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();
// con
this.pInst.blendMode(constants.BLEND);


this.pInst.drawingContext.drawImage(this.canvas, 0, 0, this.pInst.width, this.pInst.height);
}
this.pInst.pop();
}
}

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);
}
32 changes: 6 additions & 26 deletions src/image/pixels.js
Original file line number Diff line number Diff line change
Expand Up @@ -752,34 +752,14 @@ 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) {
this._renderer.filterRenderer.setOperation(operation, value, shader);
} else {
this._renderer.filterRenderer.setOperation(operation, value);
}

// 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
this._renderer.filterRenderer.applyFilter();
}
};

Expand Down
2 changes: 1 addition & 1 deletion src/webgl/material.js
Original file line number Diff line number Diff line change
Expand Up @@ -651,7 +651,7 @@ function material(p5, fn){
if (this._renderer.GL) {
shader.ensureCompiledOnContext(this._renderer);
} else {
shader.ensureCompiledOnContext(this._renderer.getFilterGraphicsLayer()._renderer);
shader.ensureCompiledOnContext(this);
}
return shader;
};
Expand Down
10 changes: 3 additions & 7 deletions src/webgl/p5.RendererGL.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Graphics } from "../core/p5.Graphics";
import { Element } from "../dom/p5.Element";
import { ShapeBuilder } from "./ShapeBuilder";
import { GeometryBufferCache } from "./GeometryBufferCache";
import { filterParamDefaults } from '../image/const';

import lightingShader from "./shaders/lighting.glsl";
import webgl2CompatibilityShader from "./shaders/webgl2Compatibility.glsl";
Expand Down Expand Up @@ -1150,13 +1151,8 @@ class RendererGL extends Renderer {
let operation = undefined;
if (typeof args[0] === "string") {
operation = args[0];
let defaults = {
[constants.BLUR]: 3,
[constants.POSTERIZE]: 4,
[constants.THRESHOLD]: 0.5,
};
let useDefaultParam = operation in defaults && args[1] === undefined;
filterParameter = useDefaultParam ? defaults[operation] : args[1];
let useDefaultParam = operation in filterParamDefaults && args[1] === undefined;
filterParameter = useDefaultParam ? filterParamDefaults[operation] : args[1];

// Create and store shader for constants once on initial filter call.
// Need to store multiple in case user calls different filters,
Expand Down
5 changes: 3 additions & 2 deletions src/webgl/p5.Shader.js
Original file line number Diff line number Diff line change
Expand Up @@ -625,11 +625,12 @@ class Shader {
'The shader being run is attached to a different context. Do you need to copy it to this context first with .copyToContext()?'
);
} else if (this._glProgram === 0) {
this._renderer = context;
this._renderer = context?._renderer?.filterRenderer?._renderer || context;
this.init();
}
}



/**
* Queries the active attributes for this shader and loads
* their names and locations into the attributes array.
Expand Down
Loading
Loading