Gimbals, quaternions, and manual rotation

I want to sketch a gimbal. It will be similar to this: <https://www.openprocessing.org/sketch/414889> by @behreajj

I have no problem with drawing the torus, nor (yet) with the spatial rotations.

I want to manually rotate the rings. Can anyone help me to think about this? How would I drag a ring in order to rotate it?

1 Like

Hi @paulstgeorge ,

This thread 3D Rotations: Rotating around the screen axes may be a place to start. (The code discussed there doesn’t actually use any ‘picking’ function to see if the mouse has selected the 3D shape with a ray, which is a separate task from rotation.)

Best,
Jeremy

2 Likes

Thank you, and thank you for the excellent Medium article <https://medium.com/@behreajj/3d-rotations-in-processing-vectors-matrices-quaternions-10e2fed5f0a3>. My question is partly about picking and partly about quaternions and gimbal ‘lock’.

I am using quaternions to avoid so called lock. But, if I pick the ring (either by giving each ring a unique colour and then testing what colour is under the mouse, or by ray tracing from the mouse’s location) am I reintroducing gimbal ‘lock’? One ring maybe be occluded by another nearer ring. Or, are quaternions immune from either or both of these picking methods?

Hi @paulstgeorge,

I’m having a hard time picturing how either picking method (based on a ray-cast, based on pixel testing) is related to rotation free of gimbal lock. It’d help if you posted some code, an image or more of a description of how you want the tori to respond in relation user input.

The OpenProcessing sketch is difficult to work off of because it is a counter-example of what is typically undesirable behavior.

I posted this code on the p5 web editor to show how I’m currently imagining things. I assumed JavaScript because it’s easier to share, run and edit than Java. Here’s a screenshot:

download

The problematic picking case you mentioned – where the occlusion of one torus by another relative to the camera prevents the occluded from being picked – would depend on how your mouse picker responds to multiple intersections, and then beyond that multiple intersections from multiple entities. For some uses, I’d imagine picking the occluder over the occluded would be the desirable result (when I want to move the entity nearest to me, not the one behind it). I’ve not really done a successful mouse picker, though, so it’d help to narrow down which of the two approaches you’re trying out.

Best,
Jeremy

2 Likes

Yes, it will take some time but I will start to make my manually draggable gimbal and then post code when, and if, I have a problem.

First step: the picker!

I will stick to Java mode because this is part of a larger Processing project, but the p5 sketch is a helpful way to share ideas and code. Thank you.

I just want to mention an old concept here regarding mouse picker.

Instead of using the colors you see, you can draw the same scene on an invisible PGraphics and check the color underneath the mouse on the pg. (you could also have one pg per ring avoiding overlapping of rings).

On the pg the colors come out more precise because you don’t have lights() and anti aliasing / smooth on the pg.

The pg must have the same size as your canvas and then you check the color underneath the mouse with pg.get(mouseX,mouseY).

(Also, when you use a drag technique with the mouse, overlapping doesn’t play a role since the id of the ring is fixed (once the mouse button is pressed and the ring is selected))

2 Likes

Wow, that is what I need. Have I done it right (below)? Should I be able to rotate the PGraphics content with rotateX()?

PGraphics pg1;
PGraphics pgx;  // mirror of pg1
color c;

void setup() {
  size(200, 200, P3D);

  pg1 = createGraphics(200, 200, P3D);
  pgx = createGraphics(200, 200, P3D);
}

void draw() {
  background(255);

  //translate(100, 100, 0); // translate origin ready for rotateX


  //
  pg1.beginDraw();
  pg1.noFill();
  pg1.stroke(127, 127, 127);
  pg1.strokeWeight(4);
  pg1.ellipse(pgx.width*0.5, pgx.height*0.5, 100, 100);
  pg1.endDraw();
  image(pg1, 0, 0); 
  //

  //
  pgx.beginDraw();
  pgx.noFill();
  pgx.stroke(255, 0, 0);
  pgx.strokeWeight(4);
  pgx.ellipse(pgx.width*0.5, pgx.height*0.5, 100, 100);
  pgx.endDraw();
  image(pgx, width, 0); 
  //

  c = pgx.get(mouseX, mouseY);
  if (c != 0) {
  println(red(c));
  }
  //println(c >> 16 & 0xFF);
}
1 Like

