diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 7935255b49..cd3daf823f 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -18,6 +18,28 @@ class Renderer2D extends p5.Renderer{ this._pInst._setProperty('drawingContext', this.drawingContext); } + getFilterGraphicsLayer() { + // 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._pInst instanceof p5.Graphics ? + this._pInst._pInst : + this._pInst; + + // create secondary layer + this.filterGraphicsLayer = + new p5.Graphics( + this.width, + this.height, + constants.WEBGL, + pInst + ); + } + + return this.filterGraphicsLayer; + } + _applyDefaults() { this._cachedFillStyle = this._cachedStrokeStyle = undefined; this._cachedBlendMode = constants.BLEND; diff --git a/src/image/pixels.js b/src/image/pixels.js index eef7fb5171..93ae5b84ac 100644 --- a/src/image/pixels.js +++ b/src/image/pixels.js @@ -8,7 +8,6 @@ import p5 from '../core/main'; import Filters from './filters'; import '../color/p5.Color'; -import * as constants from '../core/constants'; /** * An array containing the color of each pixel on the canvas. Colors are @@ -544,6 +543,15 @@ p5.prototype._copyHelper = ( * */ +/** + * @method getFilterGraphicsLayer + * @private + * @returns {p5.Graphics} + */ +p5.prototype.getFilterGraphicsLayer = function() { + return this._renderer.getFilterGraphicsLayer(); +}; + /** * @method filter * @param {Constant} filterType @@ -560,7 +568,7 @@ p5.prototype.filter = function(...args) { let { shader, operation, value, useWebGL } = parseFilterArgs(...args); // when passed a shader, use it directly - if (shader) { + if (this._renderer.isP3D && shader) { p5.RendererGL.prototype.filter.call(this._renderer, shader); return; } @@ -586,27 +594,11 @@ p5.prototype.filter = function(...args) { // 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 - ); - } + const filterGraphicsLayer = this.getFilterGraphicsLayer(); // copy p2d canvas contents to secondary webgl renderer // dest - this.filterGraphicsLayer.copy( + filterGraphicsLayer.copy( // src this._renderer, // src coods @@ -617,11 +609,11 @@ p5.prototype.filter = function(...args) { //clearing the main canvas this._renderer.clear(); // filter it with shaders - this.filterGraphicsLayer.filter(operation, value); + filterGraphicsLayer.filter(...args); // 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 + this._renderer._pInst.image(filterGraphicsLayer, 0, 0); + filterGraphicsLayer.clear(); // prevent feedback effects on p2d canvas } }; diff --git a/src/webgl/material.js b/src/webgl/material.js index 849e901a63..822a861112 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -204,13 +204,13 @@ p5.prototype.createShader = function(vertSrc, fragSrc) { * Creates a new p5.Shader using only a fragment shader, as a convenience method for creating image effects. * It's like createShader() but with a default vertex shader included. * - * createFilterShader() is intended to be used along with filter() for filtering the contents of a canvas in WebGL mode. + * createFilterShader() is intended to be used along with filter() for filtering the contents of a canvas. * A filter shader will not be applied to any geometries. * * 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`) + * - `vec2 canvasSize`, which is the p5 width and height of the canvas (not including pixel density) + * - `vec2 texelSize`, which is the size of a physical pixel including pixel density (`1.0/(width*density)`, `1.0/(height*density)`) * * For more info about filters and shaders, see Adam Ferriss' repo of shader examples * or the introduction to shaders page. @@ -278,7 +278,6 @@ p5.prototype.createShader = function(vertSrc, fragSrc) { * */ p5.prototype.createFilterShader = function(fragSrc) { - this._assert3d('createFilterShader'); p5._validateParameters('createFilterShader', arguments); let defaultVertV1 = ` uniform mat4 uModelViewMatrix; @@ -322,7 +321,17 @@ p5.prototype.createFilterShader = function(fragSrc) { `; let vertSrc = fragSrc.includes('#version 300 es') ? defaultVertV2 : defaultVertV1; const shader = new p5.Shader(this._renderer, vertSrc, fragSrc); - shader.ensureCompiledOnContext(this._renderer.getFilterGraphicsLayer()); + if (this._renderer.isP3D) { + shader.ensureCompiledOnContext(this._renderer.getFilterGraphicsLayer()); + } else { + // In 2D mode, the image is copied to a WebGL p5.Graphics, and then the + // filter is applied there, which has its own graphic for running the + // shader. This may be simplified in the future by using framebuffers on + // a WebGL canvas instead of separate graphics. + shader.ensureCompiledOnContext( + this._renderer.getFilterGraphicsLayer().getFilterGraphicsLayer() + ); + } return shader; }; diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 587247cd1f..0366df75e4 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -114,6 +114,18 @@ p5.Shader = class { * @param {p5|p5.Graphics} context The graphic or instance to copy this shader to. * Pass `window` if you need to copy to the main canvas. * @returns {p5.Shader} A new shader on the target context. + * + * @example + *
+ * + * let graphic = createGraphics(200, 200, WEBGL); + * let graphicShader = graphic.createShader(vert, frag); + * graphic.shader(graphicShader); // Use graphicShader on the graphic + * + * let mainShader = graphicShader.copyToContext(window); + * shader(mainShader); // Use `mainShader` on the main canvas + * + *
*/ copyToContext(context) { const shader = new p5.Shader( diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 216a89cd5e..0f2d4869f0 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -13,7 +13,7 @@ suite('p5.RendererGL', function() { }); teardown(function() { - //myp5.remove(); + myp5.remove(); }); suite('createCanvas(w, h, WEBGL)', function() { @@ -163,6 +163,44 @@ suite('p5.RendererGL', function() { }; }); + suite('custom shaders', function() { + function testFilterShader(target) { + const fragSrc = `precision highp float; + void main() { + gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0); + }`; + const s = target.createFilterShader(fragSrc); + target.filter(s); + target.loadPixels(); + assert.deepEqual( + target.get(target.width/2, target.height/2), + [255, 255, 0, 255] + ); + } + + test('work with a 2D main canvas', function() { + myp5.createCanvas(10, 10); + testFilterShader(myp5); + }); + + test('work with a WebGL main canvas', function() { + myp5.createCanvas(10, 10, myp5.WEBGL); + testFilterShader(myp5); + }); + + test('work with a 2D graphic', function() { + myp5.createCanvas(10, 10); + const graphic = myp5.createGraphics(10, 10); + testFilterShader(graphic); + }); + + test('work with a WebGL graphic', function() { + myp5.createCanvas(10, 10); + const graphic = myp5.createGraphics(10, 10, myp5.WEBGL); + testFilterShader(graphic); + }); + }); + test('filter accepts correct params', function() { myp5.createCanvas(5, 5, myp5.WEBGL); let s = myp5.createShader(vert, frag); @@ -293,13 +331,13 @@ suite('p5.RendererGL', function() { test('filter() uses WEBGL implementation behind main P2D canvas', function() { let renderer = myp5.createCanvas(3,3); myp5.filter(myp5.BLUR); - assert.isDefined(renderer._pInst.filterGraphicsLayer); + assert.isDefined(renderer.filterGraphicsLayer); }); test('filter() can opt out of WEBGL implementation', function() { let renderer = myp5.createCanvas(3,3); myp5.filter(myp5.BLUR, useWebGL=false); - assert.isUndefined(renderer._pInst.filterGraphicsLayer); + assert.isUndefined(renderer.filterGraphicsLayer); }); test('filters make changes to canvas', function() {