Picking using color buffer with alpha channel

edited August 2016 in Questions about Code

Hello, everyone!

In a 2D scenario, I need to implement an object picker using a color buffer as a technique: a color is an unique ID.

It was easy to implement it by creating a new PGraphics where to render my objects using a unique color. However, this only works until I draw the alpha channel at the maximum opacity (stick on the the default: 255); but I'd like to use the alpha channel too, so that I can theoretically handle more elements.

In order to take advantage of the alpha-channel too, I ended up using both a PGraphics and a PImage objects. Briefly, my idea was:

  1. render one element on the PGraphics dedicated to the color-picking using the maximum opacity;
  2. iterate over all the PGraphics pixels and, for those pixels that are valid IDs (i.e. != 0,0,0,255), copy them to the PImage pixels taking care of manually adding the right transparency;

I'd be glad to know if somebody's got suggestions for a "better" (e.g. cleaner, faster, easier, ...) implementation of this picker (taking in account that the aim is to have a color-picker that uses all the ARGB channels).

Here's the code (in the sketch, use 'c' to add a circle and mouse-click to select an element). The juice is in the draw() method.

Thank you! ; )

sketch.pde

import java.util.List;

PGraphics pickingBuffer;
PImage pickingImage;

List<Circle> circles;
Circle ghostCircle;

void setup() {
  size(500, 300);

  pickingBuffer = createGraphics(width, height);
  pickingImage = createImage(width, height, ARGB);

  circles = new ArrayList<Circle>();
  ghostCircle = new Circle(-1, 0xaaeeeeee);
}

void draw() {

  // Picker
  //
  pickingBuffer.noSmooth();
  pickingBuffer.beginDraw();

  pickingBuffer.noStroke();

  // Iterate over all the elements I need to be drawn
  for (Circle c : circles) {
    int id = c.getID();

    // First, draw the current element on pickingBuffer
    // using the full opacity.
    // Notice that the 0,0,0,255 color is the id of no elements.
    pickingBuffer.background(id2r(0), id2g(0), id2b(0));
    c.draw(pickingBuffer, color(id2r(id), id2g(id), id2b(id)));

    // Then, iterate over all the pixels of the buffer:
    // those that are different from 0,0,0,255, take them
    // and write the right opacity to their alpha-channel
    pickingBuffer.loadPixels();
    pickingImage.loadPixels();

    for (int i = 0; i < width * height; i++) {
      int px = pickingBuffer.pixels[i];

      if (px != 0xff000000) {
        pickingImage.pixels[i] = (id2a(id) << 24) | (px & 0x00ffffff);
      }
    }

    pickingImage.updatePixels();
  }

  pickingBuffer.endDraw();

  // Rendering
  //
  background(128);

  for (Circle c : circles) {
    if (c.isSelected()) {
      stroke(255, 0, 0);
      strokeWeight(5);
    } else {
      noStroke();
    }

    c.draw(g);
  }

  ghostCircle.setX(mouseX);
  ghostCircle.setY(mouseY);
  noStroke();
  ghostCircle.draw(g);
}

void keyReleased() {
  if (key == 'c') {
    Circle c = new Circle(circles.size() + 1, 
      int(random(0xff000000, 0xffffffff)));
    c.setX(mouseX);
    c.setY(mouseY);
    circles.add(c);

    int id = c.getID();
    System.out.println("registering "
      + "id:" + id + "\t"
      + "r:" + id2r(id) + ", "
      + "g:" +id2g(id) + ", "
      + "b:" +id2b(id) + ", "
      + "a:" +id2a(id));
  }
}

void mousePressed() {
  pickingImage.loadPixels();
  int argb = pickingImage.pixels[mouseY * width + mouseX];
  int a = (argb >> 24) & 0xff;
  int r = (argb >> 16) & 0xff;
  int g = (argb >> 8) & 0xff;
  int b = argb & 0xff;
  int id = argb2id(a, r, g, b);

  System.out.println("argb: " + argb
    + "\tr: " + r
    + "\tg: " + g
    + "\tb: " + b
    + "\ta: " + a
    + "\tid: " + id);

  id = id - 1;

  if (id >= 0 && id < circles.size()) {
    circles.get(id).toggleSelection();
  }
}

int argb2id(int a, int r, int g, int b) {
  return (a << 24) | (r << 16) | (g << 8) | b;
}

int id2a(int id) {
  return (id & 0xff000000) >>> 24;
}

int id2r(int id) {
  return (id & 0x00ff0000) >>> 16;
}

int id2g(int id) {
  return (id & 0x0000ff00) >>> 8;
}

int id2b(int id) {
  return (id & 0x000000ff);
}

circle.pde

class Circle {
  private int id;
  private color c;
  private PVector position;
  private float diameter;
  private boolean selected = false;

  public int getID() { return id; }

  public float getX() { return position.x; }
  public void setX(float x) { position.x = x; }

  public float getY() { return position.y; }
  public void setY(float y) { position.y = y; }

  public float getDiameter() { return diameter; }
  public void setDiameter(float diameter) {
    if (diameter < 0) {
      throw new IllegalArgumentException("Diameter must be at least 0");
    }

    this.diameter = diameter;
  }

  public boolean isSelected() { return selected; }
  public void toggleSelection() {
    selected = !selected;
  }

  public Circle(int id, color c) {
    this.id = id;
    this.c = c;
    position = new PVector(0, 0);
    this.diameter = 60;
  }

  public void draw(PGraphics g) {
    draw(g, c);
  }

  public void draw(PGraphics g, color c) {
    g.fill(c);
    g.ellipseMode(CENTER);
    g.pushMatrix();
    g.translate(position.x, position.y);
    g.ellipse(0, 0, diameter, diameter);
    g.popMatrix();
  }
}
Sign In or Register to comment.