Skip to content

Commit

Permalink
Merge pull request #7409 from perminder-17/2d-build-filter
Browse files Browse the repository at this point in the history
 FilterRenderer2D for a 2d-Build
  • Loading branch information
davepagurek authored Dec 17, 2024
2 parents 685ca3c + 80a5c5d commit 2e8cdaf
Show file tree
Hide file tree
Showing 15 changed files with 313 additions and 49 deletions.
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);
}
// 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

0 comments on commit 2e8cdaf

Please sign in to comment.