diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index 920184d065..344e6810b2 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -3434,12 +3434,14 @@ p5.RendererGL.prototype.image = function( v1 = (sy + sHeight) / img.height; } + this._drawingImage = true; this.beginShape(); this.vertex(dx, dy, 0, u0, v0); this.vertex(dx + dWidth, dy, 0, u1, v0); this.vertex(dx + dWidth, dy + dHeight, 0, u1, v1); this.vertex(dx, dy + dHeight, 0, u0, v1); this.endShape(constants.CLOSE); + this._drawingImage = false; this._pInst.pop(); diff --git a/src/webgl/material.js b/src/webgl/material.js index 642a40b3fd..5e86b877d1 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -48,9 +48,9 @@ import './p5.Texture'; * @method loadShader * @param {String} vertFilename path of the vertex shader to be loaded. * @param {String} fragFilename path of the fragment shader to be loaded. - * @param {function} [successCallback] function to call once the shader is loaded. Can be passed the + * @param {Function} [successCallback] function to call once the shader is loaded. Can be passed the * p5.Shader object. - * @param {function} [failureCallback] function to call if the shader fails to load. Can be passed an + * @param {Function} [failureCallback] function to call if the shader fails to load. Can be passed an * `Error` event object. * @return {p5.Shader} new shader created from the vertex and fragment shader files. * @@ -693,12 +693,8 @@ p5.prototype.createFilterShader = function (fragSrc) { * * The parameter, `s`, is the p5.Shader object to * apply. For example, calling `shader(myShader)` applies `myShader` to - * process each pixel on the canvas. The shader will be used for: - * - Fills when a texture is enabled if it includes a uniform `sampler2D`. - * - Fills when lights are enabled if it includes the attribute `aNormal`, or if it has any of the following uniforms: `uUseLighting`, `uAmbientLightCount`, `uDirectionalLightCount`, `uPointLightCount`, `uAmbientColor`, `uDirectionalDiffuseColors`, `uDirectionalSpecularColors`, `uPointLightLocation`, `uPointLightDiffuseColors`, `uPointLightSpecularColors`, `uLightingDirection`, or `uSpecular`. - * - Fills whenever there are no lights or textures. - * - Strokes if it includes the uniform `uStrokeWeight`. - * + * process each pixel on the canvas. This only changes the fill (the inner part of shapes), + * but does not affect the outlines (strokes) or any images drawn using the `image()` function. * The source code from a p5.Shader object's * fragment and vertex shaders will be compiled the first time it's passed to * `shader()`. See @@ -710,6 +706,17 @@ p5.prototype.createFilterShader = function (fragSrc) { * * Note: Shaders can only be used in WebGL mode. * + *
+ *

+ * + * If you want to apply shaders to strokes or images, use the following methods: + * - **[strokeShader()](#/p5/strokeShader)**: Applies a shader to the stroke (outline) of shapes, allowing independent control over the stroke rendering using shaders. + * - **[imageShader()](#/p5/imageShader)**: Applies a shader to images or textures, controlling how the shader modifies their appearance during rendering. + * + *

+ *
+ * + * * @method shader * @chainable * @param {p5.Shader} s p5.Shader object @@ -718,171 +725,492 @@ p5.prototype.createFilterShader = function (fragSrc) { * @example *
* - * // Note: A "uniform" is a global variable within a shader program. + * let fillShader; * - * // Create a string with the vertex shader program. - * // The vertex shader is called for each vertex. * let vertSrc = ` * precision highp float; + * attribute vec3 aPosition; * uniform mat4 uModelViewMatrix; * uniform mat4 uProjectionMatrix; - * - * attribute vec3 aPosition; - * attribute vec2 aTexCoord; - * varying vec2 vTexCoord; + * varying vec3 vPosition; * * void main() { - * vTexCoord = aTexCoord; - * vec4 positionVec4 = vec4(aPosition, 1.0); - * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; + * vPosition = aPosition; + * gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aPosition, 1.0); * } * `; * - * // Create a string with the fragment shader program. - * // The fragment shader is called for each pixel. * let fragSrc = ` * precision highp float; + * uniform vec3 uLightDir; + * varying vec3 vPosition; * * void main() { - * // Set each pixel's RGBA value to yellow. - * gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0); + * vec3 lightDir = normalize(uLightDir); + * float brightness = dot(lightDir, normalize(vPosition)); + * brightness = clamp(brightness, 0.4, 1.0); + * vec3 color = vec3(0.3, 0.5, 1.0); + * color = color * brightness * 3.0; + * gl_FragColor = vec4(color, 1.0); * } * `; * * function setup() { * createCanvas(100, 100, WEBGL); + * fillShader = createShader(vertSrc, fragSrc); + * noStroke(); + * describe('A rotating torus with simulated directional lighting.'); + * } * - * // Create a p5.Shader object. - * let shaderProgram = createShader(vertSrc, fragSrc); + * function draw() { + * background(20, 20, 40); + * let lightDir = [0.5, 0.5, -1.0]; + * fillShader.setUniform('uLightDir', lightDir); + * shader(fillShader); + * rotateY(frameCount * 0.02); + * rotateX(frameCount * 0.02); + * //lights(); + * torus(25, 10, 30, 30); + * } + * + *
* - * // Apply the p5.Shader object. - * shader(shaderProgram); + * @example + *
+ * + * let fillShader; * - * // Style the drawing surface. + * let vertSrc = ` + * precision highp float; + * attribute vec3 aPosition; + * uniform mat4 uProjectionMatrix; + * uniform mat4 uModelViewMatrix; + * varying vec3 vPosition; + * void main() { + * vPosition = aPosition; + * gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aPosition, 1.0); + * } + * `; + * + * let fragSrc = ` + * precision highp float; + * uniform vec3 uLightPos; + * uniform vec3 uFillColor; + * varying vec3 vPosition; + * void main() { + * float brightness = dot(normalize(uLightPos), normalize(vPosition)); + * brightness = clamp(brightness, 0.0, 1.0); + * vec3 color = uFillColor * brightness; + * gl_FragColor = vec4(color, 1.0); + * } + * `; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * fillShader = createShader(vertSrc, fragSrc); + * shader(fillShader); * noStroke(); + * describe('A square affected by both fill color and lighting, with lights controlled by mouse.'); + * } * - * // Add a plane as a drawing surface. + * function draw() { + * let lightPos = [(mouseX - width / 2) / width, + * (mouseY - height / 2) / height, 1.0]; + * fillShader.setUniform('uLightPos', lightPos); + * let fillColor = [map(mouseX, 0, width, 0, 1), + * map(mouseY, 0, height, 0, 1), 0.5]; + * fillShader.setUniform('uFillColor', fillColor); * plane(100, 100); + * } + * + *
* - * describe('A yellow square.'); + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * myShader = baseMaterialShader().modify({ + * declarations: 'uniform float time;', + * 'vec4 getFinalColor': `(vec4 color) { + * float r = 0.2 + 0.5 * abs(sin(time + 0.0)); + * float g = 0.2 + 0.5 * abs(sin(time + 1.0)); + * float b = 0.2 + 0.5 * abs(sin(time + 2.0)); + * color.rgb = vec3(r, g, b); + * return color; + * }` + * }); + * + * noStroke(); + * describe('A 3D cube with dynamically changing colors on a beige background.'); + * } + * + * function draw() { + * background(245, 245, 220); + * shader(myShader); + * myShader.setUniform('time', millis() / 1000.0); + * + * box(50); * } * *
+ */ + +p5.prototype.shader = function (s) { + this._assert3d('shader'); + p5._validateParameters('shader', arguments); + + s.ensureCompiledOnContext(this); + + // Always set the shader as a fill shader + this._renderer.userFillShader = s; + this._renderer._useNormalMaterial = false; + s.setDefaultUniforms(); + + return this; +}; + +/** + * Sets the p5.Shader object to apply for strokes. * - *
+ * This method applies the given shader to strokes, allowing customization of + * how lines and outlines are drawn in 3D space. The shader will be used for + * strokes until resetShader() is called or another + * strokeShader is applied. + * + * The shader will be used for: + * - Strokes only, regardless of whether the uniform `uStrokeWeight` is present. + * + * To further customize its behavior, refer to the various hooks provided by + * the baseStrokeShader() method, which allow + * control over stroke weight, vertex positions, colors, and more. + * + * @method strokeShader + * @chainable + * @param {p5.Shader} s p5.Shader object + * to apply for strokes. + * + * @example + *
* - * // Note: A "uniform" is a global variable within a shader program. + * let animatedStrokeShader; * - * let mandelbrot; + * let vertSrc = ` + * precision mediump int; * - * // Load the shader and create a p5.Shader object. - * function preload() { - * mandelbrot = loadShader('assets/shader.vert', 'assets/shader.frag'); + * uniform mat4 uModelViewMatrix; + * uniform mat4 uProjectionMatrix; + * uniform float uStrokeWeight; + * + * uniform bool uUseLineColor; + * uniform vec4 uMaterialColor; + * + * uniform vec4 uViewport; + * uniform int uPerspective; + * uniform int uStrokeJoin; + * + * attribute vec4 aPosition; + * attribute vec3 aTangentIn; + * attribute vec3 aTangentOut; + * attribute float aSide; + * attribute vec4 aVertexColor; + * + * void main() { + * vec4 posp = uModelViewMatrix * aPosition; + * vec4 posqIn = uModelViewMatrix * (aPosition + vec4(aTangentIn, 0)); + * vec4 posqOut = uModelViewMatrix * (aPosition + vec4(aTangentOut, 0)); + * + * float facingCamera = pow( + * abs(normalize(posqIn-posp).z), + * 0.25 + * ); + * + * float scale = mix(1., 0.995, facingCamera); + * + * posp.xyz = posp.xyz * scale; + * posqIn.xyz = posqIn.xyz * scale; + * posqOut.xyz = posqOut.xyz * scale; + * + * vec4 p = uProjectionMatrix * posp; + * vec4 qIn = uProjectionMatrix * posqIn; + * vec4 qOut = uProjectionMatrix * posqOut; + * + * vec2 tangentIn = normalize((qIn.xy*p.w - p.xy*qIn.w) * uViewport.zw); + * vec2 tangentOut = normalize((qOut.xy*p.w - p.xy*qOut.w) * uViewport.zw); + * + * vec2 curPerspScale; + * if(uPerspective == 1) { + * curPerspScale = (uProjectionMatrix * vec4(1, sign(uProjectionMatrix[1][1]), 0, 0)).xy; + * } else { + * curPerspScale = p.w / (0.5 * uViewport.zw); + * } + * + * vec2 offset; + * vec2 tangent = aTangentIn == vec3(0.) ? tangentOut : tangentIn; + * vec2 normal = vec2(-tangent.y, tangent.x); + * float normalOffset = sign(aSide); + * float tangentOffset = abs(aSide) - 1.; + * offset = (normal * normalOffset + tangent * tangentOffset) * + * uStrokeWeight * 0.5; + * + * gl_Position.xy = p.xy + offset.xy * curPerspScale; + * gl_Position.zw = p.zw; + * } + * `; + * + * let fragSrc = ` + * precision mediump float; + * uniform float uTime; + * + * void main() { + * float wave = sin(gl_FragCoord.x * 0.1 + uTime) * 0.5 + 0.5; + * gl_FragColor = vec4(wave, 0.5, 1.0, 1.0); // Animated color based on time * } + * `; * * function setup() { * createCanvas(100, 100, WEBGL); + * animatedStrokeShader = createShader(vertSrc, fragSrc); + * strokeShader(animatedStrokeShader); + * strokeWeight(4); * - * // Use the p5.Shader object. - * shader(mandelbrot); + * describe('A hollow cube rotating continuously with its stroke colors changing dynamically over time against a static gray background.'); + * } * - * // Set the shader uniform p to an array. - * mandelbrot.setUniform('p', [-0.74364388703, 0.13182590421]); + * function draw() { + * animatedStrokeShader.setUniform('uTime', millis() / 1000.0); + * background(250); + * rotateY(frameCount * 0.02); + * noFill(); + * orbitControl(); + * box(50); + * } + * + *
* - * describe('A fractal image zooms in and out of focus.'); + * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseStrokeShader().modify({ + * 'float random': `(vec2 p) { + * vec3 p3 = fract(vec3(p.xyx) * 0.1471); + * p3 += dot(p3, p3.yzx + 32.33); + * return fract((p3.x + p3.y) * p3.z); + * }`, + * 'Inputs getPixelInputs': `(Inputs inputs) { + * // Modify alpha with dithering effect + * float a = inputs.color.a; + * inputs.color.a = 1.0; + * inputs.color *= random(inputs.position.xy) > a ? 0.0 : 1.0; + * return inputs; + * }` + * }); * } * * function draw() { - * // Set the shader uniform r to a value that oscillates between 0 and 2. - * mandelbrot.setUniform('r', sin(frameCount * 0.01) + 1); - * - * // Add a quad as a display surface for the shader. - * quad(-1, -1, 1, -1, 1, 1, -1, 1); + * background(255); + * strokeShader(myShader); + * strokeWeight(12); + * beginShape(); + * for (let i = 0; i <= 50; i++) { + * stroke( + * map(i, 0, 50, 150, 255), + * 100 + 155 * sin(i / 5), + * 255 * map(i, 0, 50, 1, 0) + * ); + * vertex( + * map(i, 0, 50, 1, -1) * width / 3, + * 50 * cos(i / 10 + frameCount / 80) + * ); + * } + * endShape(); * } * *
+ */ +p5.prototype.strokeShader = function (s) { + this._assert3d('strokeShader'); + p5._validateParameters('strokeShader', arguments); + + s.ensureCompiledOnContext(this); + + this._renderer.userStrokeShader = s; + + return this; +}; + + +/** + * Sets the p5.Shader object to apply for images. * - *
- * - * // Note: A "uniform" is a global variable within a shader program. + * This method allows the user to apply a custom shader to images, enabling + * advanced visual effects such as pixel manipulation, color adjustments, + * or dynamic behavior. The shader will be applied to the image drawn using + * the image() function. + * + * The shader will be used exclusively for: + * - `image()` calls, applying only when drawing 2D images. + * - This shader will NOT apply to images used in texture() or other 3D contexts. + * Any attempts to use the imageShader in these cases will be ignored. * - * let redGreen; - * let orangeBlue; - * let showRedGreen = false; + * @method imageShader + * @chainable + * @param {p5.Shader} s p5.Shader object + * to apply for images. + * + * @example + *
+ * + * let img; + * let imgShader; * - * // Load the shader and create two separate p5.Shader objects. * function preload() { - * redGreen = loadShader('assets/shader.vert', 'assets/shader-gradient.frag'); - * orangeBlue = loadShader('assets/shader.vert', 'assets/shader-gradient.frag'); + * img = loadImage('assets/outdoor_image.jpg'); * } * * function setup() { * createCanvas(100, 100, WEBGL); + * noStroke(); * - * // Initialize the redGreen shader. - * shader(redGreen); - * - * // Set the redGreen shader's center and background color. - * redGreen.setUniform('colorCenter', [1.0, 0.0, 0.0]); - * redGreen.setUniform('colorBackground', [0.0, 1.0, 0.0]); - * - * // Initialize the orangeBlue shader. - * shader(orangeBlue); - * - * // Set the orangeBlue shader's center and background color. - * orangeBlue.setUniform('colorCenter', [1.0, 0.5, 0.0]); - * orangeBlue.setUniform('colorBackground', [0.226, 0.0, 0.615]); + * imgShader = createShader(` + * precision mediump float; + * attribute vec3 aPosition; + * attribute vec2 aTexCoord; + * varying vec2 vTexCoord; + * uniform mat4 uModelViewMatrix; + * uniform mat4 uProjectionMatrix; + * + * void main() { + * vTexCoord = aTexCoord; + * gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aPosition, 1.0); + * } + * `, ` + * precision mediump float; + * varying vec2 vTexCoord; + * uniform sampler2D uTexture; + * uniform vec2 uMousePos; + * + * void main() { + * vec4 texColor = texture2D(uTexture, vTexCoord); + * // Adjust the color based on mouse position + * float r = uMousePos.x * texColor.r; + * float g = uMousePos.y * texColor.g; + * gl_FragColor = vec4(r, g, texColor.b, texColor.a); + * } + * `); * * describe( - * 'The scene toggles between two circular gradients when the user double-clicks. An orange and blue gradient vertically, and red and green gradient moves horizontally.' + * 'An image on a gray background where the colors change based on the mouse position.' * ); * } * * function draw() { - * // Update the offset values for each shader. - * // Move orangeBlue vertically. - * // Move redGreen horizontally. - * orangeBlue.setUniform('offset', [0, sin(frameCount * 0.01) + 1]); - * redGreen.setUniform('offset', [sin(frameCount * 0.01), 1]); - * - * if (showRedGreen === true) { - * shader(redGreen); - * } else { - * shader(orangeBlue); - * } + * background(220); * - * // Style the drawing surface. + * imageShader(imgShader); + * + * // Map the mouse position to a range between 0 and 1 + * let mousePosX = map(mouseX, 0, width, 0, 1); + * let mousePosY = map(mouseY, 0, height, 0, 1); + * + * // Pass the mouse position to the shader as a uniform + * imgShader.setUniform('uMousePos', [mousePosX, mousePosY]); + * + * // Bind the image texture to the shader + * imgShader.setUniform('uTexture', img); + * + * image(img, -width / 2, -height / 2, width, height); + * } + * + * + *
+ * + * @example + *
+ * + * let img; + * let imgShader; + * + * function preload() { + * img = loadImage('assets/outdoor_image.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); * noStroke(); * - * // Add a quad as a drawing surface. - * quad(-1, -1, 1, -1, 1, 1, -1, 1); + * imgShader = createShader(` + * precision mediump float; + * attribute vec3 aPosition; + * attribute vec2 aTexCoord; + * varying vec2 vTexCoord; + * uniform mat4 uModelViewMatrix; + * uniform mat4 uProjectionMatrix; + * + * void main() { + * vTexCoord = aTexCoord; + * gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aPosition, 1.0); + * } + * `, ` + * precision mediump float; + * varying vec2 vTexCoord; + * uniform sampler2D uTexture; + * uniform vec2 uMousePos; + * + * void main() { + * // Distance from the current pixel to the mouse + * float distFromMouse = distance(vTexCoord, uMousePos); + * + * // Adjust pixelation based on distance (closer = more detail, farther = blockier) + * float pixelSize = mix(0.002, 0.05, distFromMouse); + * vec2 pixelatedCoord = vec2(floor(vTexCoord.x / pixelSize) * pixelSize, + * floor(vTexCoord.y / pixelSize) * pixelSize); + * + * vec4 texColor = texture2D(uTexture, pixelatedCoord); + * gl_FragColor = texColor; + * } + * `); + * + * describe('A static image with a grid-like, pixelated effect created by the shader. Each cell in the grid alternates visibility, producing a dithered visual effect.'); * } * - * // Toggle between shaders when the user double-clicks. - * function doubleClicked() { - * showRedGreen = !showRedGreen; + * function draw() { + * background(220); + * imageShader(imgShader); + * + * let mousePosX = map(mouseX, 0, width, 0, 1); + * let mousePosY = map(mouseY, 0, height, 0, 1); + * + * imgShader.setUniform('uMousePos', [mousePosX, mousePosY]); + * imgShader.setUniform('uTexture', img); + * image(img, -width / 2, -height / 2, width, height); * } * *
*/ -p5.prototype.shader = function (s) { - this._assert3d('shader'); - p5._validateParameters('shader', arguments); + +p5.prototype.imageShader = function (s) { + this._assert3d('imageShader'); + p5._validateParameters('imageShader', arguments); s.ensureCompiledOnContext(this); - if (s.isStrokeShader()) { - this._renderer.userStrokeShader = s; - } else { - this._renderer.userFillShader = s; - this._renderer._useNormalMaterial = false; - } + this._renderer.userImageShader = s; s.setDefaultUniforms(); return this; }; + /** * Get the default shader used with lights, materials, * and textures. @@ -1516,7 +1844,7 @@ p5.prototype.baseColorShader = function() { * * function draw() { * background(255); - * shader(myShader); + * strokeShader(myShader); * strokeWeight(30); * line( * -width/3, @@ -1559,7 +1887,7 @@ p5.prototype.baseColorShader = function() { * * function draw() { * background(255); - * shader(myShader); + * strokeShader(myShader); * myShader.setUniform('time', millis()); * strokeWeight(10); * beginShape(); @@ -1600,7 +1928,7 @@ p5.prototype.baseColorShader = function() { * * function draw() { * background(255); - * shader(myShader); + * strokeShader(myShader); * strokeWeight(10); * beginShape(); * for (let i = 0; i <= 50; i++) { @@ -1629,7 +1957,8 @@ p5.prototype.baseStrokeShader = function() { * Restores the default shaders. * * `resetShader()` deactivates any shaders previously applied by - * shader(). + * shader(), strokeShader(), + * or imageShader(). * * Note: Shaders can only be used in WebGL mode. * @@ -1709,7 +2038,11 @@ p5.prototype.baseStrokeShader = function() { *
*/ p5.prototype.resetShader = function () { - this._renderer.userFillShader = this._renderer.userStrokeShader = null; + + this._renderer.userFillShader = null; + this._renderer.userStrokeShader = null; + this._renderer.userImageShader = null; + return this; }; @@ -1997,7 +2330,7 @@ p5.prototype.texture = function (tex) { * Note: `textureMode()` can only be used in WebGL mode. * * @method textureMode - * @param {Constant} mode either IMAGE or NORMAL. + * @param {(IMAGE|NORMAL)} mode either IMAGE or NORMAL. * * @example *
@@ -2178,8 +2511,8 @@ p5.prototype.textureMode = function (mode) { * Note: `textureWrap()` can only be used in WebGL mode. * * @method textureWrap - * @param {Constant} wrapX either CLAMP, REPEAT, or MIRROR - * @param {Constant} [wrapY] either CLAMP, REPEAT, or MIRROR + * @param {(CLAMP|REPEAT|MIRROR)} wrapX either CLAMP, REPEAT, or MIRROR + * @param {(CLAMP|REPEAT|MIRROR)} [wrapY=wrapX] either CLAMP, REPEAT, or MIRROR * * @example *
@@ -3191,7 +3524,7 @@ p5.prototype.metalness = function (metallic) { * transparency internally, e.g. via vertex colors * @return {Number[]} Normalized numbers array */ -p5.RendererGL.prototype._applyColorBlend = function(colors, hasTransparency) { +p5.RendererGL.prototype._applyColorBlend = function (colors, hasTransparency) { const gl = this.GL; const isTexture = this.drawMode === constants.TEXTURE; diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 31ce48f630..1f25bbc6e9 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -137,9 +137,10 @@ p5.RendererGL.prototype.vertex = function(x, y) { } else if ( this.userFillShader !== undefined || this.userStrokeShader !== undefined || - this.userPointShader !== undefined + this.userPointShader !== undefined || + this.userImageShader !== undefined ) { - // Do nothing if user-defined shaders are present + // Do nothing if user-defined shaders are present } else if ( this._tex === null && arguments.length >= 4 @@ -214,7 +215,7 @@ p5.RendererGL.prototype.endShape = function( if (this.immediateMode.geometry.vertices.length === 3 && this.immediateMode.shapeMode === constants.TESS ) { - this.immediateMode.shapeMode = constants.TRIANGLES; + this.immediateMode.shapeMode === constants.TRIANGLES; } this.isProcessingVertices = true; @@ -512,8 +513,7 @@ p5.RendererGL.prototype._drawImmediateFill = function(count = 1) { this._useVertexColor = (this.immediateMode.geometry.vertexColors.length > 0); let shader; - shader = this._getImmediateFillShader(); - + shader = this._getFillShader(); this._setFillUniforms(shader); for (const buff of this.immediateMode.buffers.fill) { @@ -579,4 +579,5 @@ p5.RendererGL.prototype._drawImmediateStroke = function() { shader.unbindShader(); }; + export default p5.RendererGL; diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index 49f2dd772b..54935f9966 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -133,8 +133,15 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { this.retainedMode.geometry[gId].vertexCount > 0 ) { this._useVertexColor = (geometry.model.vertexColors.length > 0); - const fillShader = this._getRetainedFillShader(); + let fillShader; + if(this._drawingFilter && this.userFillShader){ + fillShader=this.userFillShader; + } + else{ + fillShader = this._getFillShader(); + } this._setFillUniforms(fillShader); + for (const buff of this.retainedMode.buffers.fill) { buff._prepareBuffer(geometry, fillShader); } diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index f1e52b174e..d543e7b5d2 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -566,6 +566,8 @@ p5.RendererGL = class RendererGL extends p5.Renderer { this.executeZoom = false; this.executeRotateAndMove = false; + this._drawingFilter = false; + this.specularShader = undefined; this.sphereMapping = undefined; this.diffusedShader = undefined; @@ -578,6 +580,7 @@ p5.RendererGL = class RendererGL extends p5.Renderer { this.userFillShader = undefined; this.userStrokeShader = undefined; this.userPointShader = undefined; + this.userImageShader = undefined; // Default drawing is done in Retained Mode // Geometry and Material hashes stored here @@ -1167,6 +1170,7 @@ p5.RendererGL = class RendererGL extends p5.Renderer { target.filterCamera._resize(); this._pInst.setCamera(target.filterCamera); this._pInst.resetMatrix(); + this._drawingFilter = true; this._pInst.image(fbo, -target.width / 2, -target.height / 2, target.width, target.height); this._pInst.clearDepth(); @@ -1636,6 +1640,7 @@ p5.RendererGL = class RendererGL extends p5.Renderer { properties.userFillShader = this.userFillShader; properties.userStrokeShader = this.userStrokeShader; + properties.userImageShader = this.userImageShader; properties.userPointShader = this.userPointShader; properties.pointSize = this.pointSize; @@ -1710,10 +1715,10 @@ p5.RendererGL = class RendererGL extends p5.Renderer { _getImmediateStrokeShader() { // select the stroke shader to use const stroke = this.userStrokeShader; - if (!stroke || !stroke.isStrokeShader()) { - return this._getLineShader(); + if (stroke) { + return stroke; } - return stroke; + return this._getLineShader(); } @@ -1737,54 +1742,37 @@ p5.RendererGL = class RendererGL extends p5.Renderer { } /* - * selects which fill shader should be used based on renderer state, - * for use with begin/endShape and immediate vertex mode. + * This method will handle both image shaders and + * fill shaders, returning the appropriate shader + * depending on the current context (image or shape). */ - _getImmediateFillShader() { - const fill = this.userFillShader; - if (this._useNormalMaterial) { - if (!fill || !fill.isNormalShader()) { - return this._getNormalShader(); + _getFillShader() { + // If drawing an image, check for user-defined image shader and filters + if (this._drawingImage) { + // Use user-defined image shader if available and no filter is applied + if (this.userImageShader && !this._drawingFilter) { + return this.userImageShader; + } else { + return this._getLightShader(); // Fallback to light shader } } - if (this._enableLighting) { - if (!fill || !fill.isLightShader()) { - return this._getLightShader(); - } - } else if (this._tex) { - if (!fill || !fill.isTextureShader()) { - return this._getLightShader(); - } - } else if (!fill /*|| !fill.isColorShader()*/) { - return this._getImmediateModeShader(); + // If user has defined a fill shader, return that + else if (this.userFillShader) { + return this.userFillShader; } - return fill; - } - - /* - * selects which fill shader should be used based on renderer state - * for retained mode. - */ - _getRetainedFillShader() { - if (this._useNormalMaterial) { + // Use normal shader if normal material is active + else if (this._useNormalMaterial) { return this._getNormalShader(); } - - const fill = this.userFillShader; - if (this._enableLighting) { - if (!fill || !fill.isLightShader()) { - return this._getLightShader(); - } - } else if (this._tex) { - if (!fill || !fill.isTextureShader()) { - return this._getLightShader(); - } - } else if (!fill /* || !fill.isColorShader()*/) { - return this._getColorShader(); + // Use light shader if lighting or textures are enabled + else if (this._enableLighting || this._tex) { + return this._getLightShader(); } - return fill; + // Default to color shader if no other conditions are met + return this._getColorShader(); } + _getImmediatePointShader() { // select the point shader to use const point = this.userPointShader; @@ -1942,6 +1930,7 @@ p5.RendererGL = class RendererGL extends p5.Renderer { return this._defaultColorShader; } + /** * TODO(dave): un-private this when there is a way to actually override the * shader used for points @@ -2051,6 +2040,7 @@ p5.RendererGL = class RendererGL extends p5.Renderer { return this._defaultFontShader; } + _webGL2CompatibilityPrefix( shaderType, floatPrecision diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 5f0b38ce66..2a256f5d4b 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -952,12 +952,8 @@ p5.Shader = class { const modelViewProjectionMatrix = modelViewMatrix.copy(); modelViewProjectionMatrix.mult(projectionMatrix); - if (this.isStrokeShader()) { - this.setUniform( - 'uPerspective', - this._renderer._curCamera.useLinePerspective ? 1 : 0 - ); - } + this.setUniform('uPerspective', this._renderer._curCamera.useLinePerspective ? 1 : 0); + this.setUniform('uPerspective', this._renderer._curCamera.useLinePerspective ? 1 : 0); this.setUniform('uViewMatrix', viewMatrix.mat4); this.setUniform('uProjectionMatrix', projectionMatrix.mat4); this.setUniform('uModelMatrix', modelMatrix.mat4); @@ -1349,23 +1345,6 @@ p5.Shader = class { * **/ - isLightShader() { - return [ - this.attributes.aNormal, - this.uniforms.uUseLighting, - this.uniforms.uAmbientLightCount, - this.uniforms.uDirectionalLightCount, - this.uniforms.uPointLightCount, - this.uniforms.uAmbientColor, - this.uniforms.uDirectionalDiffuseColors, - this.uniforms.uDirectionalSpecularColors, - this.uniforms.uPointLightLocation, - this.uniforms.uPointLightDiffuseColors, - this.uniforms.uPointLightSpecularColors, - this.uniforms.uLightingDirection, - this.uniforms.uSpecular - ].some(x => x !== undefined); - } isNormalShader() { return this.attributes.aNormal !== undefined; @@ -1382,13 +1361,6 @@ p5.Shader = class { ); } - isTexLightShader() { - return this.isLightShader() && this.isTextureShader(); - } - - isStrokeShader() { - return this.uniforms.uStrokeWeight !== undefined; - } /** * @method enableAttrib diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index 2822b1ead8..6708349dc0 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -128,4 +128,99 @@ visualSuite('WebGL', function() { } ); }); + + visualSuite('ShaderFunctionality', function() { + visualTest('FillShader', async (p5, screenshot) => { + return new Promise(async resolve => { + p5.createCanvas(50, 50, p5.WEBGL); + const img = await new Promise(resolve => p5.loadImage('unit/assets/cat.jpg', resolve)); + const fillShader = p5.createShader( + ` + attribute vec3 aPosition; + void main() { + gl_Position = vec4(aPosition, 1.0); + } + `, + ` + precision mediump float; + void main() { + gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + } + ` + ); + p5.shader(fillShader); + p5.lights(); + p5.texture(img); + p5.noStroke(); + p5.rect(-p5.width / 2, -p5.height / 2, p5.width, p5.height); + screenshot(); + resolve(); + }); + }); + + visualTest('StrokeShader', (p5, screenshot) => { + return new Promise(resolve => { + p5.createCanvas(50, 50, p5.WEBGL); + // Create a stroke shader with a fading effect based on distance + const strokeshader = p5.baseStrokeShader().modify({ + 'Inputs getPixelInputs': `(Inputs inputs) { + float opacity = 1.0 - smoothstep( + 0.0, + 15.0, + length(inputs.position - inputs.center) + ); + inputs.color *= opacity; + return inputs; + }` + }); + + p5.strokeShader(strokeshader); + p5.strokeWeight(15); + p5.line( + -p5.width / 3, + p5.sin(p5.millis() * 0.001) * p5.height / 4, + p5.width / 3, + p5.sin(p5.millis() * 0.001 + 1) * p5.height / 4 + ); + screenshot(); + resolve(); + }); + }); + + visualTest('ImageShader', async (p5, screenshot) => { + p5.createCanvas(50, 50, p5.WEBGL); + const img = await new Promise(resolve => p5.loadImage('unit/assets/cat.jpg', resolve)); + const imgShader = p5.createShader( + ` + precision mediump float; + attribute vec3 aPosition; + attribute vec2 aTexCoord; + varying vec2 vTexCoord; + uniform mat4 uModelViewMatrix; + uniform mat4 uProjectionMatrix; + + void main() { + vTexCoord = aTexCoord; + gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aPosition, 1.0); + } + `, + ` + precision mediump float; + varying vec2 vTexCoord; + uniform sampler2D uTexture; + + void main() { + vec4 texColor = texture2D(uTexture, vTexCoord); + gl_FragColor = texColor * vec4(1.5, 0.5, 0.5, 1.0); + } + ` + ); + + p5.imageShader(imgShader); + imgShader.setUniform('uTexture', img); + p5.noStroke(); + p5.image(img, -p5.width / 2, -p5.height / 2, p5.width, p5.height); + screenshot(); + }); + }); }); diff --git a/test/unit/visual/screenshots/WebGL/ShaderFunctionality/FillShader/000.png b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/FillShader/000.png new file mode 100644 index 0000000000..c2c2842507 Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/FillShader/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/ShaderFunctionality/FillShader/metadata.json b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/FillShader/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/FillShader/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/ShaderFunctionality/ImageShader/000.png b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/ImageShader/000.png new file mode 100644 index 0000000000..bda8586d1c Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/ImageShader/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/ShaderFunctionality/ImageShader/metadata.json b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/ImageShader/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/ImageShader/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/ShaderFunctionality/StrokeShader/000.png b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/StrokeShader/000.png new file mode 100644 index 0000000000..0c8494d309 Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/StrokeShader/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/ShaderFunctionality/StrokeShader/metadata.json b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/StrokeShader/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/StrokeShader/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 3c7e3df1a2..2019b64be4 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -805,7 +805,7 @@ suite('p5.RendererGL', function() { done(); }); - test('push/pop and shader() works with fill', function(done) { + test('push/pop and shader() works with fill shaders by default', function(done) { myp5.createCanvas(100, 100, myp5.WEBGL); var fillShader1 = myp5._renderer._getLightShader(); var fillShader2 = myp5._renderer._getColorShader(); diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index 044bf6ec0a..1416a7c526 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -158,8 +158,8 @@ suite('p5.Shader', function() { var retainedColorShader = myp5._renderer._getColorShader(); var texLightShader = myp5._renderer._getLightShader(); var immediateColorShader = myp5._renderer._getImmediateModeShader(); - var selectedRetainedShader = myp5._renderer._getRetainedFillShader(); - var selectedImmediateShader = myp5._renderer._getImmediateFillShader(); + var selectedRetainedShader = myp5._renderer._getFillShader(); + var selectedImmediateShader = myp5._renderer._getFillShader(); // both color and light shader are valid, depending on // conditions set earlier. @@ -177,8 +177,8 @@ suite('p5.Shader', function() { test('Normal Shader is set after normalMaterial()', function() { myp5.normalMaterial(); var normalShader = myp5._renderer._getNormalShader(); - var selectedRetainedShader = myp5._renderer._getRetainedFillShader(); - var selectedImmediateShader = myp5._renderer._getRetainedFillShader(); + var selectedRetainedShader = myp5._renderer._getFillShader(); + var selectedImmediateShader = myp5._renderer._getFillShader(); assert( normalShader === selectedRetainedShader, "_renderer's retain mode shader was not normal shader" @@ -191,8 +191,8 @@ suite('p5.Shader', function() { test('Light shader set after ambientMaterial()', function() { var lightShader = myp5._renderer._getLightShader(); myp5.ambientMaterial(128); - var selectedRetainedShader = myp5._renderer._getRetainedFillShader(); - var selectedImmediateShader = myp5._renderer._getImmediateFillShader(); + var selectedRetainedShader = myp5._renderer._getFillShader(); + var selectedImmediateShader = myp5._renderer._getFillShader(); assert( lightShader === selectedRetainedShader, "_renderer's retain mode shader was not light shader " + @@ -207,8 +207,8 @@ suite('p5.Shader', function() { test('Light shader set after specularMaterial()', function() { var lightShader = myp5._renderer._getLightShader(); myp5.specularMaterial(128); - var selectedRetainedShader = myp5._renderer._getRetainedFillShader(); - var selectedImmediateShader = myp5._renderer._getImmediateFillShader(); + var selectedRetainedShader = myp5._renderer._getFillShader(); + var selectedImmediateShader = myp5._renderer._getFillShader(); assert( lightShader === selectedRetainedShader, "_renderer's retain mode shader was not light shader " + @@ -223,8 +223,8 @@ suite('p5.Shader', function() { test('Light shader set after emissiveMaterial()', function() { var lightShader = myp5._renderer._getLightShader(); myp5.emissiveMaterial(128); - var selectedRetainedShader = myp5._renderer._getRetainedFillShader(); - var selectedImmediateShader = myp5._renderer._getImmediateFillShader(); + var selectedRetainedShader = myp5._renderer._getFillShader(); + var selectedImmediateShader = myp5._renderer._getFillShader(); assert( lightShader === selectedRetainedShader, "_renderer's retain mode shader was not light shader " + @@ -261,30 +261,6 @@ suite('p5.Shader', function() { assert.isTrue(curShader === null); }); - test('isTextureShader returns true if there is a sampler', function() { - var s = myp5._renderer._getLightShader(); - myp5.shader(s); - assert.isTrue(s.isTextureShader()); - }); - - test('isTextureShader returns false if there is no sampler', function() { - var s = myp5._renderer._getColorShader(); - myp5.shader(s); - assert.isFalse(s.isTextureShader()); - }); - - test('isLightShader returns true if there are lighting uniforms', function() { - var s = myp5._renderer._getLightShader(); - myp5.shader(s); - assert.isTrue(s.isLightShader()); - }); - - test('isLightShader returns false if there are no lighting uniforms', function() { - var s = myp5._renderer._getPointShader(); - myp5.shader(s); - assert.isFalse(s.isLightShader()); - }); - test('isNormalShader returns true if there is a normal attribute', function() { var s = myp5._renderer._getNormalShader(); myp5.shader(s); @@ -297,17 +273,6 @@ suite('p5.Shader', function() { assert.isFalse(s.isNormalShader()); }); - test('isStrokeShader returns true if there is a stroke weight uniform', function() { - var s = myp5._renderer._getLineShader(); - myp5.shader(s); - assert.isTrue(s.isStrokeShader()); - }); - - test('isStrokeShader returns false if there is no stroke weight uniform', function() { - var s = myp5._renderer._getLightShader(); - myp5.shader(s); - assert.isFalse(s.isStrokeShader()); - }); suite('Hooks', function() { let myShader; diff --git a/test/unit/webgl/p5.Texture.js b/test/unit/webgl/p5.Texture.js index e2d3a0bf3f..a4db6222c2 100644 --- a/test/unit/webgl/p5.Texture.js +++ b/test/unit/webgl/p5.Texture.js @@ -54,7 +54,7 @@ suite('p5.Texture', function() { var testTextureSet = function(src) { test('Light shader set after texture()', function() { var lightShader = myp5._renderer._getLightShader(); - var selectedShader = myp5._renderer._getRetainedFillShader(); + var selectedShader = myp5._renderer._getFillShader(); assert( lightShader === selectedShader, "_renderer's retain mode shader was not light shader " +