From 25613011566618c57219c858ca1e7cbfa3bd2e78 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 17 Oct 2023 19:05:51 -0400 Subject: [PATCH] Auto-bind filter shaders to the filter graphic --- src/webgl/material.js | 10 ++-- src/webgl/p5.RendererGL.js | 95 +++++++++++++++++++++----------------- src/webgl/p5.Shader.js | 43 ++++++++++++++--- 3 files changed, 92 insertions(+), 56 deletions(-) diff --git a/src/webgl/material.js b/src/webgl/material.js index 857519d71e..6948826622 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -304,7 +304,9 @@ p5.prototype.createFilterShader = function(fragSrc) { } `; let vertSrc = fragSrc.includes('#version 300 es') ? defaultVertV2 : defaultVertV1; - return new p5.Shader(this._renderer, vertSrc, fragSrc); + const shader = new p5.Shader(this._renderer, vertSrc, fragSrc); + shader.ensureCompiledOnContext(this._renderer.getFilterGraphicsLayer()); + return shader; }; /** @@ -393,11 +395,7 @@ p5.prototype.shader = function(s) { this._assert3d('shader'); p5._validateParameters('shader', arguments); - if (s._renderer === undefined) { - s._renderer = this._renderer; - } - - s.init(); + s.ensureCompiledOnContext(this); if (s.isStrokeShader()) { this._renderer.userStrokeShader = s; diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 077cbc99ee..3542584674 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -975,10 +975,10 @@ p5.RendererGL = class RendererGL extends p5.Renderer { this.curStrokeJoin = join; } - 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. + getFilterGraphicsLayer() { + // Lazily initialize the filter graphics layer. We only do this on demand + // because the graphics layer itself has a p5.RendererGL, which would then + // try to make its own filter layer, infinitely looping. if (!this.filterGraphicsLayer) { // the real _pInst is buried when this is a secondary p5.Graphics @@ -996,17 +996,53 @@ p5.RendererGL = class RendererGL extends p5.Renderer { // geometries/borders on this layer should always be invisible this.filterGraphicsLayer.noStroke(); } - else if( + if ( this.filterGraphicsLayer.width !== this.width || - this.filterGraphicsLayer.height !== this.height || - this.filterGraphicsLayer.pixelDensity() !== this._pInst.pixelDensity() - ){ + this.filterGraphicsLayer.height !== this.height + ) { // Resize the graphics layer this.filterGraphicsLayer.resizeCanvas(this.width, this.height); + } + if ( + this.filterGraphicsLayer.pixelDensity() !== this._pInst.pixelDensity() + ) { this.filterGraphicsLayer.pixelDensity(this._pInst.pixelDensity()); } + return this.filterGraphicsLayer; + } - let pg = this.filterGraphicsLayer; + getFilterGraphicsLayerTemp() { + // two-pass blur filter needs another graphics layer + if (!this.filterGraphicsLayerTemp) { + const pInst = this._pInst instanceof p5.Graphics ? + this._pInst._pInst : this._pInst; + // create secondary layer + this.filterGraphicsLayerTemp = + new p5.Graphics( + this.width, + this.height, + constants.WEBGL, + pInst + ); + this.filterGraphicsLayerTemp.noStroke(); + } + if ( + this.filterGraphicsLayerTemp.width !== this.width || + this.filterGraphicsLayerTemp.height !== this.height + ) { + // Resize the graphics layer + this.filterGraphicsLayerTemp.resizeCanvas(this.width, this.height); + } + if ( + this.filterGraphicsLayerTemp.pixelDensity() !== this._pInst.pixelDensity() + ) { + this.filterGraphicsLayerTemp.pixelDensity(this._pInst.pixelDensity()); + } + return this.filterGraphicsLayerTemp; + } + + filter(...args) { + let pg = this.getFilterGraphicsLayer(); // use internal shader for filter constants BLUR, INVERT, etc let filterParameter = undefined; @@ -1030,41 +1066,13 @@ p5.RendererGL = class RendererGL extends p5.Renderer { filterShaderVert, filterShaderFrags[operation] ); - - // two-pass blur filter needs another graphics layer - if(!this.filterGraphicsLayerTemp) { - const pInst = this._pInst instanceof p5.Graphics ? - this._pInst._pInst : this._pInst; - // create secondary layer - this.filterGraphicsLayerTemp = - new p5.Graphics( - this.width, - this.height, - constants.WEBGL, - pInst - ); - this.filterGraphicsLayerTemp.noStroke(); - } } this.filterShader = this.defaultFilterShaders[operation]; } // 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; - } + this.filterShader = args[0]; } pg.clear(); // prevent undesirable feedback effects accumulating secretly @@ -1075,14 +1083,15 @@ p5.RendererGL = class RendererGL extends p5.Renderer { // apply blur shader with multiple passes if (operation === constants.BLUR) { - this.filterGraphicsLayerTemp.clear(); // prevent feedback effects here too + const tmp = this.getFilterGraphicsLayerTemp(); + tmp.clear(); // prevent feedback effects here too // setup this._pInst.push(); this._pInst.noStroke(); // draw main to temp buffer - this.filterGraphicsLayerTemp.image(this, -this.width/2, -this.height/2); + tmp.image(this, -this.width/2, -this.height/2); pg.shader(this.filterShader); this.filterShader.setUniform('texelSize', texelSize); @@ -1091,15 +1100,15 @@ p5.RendererGL = class RendererGL extends p5.Renderer { // horiz pass this.filterShader.setUniform('direction', [1, 0]); - this.filterShader.setUniform('tex0', this.filterGraphicsLayerTemp); + this.filterShader.setUniform('tex0', tmp); pg.rect(-this.width/2, -this.height/2, this.width, this.height); // read back to temp buffer - this.filterGraphicsLayerTemp.image(pg, -this.width/2, -this.height/2); + tmp.image(pg, -this.width/2, -this.height/2); // vert pass this.filterShader.setUniform('direction', [0, 1]); - this.filterShader.setUniform('tex0', this.filterGraphicsLayerTemp); + this.filterShader.setUniform('tex0', tmp); pg.rect(-this.width/2, -this.height/2, this.width, this.height); this._pInst.pop(); diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 170c3f9954..587247cd1f 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -103,6 +103,42 @@ p5.Shader = class { return this; } + /** + * Shaders belong to the main canvas or a p5.Graphics. Once they are compiled, + * they can only be used in the context they were compiled on. + * + * Use this method to make a new copy of a shader that gets compiled on a + * different context. + * + * @method copyToContext + * @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. + */ + copyToContext(context) { + const shader = new p5.Shader( + context._renderer, + this._vertSrc, + this._fragSrc + ); + shader.ensureCompiledOnContext(context); + return shader; + } + + /** + * @private + */ + ensureCompiledOnContext(context) { + if (this._glProgram !== 0 && this._renderer !== context._renderer) { + throw new Error( + 'The shader being run is attached to a different context. Do you need to copy it to this context first with .copyToContext()?' + ); + } else if (this._glProgram === 0) { + this._renderer = context._renderer; + this.init(); + } + } + /** * Queries the active attributes for this shader and loads * their names and locations into the attributes array. @@ -392,13 +428,6 @@ 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;