[SOLVED] Strange behavior using PGraphics as a recursive draw buffer

Recently, I’ve been attempting to recreate a lot of my old Adobe Flash visualizations using Processing.

Unfortunately, certain things seem to be much more difficult in Processing, at least for me right now.

One thing I’m trying to accomplish right now, should be very easy. Basically, what I’m trying to do is create fractals without explicit recursive code. Instead, I’m using the draw buffer and/or display as both an input and an output. This is similar to if you have a video camera whose output is being displayed on a monitor, and you point the video camera at the monitor. This will result in really cool fractal patterns if you get everything just right.

I have a working code example of this in Processing, but there are some weird hacks I need to do in order to get it to work properly. Before I stumbled upon this workaround, I was getting extremely frustrated that even simple examples of using PGraphics to draw recursively weren’t working.

If you save this image as “StareCat.png” in your data folder and use the source code I provided below, you can see an example of what I’m talking about.

The hack here is for some reason, I have to include a .tint() method call on my PGraphics instances in order to get this effect to work. If you comment out the .tint() calls, the effect no longer does anything! What is going on here? Has anyone else done similar stuff as this?

The next thing I want to do is similar to this, but moving forward is difficult when things aren’t working consistently.

PImage stareCatImage;
PGraphics bufferA;
PGraphics bufferB;
int canvasWidth = 1024;
int canvasHeight = 768;
int imageWidth = 512;
int imageHeight = 512;
float rotationValue = -0.5;
float rotationIncrement = 0.05;
float offset = 0;
float offsetIncrement = 0.1;
float scaleFactorA = 0.5;
float baseScaleFactorB = 2;
float scaleFactorB = sqrt(baseScaleFactorB) * scaleFactorA;

float bufferAX = canvasWidth - imageWidth * 0.5;
float bufferAY = 2 * canvasHeight - imageHeight;
float bufferATranslateX = 0;
float bufferATranslateY = 0;

int ax = 1;
int ay = 1;
int ar = 1;
int bx = 1;
int by = 1;
int br = 1;

void keyPressed() {
  switch (key) {
    case '1':
      ax = incrementInteger(ax, -1, 1);
      break;
    case '2':
      ay = incrementInteger(ay, -1, 1);
      break;
    case '3':
      ar = incrementInteger(ar, -1, 1);
      break;
    case '4':
      bx = incrementInteger(bx, -1, 1);
      break;
    case '5':
      by = incrementInteger(by, -1, 1);
      break;
    case '6':
      br = incrementInteger(br, -1, 1);
      break;
  }
  print("ax:"+ax+"; ay:"+ay+"; ar:"+ar+"; bx:"+bx+"; by:"+by+"; br:"+br+";scaleFactorA:"+scaleFactorA+";\n");
}

float incrementFloat(float input, float min, float max, float amount) {
  input += amount;
  if (input > max) {
    input = min;
  }
  return input;
}

int incrementInteger(int input, int min, int max) {
  input++;
  if (input > max) {
    input = min;
  }
  return input;
}

