diff --git a/contributor_docs/unit_testing.md b/contributor_docs/unit_testing.md index 64ef0f73b9..072ba65705 100644 --- a/contributor_docs/unit_testing.md +++ b/contributor_docs/unit_testing.md @@ -139,3 +139,29 @@ If you need to add a new test file, add it to that folder, then add the filename When you add a new test, running `npm test` will generate new screenshots for any visual tests that do not yet have them. Those screenshots will then be used as a reference the next time tests run to make sure the sketch looks the same. If a test intentionally needs to look different, you can delete the folder matching the test name in the `test/unit/visual/screenshots` folder, and then re-run `npm test` to generate a new one. To manually inspect all visual tests, run `grunt yui:dev` to launch a local server, then go to http://127.0.0.1:9001/test/visual.html to see a list of all test cases. + + +In a continuous integration (CI) environment, optimizing test speed is essential. It is advantageous to keep the code concise, avoid unnecessary frames, minimize canvas size, and load assets only when essential for the specific functionality under test. +To address scenarios involving operations like asynchronous 3D model rendering, consider returning a promise that resolves upon completing all the necessary tests, ensuring efficiency in your visual testing approach. Here's an example of how you can asynchronous 3D model rendering in your visual tests: + +```js +visualSuite('3D Model rendering', function() { + visualTest('OBJ model is displayed correctly', function(p5, screenshot) { + // Return a Promise to ensure the test runner waits for the asynchronous operation to complete + return new Promise(resolve => { + p5.createCanvas(50, 50, p5.WEBGL); + // Load the model asynchronously + p5.loadModel('unit/assets/teapot.obj', model => { + p5.background(200); + p5.rotateX(10 * 0.01); + p5.rotateY(10 * 0.01); + p5.model(model); + // Take a screenshot for visual comparison + screenshot(); + // Resolve the Promise to indicate completion + resolve(); + }); + }); + }); +}); +``` diff --git a/src/webgl/material.js b/src/webgl/material.js index 854917c7de..e94d148538 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -1279,14 +1279,17 @@ p5.prototype.metalness = function (metallic) { * @private blends colors according to color components. * If alpha value is less than 1, or non-standard blendMode * we need to enable blending on our gl context. - * @param {Number[]} color [description] - * @return {Number[]} Normalized numbers array + * @param {Number[]} color The currently set color, with values in 0-1 range + * @param {Boolean} [hasTransparency] Whether the shape being drawn has other + * transparency internally, e.g. via vertex colors + * @return {Number[]]} Normalized numbers array */ -p5.RendererGL.prototype._applyColorBlend = function (colors) { +p5.RendererGL.prototype._applyColorBlend = function(colors, hasTransparency) { const gl = this.GL; const isTexture = this.drawMode === constants.TEXTURE; const doBlend = + hasTransparency || this.userFillShader || this.userStrokeShader || this.userPointShader || diff --git a/src/webgl/p5.Camera.js b/src/webgl/p5.Camera.js index 6dc0be1e97..9e261bed5d 100644 --- a/src/webgl/p5.Camera.js +++ b/src/webgl/p5.Camera.js @@ -205,7 +205,7 @@ p5.prototype.perspective = function (...args) { * maximum z values. * * If no parameters are given, the following default is used: - * ortho(-width/2, width/2, -height/2, height/2, 0, max(width, height)). + * ortho(-width/2, width/2, -height/2, height/2, 0, max(width, height) + 800). * @method ortho * @for p5 * @param {Number} [left] camera frustum left plane @@ -222,8 +222,7 @@ p5.prototype.perspective = function (...args) { * //there's no vanishing point * function setup() { * createCanvas(100, 100, WEBGL); - * camera(0, 0, 50*sqrt(3), 0, 0, 0, 0, 1, 0); - * ortho(-width / 2, width / 2, height / 2, -height / 2, 0, 500); + * ortho(); * describe( * 'two 3D boxes move back and forth along same plane, rotating as mouse is dragged.' * ); diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 18731dfcb5..a1159d244b 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -66,6 +66,9 @@ p5.Geometry = class Geometry { this.detailY = detailY !== undefined ? detailY : 1; this.dirtyFlags = {}; + this._hasFillTransparency = undefined; + this._hasStrokeTransparency = undefined; + if (callback instanceof Function) { callback.call(this); } @@ -73,6 +76,9 @@ p5.Geometry = class Geometry { } reset() { + this._hasFillTransparency = undefined; + this._hasStrokeTransparency = undefined; + this.lineVertices.clear(); this.lineTangentsIn.clear(); this.lineTangentsOut.clear(); @@ -88,6 +94,32 @@ p5.Geometry = class Geometry { this.dirtyFlags = {}; } + + hasFillTransparency() { + if (this._hasFillTransparency === undefined) { + this._hasFillTransparency = false; + for (let i = 0; i < this.vertexColors.length; i += 4) { + if (this.vertexColors[i + 3] < 1) { + this._hasFillTransparency = true; + break; + } + } + } + return this._hasFillTransparency; + } + hasStrokeTransparency() { + if (this._hasStrokeTransparency === undefined) { + this._hasStrokeTransparency = false; + for (let i = 0; i < this.lineVertexColors.length; i += 4) { + if (this.lineVertexColors[i + 3] < 1) { + this._hasStrokeTransparency = true; + break; + } + } + } + return this._hasStrokeTransparency; + } + /** * Removes the internal colors of p5.Geometry. * Using `clearColors()`, you can use `fill()` to supply new colors before drawing each shape. diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 673fe0521e..fbe5472561 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -519,7 +519,10 @@ p5.RendererGL.prototype._drawImmediateFill = function(count = 1) { } shader.disableRemainingAttributes(); - this._applyColorBlend(this.curFillColor); + this._applyColorBlend( + this.curFillColor, + this.immediateMode.geometry.hasFillTransparency() + ); if (count === 1) { gl.drawArrays( @@ -561,7 +564,10 @@ p5.RendererGL.prototype._drawImmediateStroke = function() { buff._prepareBuffer(this.immediateMode.geometry, shader); } shader.disableRemainingAttributes(); - this._applyColorBlend(this.curStrokeColor); + this._applyColorBlend( + this.curStrokeColor, + this.immediateMode.geometry.hasFillTransparency() + ); gl.drawArrays( gl.TRIANGLES, diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index bfeb562450..49f2dd772b 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -143,7 +143,10 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { //vertex index buffer this._bindBuffer(geometry.indexBuffer, gl.ELEMENT_ARRAY_BUFFER); } - this._applyColorBlend(this.curFillColor); + this._applyColorBlend( + this.curFillColor, + geometry.model.hasFillTransparency() + ); this._drawElements(gl.TRIANGLES, gId); fillShader.unbindShader(); } @@ -156,7 +159,10 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { buff._prepareBuffer(geometry, strokeShader); } strokeShader.disableRemainingAttributes(); - this._applyColorBlend(this.curStrokeColor); + this._applyColorBlend( + this.curStrokeColor, + geometry.model.hasStrokeTransparency() + ); this._drawArrays(gl.TRIANGLES, gId); strokeShader.unbindShader(); } diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 3f1c2cc437..e7f7675b47 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -1128,8 +1128,8 @@ p5.RendererGL = class RendererGL extends p5.Renderer { target.filterCamera._resize(); this._pInst.setCamera(target.filterCamera); this._pInst.resetMatrix(); - this._pInst.image(fbo, -this.width / 2, -this.height / 2, - this.width, this.height); + this._pInst.image(fbo, -target.width / 2, -target.height / 2, + target.width, target.height); this._pInst.pop(); this._pInst.pop(); } diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index c45ebda9a5..0e1b7269e7 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -24,6 +24,52 @@ visualSuite('WebGL', function() { }); }); + visualSuite('filter', function() { + visualTest('On the main canvas', function(p5, screenshot) { + p5.createCanvas(50, 50, p5.WEBGL); + p5.noStroke(); + p5.fill('red'); + p5.circle(0, 0, 20); + p5.filter(p5.GRAY); + screenshot(); + }); + + visualTest('On a framebuffer', function(p5, screenshot) { + p5.createCanvas(50, 50, p5.WEBGL); + const fbo = p5.createFramebuffer({ antialias: true }); + fbo.begin(); + p5.noStroke(); + p5.fill('red'); + p5.circle(0, 0, 20); + p5.filter(p5.GRAY); + fbo.end(); + p5.imageMode(p5.CENTER); + p5.image(fbo, 0, 0); + screenshot(); + }); + + visualTest( + 'On a framebuffer sized differently from the main canvas', + function(p5, screenshot) { + p5.createCanvas(50, 50, p5.WEBGL); + const fbo = p5.createFramebuffer({ + width: 26, + height: 26, + antialias: true + }); + fbo.begin(); + p5.noStroke(); + p5.fill('red'); + p5.circle(0, 0, 20); + p5.filter(p5.GRAY); + fbo.end(); + p5.imageMode(p5.CENTER); + p5.image(fbo, 0, 0); + screenshot(); + } + ); + }); + visualSuite('Lights', function() { visualTest('Fill color and default ambient material', function(p5, screenshot) { p5.createCanvas(50, 50, p5.WEBGL); diff --git a/test/unit/visual/screenshots/WebGL/filter/On a framebuffer sized differently from the main canvas/000.png b/test/unit/visual/screenshots/WebGL/filter/On a framebuffer sized differently from the main canvas/000.png new file mode 100644 index 0000000000..90ccca1c5d Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/filter/On a framebuffer sized differently from the main canvas/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/filter/On a framebuffer sized differently from the main canvas/metadata.json b/test/unit/visual/screenshots/WebGL/filter/On a framebuffer sized differently from the main canvas/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/filter/On a framebuffer sized differently from the main canvas/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/filter/On a framebuffer/000.png b/test/unit/visual/screenshots/WebGL/filter/On a framebuffer/000.png new file mode 100644 index 0000000000..90ccca1c5d Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/filter/On a framebuffer/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/filter/On a framebuffer/metadata.json b/test/unit/visual/screenshots/WebGL/filter/On a framebuffer/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/filter/On a framebuffer/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/filter/On the main canvas/000.png b/test/unit/visual/screenshots/WebGL/filter/On the main canvas/000.png new file mode 100644 index 0000000000..176f683c62 Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/filter/On the main canvas/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/filter/On the main canvas/metadata.json b/test/unit/visual/screenshots/WebGL/filter/On the main canvas/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/filter/On the main canvas/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/visualTest.js b/test/unit/visual/visualTest.js index 24dbec2f53..ec66fc3e07 100644 --- a/test/unit/visual/visualTest.js +++ b/test/unit/visual/visualTest.js @@ -151,6 +151,10 @@ window.visualTest = function( actual.push(myp5.get()); }); + + if (actual.length === 0) { + throw new Error('No screenshots were generated. Check if your test generates screenshots correctly. If the test includes asynchronous operations, ensure they complete before the test ends.'); + } if (expectedScreenshots && actual.length !== expectedScreenshots) { throw new Error( `Expected ${expectedScreenshots} screenshot(s) but generated ${actual.length}` diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 73cbdc87ba..e747b12d36 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -1258,6 +1258,30 @@ suite('p5.RendererGL', function() { assert.deepEqual(myp5.get(16, 16), [255, 0, 255, 255]); done(); }); + + test('transparency works the same with per-vertex colors', function() { + myp5.createCanvas(20, 20, myp5.WEBGL); + myp5.noStroke(); + + function drawShapes() { + myp5.fill(255, 0, 0, 100); + myp5.rect(-10, -10, 15, 15); + myp5.fill(0, 0, 255, 100); + myp5.rect(-5, -5, 15, 15); + } + + drawShapes(); + myp5.loadPixels(); + const eachShapeResult = [...myp5.pixels]; + + myp5.clear(); + const shapes = myp5.buildGeometry(drawShapes); + myp5.model(shapes); + myp5.loadPixels(); + const singleShapeResult = [...myp5.pixels]; + + assert.deepEqual(eachShapeResult, singleShapeResult); + }); }); suite('BufferDef', function() {