Skip to content

Commit bb4fb81

Browse files
authored
Merge pull request #6324 from wong-justin/shader-filters
Add shaders for filter() constants, and use them by default in P2D
2 parents 45ada83 + 121137f commit bb4fb81

File tree

16 files changed

+705
-137
lines changed

16 files changed

+705
-137
lines changed

src/image/filters.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ const Filters = {
308308
},
309309

310310
/**
311-
* reduces the bright areas in an image
311+
* increases the bright areas in an image
312312
* @private
313313
* @param {Canvas} canvas
314314
*/
@@ -395,7 +395,7 @@ const Filters = {
395395
},
396396

397397
/**
398-
* increases the bright areas in an image
398+
* reduces the bright areas in an image
399399
* @private
400400
* @param {Canvas} canvas
401401
*/

src/image/pixels.js

Lines changed: 149 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import p5 from '../core/main';
99
import Filters from './filters';
1010
import '../color/p5.Color';
11+
import * as constants from '../core/constants';
1112

1213
/**
1314
* <a href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference
@@ -325,12 +326,14 @@ p5.prototype._copyHelper = (
325326
* `POSTERIZE`
326327
* Limits each channel of the image to the number of colors specified as the
327328
* parameter. The parameter can be set to values between 2 and 255, but
328-
* results are most noticeable in the lower ranges.
329+
* results are most noticeable in the lower ranges. The default parameter is 4.
329330
*
330331
* `BLUR`
331-
* Executes a Gaussian blur with the level parameter specifying the extent
332+
* Executes a blur with the level parameter specifying the extent
332333
* of the blurring. If no parameter is used, the blur is equivalent to
333-
* Gaussian blur of radius 1. Larger values increase the blur.
334+
* a blur of radius 4. Larger values increase the blur. In P2D mode a
335+
* gaussian blur is performed on the CPU. When in webGL mode, a box blur is
336+
* used instead.
334337
*
335338
* `ERODE`
336339
* Reduces the light areas. No parameter is used.
@@ -340,23 +343,24 @@ p5.prototype._copyHelper = (
340343
*
341344
* ---
342345
*
343-
* In WEBGL mode, `filter()` can also accept a shader. The fragment shader
344-
* is given a `uniform sampler2D` named `tex0` that contains the current
345-
* state of the canvas. For more information on using shaders, check
346-
* <a href="https://p5js.org/learn/getting-started-in-webgl-shaders.html">
347-
* the introduction to shaders</a> tutorial.
346+
* These filter options use WebGL in the background by default (they're faster that way).
347+
* To opt out of this in P2D mode, add a `false` parameter when calling `filter()`.
348+
* This may be useful to keep computation off the GPU or to work around a lack of WebGL support.
349+
*
350+
* On a renderer in WEBGL mode, `filter()` can also accept a user-provided shader.
351+
* For more information, see <a href="#/p5/createFilterShader">createFilterShader()</a>.
348352
*
349-
* See also <a href="https://github.com/aferriss/p5jsShaderExamples"
350-
* target='_blank'>a selection of shader examples</a> by Adam Ferriss
351-
* that contains many similar filter effects.
352353
*
353354
* @method filter
354355
* @param {Constant} filterType either THRESHOLD, GRAY, OPAQUE, INVERT,
355356
* POSTERIZE, BLUR, ERODE, DILATE or BLUR.
356357
* See Filters.js for docs on
357358
* each available filter
358-
* @param {Number} [filterParam] an optional parameter unique
359+
* @param {Number} filterParam an optional parameter unique
359360
* to each filter, see above
361+
* @param {Boolean} [useWebGL] a flag to control whether to use fast
362+
* WebGL filters (GPU) or original image
363+
* filters (CPU); defaults to true
360364
*
361365
* @example
362366
* <div>
@@ -465,39 +469,45 @@ p5.prototype._copyHelper = (
465469
*
466470
* <div>
467471
* <code>
468-
* createCanvas(100, 100, WEBGL);
469-
* let myShader = createShader(
470-
* `attribute vec3 aPosition;
471-
* attribute vec2 aTexCoord;
472+
* let img;
473+
* function preload() {
474+
* img = loadImage('assets/bricks.jpg');
475+
* }
476+
* function setup() {
477+
* image(img, 0, 0);
478+
* filter(BLUR, 3, useWebGL=false);
479+
* }
480+
* </code>
481+
* </div>
472482
*
473-
* varying vec2 vTexCoord;
483+
* <div>
484+
* <code>
485+
* let img, s;
486+
* function preload() {
487+
* img = loadImage('assets/bricks.jpg');
488+
* }
489+
* function setup() {
490+
* let fragSrc = `precision highp float;
474491
*
475-
* void main() {
476-
* vTexCoord = aTexCoord;
477-
* vec4 positionVec4 = vec4(aPosition, 1.0);
478-
* positionVec4.xy = positionVec4.xy * 2.0 - 1.0;
479-
* gl_Position = positionVec4;
480-
* }`,
481-
* `precision mediump float;
482-
* varying mediump vec2 vTexCoord;
483-
*
484-
* uniform sampler2D tex0;
485-
*
486-
* float luma(vec3 color) {
487-
* return dot(color, vec3(0.299, 0.587, 0.114));
488-
* }
492+
* varying vec2 vTexCoord; // x,y coordinates
493+
* uniform sampler2D tex0; // the canvas contents
489494
*
490495
* void main() {
491-
* vec2 uv = vTexCoord;
492-
* uv.y = 1.0 - uv.y;
493-
* vec4 sampledColor = texture2D(tex0, uv);
494-
* float gray = luma(sampledColor.rgb);
495-
* gl_FragColor = vec4(gray, gray, gray, 1);
496-
* }`
497-
* );
498-
* background('RED');
499-
* filter(myShader);
500-
* describe('a canvas becomes gray after being filtered by shader');
496+
* // get the color at current pixel
497+
* vec4 color = texture2D(tex0, vTexCoord);
498+
* // set the output color
499+
* color.b = 1.0;
500+
* gl_FragColor = vec4(color);
501+
* }`;
502+
*
503+
* createCanvas(100, 100, WEBGL);
504+
* s = createFilterShader(fragSrc);
505+
* }
506+
* function draw() {
507+
* image(img, -50, -50);
508+
* filter(s);
509+
* describe('a image of bricks tinted blue');
510+
* }
501511
* </code>
502512
* </div>
503513
*
@@ -514,27 +524,117 @@ p5.prototype._copyHelper = (
514524
* gray square
515525
*/
516526

