| versions | 0095+ |
|---|---|
| contributors | matt.patey |
| started on | 2008-04-30 23:11 |
Processing offers the use of an off-screen buffer, which can be very useful for creating masking effects. In this hack I outline the basics of writing and reading pixels from an off-screen buffer and suggest some practical ways of applying this technique.
Processing offers the createGraphics method to create an off-screen graphics buffer. This buffer is in most ways the same as the buffer used to draw on-screen images, but it is different in that when you draw to it you don't actually see anything on the screen. This can be very handy for many reasons. For example, drawing to an off-screen buffer can save CPU cycles by letting you draw pixels using Processing drawing routines then save those pixels to a PImage, and draw the PImage to the screen instead of re-drawing the image each frame. Another use of off-screen buffers is the ability to mask areas of an image drawn in Processing to give the effect of a 'viewport' or scrollable area. In this hack I will show how you can use both of these techniques.
First you need to create a new buffer. We do this by creating a PGraphics instance via the createGraphics method:
PGraphics buf = createGraphics(500, 500, P3D);
This creates an off-screen buffer (buf) that is 500 pixels wide by 500 pixels high. Drawing to the buffer is no different than drawing to the regular Processing graphics buffer with the exception that drawing methods must be wrapped between the beginDraw and endDraw methods. For example, if we wanted to draw a rectangle within the off-screen buffer we'd write something like this:
buf.beginDraw(); buf.rectMode(CORNER); buf.noStroke(); buf.fill(255); buf.rect(0, 0, 100, 100); buf.endDraw();
Invoking drawing methods on an off-screen buffer will not be shown on the screen. In order to show what you've drawn in the off-screen buffer you'll need to copy the buffer (or part of the buffer) to the main screen. To do this, you use the get method, which copies pixels from the buffer and returns a PImage instance that you can use to display on screen. For example:
PGraphics buffer; PImage img; void setup() { size(500, 500, P3D); // Create an off-screen buffer. buffer = createGraphics(500, 500, P3D); // Draw a rectangle in the off-screen buffer. buffer.beginDraw(); buffer.noStroke(); buffer.fill(255, 0, 0); buffer.rect(0, 0, 100, 100); buffer.endDraw(); // Copy some pixels from the off-screen buffer to display // them on the screen. img = buffer.get(0, 0, buffer.width, buffer.height); } void draw() { background(0); image(img, 0, 0); }
The above code will draw a rectangle in the off-screen buffer, then copy the buffer's contents to the screen using the get and image methods. This technique can be especially helpful when you want to draw complex imagery on the screen but maintain a decent frame rate.
One benefit of off-screen buffers is that they can be much much larger than the size of the Processing 'canvas', so you can use them as storage areas for graphics. You can then reference specific parts of that canvas and draw them on screen. This ability comes in handy if you have very large graphics that you want to let the user pan around or long text you want to scroll through.
Let's say you have a patch that generates complex imagery, say hair or grass for example. You're not interested in animating it but you don't want to slow down the rest of your patch with your complex algorithm. This technique lets you do this by drawing pixels to an off-screen buffer once, then rendering it as a static image on the main canvas. Another benefit of this technique is that since the snapshot of the off-screen buffer is constantly being re-drawn (as a static image) you can move it around or draw over or under it without having to worry about graphics that overlap it 'sticking' to the screen.
/** * offscreenComplex.pde * * Demonstrates how off-screen buffers can be used to maintain a * high frame-rate even while drawing complex imagery that normally * causes the frame rate to plummet significantly. * * @author Matt Patey */ PGraphics buffer; PImage img; PFont font; boolean mode; int targetFrameRate; void setup() { size(500, 500); targetFrameRate = 30; frameRate(targetFrameRate); // Create an off-screen buffer. buffer = createGraphics(500, 500, JAVA2D); // Draw something complex in the off-screen buffer. renderComplexImage(buffer); font = loadFont("CourierNew36.vlw"); // Copy some pixels from the off-screen buffer to display // them on the screen. updateBuffer(); } /** * Populates the off-screen buffer with a complex image then copies * the buffer contents to an image that we will display on screen. */ void updateBuffer() { renderComplexImage(buffer); img = buffer.get(0, 0, buffer.width, buffer.height); } void keyPressed() { mode = !mode; } void mousePressed() { updateBuffer(); } void draw() { background(255); if(!mode) { image(img, 0, 0); } else { renderComplexImage(buffer); image(img, 0, 0); } // We can still animate things on the main canvas. noStroke(); fill(255, 0, 0, 128); ellipse(random(width), random(height), 20, 20); drawInfo(); } /** * Draws a complex drawing into an off-screen buffer. */ void renderComplexImage(PGraphics buffer) { buffer.beginDraw(); buffer.background(255); buffer.smooth(); buffer.noFill(); for(int i = 0; i < buffer.width; i++) { for(int j = 0; j < buffer.height; j++) { if(i % 8 == 0 && j % 8 == 0) { buffer.stroke(random(30) + 80, random(30) + 40, 0, random(60) + 24); float x = width / 2 + (random(60) - 30); float y = 0; float x2 = x + (random(160) - 80); float y2 = random(height / 2) + height / 2; buffer.bezier(x, y, x + (random(x/2) - x/2), y + (random(y/2) - y/2), x2 + (random(x2/2) - x2/2), y2 + (random(y2/2) - y2/2), x2, y2); } } } buffer.endDraw(); } /** * Draws information about the frame rate and mode. */ void drawInfo() { fill(0); textFont(font); textSize(18); text("are we re-drawing every frame: " + mode, 5, height - 40); text("target frame rate: " + targetFrameRate, 5, height - 25); text("current frame rate: " + round(frameRate), 5, height - 10); }
This technique can also be used to draw thousands of things to the screen without slowing the applet to a halt. To do this simply draw an image to an off-screen buffer then make as many copies of the image into an array of PImageS then draw each of those via the image method.
I've found one of the most immediately gratifying aspects of using off-screen buffers is their ability to let you display only specific parts of graphics drawn in Processing. In the following example we'll create a 400 x 400 pixel canvas, and an off-screen buffer used to accommodate an 800 x 500 pixel image. Pressing the arrow keys will change which part of the off-screen buffer are copied to the screen, thus giving the appearance of being able to pan to unseen parts of the image.
/** * PanDemo.pde * * Processing sketch illustrates how an off-screen buffer can be * used to create a panning tool to view an image larger than the * base canvas. Use the arrow keys to scroll to the limits of * the displayed image. * * @author Matt Patey */ PImage imgBorealis; PImage bufSlice; PGraphics buf; int copyOffsetX; int copyOffsetY; int copyWidth; int copyHeight; void setup() { size(400, 400, P3D); // Load our image. imgBorealis = loadImage("borealis.jpg"); // Create an off-screen buffer that will contain the entire image. buf = createGraphics(imgBorealis.width, imgBorealis.height, P3D); buf.beginDraw(); buf.image(imgBorealis, 0, 0); buf.endDraw(); copyOffsetX = 0; copyOffsetY = 0; copyWidth = width; copyHeight = height; } void draw() { background(0); image(getBufSlice(), 0, 0); } /** * Updates the copied version of the off-screen buffer. */ PImage getBufSlice() { return buf.get(copyOffsetX, copyOffsetY, copyWidth, copyHeight); } /** * Handle key presses. */ void keyPressed() { switch(keyCode) { case LEFT: if(copyOffsetX < buf.width - width) { copyOffsetX++; } break; case RIGHT: if(copyOffsetX > 0) { copyOffsetX--; } break; case UP: if(copyOffsetY < buf.height - height) { copyOffsetY++; } break; case DOWN: if(copyOffsetY > 0) { copyOffsetY--; } break; } }
This code will render a simple two-color gradient to a new PImage. Of course you can create gradient images manually in an image editor, but it's cool to generate it in-app (plus you can alter the colors on the fly). For a static background you can render this once in setup and then drawing each frame should be pretty quick. I found that in some cases if you use this as a full-window background, you still have to call background(0) first or else you'll get trails.
/** Generate a vertical gradient image */ PImage generateGradient(color top, color bottom, int w, int h) { int tR = (top >> 16) & 0xFF; int tG = (top >> 8) & 0xFF; int tB = top & 0xFF; int bR = (bottom >> 16) & 0xFF; int bG = (bottom >> 8) & 0xFF; int bB = bottom & 0xFF; PImage bg = createImage(w,h,RGB); bg.loadPixels(); for(int i=0; i < bg.pixels.length; i++) { int y = i/bg.width; float n = y/(float)bg.height; // for a horizontal gradient: // float n = x/(float)bg.width; bg.pixels[i] = color( lerp(tR,bR,n), lerp(tG,bG,n), lerp(tB,bB,n), 255); } bg.updatePixels(); return bg; }