Skip to content

Adds createFilterShader() and custom shader support to the webGL filter() function #6237

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

Merged
merged 50 commits into from
Aug 2, 2023
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
16def01
declare framebuffer usage
wong-justin Jun 23, 2023
c7abb8c
new properties for shader filters
wong-justin Jun 23, 2023
9dbe595
partial shader filter implementation instead of error
wong-justin Jun 23, 2023
a096914
add new filter() signature
wong-justin Jun 23, 2023
87f347a
branch filter() for either webgl mode or 2d mode, not just 2d
wong-justin Jun 23, 2023
e0126bf
revert to p5.Graphics instead of p5.Framebuffer, at least for now
wong-justin Jun 29, 2023
cb1979b
apply shader to secondary graphics, then draw onto main
wong-justin Jun 29, 2023
644e61a
Merge remote-tracking branch 'upstream/main' into shader-filters
wong-justin Jun 29, 2023
628fc18
add manual test for webgl filter()
wong-justin Jun 30, 2023
359d16b
add some automated webgl filter tests
wong-justin Jun 30, 2023
e857d5b
remove confused arguments
wong-justin Jun 30, 2023
a762869
create new shader bound to secondary graphics
wong-justin Jun 30, 2023
2eb64a0
add tests to check sizing
wong-justin Jun 30, 2023
0c5d0ff
docs for filter(shader)
wong-justin Jul 7, 2023
10d85ff
add resizing to secondary graphics
wong-justin Jul 7, 2023
f324437
only create secondary shader once
wong-justin Jul 7, 2023
7f39f69
leave out filter(CONSTANT) test until that feature is added
wong-justin Jul 7, 2023
fc50e52
remove resolution check because pixel density wont change
wong-justin Jul 7, 2023
f3a41e7
remove framebuffer test since framebuffer isnt used for now
wong-justin Jul 7, 2023
7888ad4
fill in empty tests
wong-justin Jul 7, 2023
b52a467
wip: copy uniforms to shader copy
wong-justin Jul 7, 2023
02113bb
add performance visual to filter test page
wong-justin Jul 13, 2023
be7c01c
Merge remote-tracking branch 'upstream/main' into shader-filters
wong-justin Jul 13, 2023
aa1f048
expose the built library
wong-justin Jul 13, 2023
5d50053
rebuild p5.min.js
wong-justin Jul 13, 2023
4e52976
add non-minified library too
wong-justin Jul 13, 2023
590d025
fix parent shader check
wong-justin Jul 13, 2023
f2e69ae
remove tabs and console.log
wong-justin Jul 14, 2023
6e4c04e
add createFilterShader that includes default vertex shader
wong-justin Jul 14, 2023
f23d072
update output lib
wong-justin Jul 14, 2023
034ebb5
clean whitespace
wong-justin Jul 14, 2023
7bfa2f2
stop tracking output libs
wong-justin Jul 14, 2023
f6fbdc2
test for createFilterShader()
wong-justin Jul 14, 2023
86cb79d
mark todos for next PR
wong-justin Jul 14, 2023
bf282f3
fix gitignore
wong-justin Jul 14, 2023
25424c7
document uniforms
wong-justin Jul 21, 2023
d1dd620
refine example fragment shaders
wong-justin Jul 21, 2023
fa178ba
default vertex shader
wong-justin Jul 21, 2023
68ad8ae
rest of createFilterShader docs
wong-justin Jul 21, 2023
c039991
adjust test shaders to match default filter shaders
wong-justin Jul 21, 2023
c476759
add test comparing default vertex shader to a supplied one
wong-justin Jul 21, 2023
4b1e56e
revert pre-flipping the y in the vertex shader
wong-justin Jul 28, 2023
ec76701
add test for edge case of filter shader on graphics context
wong-justin Jul 28, 2023
8220071
move uniform documentation to setUniform
wong-justin Jul 28, 2023
44ad94f
add disclaimers to createFilterShader docs
wong-justin Jul 28, 2023
a532146
Merge remote-tracking branch 'upstream/main' into shader-filters
wong-justin Jul 28, 2023
479aca3
go back to flipping the y
wong-justin Jul 29, 2023
4250399
fix filter() not working on a secondary p5.Graphics
wong-justin Jul 29, 2023
5abd47d
small wording changes in createFilterShader docs
wong-justin Jul 29, 2023
51aa874
remove unused commented code from test
wong-justin Aug 2, 2023
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
85 changes: 71 additions & 14 deletions src/image/pixels.js
Original file line number Diff line number Diff line change
Expand Up @@ -307,43 +307,48 @@ p5.prototype._copyHelper = (
/**
* Applies a filter to the canvas. The presets options are:
*
* THRESHOLD
* `THRESHOLD`
* Converts the image to black and white pixels depending if they are above or
* below the threshold defined by the level parameter. The parameter must be
* between 0.0 (black) and 1.0 (white). If no level is specified, 0.5 is used.
*
* GRAY
* `GRAY`
* Converts any colors in the image to grayscale equivalents. No parameter
* is used.
*
* OPAQUE
* `OPAQUE`
* Sets the alpha channel to entirely opaque. No parameter is used.
*
* INVERT
* `INVERT`
* Sets each pixel to its inverse value. No parameter is used.
*
* POSTERIZE
* `POSTERIZE`
* Limits each channel of the image to the number of colors specified as the
* parameter. The parameter can be set to values between 2 and 255, but
* results are most noticeable in the lower ranges.
*
* BLUR
* `BLUR`
* Executes a Gaussian blur with the level parameter specifying the extent
* of the blurring. If no parameter is used, the blur is equivalent to
* Gaussian blur of radius 1. Larger values increase the blur.
*
* ERODE
* `ERODE`
* Reduces the light areas. No parameter is used.
*
* DILATE
* `DILATE`
* Increases the light areas. No parameter is used.
*
* filter() does not work in WEBGL mode.
* A similar effect can be achieved in WEBGL mode using custom
* shaders. Adam Ferriss has written
* a <a href="https://github.com/aferriss/p5jsShaderExamples"
* target='_blank'>selection of shader examples</a> that contains many
* of the effects present in the filter examples.
* ---
*
* In WEBGL mode, `filter()` can also accept a shader. The fragment shader
* is given a `uniform sampler2D` named `tex0` that contains the current
* state of the canvas. For more information on using shaders, check
* <a href="https://p5js.org/learn/getting-started-in-webgl-shaders.html">
* the introduction to shaders</a> tutorial.
*
* See also <a href="https://github.com/aferriss/p5jsShaderExamples"
* target='_blank'>a selection of shader examples</a> by Adam Ferriss
* that contains many similar filter effects.
*
* @method filter
* @param {Constant} filterType either THRESHOLD, GRAY, OPAQUE, INVERT,
Expand Down Expand Up @@ -458,6 +463,44 @@ p5.prototype._copyHelper = (
* </code>
* </div>
*
* <div>
* <code>
* createCanvas(100, 100, WEBGL);
* let myShader = createShader(
* `attribute vec3 aPosition;
* attribute vec2 aTexCoord;
*
* varying vec2 vTexCoord;
*
* void main() {
* vTexCoord = aTexCoord;
* vec4 positionVec4 = vec4(aPosition, 1.0);
* positionVec4.xy = positionVec4.xy * 2.0 - 1.0;
* gl_Position = positionVec4;
* }`,
* `precision mediump float;
* varying mediump vec2 vTexCoord;
*
* uniform sampler2D tex0;
*
* float luma(vec3 color) {
* return dot(color, vec3(0.299, 0.587, 0.114));
* }
*
* void main() {
* vec2 uv = vTexCoord;
* uv.y = 1.0 - uv.y;
* vec4 sampledColor = texture2D(tex0, uv);
* float gray = luma(sampledColor.rgb);
* gl_FragColor = vec4(gray, gray, gray, 1);
* }`
* );
* background('RED');
* filter(myShader);
* describe('a canvas becomes gray after being filtered by shader');
* </code>
* </div>
*
* @alt
* black and white image of a brick wall.
* greyscale image of a brickwall
Expand All @@ -468,9 +511,23 @@ p5.prototype._copyHelper = (
* blurry image of a brickwall
* image of a brickwall
* image of a brickwall with less detail
* gray square
*/

/**
* @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._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);
return;
}

if (this.canvas !== undefined) {
Filters.apply(this.canvas, Filters[operation], value);
} else {
Expand Down
104 changes: 104 additions & 0 deletions src/webgl/material.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,110 @@ p5.prototype.createShader = function(vertSrc, fragSrc) {
return new p5.Shader(this._renderer, vertSrc, fragSrc);
};

/**
* Creates a new <a href="#/p5.Shader">p5.Shader</a> object with only a fragment shader, intended for creating image effects on the canvas.
* Like <a href="#/createShader">createShader()</a>, but with a default vertex shader included.
*
* Note:
* - The fragment shader is given a uniform, or variable, called `tex0`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we change the language to clarify that the default input is a texture called tex0?

Something like: "The fragment shader is provided with a single texture input uniform called tex0"

* This is created specificially for filter shaders to access the canvas contents.
*
* - `vTexCoord.y` must be flipped in order for things to render right-side up.
*
* - A filter shader will not apply to a 3D geometry.
*
* - Shaders can only be used in `WEBGL` mode.
*
* For more info about filters and shaders, see Adam Ferriss' <a href="https://github.com/aferriss/p5jsShaderExamples">repo of shader examples</a>
* or the <a href="https://p5js.org/learn/getting-started-in-webgl-shaders.html">introduction to shaders</a> page.
*
* @method createFilterShader
* @param {String} fragSrc source code for the fragment shader
* @returns {p5.Shader} a shader object created from the provided
* fragment shader.
* @example
* <div modernizr='webgl'>
* <code>
* function setup() {
* let fragSrc = `precision highp float;
* void main() {
* gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);
* }`;
*
* createCanvas(100, 100, WEBGL);
* let s = createFilterShader(fragSrc);
* filter(s);
* describe('a yellow square');
* }
* </code>
* </div>
*
* <div modernizr='webgl'>
* <code>
* let img, s;
* function preload() {
* img = loadImage('assets/bricks.jpg');
* }
* function setup() {
* let fragSrc = `precision highp float;
*
* // x,y coordinates, given from the vertex shader
* varying vec2 vTexCoord;
*
* // the canvas contents, given from filter()
* uniform sampler2D tex0;
* // a custom variable from the sketch
* uniform float darkness;
*
* void main() {
* // unflip the y coordinates
* vec2 uv = vTexCoord;
* uv.y = 1.0 - uv.y;
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 preflip the y uv so that the user doesn't need to deal with this?

Copy link
Contributor

@aceslowman aceslowman Jul 21, 2023

Choose a reason for hiding this comment

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

Just curious, would the user be able to understand that this is being done for them? Having difficulty articulating what I mean, but if certain things are done in the vertex shader I wonder whether or not it might leave people new to shaders with some misconceptions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, after trying the pre-flipped vTexCoords.y it for a week I came across passing mouseY as a uniform and forgetting to invert the coordinates. So the user will eventually find out that they have to flip y, and the inconsistency might be confusing between when it's done for them (maybe this one time out of convenience) and when it's not (the rest of the time).

* // get the color at current pixel
* vec4 color = texture2D(tex0, uv);
* // set the output color
* color.b = 1.0;
* color *= darkness;
* gl_FragColor = vec4(color.rgb, 1.0);
* }`;
*
* createCanvas(100, 100, WEBGL);
* s = createFilterShader(fragSrc);
* }
* function draw() {
* image(img, -50, -50);
* s.setUniform('darkness', 0.5);
* filter(s);
* describe('a image of bricks tinted dark blue');
* }
* </code>
* </div>
*/
p5.prototype.createFilterShader = function(fragSrc) {
this._assert3d('createFilterShader');
p5._validateParameters('createFilterShader', arguments);
let defaultVertSrc = `
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;

gl_Position = positionVec4;
}
`;
return new p5.Shader(this._renderer, defaultVertSrc, fragSrc);
};

/**
* Sets the <a href="#/p5.Shader">p5.Shader</a> object to
* be used to render subsequent shapes.
Expand Down
59 changes: 54 additions & 5 deletions src/webgl/p5.RendererGL.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import './p5.Shader';
import './p5.Camera';
import '../core/p5.Renderer';
import './p5.Matrix';
import './p5.Framebuffer';
import { readFileSync } from 'fs';
import { join } from 'path';

Expand Down Expand Up @@ -576,6 +577,10 @@ p5.RendererGL = class RendererGL extends p5.Renderer {
// set of framebuffers in use
this.framebuffers = new Set();

// for post processing step
this.filterShader = undefined;
this.filterGraphicsLayer = undefined;

this.textureMode = constants.IMAGE;
// default wrap settings
this.textureWrapX = constants.CLAMP;
Expand Down Expand Up @@ -872,12 +877,51 @@ p5.RendererGL = class RendererGL extends p5.Renderer {
this.curStrokeJoin = join;
}

filter(filterType) {
// filter can be achieved using custom shaders.
// https://github.com/aferriss/p5jsShaderExamples
// https://itp-xstory.github.io/p5js-shaders/#/
p5._friendlyError('filter() does not work in WEBGL mode');
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.
if (!this.filterGraphicsLayer) {
this.filterGraphicsLayer =
new p5.Graphics(this.width, this.height, constants.WEBGL, this._pInst);
Copy link
Contributor

Choose a reason for hiding this comment

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

Could it not be working with pgraphics because this._pInst is referring to the main sketch?

}
let pg = this.filterGraphicsLayer;

if (typeof args[0] === 'string') {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this the right check? I think we need to know if args[0] is a p5.shader, or a filter constant ( BLUR, INVERT, etc), and if not, we print an error. Aren't the filter constants strings?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that's the check. Although there shouldn't need to be a third case handling errors, since the args get validated in the parent filter() function: pixels.js#L472

Right now the string / filter constant case has an error placeholder. It will be replaced in the next PR that implements the shader versions of filter constants. And the current code handles the p5.Shader case.

// 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 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);
Copy link
Contributor

Choose a reason for hiding this comment

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

If we're supplying a shader created with createFilterShader() can we skip this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed to skip this tiny optimization for now

this.filterShader.parentShader = userShader;
}

// apply shader to pg
pg.shader(this.filterShader);
this.filterShader.setUniform('tex0', this);
pg.rect(0,0,this.width,this.height);

// draw pg contents onto main renderer
this._pInst.push();
this._pInst.noStroke(); // don't draw triangles for plane() geometry
this._pInst.texture(pg);
this._pInst.plane(this.width, this.height);
this._pInst.pop();
}

blendMode(mode) {
if (
mode === constants.DARKEST ||
Expand Down Expand Up @@ -1103,6 +1147,11 @@ p5.RendererGL = class RendererGL extends p5.Renderer {
// can also update their size
framebuffer._canvasSizeChanged();
}

// resize filter graphics layer
if (this.filterGraphicsLayer) {
p5.Renderer.prototype.resize.call(this.filterGraphicsLayer, w, h);
}
}

/**
Expand Down
17 changes: 17 additions & 0 deletions src/webgl/p5.Shader.js
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,16 @@ p5.Shader = class {
* (which run on the GPU) with values from a sketch
* (which runs on the CPU).
*
* Here are some examples of uniforms you can make:
* - booleans
* - Example: `setUniform('x', true)` becomes `uniform float x` with the value `1.0`
* - numbers
* - Example: `setUniform('x', -2)` becomes `uniform float x` with the value `-2.0`
* - arrays of numbers
* - Example: `setUniform('x', [0, 0.5, 1])` becomes `uniform vec3 x` with the value `vec3(0.0, 0.5, 1.0)`
* - a p5.Image, p5.Graphics, p5.MediaElement, or p5.Texture
* - Example: `setUniform('x', img)` becomes `uniform sampler2D x`
*
* @method setUniform
* @chainable
* @param {String} uniformName the name of the uniform.
Expand Down Expand Up @@ -382,6 +392,13 @@ p5.Shader = class {
* canvas toggles between a circular gradient of orange and blue vertically. and a circular gradient of red and green moving horizontally when mouse is clicked/pressed.
*/
setUniform(uniformName, data) {
// detect when to set uniforms on duplicate filter shader copy
let other = this._renderer.filterShader;
if (other !== undefined && other.parentShader === this) {
other.setUniform(uniformName, data);
return;
}

const uniform = this.uniforms[uniformName];
if (!uniform) {
return;
Expand Down
17 changes: 17 additions & 0 deletions test/manual-test-examples/webgl/filter/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<link rel="stylesheet" href="../../styles.css">
<script language="javascript" type="text/javascript" src="../../../../lib/p5.js"></script>
<script language="javascript" type="text/javascript" src="sketch.js"></script>
<script src="../stats.js"></script>
</head>

<body>
</body>

</html>
Loading