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);