diff --git a/src/image/pixels.js b/src/image/pixels.js index 16c9924b8b..14731cb635 100644 --- a/src/image/pixels.js +++ b/src/image/pixels.js @@ -510,8 +510,8 @@ p5.prototype.filter = function(operation, value) { * @method get * @param {Number} x x-coordinate of the pixel * @param {Number} y y-coordinate of the pixel - * @param {Number} w width - * @param {Number} h height + * @param {Number} w width of the section to be returned + * @param {Number} h height of the section to be returned * @return {p5.Image} the rectangle p5.Image * @example *
diff --git a/src/webgl/p5.Framebuffer.js b/src/webgl/p5.Framebuffer.js index 8727b16b4d..5c26c9c601 100644 --- a/src/webgl/p5.Framebuffer.js +++ b/src/webgl/p5.Framebuffer.js @@ -6,6 +6,7 @@ import p5 from '../core/main'; import * as constants from '../core/constants'; import { checkWebGLCapabilities } from './p5.Texture'; +import { readPixelsWebGL, readPixelWebGL } from './p5.RendererGL'; class FramebufferCamera extends p5.Camera { /** @@ -109,6 +110,24 @@ class Framebuffer { this.target = target; this.target._renderer.framebuffers.add(this); + /** + * A Uint8ClampedArray + * containing the values for all the pixels in the Framebuffer. + * + * Like the main canvas pixels property, call + * loadPixels() before reading + * it, and call updatePixels() + * afterwards to update its data. + * + * Note that updating pixels via this property will be slower than + * drawing to the framebuffer directly. + * Consider using a shader instead of looping over pixels. + * + * @property {Number[]} pixels + */ + this.pixels = []; + this.format = settings.format || constants.UNSIGNED_BYTE; this.channels = settings.channels || ( target._renderer._pInst._glAttributes.alpha @@ -931,6 +950,276 @@ class Framebuffer { callback(); this.end(); } + + /** + * Call this befpre updating pixels + * and calling updatePixels + * to replace the content of the framebuffer with the data in the pixels + * array. + */ + loadPixels() { + const gl = this.gl; + const prevFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING); + gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); + const colorFormat = this._glColorFormat(); + this.pixels = readPixelsWebGL( + this.pixels, + gl, + this.framebuffer, + 0, + 0, + this.width * this.density, + this.height * this.density, + colorFormat.format, + colorFormat.type + ); + gl.bindFramebuffer(gl.FRAMEBUFFER, prevFramebuffer); + } + + /** + * Get a region of pixels from the canvas in the form of a + * p5.Image, or a single pixel as an array of + * numbers. + * + * Returns an array of [R,G,B,A] values for any pixel or grabs a section of + * an image. If the Framebuffer has been set up to not store alpha values, then + * only [R,G,B] will be returned. If no parameters are specified, the entire + * image is returned. + * Use the x and y parameters to get the value of one pixel. Get a section of + * the display window by specifying additional w and h parameters. When + * getting an image, the x and y parameters define the coordinates for the + * upper-left corner of the image, regardless of the current imageMode(). + * + * @method get + * @param {Number} x x-coordinate of the pixel + * @param {Number} y y-coordinate of the pixel + * @param {Number} w width of the section to be returned + * @param {Number} h height of the section to be returned + * @return {p5.Image} the rectangle p5.Image + */ + /** + * @method get + * @return {p5.Image} the whole p5.Image + */ + /** + * @method get + * @param {Number} x + * @param {Number} y + * @return {Number[]} color of pixel at x,y in array format [R, G, B, A] + */ + get(x, y, w, h) { + p5._validateParameters('p5.Framebuffer.get', arguments); + const colorFormat = this._glColorFormat(); + if (x === undefined && y === undefined) { + x = 0; + y = 0; + w = this.width; + h = this.height; + } else if (w === undefined && h === undefined) { + if (x < 0 || y < 0 || x >= this.width || y >= this.height) { + console.warn( + 'The x and y values passed to p5.Framebuffer.get are outside of its range and will be clamped.' + ); + x = this.target.constrain(x, 0, this.width - 1); + y = this.target.constrain(y, 0, this.height - 1); + } + + return readPixelWebGL( + this.gl, + this.framebuffer, + x * this.density, + y * this.density, + colorFormat.format, + colorFormat.type + ); + } + + x = this.target.constrain(x, 0, this.width - 1); + y = this.target.constrain(y, 0, this.height - 1); + w = this.target.constrain(w, 1, this.width - x); + h = this.target.constrain(h, 1, this.height - y); + + const rawData = readPixelsWebGL( + undefined, + this.gl, + this.framebuffer, + x * this.density, + y * this.density, + w * this.density, + h * this.density, + colorFormat.format, + colorFormat.type + ); + // Framebuffer data might be either a Uint8Array or Float32Array + // depending on its format, and it may or may not have an alpha channel. + // To turn it into an image, we have to normalize the data into a + // Uint8ClampedArray with alpha. + const fullData = new Uint8ClampedArray( + w * h * this.density * this.density * 4 + ); + + // Default channels that aren't in the framebuffer (e.g. alpha, if the + // framebuffer is in RGB mode instead of RGBA) to 255 + fullData.fill(255); + + const channels = colorFormat.type === this.gl.RGB ? 3 : 4; + for (let y = 0; y < h * this.density; y++) { + for (let x = 0; x < w * this.density; x++) { + for (let channel = 0; channel < 4; channel++) { + const idx = (y * w * this.density + x) * 4 + channel; + if (channel < channels) { + // Find the index of this pixel in `rawData`, which might have a + // different number of channels + const rawDataIdx = channels === 4 + ? idx + : (y * w * this.density + x) * channels + channel; + fullData[idx] = rawData[rawDataIdx]; + } + } + } + } + + // Create an image from the data + const region = new p5.Image(w * this.density, h * this.density); + region.imageData = region.canvas.getContext('2d').createImageData( + region.width, + region.height + ); + region.imageData.data.set(fullData); + region.pixels = region.imageData.data; + region.updatePixels(); + if (this.density !== 1) { + // TODO: support get() at a pixel density > 1 + region.resize(w, h); + } + return region; + } + + /** + * Call this after initially calling + * loadPixels() and updating pixels + * to replace the content of the framebuffer with the data in the pixels + * array. + * + * This will also clear the depth buffer so that any future drawing done + * afterwards will go on top. + * + * @example + *
+ * + * let framebuffer; + * function setup() { + * createCanvas(100, 100, WEBGL); + * framebuffer = createFramebuffer(); + * } + + * function draw() { + * noStroke(); + * lights(); + * + * // Draw a sphere to the framebuffer + * framebuffer.begin(); + * background(0); + * sphere(25); + * framebuffer.end(); + * + * // Load its pixels and draw a gradient over the lower half of the canvas + * framebuffer.loadPixels(); + * for (let y = height/2; y < height; y++) { + * for (let x = 0; x < width; x++) { + * const idx = (y * width + x) * 4; + * framebuffer.pixels[idx] = (x / width) * 255; + * framebuffer.pixels[idx + 1] = (y / height) * 255; + * framebuffer.pixels[idx + 2] = 255; + * framebuffer.pixels[idx + 3] = 255; + * } + * } + * framebuffer.updatePixels(); + * + * // Draw a cube on top of the pixels we just wrote + * framebuffer.begin(); + * push(); + * translate(20, 20); + * rotateX(0.5); + * rotateY(0.5); + * box(20); + * pop(); + * framebuffer.end(); + * + * image(framebuffer, -width/2, -height/2); + * noLoop(); + * } + * + *
+ * + * @alt + * A sphere partly occluded by a gradient from cyan to white to magenta on + * the lower half of the canvas, with a 3D cube drawn on top of that in the + * lower right corner. + */ + updatePixels() { + const gl = this.gl; + this.colorP5Texture.bindTexture(); + const colorFormat = this._glColorFormat(); + + const channels = colorFormat.format === gl.RGBA ? 4 : 3; + const len = + this.width * this.height * this.density * this.density * channels; + const TypedArrayClass = colorFormat.type === gl.UNSIGNED_BYTE + ? Uint8Array + : Float32Array; + if ( + !(this.pixels instanceof TypedArrayClass) || this.pixels.length !== len + ) { + throw new Error( + 'The pixels array has not been set correctly. Please call loadPixels() before updatePixels().' + ); + } + + gl.texImage2D( + gl.TEXTURE_2D, + 0, + colorFormat.internalFormat, + this.width * this.density, + this.height * this.density, + 0, + colorFormat.format, + colorFormat.type, + this.pixels + ); + this.colorP5Texture.unbindTexture(); + + this.prevFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING); + if (this.antialias) { + // We need to make sure the antialiased framebuffer also has the updated + // pixels so that if more is drawn to it, it goes on top of the updated + // pixels instead of replacing them. + // We can't blit the framebuffer to the multisampled antialias + // framebuffer to leave both in the same state, so instead we have + // to use image() to put the framebuffer texture onto the antialiased + // framebuffer. + this.begin(); + this.target.push(); + this.target.imageMode(this.target.CENTER); + this.target.resetMatrix(); + this.target.noStroke(); + this.target.clear(); + this.target.image(this, 0, 0); + this.target.pop(); + if (this.useDepth) { + gl.clearDepth(1); + gl.clear(gl.DEPTH_BUFFER_BIT); + } + this.end(); + } else { + gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); + if (this.useDepth) { + gl.clearDepth(1); + gl.clear(gl.DEPTH_BUFFER_BIT); + } + gl.bindFramebuffer(gl.FRAMEBUFFER, this.prevFramebuffer); + } + } } /** diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index e32ad93d41..a6f9931333 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -118,6 +118,22 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { const gl = this.GL; const geometry = this.retainedMode.geometry[gId]; + if (this._doFill) { + this._useVertexColor = (geometry.model.vertexColors.length > 0); + const fillShader = this._getRetainedFillShader(); + this._setFillUniforms(fillShader); + for (const buff of this.retainedMode.buffers.fill) { + buff._prepareBuffer(geometry, fillShader); + } + if (geometry.indexBuffer) { + //vertex index buffer + this._bindBuffer(geometry.indexBuffer, gl.ELEMENT_ARRAY_BUFFER); + } + this._applyColorBlend(this.curFillColor); + this._drawElements(gl.TRIANGLES, gId); + fillShader.unbindShader(); + } + if (this._doStroke && geometry.lineVertexCount > 0) { const faceCullingEnabled = gl.isEnabled(gl.CULL_FACE); // Prevent strokes from getting removed by culling @@ -136,21 +152,6 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { strokeShader.unbindShader(); } - if (this._doFill) { - this._useVertexColor = (geometry.model.vertexColors.length > 0); - const fillShader = this._getRetainedFillShader(); - this._setFillUniforms(fillShader); - for (const buff of this.retainedMode.buffers.fill) { - buff._prepareBuffer(geometry, fillShader); - } - if (geometry.indexBuffer) { - //vertex index buffer - this._bindBuffer(geometry.indexBuffer, gl.ELEMENT_ARRAY_BUFFER); - } - this._applyColorBlend(this.curFillColor); - this._drawElements(gl.TRIANGLES, gId); - fillShader.unbindShader(); - } return this; }; diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 5c26a0e832..99d3fa28d6 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -855,20 +855,16 @@ p5.RendererGL.prototype.strokeWeight = function(w) { // x,y are canvas-relative (pre-scaled by _pixelDensity) p5.RendererGL.prototype._getPixel = function(x, y) { - let imageData, index; - imageData = new Uint8Array(4); - this.drawingContext.readPixels( - x, y, 1, 1, - this.drawingContext.RGBA, this.drawingContext.UNSIGNED_BYTE, - imageData + const gl = this.GL; + return readPixelWebGL( + gl, + null, + x, + y, + gl.RGBA, + gl.UNSIGNED_BYTE, + this._pInst.height * this._pInst.pixelDensity() ); - index = 0; - return [ - imageData[index + 0], - imageData[index + 1], - imageData[index + 2], - imageData[index + 3] - ]; }; /** @@ -891,22 +887,169 @@ p5.RendererGL.prototype.loadPixels = function() { return; } - //if there isn't a renderer-level temporary pixels buffer - //make a new one - let pixels = pixelsState.pixels; - const len = this.GL.drawingBufferWidth * this.GL.drawingBufferHeight * 4; - if (!(pixels instanceof Uint8Array) || pixels.length !== len) { - pixels = new Uint8Array(len); - this._pixelsState._setProperty('pixels', pixels); + const pd = this._pInst._pixelDensity; + const gl = this.GL; + + pixelsState._setProperty( + 'pixels', + readPixelsWebGL( + pixelsState.pixels, + gl, + null, + 0, + 0, + this.width * pd, + this.height * pd, + gl.RGBA, + gl.UNSIGNED_BYTE, + this.height * pd + ) + ); +}; + +p5.RendererGL.prototype.updatePixels = function() { + const fbo = this._getTempFramebuffer(); + fbo.pixels = this._pixelsState.pixels; + fbo.updatePixels(); + this._pInst.push(); + this._pInst.resetMatrix(); + this._pInst.clear(); + this._pInst.imageMode(constants.CENTER); + this._pInst.image(fbo, 0, 0); + this._pInst.pop(); + this.GL.clearDepth(1); + this.GL.clear(this.GL.DEPTH_BUFFER_BIT); +}; + +/** + * @private + * @returns {p5.Framebuffer} A p5.Framebuffer set to match the size and settings + * of the renderer's canvas. It will be created if it does not yet exist, and + * reused if it does. + */ +p5.RendererGL.prototype._getTempFramebuffer = function() { + if (!this._tempFramebuffer) { + this._tempFramebuffer = this._pInst.createFramebuffer({ + format: constants.UNSIGNED_BYTE, + useDepth: this._pInst._glAttributes.depth, + depthFormat: constants.UNSIGNED_INT, + antialias: this._pInst._glAttributes.antialias + }); } + return this._tempFramebuffer; +}; - const pd = this._pInst._pixelDensity; - this.GL.readPixels( - 0, 0, this.width * pd, this.height * pd, - this.GL.RGBA, this.GL.UNSIGNED_BYTE, +/** + * @private + * @param {Uint8Array|Float32Array|undefined} pixels An existing pixels array to reuse if the size is the same + * @param {WebGLRenderingContext} gl The WebGL context + * @param {WebGLFramebuffer|null} framebuffer The Framebuffer to read + * @param {Number} x The x coordiante to read, premultiplied by pixel density + * @param {Number} y The y coordiante to read, premultiplied by pixel density + * @param {Number} width The width in pixels to be read (factoring in pixel density) + * @param {Number} height The height in pixels to be read (factoring in pixel density) + * @param {GLEnum} format Either RGB or RGBA depending on how many channels to read + * @param {GLEnum} type The datatype of each channel, e.g. UNSIGNED_BYTE or FLOAT + * @param {Number|undefined} flipY If provided, the total height with which to flip the y axis about + * @returns {Uint8Array|Float32Array} pixels A pixels array with the current state of the + * WebGL context read into it + */ +export function readPixelsWebGL( + pixels, + gl, + framebuffer, + x, + y, + width, + height, + format, + type, + flipY +) { + // Record the currently bound framebuffer so we can go back to it after, and + // bind the framebuffer we want to read from + const prevFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING); + gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); + + const channels = format === gl.RGBA ? 4 : 3; + + // Make a pixels buffer if it doesn't already exist + const len = width * height * channels; + const TypedArrayClass = type === gl.UNSIGNED_BYTE ? Uint8Array : Float32Array; + if (!(pixels instanceof TypedArrayClass) || pixels.length !== len) { + pixels = new TypedArrayClass(len); + } + + gl.readPixels( + x, + flipY ? (flipY - y - height) : y, + width, + height, + format, + type, pixels ); -}; + + // Re-bind whatever was previously bound + gl.bindFramebuffer(gl.FRAMEBUFFER, prevFramebuffer); + + if (flipY) { + // WebGL pixels are inverted compared to 2D pixels, so we have to flip + // the resulting rows. Adapted from https://stackoverflow.com/a/41973289 + const halfHeight = Math.floor(height / 2); + const tmpRow = new TypedArrayClass(width * channels); + for (let y = 0; y < halfHeight; y++) { + const topOffset = y * width * 4; + const bottomOffset = (height - y - 1) * width * 4; + tmpRow.set(pixels.subarray(topOffset, topOffset + width * 4)); + pixels.copyWithin(topOffset, bottomOffset, bottomOffset + width * 4); + pixels.set(tmpRow, bottomOffset); + } + } + + return pixels; +} + +/** + * @private + * @param {WebGLRenderingContext} gl The WebGL context + * @param {WebGLFramebuffer|null} framebuffer The Framebuffer to read + * @param {Number} x The x coordinate to read, premultiplied by pixel density + * @param {Number} y The y coordinate to read, premultiplied by pixel density + * @param {GLEnum} format Either RGB or RGBA depending on how many channels to read + * @param {GLEnum} type The datatype of each channel, e.g. UNSIGNED_BYTE or FLOAT + * @param {Number|undefined} flipY If provided, the total height with which to flip the y axis about + * @returns {Number[]} pixels The channel data for the pixel at that location + */ +export function readPixelWebGL( + gl, + framebuffer, + x, + y, + format, + type, + flipY +) { + // Record the currently bound framebuffer so we can go back to it after, and + // bind the framebuffer we want to read from + const prevFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING); + gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); + + const channels = format === gl.RGBA ? 4 : 3; + const TypedArrayClass = type === gl.UNSIGNED_BYTE ? Uint8Array : Float32Array; + const pixels = new TypedArrayClass(channels); + + gl.readPixels( + x, flipY ? (flipY - y - 1) : y, 1, 1, + format, type, + pixels + ); + + // Re-bind whatever was previously bound + gl.bindFramebuffer(gl.FRAMEBUFFER, prevFramebuffer); + + return Array.from(pixels); +} ////////////////////////////////////////////// // HASH | for geometry diff --git a/src/webgl/shaders/line.vert b/src/webgl/shaders/line.vert index f167cd092d..968e4824a1 100644 --- a/src/webgl/shaders/line.vert +++ b/src/webgl/shaders/line.vert @@ -65,11 +65,6 @@ vec2 lineIntersection(vec2 aPoint, vec2 aDir, vec2 bPoint, vec2 bDir) { } void main() { - // using a scale <1 moves the lines towards the camera - // in order to prevent popping effects due to half of - // the line disappearing behind the geometry faces. - vec3 scale = vec3(0.9995); - // Caps have one of either the in or out tangent set to 0 vCap = (aTangentIn == vec3(0.)) != (aTangentOut == (vec3(0.))) ? 1. : 0.; @@ -85,6 +80,20 @@ void main() { vec4 posqIn = uModelViewMatrix * (aPosition + vec4(aTangentIn, 0)); vec4 posqOut = uModelViewMatrix * (aPosition + vec4(aTangentOut, 0)); + float facingCamera = pow( + // The word space tangent's z value is 0 if it's facing the camera + abs(normalize(posqIn-posp).z), + + // Using pow() here to ramp `facingCamera` up from 0 to 1 really quickly + // so most lines get scaled and don't get clipped + 0.25 + ); + + // using a scale <1 moves the lines towards the camera + // in order to prevent popping effects due to half of + // the line disappearing behind the geometry faces. + float scale = mix(1., 0.995, facingCamera); + // Moving vertices slightly toward the camera // to avoid depth-fighting with the fill triangles. // Discussed here: diff --git a/test/unit/webgl/p5.Framebuffer.js b/test/unit/webgl/p5.Framebuffer.js index 74c2dbaff9..b2338cca90 100644 --- a/test/unit/webgl/p5.Framebuffer.js +++ b/test/unit/webgl/p5.Framebuffer.js @@ -318,4 +318,138 @@ suite('p5.Framebuffer', function() { }); }); }); + + suite('get()', function() { + test('get(x, y) loads a pixel', function() { + myp5.createCanvas(20, 20, myp5.WEBGL); + const fbo = myp5.createFramebuffer(); + fbo.draw(() => { + myp5.noStroke(); + + myp5.push(); + myp5.translate(-myp5.width/2, -myp5.height/2); + myp5.fill('red'); + myp5.sphere(5); + myp5.pop(); + + myp5.push(); + myp5.translate(myp5.width/2, myp5.height/2); + myp5.fill('blue'); + myp5.sphere(5); + myp5.pop(); + }); + + assert.deepEqual(fbo.get(0, 0), [255, 0, 0, 255]); + assert.deepEqual(fbo.get(10, 10), [0, 0, 0, 0]); + assert.deepEqual(fbo.get(19, 19), [0, 0, 255, 255]); + }); + + test('get() creates a p5.Image with equivalent pixels', function() { + myp5.createCanvas(20, 20, myp5.WEBGL); + const fbo = myp5.createFramebuffer(); + fbo.draw(() => { + myp5.noStroke(); + + myp5.push(); + myp5.translate(-myp5.width/2, -myp5.height/2); + myp5.fill('red'); + myp5.sphere(5); + myp5.pop(); + + myp5.push(); + myp5.translate(myp5.width/2, myp5.height/2); + myp5.fill('blue'); + myp5.sphere(5); + myp5.pop(); + }); + const img = fbo.get(); + + fbo.loadPixels(); + img.loadPixels(); + + expect(img.width).to.equal(fbo.width); + expect(img.height).to.equal(fbo.height); + for (let i = 0; i < img.pixels.length; i++) { + expect(img.pixels[i]).to.be.closeTo(fbo.pixels[i], 1); + } + }); + + test('get() creates a p5.Image with 1x pixel density', function() { + const mainCanvas = myp5.createCanvas(20, 20, myp5.WEBGL); + myp5.pixelDensity(2); + const fbo = myp5.createFramebuffer(); + fbo.draw(() => { + myp5.noStroke(); + myp5.background(255); + + myp5.push(); + myp5.translate(-myp5.width/2, -myp5.height/2); + myp5.fill('red'); + myp5.sphere(5); + myp5.pop(); + + myp5.push(); + myp5.translate(myp5.width/2, myp5.height/2); + myp5.fill('blue'); + myp5.sphere(5); + myp5.pop(); + }); + const img = fbo.get(); + const p2d = myp5.createGraphics(20, 20); + p2d.pixelDensity(1); + myp5.image(fbo, -10, -10); + p2d.image(mainCanvas, 0, 0); + + fbo.loadPixels(); + img.loadPixels(); + p2d.loadPixels(); + + expect(img.width).to.equal(fbo.width); + expect(img.height).to.equal(fbo.height); + expect(img.pixels.length).to.equal(fbo.pixels.length / 4); + // The pixels should be approximately the same in the 1x image as when we + // draw the framebuffer onto a 1x canvas + for (let i = 0; i < img.pixels.length; i++) { + expect(img.pixels[i]).to.be.closeTo(p2d.pixels[i], 2); + } + }); + }); + + test( + 'loadPixels works in arbitrary order for multiple framebuffers', + function() { + myp5.createCanvas(20, 20, myp5.WEBGL); + const fbo1 = myp5.createFramebuffer(); + const fbo2 = myp5.createFramebuffer(); + + fbo1.loadPixels(); + fbo2.loadPixels(); + for (let i = 0; i < fbo1.pixels.length; i += 4) { + // Set everything red + fbo1.pixels[i] = 255; + fbo1.pixels[i + 1] = 0; + fbo1.pixels[i + 2] = 0; + fbo1.pixels[i + 3] = 255; + } + for (let i = 0; i < fbo2.pixels.length; i += 4) { + // Set everything blue + fbo2.pixels[i] = 0; + fbo2.pixels[i + 1] = 0; + fbo2.pixels[i + 2] = 255; + fbo2.pixels[i + 3] = 255; + } + fbo2.updatePixels(); + fbo1.updatePixels(); + + myp5.imageMode(myp5.CENTER); + + myp5.clear(); + myp5.image(fbo1, 0, 0); + assert.deepEqual(myp5.get(0, 0), [255, 0, 0, 255]); + + myp5.clear(); + myp5.image(fbo2, 0, 0); + assert.deepEqual(myp5.get(0, 0), [0, 0, 255, 255]); + } + ); }); diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index c779b46810..5a736625f4 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -92,6 +92,30 @@ suite('p5.RendererGL', function() { ); done(); }); + + test('coplanar strokes match 2D', function(done) { + const getColors = function(mode) { + myp5.createCanvas(20, 20, mode); + myp5.pixelDensity(1); + myp5.background(255); + myp5.strokeCap(myp5.SQUARE); + myp5.strokeJoin(myp5.MITER); + if (mode === myp5.WEBGL) { + myp5.translate(-myp5.width/2, -myp5.height/2); + } + myp5.stroke('black'); + myp5.strokeWeight(4); + myp5.fill('red'); + myp5.rect(10, 10, 15, 15); + myp5.fill('blue'); + myp5.rect(0, 0, 15, 15); + myp5.loadPixels(); + return [...myp5.pixels]; + }; + + assert.deepEqual(getColors(myp5.P2D), getColors(myp5.WEBGL)); + done(); + }); }); suite('text shader', function() { @@ -360,6 +384,42 @@ suite('p5.RendererGL', function() { assert.isTrue(img.length === 4); done(); }); + + test('updatePixels() matches 2D mode', function() { + myp5.createCanvas(20, 20); + myp5.pixelDensity(1); + const getColors = function(mode) { + const g = myp5.createGraphics(20, 20, mode); + g.pixelDensity(1); + g.background(255); + g.loadPixels(); + for (let y = 0; y < g.height; y++) { + for (let x = 0; x < g.width; x++) { + const idx = (y * g.width + x) * 4; + g.pixels[idx] = (x / g.width) * 255; + g.pixels[idx + 1] = (y / g.height) * 255; + g.pixels[idx + 2] = 255; + g.pixels[idx + 3] = 255; + } + } + g.updatePixels(); + return g; + }; + + const p2d = getColors(myp5.P2D); + const webgl = getColors(myp5.WEBGL); + myp5.image(p2d, 0, 0); + myp5.blendMode(myp5.DIFFERENCE); + myp5.image(webgl, 0, 0); + myp5.loadPixels(); + + // There should be no difference, so the result should be all black + // at 100% opacity. We add +/- 1 for wiggle room to account for precision + // loss. + for (let i = 0; i < myp5.pixels.length; i++) { + expect(myp5.pixels[i]).to.be.closeTo(i % 4 === 3 ? 255 : 0, 1); + } + }); }); suite('get()', function() { @@ -1210,7 +1270,7 @@ suite('p5.RendererGL', function() { renderer.bezierVertex(128, -128, 128, 128, -128, 128); renderer.endShape(); - assert.deepEqual(myp5.get(128, 128), [255, 129, 129, 255]); + assert.deepEqual(myp5.get(128, 127), [255, 129, 129, 255]); done(); }); @@ -1231,7 +1291,7 @@ suite('p5.RendererGL', function() { renderer.bezierVertex(128, -128, 128, 128, -128, 128); renderer.endShape(); - assert.deepEqual(myp5.get(190, 128), [255, 128, 128, 255]); + assert.deepEqual(myp5.get(190, 127), [255, 128, 128, 255]); done(); }); @@ -1250,7 +1310,7 @@ suite('p5.RendererGL', function() { renderer.quadraticVertex(256, 0, -128, 128); renderer.endShape(); - assert.deepEqual(myp5.get(128, 128), [255, 128, 128, 255]); + assert.deepEqual(myp5.get(128, 127), [255, 128, 128, 255]); done(); }); @@ -1271,7 +1331,7 @@ suite('p5.RendererGL', function() { renderer.quadraticVertex(256, 0, -128, 128); renderer.endShape(); - assert.deepEqual(myp5.get(190, 128), [255, 128, 128, 255]); + assert.deepEqual(myp5.get(190, 127), [255, 128, 128, 255]); done(); }); @@ -1319,7 +1379,7 @@ suite('p5.RendererGL', function() { myp5.model(myGeom); assert.equal(renderer._useLineColor, true); - assert.deepEqual(myp5.get(128, 0), [127, 0, 128, 255]); + assert.deepEqual(myp5.get(128, 255), [127, 0, 128, 255]); done(); }); @@ -1340,7 +1400,7 @@ suite('p5.RendererGL', function() { myp5.endShape(myp5.CLOSE); assert.equal(renderer._useLineColor, true); - assert.deepEqual(myp5.get(128, 0), [127, 0, 128, 255]); + assert.deepEqual(myp5.get(128, 255), [127, 0, 128, 255]); done(); }); }); @@ -1503,6 +1563,7 @@ suite('p5.RendererGL', function() { }); myp5.fill(255); + myp5.noStroke(); myp5.directionalLight(255, 255, 255, 0, 0, -1); myp5.triangle(-8, -8, 8, -8, 8, 8);