void setup() {
  size(1024, 768);
  
  background(#808080);
  
  stareCatImage = loadImage("StareCat.png");
  
  bufferA = createGraphics(canvasWidth, canvasHeight);
  bufferA.beginDraw();
  bufferA.scale(scaleFactorA);
  bufferA.image(stareCatImage, bufferAX, bufferAY);
  bufferA.endDraw();
  
  bufferB = createGraphics(canvasWidth, canvasHeight);
  bufferB.beginDraw();
  bufferB.image(bufferA, 0, 0);
  bufferB.endDraw();
}

void draw() {
  offset += offsetIncrement;
  rotationValue += rotationIncrement;
  float rotationSin = sin(rotationValue);
  float rotationSizeOffset = map(rotationSin, -1, 1, -256, 256);
  
  background(#808080);
  
  bufferA.clear();
  bufferA.beginDraw();
  bufferA.tint(255, 192, 192, 254); // why is this is necessary?
  bufferA.scale(scaleFactorB);
  bufferA.translate(1033 + rotationSizeOffset * ax, 596 + rotationSizeOffset * ay);
  bufferA.rotate(rotationValue * ar);
  bufferA.image(bufferB, -512, -640);
  bufferA.endDraw();
  
  bufferA.beginDraw();
  bufferA.scale(scaleFactorA);
  bufferA.tint(192, 255, 192, 254); // why is this is necessary?
  bufferA.translate(-bufferATranslateX, -bufferATranslateY);
  bufferA.image(stareCatImage, bufferAX, bufferAY);
  bufferA.endDraw();
  
  bufferA.beginDraw();
  bufferA.tint(192, 192, 255, 254); // why is this is necessary?
  bufferA.scale(scaleFactorB);
  bufferA.translate(415 - rotationSizeOffset * bx, 596 + rotationSizeOffset * by);
  bufferA.rotate(rotationValue * br);
  bufferA.image(bufferB, -512, -640);
  bufferA.endDraw();
  
  bufferB.clear();
  bufferB.beginDraw();
  bufferB.image(bufferA, 0, 0);
  bufferB.endDraw();
  
  image(bufferB, 0, 0);
}
1 Like

Why do you have three different calls to bufferA.beginDraw()?

It’s an alternative to using push and pop matrix, because at the time I wrote this I didn’t understand how that worked exactly.

Here’s a version of the code with push and pop matrix instead of 3 draw methods:

PImage stareCatImage;
PGraphics bufferA;
PGraphics bufferB;
int canvasWidth = 1024;
int canvasHeight = 768;
int imageWidth = 512;
int imageHeight = 512;
float rotationValue = -0.5;
float rotationIncrement = 0.05;
float offset = 0;
float offsetIncrement = 0.1;
float scaleFactorA = 0.5;
float baseScaleFactorB = 2;
float scaleFactorB = sqrt(baseScaleFactorB) * scaleFactorA;

float bufferAX = canvasWidth - imageWidth * 0.5;
float bufferAY = 2 * canvasHeight - imageHeight;
float bufferATranslateX = 0;
float bufferATranslateY = 0;

int ax = 1;
int ay = 1;
int ar = 1;
int bx = 1;
int by = 1;
int br = 1;

void keyPressed() {
  switch (key) {
    case '1':
      ax = incrementInteger(ax, -1, 1);
      break;
    case '2':
      ay = incrementInteger(ay, -1, 1);
      break;
    case '3':
      ar = incrementInteger(ar, -1, 1);
      break;
    case '4':
      bx = incrementInteger(bx, -1, 1);
      break;
    case '5':
      by = incrementInteger(by, -1, 1);
      break;
    case '6':
      br = incrementInteger(br, -1, 1);
      break;
  }
  print("ax:"+ax+"; ay:"+ay+"; ar:"+ar+"; bx:"+bx+"; by:"+by+"; br:"+br+";scaleFactorA:"+scaleFactorA+";\n");
}

float incrementFloat(float input, float min, float max, float amount) {
  input += amount;
  if (input > max) {
    input = min;
  }
  return input;
}

int incrementInteger(int input, int min, int max) {
  input++;
  if (input > max) {
    input = min;
  }
  return input;
}

void setup() {
  size(1024, 768);
  
  background(#808080);
  
  stareCatImage = loadImage("StareCat.png");
  
  bufferA = createGraphics(canvasWidth, canvasHeight);
  bufferA.beginDraw();
  bufferA.scale(scaleFactorA);
  bufferA.image(stareCatImage, bufferAX, bufferAY);
  bufferA.endDraw();
  
  bufferB = createGraphics(canvasWidth, canvasHeight);
  bufferB.beginDraw();
  bufferB.image(bufferA, 0, 0);
  bufferB.endDraw();
}

void draw() {
  offset += offsetIncrement;
  rotationValue += rotationIncrement;
  float rotationSin = sin(rotationValue);
  float rotationSizeOffset = map(rotationSin, -1, 1, -256, 256);
  
  background(#808080);
  
  bufferA.clear();
  bufferA.beginDraw();
  
  bufferA.pushMatrix();
  bufferA.tint(254, 254, 128, 254);
  bufferA.scale(scaleFactorB);
  bufferA.translate(1033 + rotationSizeOffset * ax, 596 + rotationSizeOffset * ay);
  bufferA.rotate(rotationValue * ar);
  bufferA.image(bufferB, -512, -640);
  bufferA.popMatrix();
  
  bufferA.pushMatrix();
  bufferA.scale(scaleFactorA);
  bufferA.tint(254, 128, 254, 254);
  bufferA.translate(-bufferATranslateX, -bufferATranslateY);
  bufferA.image(stareCatImage, bufferAX, bufferAY);
  bufferA.popMatrix();
  
  bufferA.pushMatrix();
  bufferA.tint(128, 254, 254, 254);
  bufferA.scale(scaleFactorB);
  bufferA.translate(415 - rotationSizeOffset * bx, 596 + rotationSizeOffset * by);
  bufferA.rotate(rotationValue * br);
  bufferA.image(bufferB, -512, -640);
  bufferA.popMatrix();
  
  bufferA.endDraw();
  
  bufferB.clear();
  bufferB.beginDraw();
  bufferB.image(bufferA, 0, 0);
  bufferB.endDraw();
  
  image(bufferB, 0, 0);
}

This seems to be a bug in the Java2D (default) renderer. Because Java2D doesn’t natively support tint() I think this forces a fallback that works for you. You’d be better using the P2D renderer for this anyway, as it’ll work much better anyway and tint is supported on the GPU. Try the following which switches the main canvas and both createGraphics to P2D, and also moves the clear() calls inside beginDraw().

This was a fun problem to debug - everything is enhanced by a staring cat! :smile:

PImage stareCatImage;
PGraphics bufferA;
PGraphics bufferB;
int canvasWidth = 1024;
int canvasHeight = 768;
int imageWidth = 512;
int imageHeight = 512;
float rotationValue = -0.5;
float rotationIncrement = 0.05;
float offset = 0;
float offsetIncrement = 0.1;
float scaleFactorA = 0.5;
float baseScaleFactorB = 2;
float scaleFactorB = sqrt(baseScaleFactorB) * scaleFactorA;

float bufferAX = canvasWidth - imageWidth * 0.5;
float bufferAY = 2 * canvasHeight - imageHeight;
float bufferATranslateX = 0;
float bufferATranslateY = 0;

int ax = 1;
int ay = 1;
int ar = 1;
int bx = 1;
int by = 1;
int br = 1;

void keyPressed() {
  switch (key) {
    case '1':
      ax = incrementInteger(ax, -1, 1);
      break;
    case '2':
      ay = incrementInteger(ay, -1, 1);
      break;
    case '3':
      ar = incrementInteger(ar, -1, 1);
      break;
    case '4':
      bx = incrementInteger(bx, -1, 1);
      break;
    case '5':
      by = incrementInteger(by, -1, 1);
      break;
    case '6':
      br = incrementInteger(br, -1, 1);
      break;
  }
  print("ax:"+ax+"; ay:"+ay+"; ar:"+ar+"; bx:"+bx+"; by:"+by+"; br:"+br+";scaleFactorA:"+scaleFactorA+";\n");
}

float incrementFloat(float input, float min, float max, float amount) {
  input += amount;
  if (input > max) {
    input = min;
  }
  return input;
}

int incrementInteger(int input, int min, int max) {
  input++;
  if (input > max) {
    input = min;
  }
  return input;
}

void setup() {
  size(1024, 768, P2D);
  
  background(#808080);
  
  stareCatImage = loadImage("StareCat.png");
  
  bufferA = createGraphics(canvasWidth, canvasHeight, P2D);
  bufferA.beginDraw();
  bufferA.scale(scaleFactorA);
  bufferA.image(stareCatImage, bufferAX, bufferAY);
  bufferA.endDraw();
  
  bufferB = createGraphics(canvasWidth, canvasHeight, P2D);
  bufferB.beginDraw();
  bufferB.image(bufferA, 0, 0);
  bufferB.endDraw();
}

void draw() {
  offset += offsetIncrement;
  rotationValue += rotationIncrement;
  float rotationSin = sin(rotationValue);
  float rotationSizeOffset = map(rotationSin, -1, 1, -256, 256);
  
  background(#808080);
  
  
  bufferA.beginDraw();
  bufferA.clear();
  bufferA.pushMatrix();
  //bufferA.tint(254, 254, 128, 254);
  bufferA.scale(scaleFactorB);
  bufferA.translate(1033 + rotationSizeOffset * ax, 596 + rotationSizeOffset * ay);
  bufferA.rotate(rotationValue * ar);
  bufferA.image(bufferB, -512, -640);
  bufferA.popMatrix();
  
  bufferA.pushMatrix();
  bufferA.scale(scaleFactorA);
  //bufferA.tint(254, 128, 254, 254);
  bufferA.translate(-bufferATranslateX, -bufferATranslateY);
  bufferA.image(stareCatImage, bufferAX, bufferAY);
  bufferA.popMatrix();
  
  bufferA.pushMatrix();
  //bufferA.tint(128, 254, 254, 254);
  bufferA.scale(scaleFactorB);
  bufferA.translate(415 - rotationSizeOffset * bx, 596 + rotationSizeOffset * by);
  bufferA.rotate(rotationValue * br);
  bufferA.image(bufferB, -512, -640);
  bufferA.popMatrix();
  
  bufferA.endDraw();
  
  bufferB.beginDraw();
  bufferB.clear();
  bufferB.image(bufferA, 0, 0);
  bufferB.endDraw();
  
  image(bufferB, 0, 0);
}
1 Like

Oh, cool, thanks, I totally forgot that the P2D thing even existed.

Nearly everything I do is 2D anyway, so I should probably use that in most cases.

I appreciate you taking the time to look at this for me.

And oh man, that is SO MUCH faster now! I actually need to limit the framerate!

No problem! I do lots of recursive stuff like this in PraxisLIVE so I know Processing can do this, but that software only uses P2D/P3D - always fun to look at different behaviour between the renderers! :wink:

Oh, neat, I haven’t heard of PraxisLive, it looks pretty cool, I’ll have to check that out sometime.

I’ve also recently been playing around with pixel shaders and WebGL and stuff lately:

http://picunrelated.com/home/plasma

1 Like

@neilcsmith – Good detective work! Do you know if this is something that already has an open github issue, or do you think it needs to be reported?

@jeremydouglass - no idea, sorry. Probably should be reported if it isn’t there. The Java2D renderer behaves a bit odd at times! :confused: I never use it unless helping someone out on here.