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 47 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
190 changes: 142 additions & 48 deletions src/image/pixels.js
Expand Up @@ -8,6 +8,7 @@
import p5 from '../core/main';
import Filters from './filters';
import '../color/p5.Color';
import * as constants from '../core/constants';

/**
* <a href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference
Expand Down Expand Up @@ -325,12 +326,12 @@ p5.prototype._copyHelper = (
* `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.
* results are most noticeable in the lower ranges. The default parameter is 4.
*
* `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.
* Gaussian blur of radius 4. Larger values increase the blur.
*
* `ERODE`
* Reduces the light areas. No parameter is used.
Expand All @@ -340,23 +341,24 @@ p5.prototype._copyHelper = (
*
* ---
*
* 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.
* These filter options use WebGL in the background by default (they're faster that way).
* To opt out of this in P2D mode, add a `false` parameter when calling `filter()`.
* This may be useful to keep computation off the GPU or to work around a lack of WebGL support.
*
* On a renderer in WEBGL mode, `filter()` can also accept a user-provided shader.
* For more information, see <a href="#/p5/createFilterShader">createFilterShader()</a>.
*
* 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,
* 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 @@ -465,39 +467,45 @@ p5.prototype._copyHelper = (
*
* <div>
* <code>
* createCanvas(100, 100, WEBGL);
* let myShader = createShader(
* `attribute vec3 aPosition;
* attribute vec2 aTexCoord;
* let img;
* function preload() {
* img = loadImage('assets/bricks.jpg');
* }
* function setup() {
* image(img, 0, 0);
* filter(BLUR, 3, useWebGL=false);
* }
* </code>
* </div>
*
* <div>
* <code>
* let img, s;
* function preload() {
* img = loadImage('assets/bricks.jpg');
* }
* function setup() {
* let fragSrc = `precision highp float;
*
* varying vec2 vTexCoord;
* varying vec2 vTexCoord; // x,y coordinates
* uniform sampler2D tex0; // the canvas contents
*
* 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));
* }
* // get the color at current pixel
* vec4 color = texture2D(tex0, vTexCoord);
* // set the output color
* color.b = 1.0;
* gl_FragColor = vec4(color);
* }`;
*
* 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');
* createCanvas(100, 100, WEBGL);
* s = createFilterShader(fragSrc);
* }
* function draw() {
* image(img, -50, -50);
* filter(s);
* describe('a image of bricks tinted blue');
* }
* </code>
* </div>
*
Expand All @@ -514,27 +522,113 @@ 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 {
// create hidden webgl renderer if it doesn't exist
if (!this.filterGraphicsLayer) {
// the real _pInst is buried when this is a secondary p5.Graphics
const pInst =
this._renderer._pInst instanceof p5.Graphics ?
this._renderer._pInst._pInst :
this._renderer._pInst;

// create secondary layer
this.filterGraphicsLayer =
new p5.Graphics(
this.width,
this.height,
constants.WEBGL,
pInst
);
}

// copy p2d canvas contents to secondary webgl renderer
// dest
this.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
);

// filter it with shaders
this.filterGraphicsLayer.filter(operation, value);

// copy secondary webgl renderer back to original p2d canvas
this._renderer._pInst.image(this.filterGraphicsLayer, 0, 0);
this.filterGraphicsLayer.clear(); // prevent feedback effects on p2d canvas
}
};

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
57 changes: 40 additions & 17 deletions src/webgl/material.js
Expand Up @@ -187,15 +187,13 @@ p5.prototype.createShader = function(vertSrc, fragSrc) {
* Creates a new <a href="#/p5.Shader">p5.Shader</a> using only a fragment shader, as a convenience method for creating image effects.
* It's like <a href="#/createShader">createShader()</a> but with a default vertex shader included.
*
* <a href="#/createFilterShader">createFilterShader()</a> is intended to be used along with <a href="#/filter">filter()</a> for filtering the entire contents of a canvas in WebGL mode.
* <a href="#/createFilterShader">createFilterShader()</a> is intended to be used along with <a href="#/filter">filter()</a> for filtering the contents of a canvas in WebGL mode.
* A filter shader will not be applied to any geometries.
*
* Note:
* - 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.
*
* - A filter shader will not apply to a 3D geometry.
*
* - Shaders can only be used in `WEBGL` mode.
* The fragment shader receives some uniforms:
* - `sampler2D tex0`, which contains the canvas contents as a texture
* - `vec2 canvasSize`, which is the width and height of the canvas
* - `vec2 texelSize`, which is the size of a pixel (`1.0/width`, `1.0/height`)
*
* 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.
Expand Down Expand Up @@ -235,7 +233,10 @@ p5.prototype.createShader = function(vertSrc, fragSrc) {
*
* // the canvas contents, given from filter()
* uniform sampler2D tex0;
* // a custom variable from the sketch
* // other useful information from the canvas
* uniform vec2 texelSize;
* uniform vec2 canvasSize;
* // a custom variable from this sketch
* uniform float darkness;
*
* void main() {
Expand All @@ -262,26 +263,48 @@ p5.prototype.createShader = function(vertSrc, fragSrc) {
p5.prototype.createFilterShader = function(fragSrc) {
this._assert3d('createFilterShader');
p5._validateParameters('createFilterShader', arguments);
let defaultVertSrc = `
let defaultVertV1 = `
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;

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

// project to 3D space
gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
}
`;
let defaultVertV2 = `#version 300 es
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;

in vec3 aPosition;
in vec2 aTexCoord;
out 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;

// project to 3D space
gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
}
`;
return new p5.Shader(this._renderer, defaultVertSrc, fragSrc);
let vertSrc = fragSrc.includes('#version 300 es') ? defaultVertV2 : defaultVertV1;
return new p5.Shader(this._renderer, vertSrc, fragSrc);
};

/**
Expand Down