Pretty good

Obviously you have to make the same translate and rotate in the pgs as in the main screen

(And later you don’t display pg obviously)

To me, nothing is obvious. :smiley:

Is this correct (below)? Or am I repeating myself? Can I rotate the mirror by referring to the original? Something like:
pgx.rotateY(pg1.rotateY()); //pseudocode

PGraphics pg1;
PGraphics pgx;
color c;
float y = 0;
float d = 0.01;

// is it 3D?
// how to rotate

void setup() {
  size(200, 200, P3D);

  pg1 = createGraphics(200, 200, P3D);
  pgx = createGraphics(200, 200, P3D); // mirror of pg1
}

void draw() {
  background(255);

  drawDisc1();
  drawDisc2();


  c = pgx.get(mouseX, mouseY);
  if (c != 0) {
    println(red(c));
  }
  //print(c >> 16 & 0xFF);

  y = y + d;
}


void drawDisc1() {
  //lights?
  pg1.beginDraw();
  pg1.background(0);
  pg1.noFill();
  pg1.stroke(127, 127, 127);
  pg1.strokeWeight(4);
  pg1.translate(pg1.width*0.5, pg1.height*0.5); // translate origin ready for rotateX
  pg1.rotateY(y);
  pg1.ellipse(0, 0, 100, 100);
  pg1.endDraw();
  image(pg1, 0, 0); 
  //
}



void drawDisc2() {
  pgx.beginDraw();
  pgx.background(0);
  pgx.noFill();
  pgx.stroke(255, 0, 0);
  pgx.strokeWeight(4);
  pgx.translate(pgx.width*0.5, pgx.height*0.5); // translate origin ready for rotateX
  pgx.rotateY(y);
  pgx.ellipse(0, 0, 100, 100);
  pgx.endDraw();
  image(pgx, width, 0);
  //
}
1 Like

Just do the same on the pgs as you do on the screen (without lights() and with noSmooth() I guess). Then test it.

Hello,

Tinkering with your code:

PGraphics pg1;
PGraphics pg2;
color c;
float y = 0, x = 0;
float d = TAU/200;

// is it 3D?
// how to rotate

void setup() 
  {
  size(200, 200, P3D);

  pg1 = createGraphics(200, 200, P3D);
  pg2 = createGraphics(200, 200, P3D); // mirror of pg1
  }  

void draw() 
  {
  background(0);

  drawDisc1();
  drawDisc2();

  c = pg2.get(mouseX, mouseY);
  if (c != 0) 
    {
    println(red(c));
    }
  //print(c >> 16 & 0xFF);

  y = y + d;
  x = x + d;
  }

void drawDisc1() 
  {
  //lights?
  pg1.beginDraw();
  pg1.background(0, 0);
  pg1.noFill();
  pg1.stroke(0, 255, 0);
  pg1.strokeWeight(4);
  pg1.translate(pg1.width/2, pg1.height/2); // translate origin ready for rotateX
  pg1.rotateY(y);
  pg1.ellipseMode(CENTER);
  pg1.ellipse(0, 0, 100, 100);
  pg1.endDraw();
  image(pg1, 0, 0); 
  //
  }

void drawDisc2() 
  {
  pg2.beginDraw();
  pg2.background(0, 0);
  pg2.noFill();
  pg2.stroke(255, 0, 0);
  pg2.strokeWeight(4);
  pg2.translate(pg2.width/2, pg2.height/2); // translate origin ready for rotateX
  pg2.rotateY(y+TAU/4);
  pg2.rotateX(y+TAU/4);
  pg2.ellipseMode(CENTER);
  pg2.ellipse(0, 0, 100, 100);
  pg2.endDraw();
  image(pg2, 0, 0);
  //
  }

