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

Add shaders for filter() constants, and use them by default in P2D #6324

Merged
merged 56 commits into from Aug 31, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
568c4ec
mark structure for loading new shader files
wong-justin Aug 3, 2023
9f15a50
destructure args passed to p5.RendererGL.filter
wong-justin Aug 3, 2023
e2f123d
change public filter() signature
wong-justin Aug 3, 2023
bd8dab4
comment overloaded function parameters for clarity
wong-justin Aug 3, 2023
9a02645
whitespace
wong-justin Aug 4, 2023
5bbb980
start adding shaders for filter constants (GRAY)
wong-justin Aug 4, 2023
c1bfee4
fix by creating shaders on secondary graphics layer
wong-justin Aug 10, 2023
ef397a1
use old luminance constants for GRAY
wong-justin Aug 10, 2023
197c905
add texelSize as a default uniform
wong-justin Aug 10, 2023
1e34031
add ERODE
wong-justin Aug 10, 2023
4bf2565
add DILATE
wong-justin Aug 10, 2023
ed40a78
fix comments explaining what DILATE/ERODE do
wong-justin Aug 10, 2023
fc4181e
add BLUR
wong-justin Aug 10, 2023
a94936e
add partial POSTERIZE
wong-justin Aug 10, 2023
586e933
fix filterParameter by moving setUniform after pg.shader() call
wong-justin Aug 11, 2023
1e02d7f
adjust some examples for filter() docs
wong-justin Aug 11, 2023
e0deeeb
adjust wording for filter() docs
wong-justin Aug 11, 2023
5dc1056
fix POSTERIZE to match old output
wong-justin Aug 11, 2023
72f7895
simpler sketch for manual testing
wong-justin Aug 11, 2023
10e0cac
wip, start using shader filters in background of P2D
wong-justin Aug 11, 2023
9ae66c5
add OPAQUE
wong-justin Aug 18, 2023
7b71bc3
add INVERT
wong-justin Aug 18, 2023
2897670
add THRESHOLD
wong-justin Aug 18, 2023
aa1a7e0
add default filter params
wong-justin Aug 18, 2023
4f1b62c
add extra uniform for canvas size
wong-justin Aug 18, 2023
e734106
Merge remote-tracking branch 'upstream/main' into shader-filters
wong-justin Aug 18, 2023
88830eb
keep opacity in THRESHOLD instead of changing it
wong-justin Aug 18, 2023
cb6af67
remove default from inside BLUR shader
wong-justin Aug 18, 2023
89aa499
document new default filter parameters
wong-justin Aug 18, 2023
23c3db5
use min/max() instead of luma() for determining brightness
wong-justin Aug 18, 2023
92c7244
simplify example shader in filter()
wong-justin Aug 18, 2023
41cf4ba
document new uniforms, and slight rephrasing in createFilterShader
wong-justin Aug 18, 2023
6c2e9ce
add some tests
wong-justin Aug 18, 2023
9dc8887
store shaders for filters BLUR, INVERT, etc
wong-justin Aug 18, 2023
b1a76ec
more tests
wong-justin Aug 20, 2023
bafee00
fix feedback effect
wong-justin Aug 20, 2023
3c4afc5
clarify comments
wong-justin Aug 21, 2023
02d187b
support webgl2 fragment shader in createFilterShader()
wong-justin Aug 21, 2023
b5d533c
fix whitespace
wong-justin Aug 21, 2023
ec41c1f
replace single-pass blur with two-pass
wong-justin Aug 22, 2023
8ccfe59
adjust initial blur two-pass by using uniform(tex0, this)
wong-justin Aug 23, 2023
347401b
add variable to reverse flipping effect of previous commit
wong-justin Aug 23, 2023
39def40
remaining steps for blur, including removing extra shader calls
wong-justin Aug 23, 2023
0e6a95b
revise default vertex shader to deal with depth issue #6367
wong-justin Aug 28, 2023
be09142
fix rect() calls
wong-justin Aug 28, 2023
3e22945
update other default vertex shader as well
wong-justin Aug 28, 2023
eca2c76
switch to using main and secondary renderers for blur passes
wong-justin Aug 28, 2023
b0ef928
prevent error when in webgl mode and attempting to use cpu filters
Aug 29, 2023
547578b
testing different manual examples
Aug 29, 2023
44d8f2c
Move blur loop into the shader and use a second temp buffer for rende…
Aug 29, 2023
b7454db
remove unused code
wong-justin Aug 29, 2023
f04ddf8
Merge remote-tracking branch 'ferriss/shader-filters' into shader-fil…
wong-justin Aug 29, 2023
d0a2fe9
fix clear()ing at the right time
wong-justin Aug 30, 2023
b3583f7
remove unused uniform
wong-justin Aug 30, 2023
1bfe9c7
add tests for filter parameters
wong-justin Aug 30, 2023
121137f
Update pixels.js
aferriss Aug 31, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/image/filters.js
Expand Up @@ -308,7 +308,7 @@ const Filters = {
},

