processingpgraphics

PGraphics seems to get cleared or frozen whenever drawn to main context


My ultimate goal is to create a "tunnel effect" as I draw a rect to a buffer, copy the buffer to another buffer, and on subsequent draw(), copy the second buffer back to the first, only slightly smaller, then draw over top of that and repeat.

I'm completely stumped by what is going on here.

First, consider this code, which works exactly as expected 1 time (no draw loop):

PGraphics canvas;
PGraphics buffer;

void setup(){
  size(500, 500);
  canvas = createGraphics(width, height);
  buffer = createGraphics(canvas.width, canvas.height);

  canvas.beginDraw();
  canvas.background(255);
  canvas.noFill();
  canvas.stroke(0);
  canvas.rect(100 + random(-50, 50), 100 + random(-50, 50), 350 + random(-50, 50), 350 + random(-50, 50));
  canvas.endDraw();

  buffer.beginDraw();
  buffer.image(canvas, 0, 0);
  buffer.endDraw();

  canvas.beginDraw();
  canvas.image(buffer, 100, 100, width-200, height-200);
  canvas.endDraw();

  image(canvas, 0, 0);

  noLoop();
}

It's a pretty dumb example, but it proves that the concept is sound: I draw to canvas, copy to buffer, copy buffer back to canvas with a reduced scale then output to main context.

But look what happens when I try to do this in the draw() loop:

PGraphics canvas;
PGraphics buffer;

void setup(){
  size(500, 500);
  canvas = createGraphics(width, height);
  buffer = createGraphics(canvas.width, canvas.height);

  canvas.beginDraw();
  canvas.background(255);
  canvas.noFill();
  canvas.stroke(0);
  canvas.rect(100 + random(-50, 50), 100 + random(-50, 50), 350 + random(-50, 50), 350 + random(-50, 50));
  canvas.endDraw();

  buffer.beginDraw();
  buffer.image(canvas, 0, 0);
  buffer.endDraw();
}

void draw(){
  canvas.beginDraw();
  canvas.image(buffer, 0, 0);
  canvas.rect(100 + random(-50, 50), 100 + random(-50, 50), 350 + random(-50, 50), 350 + random(-50, 50));
  canvas.endDraw();

  image(canvas, 0, 0);

  buffer.beginDraw();
  buffer.image(canvas, 0, 0);
  buffer.endDraw();
}

Here, what ends up happening is that the original rect that was created in setup() gets copied every frame to canvas. So the effect is that there's a rect that doesn't move and then a second rect that gets drawn and replaced every frame.

It gets weirder. Watch what happens when I simply move the image() function that draws to the main context:

PGraphics canvas;
PGraphics buffer;

void setup(){
  size(500, 500);
  canvas = createGraphics(width, height);
  buffer = createGraphics(canvas.width, canvas.height);

  canvas.beginDraw();
  canvas.background(255);
  canvas.noFill();
  canvas.stroke(0);
  canvas.rect(100 + random(-50, 50), 100 + random(-50, 50), 350 + random(-50, 50), 350 + random(-50, 50));
  canvas.endDraw();

  buffer.beginDraw();
  buffer.image(canvas, 0, 0);
  buffer.endDraw();
}

void draw(){
  canvas.beginDraw();
  canvas.image(buffer, 0, 0);
  canvas.rect(100 + random(-50, 50), 100 + random(-50, 50), 350 + random(-50, 50), 350 + random(-50, 50));
  canvas.endDraw();

  buffer.beginDraw();
  buffer.image(canvas, 0, 0);
  buffer.endDraw();

  image(canvas, 0, 0);
}

This ought not to change a thing, and yet the result is that the image "freezes" with two rects on the screen. For some reason it just seems to draw the same thing over and over again, even though canvas is being re-written every time.

Changing that last line to read

image(buffer, 0, 0);

instead, goes back to the previous behaviour of "freezing" buffer, but drawing a new rect over top of it every time.

Can anyone shed some light on what is happening?


Solution

  • Looking at the source, the issue is with image(). image() sets the PGraphics texture, by calling imageImpl(), which happens to be overridden in PGraphics. By setting the texture, rather than setting the pixels directly, the texture reference is kept and cached, which explains (at least to a degree to satisfy my curiosity) the reason that using PGraphics.image() (at least in the main drawing context) seems to "lock" the buffer PGraphics object to the point where it is useless for subsequent draw() operations.

    There are two solutions that avoid this:

    1. Still using two offscreen buffers (in my examples canvas and buffer), continue to use canvas.image() in order to be able to write the buffer to the image and possibly scale it; but in terms of writing the canvas out to the main drawing context, use set(x, y, canvas) instead. PGraphics.set() is inherited from PImage.set() and is not overridden, and sets the pixels directly pixel-by-pixel, so there is no reference to the original object. It's also faster in the Java2D context (though possibly slower in the GL context) because it's not drawing a texture.

    2. The other option (at least in my case), is to bypass the canvas object altogether, and instead, work directly with the main drawing context's pixels using g.copy(), which returns a new PImage object that contains a complete copy of the main drawing canvas (g is actually this.g, or PApplet.g, the main PGraphics context that all your drawing functions affect). Because this is a copy of the pixels, and not the PGraphics object, you can then use image() with impunity, taking advantage of the scaling that that function allows.

    Here are some examples:

    PGraphics canvas;
    PGraphics buffer;
    
    void setup(){
      size(500, 500);
      canvas = createGraphics(width, height);
      buffer = createGraphics(canvas.width, canvas.height);
    
      canvas.beginDraw();
      canvas.background(255);
      canvas.noFill();
      canvas.stroke(0);
      canvas.rect(100 + random(-50, 50), 100 + random(-50, 50), 350 + random(-50, 50), 350 + random(-50, 50));
      canvas.endDraw();
    
      buffer.beginDraw();
      buffer.image(canvas, 0, 0);
      buffer.endDraw();
    }
    
    void draw(){
      canvas.beginDraw();
      canvas.background(255);
      canvas.image(buffer, 10, 20, width-20, width-20);
      canvas.rect(100 + random(-50, 50), 100 + random(-50, 50), 350 + random(-50, 50), 350 + random(-50, 50));
      canvas.endDraw();
    
      set(0, 0, canvas);
    
      buffer.beginDraw();
      buffer.image(canvas, 0, 0);
      buffer.endDraw();
    }
    

    Above is the "two buffer" version. Note the use of set() instead of image(). The only difference here from my original example is that I've applied some scaling to image() to get that "warp" effect I was looking for in the first place.

    This second (much shorter) example uses only a single off-screen buffer and copies the main drawing context's PGraphics object via g.copy():

    PImage buffer;
    
    void setup(){
      size(500, 500);
    
      background(255);
      noFill();
      stroke(0);
    
      buffer = g.copy();
    
    }
    
    void draw(){
      background(255);
      image(buffer, 10, 20, width-20, width-20);
      rect(100 + random(-50, 50), 100 + random(-50, 50), 350 + random(-50, 50), 350 + random(-50, 50));
    
      buffer = g.copy();
    }
    

    I vastly prefer the latter, for two reasons: one, it's obviously less code, and presumably less memory so should be more performant (I haven't tested this hypothesis though), and two, it allows you to continue to use the main drawing context, which is cleaner, more idiomatic, and lets you easily adapt the technique to any existing sketches without having to rewrite every single graphics call, prepending it with beginDraw() and the name of the offscreen buffer.