image

:slight_smile:

1 Like

I am grateful, thank you. If, for now, we ignore the mirroring - that has become the easy part.
I want three orthogonal rings (one for each x,y,z axis). See image below. The three rings should rotate together. My problem is how to rotate the origins of two of the rings relative to the first before I start rotating the group of three. I tried the simplest reducible case (also below) but this just about works when rotating about the x axis. It goes haywire when I change to y or z axis.

PGraphics pg1;
PGraphics pg2;
color c;
float y = 0, x = 0, z = 0;
float d = radians(0.5); // so rotation is slow enough to observe


void setup() 
{
  size(200, 200, P3D);
}  

void draw() 
{
  background(0);

  noFill();
  pushMatrix();
  stroke(0, 255, 0);
  strokeWeight(4);
  translate(width/2, height/2); // translate origin ready for rotateX
  rotateX(x);
  ellipse(0, 0, 100, 100);
  popMatrix();
  //
  pushMatrix();
  stroke(255, 0, 0);
  strokeWeight(4);
  translate(width/2, height/2); // translate origin ready for rotateX
  rotateX(x+HALF_PI);
  ellipse(0, 0, 100, 100);
  popMatrix();
  //
  //
  pushMatrix();
  stroke(0, 0, 255);
  strokeWeight(4);
  translate(width/2, height/2); // translate origin ready for rotateX
  rotateX(x);
  rotateY(y+HALF_PI);
  ellipse(0, 0, 100, 100);
  popMatrix();
  //





  if ( key == 'x' || key == 'X') {
    x = x + d;
  } else {
    return; // stops the rotation
  }
}

Screen Shot 2020-01-16 at 11.54.23

4 Likes

More tinkering… my morning brain workout.

float y = 0, x = 0, z = 0;
float d = TAU/500; // so rotation is slow enough to observe
boolean xState = false;

void setup() 
  {
  size(200, 200, P3D);
  }  

void draw() 
  {
  background(0);
  noFill();

  translate(width/2, height/2);
  
  strokeWeight(4);
  
    if (xState) 
    {
    x=x+d;
    }
  
  pushMatrix();
  stroke(0, 255, 0);
  //strokeWeight(4);
  //translate(width/2, height/2); // translate origin ready for rotateX
  //if (xState) 
    {
    //x=x+d;
    rotateX(x);
    }
  ellipse(0, 0, 100, 100);
  popMatrix();
  //
  pushMatrix();
  stroke(255, 0, 0);
//  strokeWeight(4);
//  translate(width/2, height/2); // translate origin ready for rotateX
  rotateX(x+HALF_PI);
  ellipse(0, 0, 100, 100);
  popMatrix();
  //
  //
  pushMatrix();
  stroke(0, 0, 255);
//  strokeWeight(4);
//  translate(width/2, height/2); // translate origin ready for rotateX
  rotateX(x);
  rotateY(y+HALF_PI);
  ellipse(0, 0, 100, 100);
  popMatrix();
  
  println(x);
  }


void keyPressed()
  {
  if (key == 'x' || key == 'X') 
    {
    xState = true;
    } 
  }
  
void keyReleased()
  {
  if (key == 'x' || key == 'X') 
    {
    xState = false;
    } 
  }  

Hi @paulstgeorge,

Looks like nice progress!

Odd thing, I was messing about with your code and @glv’s code and I ran into this issue: if I tried to fatten up the stroke weight of the gimbals to make them easier to see, the stroke would flicker and clip as the circles rotated. That lead me down a rabbit hole looking for code to make a torus for Processing Java. I suppose there are libraries which have 3D shapes, but I wound up reverse engineering some Unity script here.

In response to thise question:

The three rings should rotate together. My problem is how to rotate the origins of two of the rings relative to the first before I start rotating the group of three.