527+
/**
528+
* @method filter
529+
* @param {Constant} filterType
530+
* @param {Boolean} [useWebGL]
531+
*/
517532
/**
518533
* @method filter
519534
* @param {p5.Shader} shaderFilter A shader that's been loaded, with the
520535
* frag shader using a `tex0` uniform
521536
*/
522-
p5.prototype.filter = function(operation, value) {
537+
p5.prototype.filter = function(...args) {
523538
p5._validateParameters('filter', arguments);
524539

525-
// TODO: use shader filters always, and provide an opt out
526-
if (this._renderer.isP3D) {
527-
p5.RendererGL.prototype.filter.call(this._renderer, arguments);
540+
let { shader, operation, value, useWebGL } = parseFilterArgs(...args);
541+
542+
// when passed a shader, use it directly
543+
if (shader) {
544+
p5.RendererGL.prototype.filter.call(this._renderer, shader);
528545
return;
529546
}
530547

531-
if (this.canvas !== undefined) {
532-
Filters.apply(this.canvas, Filters[operation], value);
533-
} else {
534-
Filters.apply(this.elt, Filters[operation], value);
548+
// when opting out of webgl, use old pixels method
549+
if (!useWebGL && !this._renderer.isP3D) {
550+
if (this.canvas !== undefined) {
551+
Filters.apply(this.canvas, Filters[operation], value);
552+
} else {
553+
Filters.apply(this.elt, Filters[operation], value);
554+
}
555+
return;
556+
}
557+
558+
if(!useWebGL && this._renderer.isP3D) {
559+
console.warn('filter() with useWebGL=false is not supported in WEBGL');
560+
}
561+
562+
// when this is a webgl renderer, apply constant shader filter
563+
if (this._renderer.isP3D) {
564+
p5.RendererGL.prototype.filter.call(this._renderer, operation, value);
565+
}
566+
567+
// when this is P2D renderer, create/use hidden webgl renderer
568+
else {
569+
// create hidden webgl renderer if it doesn't exist
570+
if (!this.filterGraphicsLayer) {
571+
// the real _pInst is buried when this is a secondary p5.Graphics
572+
const pInst =
573+
this._renderer._pInst instanceof p5.Graphics ?
574+
this._renderer._pInst._pInst :
575+
this._renderer._pInst;
576+
577+
// create secondary layer
578+
this.filterGraphicsLayer =
579+
new p5.Graphics(
580+
this.width,
581+
this.height,
582+
constants.WEBGL,
583+
pInst
584+
);
585+
}
586+
587+
// copy p2d canvas contents to secondary webgl renderer
588+
// dest
589+
this.filterGraphicsLayer.copy(
590+
// src
591+
this._renderer,
592+
// src coods
593+
0, 0, this.width, this.height,
594+
// dest coords
595+
-this.width/2, -this.height/2, this.width, this.height
596+
);
597+
598+
// filter it with shaders
599+
this.filterGraphicsLayer.filter(operation, value);
600+
601+
// copy secondary webgl renderer back to original p2d canvas
602+
this._renderer._pInst.image(this.filterGraphicsLayer, 0, 0);
603+
this.filterGraphicsLayer.clear(); // prevent feedback effects on p2d canvas
535604
}
536605
};
537606

607+
function parseFilterArgs(...args) {
608+
// args could be:
609+
// - operation, value, [useWebGL]
610+
// - operation, [useWebGL]
611+
// - shader
612+
613+
let result = {
614+
shader: undefined,
615+
operation: undefined,
616+
value: undefined,
617+
useWebGL: true
618+
};
619+
620+
if (args[0] instanceof p5.Shader) {
621+
result.shader = args[0];
622+
return result;
623+
}
624+
else {
625+
result.operation = args[0];
626+
}
627+
628+
if (args.length > 1 && typeof args[1] === 'number') {
629+
result.value = args[1];
630+
}
631+
632+
if (args[args.length-1] === false) {
633+
result.useWebGL = false;
634+
}
635+
return result;
636+
}
637+
538638
/**
539639
* Get a region of pixels, or a single pixel, from the canvas.
540640
*

src/webgl/material.js

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -187,15 +187,13 @@ p5.prototype.createShader = function(vertSrc, fragSrc) {
187187
* Creates a new <a href="#/p5.Shader">p5.Shader</a> using only a fragment shader, as a convenience method for creating image effects.
188188
* It's like <a href="#/createShader">createShader()</a> but with a default vertex shader included.
189189
*
190-
* <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.
190+
* <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.
191+
* A filter shader will not be applied to any geometries.
191192
*
192-
* Note:
193-
* - The fragment shader is provided with a single texture input uniform called `tex0`.
194-
* This is created specificially for filter shaders to access the canvas contents.
195-
*
196-
* - A filter shader will not apply to a 3D geometry.
197-
*
198-
* - Shaders can only be used in `WEBGL` mode.
193+
* The fragment shader receives some uniforms:
194+
* - `sampler2D tex0`, which contains the canvas contents as a texture
195+
* - `vec2 canvasSize`, which is the width and height of the canvas
196+
* - `vec2 texelSize`, which is the size of a pixel (`1.0/width`, `1.0/height`)
199197
*
200198
* For more info about filters and shaders, see Adam Ferriss' <a href="https://github.com/aferriss/p5jsShaderExamples">repo of shader examples</a>
201199
* or the <a href="https://p5js.org/learn/getting-started-in-webgl-shaders.html">introduction to shaders</a> page.
@@ -235,7 +233,10 @@ p5.prototype.createShader = function(vertSrc, fragSrc) {
235233
*
236234
* // the canvas contents, given from filter()
237235
* uniform sampler2D tex0;
238-
* // a custom variable from the sketch
236+
* // other useful information from the canvas
237+
* uniform vec2 texelSize;
238+
* uniform vec2 canvasSize;
239+
* // a custom variable from this sketch
239240
* uniform float darkness;
240241
*
241242
* void main() {
@@ -262,26 +263,48 @@ p5.prototype.createShader = function(vertSrc, fragSrc) {
262263
p5.prototype.createFilterShader = function(fragSrc) {
263264
this._assert3d('createFilterShader');
264265
p5._validateParameters('createFilterShader', arguments);
265-
let defaultVertSrc = `
266+
let defaultVertV1 = `
267+
uniform mat4 uModelViewMatrix;
268+
uniform mat4 uProjectionMatrix;
269+
266270
attribute vec3 aPosition;
267271
// texcoords only come from p5 to vertex shader
268272
// so pass texcoords on to the fragment shader in a varying variable
269273
attribute vec2 aTexCoord;
270274
varying vec2 vTexCoord;
271-
275+
276+
void main() {
277+
// transferring texcoords for the frag shader
278+
vTexCoord = aTexCoord;
279+
280+
// copy position with a fourth coordinate for projection (1.0 is normal)
281+
vec4 positionVec4 = vec4(aPosition, 1.0);
282+
283+
// project to 3D space
284+
gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
285+
}
286+
`;
287+
let defaultVertV2 = `#version 300 es
288+
uniform mat4 uModelViewMatrix;
289+
uniform mat4 uProjectionMatrix;
290+
291+
in vec3 aPosition;
292+
in vec2 aTexCoord;
293+
out vec2 vTexCoord;
294+
272295
void main() {
273296
// transferring texcoords for the frag shader
274297
vTexCoord = aTexCoord;
275-
298+
276299
// copy position with a fourth coordinate for projection (1.0 is normal)
277300
vec4 positionVec4 = vec4(aPosition, 1.0);
278-
// scale by two and center to achieve correct positioning
279-
positionVec4.xy = positionVec4.xy * 2.0 - 1.0;
280-
281-
gl_Position = positionVec4;
301+
302+
// project to 3D space
303+
gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
282304
}
283305
`;
284-
return new p5.Shader(this._renderer, defaultVertSrc, fragSrc);
306+
let vertSrc = fragSrc.includes('#version 300 es') ? defaultVertV2 : defaultVertV1;
307+
return new p5.Shader(this._renderer, vertSrc, fragSrc);
285308
};
286309

287310
/**

0 commit comments

Comments
 (0)