1

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?

Kevin Workman
  • 41,537
  • 9
  • 68
  • 107
Tom Auger
  • 19,421
  • 22
  • 81
  • 104
  • Replacing `image()` with `set(0, 0, buffer)` solves the problem but doesn't answer my question. Still curious as to what `image()` is doing that's not documented... – Tom Auger Jan 19 '17 at 13:10

2 Answers2

1

Think about exactly what you have in each PGraphics image.

Each PGraphics is a 500x500 image with a white background and a black rectangle on it.

Then you take one image and draw it overtop the other image. They are still both white images with a black rectangle on it. The important thing to note is that since they both have white backgrounds, you won't be able to see the "old" image "through" the new one. So you're just painting the same rectangle back and forth.

You can prove this by removing the call to canvas.background() in your second code block. Then you'll see the rectangles stacking on top of each other. It's still not a tunneling effect because you're just drawing the same-ish rectangle each time, but that's a separate issue.

So, to fix your problem you need to be mindful of exactly what's in each image. Especially pay attention to whether the background is transparent or not.

I'll also note that you can probably achieve this effect with only a single buffer image that you simply draw smaller and smaller, or even no buffer image at all by doing the same thing with the main canvas.

Kevin Workman
  • 41,537
  • 9
  • 68
  • 107
  • HI Kevin, thanks for your answer, but I think you're not seeing it the same way. Start with an opaque white background, and draw a rect on it. Then take that entire (opaque) image and copy it. Then "clear" the original canvas and draw the copy back onto it, a bit smaller. Now you have a white background and a (smaller) black rect. Then draw a new (larger) rect ON TOP of that. Now you should have two rects. Then copy that to the buffer, clear the canvas and repeat. So yeah, the theory works. It's `image()` that's doing something more than the documentation says, and I'm trying to find out what. – Tom Auger Jan 20 '17 at 02:12
  • @TomAuger I understand what you're **trying** to do, but that's not what you're doing in reality. Try removing `canvas.background()` like I suggested to see what I mean. You'll see that you aren't actually drawing the rectangle smaller and smaller. You're just drawing it at random coordinates, but not smaller. Your code is behaving exactly how I would expect it to, so it's definitely not the case that `image()` is doing something wrong here. – Kevin Workman Jan 20 '17 at 02:21
  • Thanks for your additional comments Kevin. Have you tried running the 3 examples I have listed above? In the 2nd and 3rd examples, take note of how I copy the buffer back to the canvas BEFORE I draw over top of it. So yeah, the buffer "clobbers" the canvas with its opaque white background + 1 rect, but then we draw a second rect ON TOP. Then we copy that to the buffer and repeat. The 2nd iteration we clobber the canvas with an opaque bg + 2 squares (from the buffer) and draw a 3rd square over top and on it goes. Interesting, if you replace image() with set() the whole thing works as advertised – Tom Auger Jan 20 '17 at 05:31
  • @Tom Yes, I've run all of your examples. Have you tried my suggestion of removing the call to `canvas.background()` to see that you aren't actually drawing the canvas smaller each time? Can you point out which line of code draws it smaller? – Kevin Workman Jan 20 '17 at 05:54
  • Oops sorry Kevin, I don't think I actually put the code in to draw it smaller. I would use `image(buffer, 20, 20, width-40, height-40)` or something to that effect. – Tom Auger Jan 20 '17 at 20:57
0

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.

Tom Auger
  • 19,421
  • 22
  • 81
  • 104