PShapes may be of interest for doing complicated stuff like this. The benefit is that you have methods that separate and clarify when transformations (like the initial rotations of each ring into orthogonal position) are established in setup and when they are animated in draw. You can also group together PShapes with addChild so they can be rotated all together or individually.

Just to be clear, using sequences of code like rotateX(1.0); rotateZ(3.0); rotateY(2.0); will give you gimbal lock (and rotateZ is bugged out for PShapes last I checked). Using the version of rotate which takes four numbers – one angle and the x, y and z of an axis – will ease the problem.

rotate(radians, 1.0, 0.0, 0.0); // rotateX
rotate(radians, 0.0, 1.0, 0.0); // rotateY
rotate(radians, 0.0, 0.0, 1.0); // rotateZ

The axis needs to have a length or magnitude of 1. So (1, 1, 0) becomes (1 / sqrt(2), 1 /sqrt(2), 0). (1, 1, 1) becomes (1 / sqrt(3), 1 / sqrt(3), 1 / sqrt(3)) .

Ok, sorry about the big dump of code… but if I broke it apart it’d be harder to copy and paste into the Processing IDE.

// Group of all three gimbals
PShape gimbals;

// Each child gimbal
PShape gmblred;
PShape gmblgreen;
PShape gmblblue;

// Axis around which group rotates
PVector axis = new PVector();

// Record of whether or not a key is pressed
boolean xPressed;
boolean yPressed;
boolean zPressed;

float rotateSpeed = radians(0.5);

