diff --git a/src/webgl/p5.Framebuffer.js b/src/webgl/p5.Framebuffer.js index e4f85fcbda..4c816d9464 100644 --- a/src/webgl/p5.Framebuffer.js +++ b/src/webgl/p5.Framebuffer.js @@ -74,15 +74,33 @@ p5.FramebufferTexture = FramebufferTexture; class Framebuffer { /** - * An object that one can draw to and then read as a texture. While similar - * to a p5.Graphics, using a p5.Framebuffer as a texture will generally run - * much faster, as it lives within the same WebGL context as the canvas it - * is created on. It only works in WebGL mode. + * A class to describe a high-performance drawing surface for textures. + * + * Each `p5.Framebuffer` object provides a dedicated drawing surface called + * a *framebuffer*. They're similar to + * p5.Graphics objects but can run much faster. + * Performance is improved because the framebuffer shares the same WebGL + * context as the canvas used to create it. + * + * `p5.Framebuffer` objects have all the drawing features of the main + * canvas. Drawing instructions meant for the framebuffer must be placed + * between calls to + * myBuffer.begin() and + * myBuffer.end(). The resulting image + * can be applied as a texture by passing the `p5.Framebuffer` object to the + * texture() function, as in `texture(myBuffer)`. + * It can also be displayed on the main canvas by passing it to the + * image() function, as in `image(myBuffer, 0, 0)`. + * + * Note: createFramebuffer() is the + * recommended way to create an instance of this class. * * @class p5.Framebuffer * @constructor - * @param {p5.Graphics|p5} target A p5 global instance or p5.Graphics - * @param {Object} [settings] A settings object + * @param {p5.Graphics|p5} target sketch instance or + * p5.Graphics + * object. + * @param {Object} [settings] configuration options. */ constructor(target, settings = {}) { this.target = target; @@ -91,20 +109,56 @@ class Framebuffer { this._isClipApplied = false; /** - * A Uint8ClampedArray - * containing the values for all the pixels in the Framebuffer. + * An array containing the color of each pixel in the framebuffer. * - * Like the main canvas pixels property, call - * loadPixels() before reading - * it, and call updatePixels() - * afterwards to update its data. + * myBuffer.loadPixels() must be + * called before accessing the `myBuffer.pixels` array. + * myBuffer.updatePixels() + * must be called after any changes are made. * - * 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. + * Note: Updating pixels via this property is slower than drawing to the + * framebuffer directly. Consider using a + * p5.Shader object instead of looping over + * `myBuffer.pixels`. * * @property {Number[]} pixels + * + * @example + *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * background(200);
+ *
+ * // Create a p5.Framebuffer object.
+ * let myBuffer = createFramebuffer();
+ *
+ * // Load the pixels array.
+ * myBuffer.loadPixels();
+ *
+ * // Get the number of pixels in the
+ * // top half of the framebuffer.
+ * let numPixels = myBuffer.pixels.length / 2;
+ *
+ * // Set the framebuffer's top half to pink.
+ * for (let i = 0; i < numPixels; i += 4) {
+ * myBuffer.pixels[i] = 255;
+ * myBuffer.pixels[i + 1] = 102;
+ * myBuffer.pixels[i + 2] = 204;
+ * myBuffer.pixels[i + 3] = 255;
+ * }
+ *
+ * // Update the pixels array.
+ * myBuffer.updatePixels();
+ *
+ * // Draw the p5.Framebuffer object to the canvas.
+ * image(myBuffer, -50, -50);
+ *
+ * describe('A pink rectangle above a gray rectangle.');
+ * }
+ *
+ *
- * let framebuffer;
+ * let myBuffer;
+ *
* function setup() {
* createCanvas(100, 100, WEBGL);
- * framebuffer = createFramebuffer();
- * noStroke();
- * }
*
- * function mouseMoved() {
- * framebuffer.resize(
- * max(20, mouseX),
- * max(20, mouseY)
- * );
+ * // Create a p5.Framebuffer object.
+ * myBuffer = createFramebuffer();
+ *
+ * describe('A multicolor sphere on a white surface. The image grows larger or smaller when the user moves the mouse, revealing a gray background.');
* }
*
* function draw() {
- * // Draw to the framebuffer
- * framebuffer.begin();
+ * background(200);
+ *
+ * // Draw to the p5.Framebuffer object.
+ * myBuffer.begin();
* background(255);
* normalMaterial();
* sphere(20);
- * framebuffer.end();
+ * myBuffer.end();
+ *
+ * // Display the p5.Framebuffer object.
+ * image(myBuffer, -50, -50);
+ * }
*
- * background(100);
- * // Draw the framebuffer to the main canvas
- * image(framebuffer, -width/2, -height/2);
+ * // Resize the p5.Framebuffer object when the
+ * // user moves the mouse.
+ * function mouseMoved() {
+ * myBuffer.resize(mouseX, mouseY);
* }
*
*
+ * let myBuffer;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Framebuffer object.
+ * myBuffer = createFramebuffer();
+ *
+ * describe("A white circle on a gray canvas. The circle's edge become fuzzy while the user presses and holds the mouse.");
+ * }
+ *
+ * function draw() {
+ * // Draw to the p5.Framebuffer object.
+ * myBuffer.begin();
+ * background(200);
+ * circle(0, 0, 40);
+ * myBuffer.end();
+ *
+ * // Display the p5.Framebuffer object.
+ * image(myBuffer, -50, -50);
+ * }
+ *
+ * // Decrease the pixel density when the user
+ * // presses the mouse.
+ * function mousePressed() {
+ * myBuffer.pixelDensity(1);
+ * }
+ *
+ * // Increase the pixel density when the user
+ * // releases the mouse.
+ * function mouseReleased() {
+ * myBuffer.pixelDensity(2);
+ * }
+ *
+ *
+ * let myBuffer;
+ * let myFont;
+ *
+ * // Load a font and create a p5.Font object.
+ * function preload() {
+ * myFont = loadFont('assets/inconsolata.otf');
+ * }
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * background(200);
+ *
+ * // Create a p5.Framebuffer object.
+ * myBuffer = createFramebuffer();
+ *
+ * // Get the p5.Framebuffer object's pixel density.
+ * let d = myBuffer.pixelDensity();
+ *
+ * // Style the text.
+ * textAlign(CENTER, CENTER);
+ * textFont(myFont);
+ * textSize(16);
+ * fill(0);
+ *
+ * // Display the pixel density.
+ * text(`Density: ${d}`, 0, 0);
+ *
+ * describe(`The text "Density: ${d}" written in black on a gray background.`);
+ * }
+ *
+ *
+ * // Double-click to toggle the autosizing mode.
+ *
+ * let myBuffer;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Framebuffer object.
+ * myBuffer = createFramebuffer();
+ *
+ * describe('A multicolor sphere on a gray background. The image resizes when the user moves the mouse.');
+ * }
+ *
+ * function draw() {
+ * background(50);
+ *
+ * // Draw to the p5.Framebuffer object.
+ * myBuffer.begin();
+ * background(200);
+ * normalMaterial();
+ * sphere(width / 4);
+ * myBuffer.end();
+ *
+ * // Display the p5.Framebuffer object.
+ * image(myBuffer, -width / 2, -height / 2);
+ * }
+ *
+ * // Resize the canvas when the user moves the mouse.
+ * function mouseMoved() {
+ * let w = constrain(mouseX, 0, 100);
+ * let h = constrain(mouseY, 0, 100);
+ * resizeCanvas(w, h);
+ * }
+ *
+ * // Toggle autoSizing when the user double-clicks.
+ * // Note: opened an issue to fix(?) this.
+ * function doubleClicked() {
+ * let isAuto = myBuffer.autoSized();
+ * myBuffer.autoSized(!isAuto);
+ * }
+ *
+ *
+ * // Double-click to toggle between cameras.
+ *
+ * let myBuffer;
+ * let cam1;
+ * let cam2;
+ * let usingCam1 = true;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Framebuffer object.
+ * myBuffer = createFramebuffer();
+ *
+ * // Create the cameras between begin() and end().
+ * myBuffer.begin();
+ *
+ * // Create the first camera.
+ * // Keep its default settings.
+ * cam1 = myBuffer.createCamera();
+ *
+ * // Create the second camera.
+ * // Place it at the top-left.
+ * // Point it at the origin.
+ * cam2 = myBuffer.createCamera();
+ * cam2.setPosition(400, -400, 800);
+ * cam2.lookAt(0, 0, 0);
+ *
+ * myBuffer.end();
+ *
+ * describe(
+ * 'A white cube on a gray background. The camera toggles between frontal and aerial views when the user double-clicks.'
+ * );
+ * }
+ *
+ * function draw() {
+ * // Draw to the p5.Framebuffer object.
+ * myBuffer.begin();
+ * background(200);
+ *
+ * // Set the camera.
+ * if (usingCam1 === true) {
+ * setCamera(cam1);
+ * } else {
+ * setCamera(cam2);
+ * }
+ *
+ * // Reset all transformations.
+ * resetMatrix();
+ *
+ * // Draw the box.
+ * box();
+ *
+ * myBuffer.end();
+ *
+ * // Display the p5.Framebuffer object.
+ * image(myBuffer, -50, -50);
+ * }
+ *
+ * // Toggle the current camera when the user double-clicks.
+ * function doubleClicked() {
+ * if (usingCam1 === true) {
+ * usingCam1 = false;
+ * } else {
+ * usingCam1 = true;
+ * }
+ * }
+ *
+ *
- * let framebuffer;
+ * // Double-click to remove the p5.Framebuffer object.
+ *
+ * let myBuffer;
+ *
* function setup() {
* createCanvas(100, 100, WEBGL);
+ *
+ * // Create an options object.
+ * let options = { width: 60, height: 60 };
+ *
+ * // Create a p5.Framebuffer object and
+ * // configure it using options.
+ * myBuffer = createFramebuffer(options);
+ *
+ * describe('A white circle at the center of a dark gray square disappears when the user double-clicks.');
* }
*
* function draw() {
- * const useFramebuffer = (frameCount % 120) > 60;
- * if (useFramebuffer && !framebuffer) {
- * // Create a new framebuffer for us to use
- * framebuffer = createFramebuffer();
- * } else if (!useFramebuffer && framebuffer) {
- * // Free the old framebuffer's resources
- * framebuffer.remove();
- * framebuffer = undefined;
- * }
+ * background(200);
*
- * background(255);
- * if (useFramebuffer) {
- * // Draw to the framebuffer
- * framebuffer.begin();
- * background(255);
- * rotateX(frameCount * 0.01);
- * rotateY(frameCount * 0.01);
- * fill(255, 0, 0);
- * box(30);
- * framebuffer.end();
- *
- * image(framebuffer, -width/2, -height/2);
+ * // Display the p5.Framebuffer object if
+ * // it's available.
+ * if (myBuffer) {
+ * // Draw to the p5.Framebuffer object.
+ * myBuffer.begin();
+ * background(100);
+ * circle(0, 0, 20);
+ * myBuffer.end();
+ *
+ * image(myBuffer, -30, -30);
* }
* }
+ *
+ * // Remove the p5.Framebuffer object when the
+ * // the user double-clicks.
+ * function doubleClicked() {
+ * // Delete the framebuffer from GPU memory.
+ * myBuffer.remove();
+ *
+ * // Delete the framebuffer from CPU memory.
+ * myBuffer = undefined;
+ * }
*
*
- * let framebuffer;
+ * let myBuffer;
+ *
* function setup() {
* createCanvas(100, 100, WEBGL);
- * framebuffer = createFramebuffer();
- * noStroke();
+ *
+ * // Create a p5.Framebuffer object.
+ * myBuffer = createFramebuffer();
+ *
+ * describe('An empty gray canvas. The canvas gets darker and a rotating, multicolor torus appears while the user presses and holds the mouse.');
* }
*
* function draw() {
- * // Draw to the framebuffer
- * framebuffer.begin();
- * background(255);
- * translate(0, 10*sin(frameCount * 0.01), 0);
- * rotateX(frameCount * 0.01);
+ * background(200);
+ *
+ * // Start drawing to the p5.Framebuffer object.
+ * myBuffer.begin();
+ *
+ * background(50);
* rotateY(frameCount * 0.01);
- * fill(255, 0, 0);
- * box(50);
- * framebuffer.end();
- *
- * background(100);
- * // Draw the framebuffer to the main canvas
- * image(framebuffer, -50, -50, 25, 25);
- * image(framebuffer, 0, 0, 35, 35);
+ * normalMaterial();
+ * torus(30);
+ *
+ * // Stop drawing to the p5.Framebuffer object.
+ * myBuffer.end();
+ *
+ * // Display the p5.Framebuffer object while
+ * // the user presses the mouse.
+ * if (mouseIsPressed === true) {
+ * image(myBuffer, -50, -50);
+ * }
* }
*
*
+ * let myBuffer;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Framebuffer object.
+ * myBuffer = createFramebuffer();
+ *
+ * describe('An empty gray canvas. The canvas gets darker and a rotating, multicolor torus appears while the user presses and holds the mouse.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Start drawing to the p5.Framebuffer object.
+ * myBuffer.begin();
+ *
+ * background(50);
+ * rotateY(frameCount * 0.01);
+ * normalMaterial();
+ * torus(30);
+ *
+ * // Stop drawing to the p5.Framebuffer object.
+ * myBuffer.end();
+ *
+ * // Display the p5.Framebuffer object while
+ * // the user presses the mouse.
+ * if (mouseIsPressed === true) {
+ * image(myBuffer, -50, -50);
+ * }
+ * }
+ *
+ *
- * let framebuffer;
+ * // Click the canvas to display the framebuffer.
+ *
+ * let myBuffer;
+ *
* function setup() {
* createCanvas(100, 100, WEBGL);
- * framebuffer = createFramebuffer();
- * noStroke();
+ *
+ * // Create a p5.Framebuffer object.
+ * myBuffer = createFramebuffer();
+ *
+ * describe('An empty gray canvas. The canvas gets darker and a rotating, multicolor torus appears while the user presses and holds the mouse.');
* }
*
* function draw() {
- * // Draw to the framebuffer
- * framebuffer.draw(function() {
- * background(255);
- * translate(0, 10*sin(frameCount * 0.01), 0);
- * rotateX(frameCount * 0.01);
- * rotateY(frameCount * 0.01);
- * fill(255, 0, 0);
- * box(50);
- * });
- *
- * background(100);
- * // Draw the framebuffer to the main canvas
- * image(framebuffer, -50, -50, 25, 25);
- * image(framebuffer, 0, 0, 35, 35);
+ * background(200);
+ *
+ * // Draw to the p5.Framebuffer object.
+ * myBuffer.draw(bagel);
+ *
+ * // Display the p5.Framebuffer object while
+ * // the user presses the mouse.
+ * if (mouseIsPressed === true) {
+ * image(myBuffer, -50, -50);
+ * }
+ * }
+ *
+ * // Draw a rotating, multicolor torus.
+ * function bagel() {
+ * background(50);
+ * rotateY(frameCount * 0.01);
+ * normalMaterial();
+ * torus(30);
* }
*
*
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * background(200);
+ *
+ * // Create a p5.Framebuffer object.
+ * let myBuffer = createFramebuffer();
+ *
+ * // Load the pixels array.
+ * myBuffer.loadPixels();
+ *
+ * // Get the number of pixels in the
+ * // top half of the framebuffer.
+ * let numPixels = myBuffer.pixels.length / 2;
+ *
+ * // Set the framebuffer's top half to pink.
+ * for (let i = 0; i < numPixels; i += 4) {
+ * myBuffer.pixels[i] = 255;
+ * myBuffer.pixels[i + 1] = 102;
+ * myBuffer.pixels[i + 2] = 204;
+ * myBuffer.pixels[i + 3] = 255;
+ * }
+ *
+ * // Update the pixels array.
+ * myBuffer.updatePixels();
+ *
+ * // Draw the p5.Framebuffer object to the canvas.
+ * image(myBuffer, -50, -50);
+ *
+ * describe('A pink rectangle above a gray rectangle.');
+ * }
+ *
+ *
- * 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;
- * }
+ *
+ * background(200);
+ *
+ * // Create a p5.Framebuffer object.
+ * let myBuffer = createFramebuffer();
+ *
+ * // Load the pixels array.
+ * myBuffer.loadPixels();
+ *
+ * // Get the number of pixels in the
+ * // top half of the framebuffer.
+ * let numPixels = myBuffer.pixels.length / 2;
+ *
+ * // Set the framebuffer's top half to pink.
+ * for (let i = 0; i < numPixels; i += 4) {
+ * myBuffer.pixels[i] = 255;
+ * myBuffer.pixels[i + 1] = 102;
+ * myBuffer.pixels[i + 2] = 204;
+ * myBuffer.pixels[i + 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();
+ *
+ * // Update the pixels array.
+ * myBuffer.updatePixels();
+ *
+ * // Draw the p5.Framebuffer object to the canvas.
+ * image(myBuffer, -50, -50);
+ *
+ * describe('A pink rectangle above a gray rectangle.');
* }
*
*
- * let framebuffer;
* function setup() {
* createCanvas(100, 100, WEBGL);
- * framebuffer = createFramebuffer();
+ *
+ * background(200);
+ *
+ * // Create a p5.Framebuffer object.
+ * let myBuffer = createFramebuffer();
+ *
+ * // Start drawing to the p5.Framebuffer object.
+ * myBuffer.begin();
+ *
+ * triangle(-25, 25, 0, -25, 25, 25);
+ *
+ * // Stop drawing to the p5.Framebuffer object.
+ * myBuffer.end();
+ *
+ * // Use the p5.Framebuffer object's WebGLTexture.
+ * texture(myBuffer.color);
+ *
+ * // Style the plane.
* noStroke();
- * }
*
- * function draw() {
- * // Draw to the framebuffer
- * framebuffer.begin();
- * background(255);
- * normalMaterial();
- * sphere(20);
- * framebuffer.end();
+ * // Draw the plane.
+ * plane(myBuffer.width, myBuffer.height);
*
- * // Draw the framebuffer to the main canvas
- * image(framebuffer.color, -width/2, -height/2);
+ * describe('A white triangle on a gray background.');
* }
*
*
- * let framebuffer;
- * let depthShader;
+ * // Note: A "uniform" is a global variable within a shader program.
*
- * const vert = `
+ * // Create a string with the vertex shader program.
+ * // The vertex shader is called for each vertex.
+ * let vertSrc = `
* precision highp float;
* attribute vec3 aPosition;
* attribute vec2 aTexCoord;
* uniform mat4 uModelViewMatrix;
* uniform mat4 uProjectionMatrix;
* varying vec2 vTexCoord;
+ *
* void main() {
* vec4 viewModelPosition = uModelViewMatrix * vec4(aPosition, 1.0);
* gl_Position = uProjectionMatrix * viewModelPosition;
@@ -1358,48 +1791,63 @@ class Framebuffer {
* }
* `;
*
- * const frag = `
+ * // Create a string with the fragment shader program.
+ * // The fragment shader is called for each pixel.
+ * let fragSrc = `
* precision highp float;
* varying vec2 vTexCoord;
* uniform sampler2D depth;
+ *
* void main() {
+ * // Get the pixel's depth value.
* float depthVal = texture2D(depth, vTexCoord).r;
+ *
+ * // Set the pixel's color based on its depth.
* gl_FragColor = mix(
- * vec4(1., 1., 0., 1.), // yellow
- * vec4(0., 0., 1., 1.), // blue
- * pow(depthVal, 6.)
- * );
+ * vec4(0., 0., 0., 1.),
+ * vec4(1., 0., 1., 1.),
+ * depthVal);
* }
* `;
*
+ * let myBuffer;
+ * let myShader;
+ *
* function setup() {
* createCanvas(100, 100, WEBGL);
- * framebuffer = createFramebuffer();
- * depthShader = createShader(vert, frag);
- * noStroke();
+ *
+ * // Create a p5.Framebuffer object.
+ * myBuffer = createFramebuffer();
+ *
+ * // Create a p5.Shader object.
+ * myShader = createShader(vertSrc, fragSrc);
+ *
+ * // Compile and apply the shader.
+ * shader(myShader);
+ *
+ * describe('The shadow of a box rotates slowly against a magenta background.');
* }
*
* function draw() {
- * // Draw to the framebuffer
- * framebuffer.begin();
+ * // Draw to the p5.Framebuffer object.
+ * myBuffer.begin();
* background(255);
* rotateX(frameCount * 0.01);
- * box(20, 20, 100);
- * framebuffer.end();
+ * box(20, 20, 80);
+ * myBuffer.end();
*
- * push();
- * shader(depthShader);
- * depthShader.setUniform('depth', framebuffer.depth);
- * plane(framebuffer.width, framebuffer.height);
- * pop();
+ * // Set the shader's depth uniform using
+ * // the framebuffer's depth texture.
+ * myShader.setUniform('depth', myBuffer.depth);
+ *
+ * // Style the plane.
+ * noStroke();
+ *
+ * // Draw the plane.
+ * plane(myBuffer.width, myBuffer.height);
* }
*
*