/**
* reduces the bright areas in an image
* increases the bright areas in an image
* @private
* @param {Canvas} canvas
*/
Expand Down Expand Up @@ -395,7 +395,7 @@ const Filters = {
},

/**
* increases the bright areas in an image
* reduces the bright areas in an image
* @private
* @param {Canvas} canvas
*/
Expand Down
74 changes: 65 additions & 9 deletions src/image/pixels.js
Expand Up @@ -355,8 +355,11 @@ p5.prototype._copyHelper = (
* POSTERIZE, BLUR, ERODE, DILATE or BLUR.
* See Filters.js for docs on
* each available filter
* @param {Number} [filterParam] an optional parameter unique
* @param {Number} filterParam an optional parameter unique
* to each filter, see above
* @param {Boolean} [useWebGL] a flag to control whether to use fast
* WebGL filters (GPU) or original image
* filters (CPU); defaults to true
*
* @example
* <div>
Expand Down Expand Up @@ -514,27 +517,80 @@ p5.prototype._copyHelper = (
* gray square
*/

/**
* @method filter
* @param {Constant} filterType
* @param {Boolean} [useWebGL]
*/
/**
* @method filter
* @param {p5.Shader} shaderFilter A shader that's been loaded, with the
* frag shader using a `tex0` uniform
*/
p5.prototype.filter = function(operation, value) {
p5.prototype.filter = function(...args) {
p5._validateParameters('filter', arguments);

// TODO: use shader filters always, and provide an opt out
if (this._renderer.isP3D) {
p5.RendererGL.prototype.filter.call(this._renderer, arguments);
let { shader, operation, value, useWebGL } = parseFilterArgs(...args);

// when passed a shader, use it directly
if (shader) {
p5.RendererGL.prototype.filter.call(this._renderer, shader);
return;
}

if (this.canvas !== undefined) {
Filters.apply(this.canvas, Filters[operation], value);
} else {
Filters.apply(this.elt, Filters[operation], value);
// when opting out of webgl, use old pixels method
if (!useWebGL) {
if (this.canvas !== undefined) {
Filters.apply(this.canvas, Filters[operation], value);
} else {
Filters.apply(this.elt, Filters[operation], value);
}
return;
}

// when this is a webgl renderer, apply constant shader filter
if (this._renderer.isP3D) {
p5.RendererGL.prototype.filter.call(this._renderer, operation, value);
}

// when this is P2D renderer, create/use hidden webgl renderer
else {
// TODO: create/use hidden webgl renderer and transfer contents to this p2d
p5._friendlyError('webgl filter implementation in progress');
}
};

function parseFilterArgs(...args) {
// args could be:
// - operation, value, [useWebGL]
// - operation, [useWebGL]
// - shader

let result = {
shader: undefined,
operation: undefined,
value: undefined,
useWebGL: true
};

if (args[0] instanceof p5.Shader) {
result.shader = args[0];
return result;
}
else {
result.operation = args[0];
}

if (args.length > 1 && typeof args[1] === 'number') {
result.value = args[1];
}

if (args[args.length-1] === false) {
result.useWebGL = false;
}
return result;
}

/**
* Get a region of pixels, or a single pixel, from the canvas.
*
Expand Down
6 changes: 3 additions & 3 deletions src/webgl/material.js
Expand Up @@ -268,16 +268,16 @@ p5.prototype.createFilterShader = function(fragSrc) {
// so pass texcoords on to the fragment shader in a varying variable
attribute vec2 aTexCoord;
varying vec2 vTexCoord;

void main() {
// transferring texcoords for the frag shader
vTexCoord = aTexCoord;

// copy position with a fourth coordinate for projection (1.0 is normal)
vec4 positionVec4 = vec4(aPosition, 1.0);
// scale by two and center to achieve correct positioning
positionVec4.xy = positionVec4.xy * 2.0 - 1.0;

gl_Position = positionVec4;
}
`;
Expand Down
61 changes: 42 additions & 19 deletions src/webgl/p5.RendererGL.js
Expand Up @@ -79,6 +79,21 @@ const defaultShaders = {
pointFrag: readFileSync(join(__dirname, '/shaders/point.frag'), 'utf-8')
};

// TODO: add remaining filter shaders
const filterShaderFrags = {
[constants.GRAY]:
readFileSync(join(__dirname, '/shaders/filters/gray.frag'), 'utf-8'),
[constants.ERODE]:
readFileSync(join(__dirname, '/shaders/filters/erode.frag'), 'utf-8'),
[constants.DILATE]:
readFileSync(join(__dirname, '/shaders/filters/dilate.frag'), 'utf-8'),
[constants.BLUR]:
readFileSync(join(__dirname, '/shaders/filters/blur.frag'), 'utf-8'),
[constants.POSTERIZE]:
readFileSync(join(__dirname, '/shaders/filters/posterize.frag'), 'utf-8')
};
const filterShaderVert = readFileSync(join(__dirname, '/shaders/filters/default.vert'), 'utf-8');

/**
* @module Rendering
* @submodule Rendering
Expand Down Expand Up @@ -951,7 +966,7 @@ p5.RendererGL = class RendererGL extends p5.Renderer {
this.curStrokeJoin = join;
}

filter(args) {
filter(...args) {
// Couldn't create graphics in RendererGL constructor
// (led to infinite loop)
// so it's just created here once on the initial filter call.
Expand All @@ -970,31 +985,39 @@ p5.RendererGL = class RendererGL extends p5.Renderer {
}
let pg = this.filterGraphicsLayer;

// use internal shader for filter constants BLUR, INVERT, etc
if (typeof args[0] === 'string') {
// TODO, handle filter constants:
// this.filterShader = map(args[0], {GRAYSCALE: grayscaleShader, ...})
// filterOperationParameter = undefined or args[1]
p5._friendlyError('webgl filter implementation in progress');
return;
let operation = args[0];
let value = args[1];
this.filterShader = new p5.Shader(
pg._renderer,
filterShaderVert,
filterShaderFrags[operation]
);
this.filterShader.setUniform('filterParameter', value);
}
let userShader = args[0];

// Copy the user shader once on the initial filter call,
// since it has to be bound to pg and not main
let isSameUserShader = (
this.filterShader !== undefined &&
userShader._vertSrc === this.filterShader._vertSrc &&
userShader._fragSrc === this.filterShader._fragSrc
);
if (!isSameUserShader) {
this.filterShader =
new p5.Shader(pg._renderer, userShader._vertSrc, userShader._fragSrc);
this.filterShader.parentShader = userShader;
// use custom user-supplied shader
else {
let userShader = args[0];

// Copy the user shader once on the initial filter call,
// since it has to be bound to pg and not main
let isSameUserShader = (
this.filterShader !== undefined &&
userShader._vertSrc === this.filterShader._vertSrc &&
userShader._fragSrc === this.filterShader._fragSrc
);
if (!isSameUserShader) {
this.filterShader =
new p5.Shader(pg._renderer, userShader._vertSrc, userShader._fragSrc);
this.filterShader.parentShader = userShader;
}
}

// apply shader to pg
pg.shader(this.filterShader);
this.filterShader.setUniform('tex0', this);
this.filterShader.setUniform('texelSize', [1.0/this.width, 1.0/this.height]);
Copy link
Contributor

Choose a reason for hiding this comment

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

should we add texelSize to the documentation for createFilterShader?

pg.rect(0,0,this.width,this.height);

// draw pg contents onto main renderer
Expand Down
50 changes: 50 additions & 0 deletions src/webgl/shaders/filters/blur.frag
@@ -0,0 +1,50 @@
// Single-pass blur filter, taken from Adam Ferriss' repo of shader examples:
Copy link
Contributor

Choose a reason for hiding this comment

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

How does the output of this blur compare to the cpu one?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here's a sketch for comparison, although bricks.jpg isn't the best example to compare blur:
https://editor.p5js.org/jwong/sketches/r9fxPerPU

// https://github.com/aferriss/p5jsShaderExamples/blob/gh-pages/4_image-effects/4-9_single-pass-blur/effect.frag

precision highp float;

// lets grab texcoords just for fun
varying vec2 vTexCoord;

// our texture coming from p5
uniform sampler2D tex0;
uniform vec2 texelSize;
uniform float filterParameter;

void main() {

vec2 uv = vTexCoord;

// a single pass blur works by sampling all the neighbor pixels and averaging them up
// this is somewhat inefficient because we have to sample the texture 9 times -- texture2D calls are slow :(
// check out the two-pass-blur example for a better blur approach
// get the webcam as a vec4 using texture2D

// spread controls how far away from the center we should pull a sample from
// you will start to see artifacts if you crank this up too high
float spread = 4.0;
Copy link
Contributor

Choose a reason for hiding this comment

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

lets just simplify this whole block to float spread = max(0.0, filterParameter); and then set a default value for the filterParameter in the js that is overridden if explicitly set.

if (filterParameter > 0.0) {
spread = filterParameter;
}

// create our offset variable by multiplying the size of a texel with spread
vec2 offset = texelSize * spread;

// get all the neighbor pixels!
vec4 tex = texture2D(tex0, uv); // middle middle -- the actual texel / pixel
tex += texture2D(tex0, uv + vec2(-offset.x, -offset.y)); // top left
tex += texture2D(tex0, uv + vec2(0.0, -offset.y)); // top middle
tex += texture2D(tex0, uv + vec2(offset.x, -offset.y)); // top right

tex += texture2D(tex0, uv + vec2(-offset.x, 0.0)); //middle left
tex += texture2D(tex0, uv + vec2(offset.x, 0.0)); //middle right

tex += texture2D(tex0, uv + vec2(-offset.x, offset.y)); // bottom left
tex += texture2D(tex0, uv + vec2(0.0, offset.y)); // bottom middle
tex += texture2D(tex0, uv + vec2(offset.x, offset.y)); // bottom right

// we added 9 textures together, so we will divide by 9 to average them out and move the values back into a 0 - 1 range
tex /= 9.0;

gl_FragColor = tex;
}
17 changes: 17 additions & 0 deletions src/webgl/shaders/filters/default.vert
@@ -0,0 +1,17 @@
attribute vec3 aPosition;
// texcoords only come from p5 to vertex shader
// so pass texcoords on to the fragment shader in a varying variable
attribute vec2 aTexCoord;
varying vec2 vTexCoord;

void main() {
// transferring texcoords for the frag shader
vTexCoord = aTexCoord;

// copy position with a fourth coordinate for projection (1.0 is normal)
vec4 positionVec4 = vec4(aPosition, 1.0);
// scale by two and center to achieve correct positioning
positionVec4.xy = positionVec4.xy * 2.0 - 1.0;
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 we should change this shader to use the model view projection matrix instead. See my issue here #6367 where this shader is causing trouble because of the depth testing.


gl_Position = positionVec4;
}
38 changes: 38 additions & 0 deletions src/webgl/shaders/filters/dilate.frag
@@ -0,0 +1,38 @@
// Increase the bright areas in an image
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 this shader can be simplified and maybe doesn't need the luma calculation. Let me know if I'm wrong but take a look here

https://www.shadertoy.com/view/dtScWD

Copy link
Contributor

Choose a reason for hiding this comment

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

Same for erode, but it will be a min instead of max

Copy link
Contributor Author

Choose a reason for hiding this comment

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

max() seems to choose that greatest of each component, compared to the luma() way of choosing the greatest overall vector. Example:

a = (1, 1, 0)
b = (0, 0, 1)
max(a,b) = (1, 1, 1)  # instead of (1, 1, 0)

Not sure if that's more desired or not. The results look similar though

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 either way is fine, if the luma calculation is more accurate to the CPU mode then disregard my comment!


precision highp float;

varying vec2 vTexCoord;

uniform sampler2D tex0;
uniform vec2 texelSize;

float luma(vec3 color) {
// based on constants 77, 151, 28 from DILATE in filters.js,
// even though that's different than the luminance constants used in GRAY
Copy link
Contributor

Choose a reason for hiding this comment

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

this is fine, haha. Probably just different implementers

Copy link
Contributor

Choose a reason for hiding this comment

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

@jwong I think that having it match what is expected for the non-webgl filters feels best, glad to see you leave a comment about it here, I think it's helpful context.

return dot(color, vec3(0.3008, 0.5898, 0.1094));
}

void main() {
vec4 curColor = texture2D(tex0, vTexCoord);
float curLuminance = luma(curColor.rgb);

// set current color as the neighbor color with highest luminance

vec4 neighbors[4];
neighbors[0] = texture2D(tex0, vTexCoord + vec2( texelSize.x, 0.0));
neighbors[1] = texture2D(tex0, vTexCoord + vec2(-texelSize.x, 0.0));
neighbors[2] = texture2D(tex0, vTexCoord + vec2(0.0, texelSize.y));
neighbors[3] = texture2D(tex0, vTexCoord + vec2(0.0, -texelSize.y));

for (int i = 0; i < 4; i++) {
vec4 color = neighbors[i];
float lum = luma(color.rgb);
if (lum > curLuminance) {
curColor = color;
curLuminance = lum;
}
}

gl_FragColor = curColor;
}
38 changes: 38 additions & 0 deletions src/webgl/shaders/filters/erode.frag
@@ -0,0 +1,38 @@
// Reduces the bright areas in an image

precision highp float;

varying vec2 vTexCoord;

uniform sampler2D tex0;
uniform vec2 texelSize;

float luma(vec3 color) {
// based on constants 77, 151, 28 from ERODE in filters.js,
// even though that's different than the luminance constants used in GRAY
return dot(color, vec3(0.3008, 0.5898, 0.1094));
}

void main() {
vec4 curColor = texture2D(tex0, vTexCoord);
float curLuminance = luma(curColor.rgb);

// set current color as the neighbor color with lowest luminance

vec4 neighbors[4];
neighbors[0] = texture2D(tex0, vTexCoord + vec2( texelSize.x, 0.0));
neighbors[1] = texture2D(tex0, vTexCoord + vec2(-texelSize.x, 0.0));
neighbors[2] = texture2D(tex0, vTexCoord + vec2(0.0, texelSize.y));
neighbors[3] = texture2D(tex0, vTexCoord + vec2(0.0, -texelSize.y));

for (int i = 0; i < 4; i++) {
vec4 color = neighbors[i];
float lum = luma(color.rgb);
if (lum < curLuminance) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure if the if statement should be replaced by some sort of step logic. I know it's good to avoid branching if statements in shaders for speed. But a simple if statement seems easier to read and maintain

curColor = color;
curLuminance = lum;
}
}

gl_FragColor = curColor;
}
16 changes: 16 additions & 0 deletions src/webgl/shaders/filters/gray.frag
@@ -0,0 +1,16 @@
precision highp float;

varying vec2 vTexCoord;

uniform sampler2D tex0;

float luma(vec3 color) {
// weighted grayscale with luminance values
return dot(color, vec3(0.2126, 0.7152, 0.0722));
}

void main() {
vec4 tex = texture2D(tex0, vTexCoord);
float gray = luma(tex.rgb);
gl_FragColor = vec4(gray, gray, gray, tex.a);
}