TrackBall: rotating 3D objects by dragging mouse

I wrote this today for a project I’m working on. Maybe useful for someone?

The idea is to drag objects in 3D to rotate them.


(watch video)

// A collection of trackballs. They can be rotated and
// they keep the rotation info.
ArrayList<TrackBall> tbs = new ArrayList<TrackBall>();

// Which of the objects is closer to the mouse, to rotate that one
TrackBall closest = null;

void setup() {
  size(600, 600, P3D);
  noStroke();

  // Create a bunch of TrackBalls
  for (int i=0; i<30; i++) {
    TrackBall tb = new TrackBall();
    tb.setPos(random(600), random(600));
    tbs.add(tb);
  }
}

void draw() {
  background(#220A27);
  lights();

  // Draw cubes using the rotation information stored
  // in the trackballs
  for (int i=0; i<tbs.size(); i++) {
    TrackBall tb = tbs.get(i);
    pushMatrix();
    translate(tb.pos.x, tb.pos.y);
    applyMatrix(tb.rotationMat);
    fill(lerpColor(#0FB5FF, #E8E37C, i / (float)tbs.size()));
    box(50 + 20 * (i%5));
    popMatrix();
  }
}

// When pressing the mouse find which TrackBall is closer to
// the mouse
void mousePressed() {
  closest = null;
  float distance = 999999;
  for (TrackBall tb : tbs) {
    float d = dist(mouseX, mouseY, tb.pos.x, tb.pos.y);
    if(d < distance) {
      distance = d;
      closest = tb;
    }
  }
}

// When dragging the mouse tell about the dragging to
// the closest TrackBall, so it updates its internal
// rotation information.
void mouseDragged() {
  if(closest != null) {
    closest.drag(pmouseX, pmouseY, mouseX, mouseY);
  }
}

// Inspired by
// https://stackoverflow.com/questions/11314384/virtual-trackball-implementation
// Using Toxiclibs for the Quaternion.
import toxi.geom.*;

// This TrackBall class allows rotating 3D objects by dragging.

class TrackBall {
  public PMatrix3D rotationMat;
  public Vec2D pos = new Vec2D();

  private int frameCountDragging;
  private Quaternion rotationQuat, tempQuat;

  TrackBall() {
    rotationQuat = new Quaternion();
    tempQuat = new Quaternion();
    rotationMat = new PMatrix3D();
  }

  // Specify the 2D coordinates of the object in the screen
  // This could be upgraded to 3D eventually.
  public void setPos(float x, float y) {
    pos.set(x, y);
  }

  // Call this method when dragging the mouse
  // with previous and current mouse position
  public void drag(int x0, int y0, int x1, int y1) {
    Vec3D v2 = project(x0 - pos.x, y0 - pos.y);
    Vec3D v1 = project(x1 - pos.x, y1 - pos.y);

    float dt = constrain(v1.distanceTo(v2) / width, -1, 1);

    Vec3D n = v2.cross(v1).normalizeTo(dt); 

    if (frameCountDragging++ == 50) { 
      frameCountDragging = 0;
      rotationQuat.normalize();
    }

    rotationQuat = tempQuat
      .set(cos(asin(dt)), n)
      .multiply(rotationQuat);
    rotationMat.set(rotationQuat.toMatrix4x4().toFloatArray(null));
  }

  private Vec3D project (float x, float y) {
    float radius = width / 2;

    Vec3D v = new Vec3D(x, y, 0);

    float len = v.magnitude();
    float tr = radius / sqrt(2);

    v.z = len < tr ? sqrt(radius * radius - len * len) : tr * tr / len;

    return v;
  }
}

I’m sure it can be improved. I leave that as an exercise for the reader :stuck_out_tongue:

2 Likes

Very nice! Thanks so much for sharing.

My first thought is that this TrackBall rotation method could probably be combined with the Picking Library – specifically the SimpleCubes example – to replace this:

  float distance = 999999;
  for (TrackBall tb : tbs) {
    float d = dist(mouseX, mouseY, tb.pos.x, tb.pos.y);
    if(d < distance) {
      distance = d;
      closest = tb;
    }
  }
1 Like