void setup() {
  size(320, 320, P3D);

  // Center camera at (0, 0, 0)
  camera(
    0.0, 0.0, height * 0.86602, 
    0.0, 0.0, 0.0, 
    0.0, 1.0, 0.0);

  gimbals = createShape(GROUP);

  // Set initial gimbals to orthogonal position
  // set their fill color
  // give them a name
  gmblred = torus(120.0, 7.5, 32, 16);
  gmblred.setFill(#ff0000);
  gmblred.rotate(radians(90), 1.0, 0.0, 0.0);
  gmblred.setName("Red Gimbal");

  gmblgreen = torus(120.0, 7.5, 32, 16);
  gmblgreen.setFill(#00ff00);
  gmblgreen.setName("Green Gimbal");

  gmblblue = torus(120.0, 7.5, 32, 16);
  gmblblue.setFill(#0000ff);
  gmblblue.rotate(radians(90), 0.0, 0.0, 1.0);
  gmblblue.setName("Blue Gimbal");

  // Add children shape to parent shape
  gimbals.addChild(gmblred);
  gimbals.addChild(gmblgreen);
  gimbals.addChild(gmblblue);
}

void draw() {
  bvec(xPressed, yPressed, zPressed, axis);
  axis.normalize();
  if (keyPressed) { 
    println(axis);
  }

  gimbals.rotate(rotateSpeed, axis.x, axis.y, axis.z);

  background(#fff7d5);
  shape(gimbals);
}

void keyPressed() {
  if (key == 'x' || key == 'X') {
    xPressed = true;
  }

  if (key == 'y' || key == 'Y') {
    yPressed = true;
  }

  if (key == 'z' || key == 'Z') {
    zPressed = true;
  }
}

void keyReleased() {
  if (key == 'x' || key == 'X') {
    xPressed = false;
  }

  if (key == 'y' || key == 'Y') {
    yPressed = false;
  }

  if (key == 'z' || key == 'Z') {
    zPressed = false;
  }
}

PVector bvec(boolean x, boolean y, boolean z) {
  return bvec(x, y, z, (PVector)null);
}

PVector bvec(
  boolean x, 
  boolean y, 
  boolean z, 
  PVector target) {
  if (target == null) target = new PVector();
  return target.set(
    boolToFloat(x), 
    boolToFloat(y), 
    boolToFloat(z));
}

float boolToFloat(boolean bool) {
  return bool ? 1.0 : 0.0;
}

// Reverse engineered from Bérenger. 
// https://wiki.unity3d.com/index.php/ProceduralPrimitives#C.23_-_Torus
PShape torus(
  float radius, 
  float thickness, 
  int sectors, 
  int tubeRes) {

  int sectors1 = sectors + 1;
  int tubeRes1 = tubeRes + 1;

  float toU = 1.0 / sectors;
  float toV = 1.0 / tubeRes;
  float toAngle1 = TAU / sectors;
  float toAngle2 = TAU / tubeRes;

  // Calculate angle and the v coordinate in uvs.
  float[] costs = new float[tubeRes1];
  float[] sints = new float[tubeRes1];
  float[] vs = new float[tubeRes1];

  for (int side = 0; side < tubeRes1; ++side) {
    int currSide = side % tubeRes;
    float theta = currSide * toAngle2;
    costs[side] = cos(theta);
    sints[side] = sin(theta);
    vs[side] = side * toV;
  }

  // Create mesh info arrays.
  PVector[] coords = new PVector[sectors1 * tubeRes1];
  PVector[] normals = new PVector[coords.length];
  PVector[] texCoords = new PVector[coords.length];

  for (int k = 0, seg = 0; seg < sectors1; ++seg) {

    // Calculate theta.
    float phi = (seg % sectors) * toAngle1;
    float cosp = cos(phi);
    float sinp = sin(phi);

    // Calculate r1
    float r1x = radius * cosp;
    float r1y = 0.0f;
    float r1z = radius * sinp;

    // Calculate horizontal texture coordinate.
    float u = seg * toU;

    // Calculate quaternion from axis and angle.
    // Assumes that the reference up from which
    // the quaternion is created is ( 0.0, 1.0, 0.0 ) .
    // For that reason qx and qz will be 0.0.
    float halfAngle = 0.5 * -phi;
    float qw = cos(halfAngle);
    float qy = sin(halfAngle);

    for (int side = 0; side < tubeRes1; ++side, ++k) {

      // Texture coordinate is easy.
      texCoords[k] = new PVector(u, vs[side], 0.0);

      // Vector to be multiplied by the quaternion.
      float mulx = sints[side];
      float muly = costs[side];

      // Multiply quaternion q and vector mul (part 1).
      float iw = -qy * muly;
      float ix = qw * mulx;
      float iy = qw * muly;
      float iz = -qy * mulx;

      // Multiply quaternion q and vector mul (part 2).
      float r2x = ix * qw + iz * qy;
      float r2y = iy * qw - iw * qy;
      float r2z = iz * qw - ix * qy;

      normals[k] = new PVector(r2x, r2y, r2z);

      // Add r1 to r2 to get coordinate.
      coords[k] = new PVector(
        r1x + r2x * thickness, 
        r1y + r2y * thickness, 
        r1z + r2z * thickness);
    }
  }

  int triangles = coords.length * 2;
  int idxLimit = (triangles * 3) - 6;
  int[][][] faces = new int[triangles][3][3];

  int idx = 0;
  int fidx = 0;

  for (int seg = 0; seg < sectors1; ++seg) {

    int currentFac = seg * tubeRes1;
    int nextFac = (seg + 1) * tubeRes1;

    for (int side = 0; side < tubeRes; ++side) {

      int current = side + currentFac;
      int next = side + (seg < sectors ? nextFac : 0);

      if (idx < idxLimit) {

        int n1 = next + 1;
        int c1 = current + 1;

        faces[fidx++] = new int[][] {
          { current, current, current }, 
          { next, next, next }, 
          { n1, n1, n1 } };

        faces[fidx++] = new int[][] {
          { current, current, current }, 
          { n1, n1, n1 }, 
          { c1, c1, c1 } };

        idx += 6;
      }
    }
  }

  return shapeMaker(coords, texCoords, normals, faces);
}

PShape shapeMaker(
  PVector[] coords, 
  PVector[] texCoords, 
  PVector[] normals, 
  int[][][] faces) {

  PShape shape = createShape(GROUP);

  // Loop through faces.
  int flen0 = faces.length;
  for (int i = 0; i < flen0; ++i) {
    int[][] f = faces[i];
    int flen1 = f.length;

    // Create a new polygon.
    PShape poly = createShape();
    poly.setStroke(false);
    poly.setFill(true);
    poly.setTextureMode(NORMAL);
    poly.beginShape(POLYGON);

    // Loop through vertices in a face.
    for (int j = 0; j < flen1; ++j) {
      int[] data = f[j];

      // Retrieve appropriate data from
      // indices in the vertex.
      int vIndex = data[0];
      PVector v = coords[vIndex];

      int vtIndex = data[1];
      PVector vt = texCoords[vtIndex];

      int vnIndex = data[2];
      PVector vn = normals[vnIndex];

      // Draw the vertex.
      poly.fill(0xffffffff);
      poly.normal(vn.x, vn.y, vn.z);
      poly.vertex(
        v.x, v.y, v.z, 
        vt.x, vt.y);
    }
    poly.endShape(CLOSE);
    shape.addChild(poly);
  }

  // Tessellate to consolidate all children
  // into one parent shape.
  shape = shape.getTessellation();
  shape.setTextureMode(NORMAL);
  return shape;
}

Best,
Jeremy

3 Likes

@glv Super! Is your tinkered version a far more elegant version of the same geometry. Shall I dare to try rotating around the y axis?

1 Like

Hello,

Go for it!

I know you appreciate insights.

It was just my morning tinkering with the code.

:slight_smile:

1 Like

Thank you @behreajj and @glv. We now have something that works and works perfectly.

Yes, I do appreciate insights. I also appreciate the tidying up, removing of repetitions and other improvements. The clearer code reveals opportunities that might otherwise remain hidden.

Jeremy, that code is wonderful and very easy to read and I (almost) understand it. I suspected this would be more difficult that it might at first seem. Hence, the topic name.

Now I need to rotate the gimbal manually. I do not think the two methods previously under discussion will work here, so I have come up with another way that I want to run past you.

Again, using a simplest reducible case study (see below).

The origin (0, 0, 0) of the box is in the centre so one of the corners is at (100, 100, 100).
(I can use a known vertex of each torus to do the same thing. Hopefully.)
Using screenX and screenY, I can position an ellipse so it is projected on to the picture plane.
I assume the ellipse can be invisible. In fact it need not exist. Just comment out.
I can use then use dist to check whether the mouse is in this ellipse.
If the mouse is in the ellipse it is over the known vertex and so I can rotate as desired.

Will this method work with the PShape code and if so, how should I get the vertex information?

float mx;
float my;
float radius = 25;


void setup() {
  size(600, 600, P3D);
  noFill();
}
void draw() {
  background(255);
  pushMatrix();
  translate(width/2, height/2);
  rotateY(frameCount * 0.005);
  box(200);
  mx = screenX(100, 100, 100);
  my = screenY(100, 100, 100); // should I also use screenZ?
  popMatrix();

  ellipse(mx, my, radius*2, radius*2); // not needed, but useful as a concept


  if (dist(mouseX, mouseY, mx, my) <= radius) {
    println("it is not true to say this does not work");
  } else {
    println("elsewhere");
  }
}
3 Likes

Hello,

https://processing.org/reference/ortho_.html
https://processing.org/reference/perspective_.html

I recall using screenX() and screenY() and the projection was a consideration.

:slight_smile:

2 Likes

Yes, I am sure you are right. I plan to use ortho(). But first, I need to find the xyz of a vertex. Any ideas?

Hi @paulstgeorge,

The short answer is, use getVertex. If you make your own shape, you should have access to all the info that went into its creation. For example, in the torus function from earlier, there is an array called coords.

Best,
Jeremy