Gimbals, quaternions, and manual rotation

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

The long answer:

If a PShape is a GROUP that contains other PShapes, then you need to either get its children first, or try to tessellate it. PShape's methods can be quite stingy, so I’ve found it’s better to store important shape data separately: namely, the transform matrix and a collection of vertices.

It’s possible for getVertex to return redundant information. Processing’s UV sphere is a good illustration of this: the same vertex at the north and south poles of a sphere is repeated for every longitude. If you know about collections you can use a Set to hold only unique instances of a vertex coordinate.

Another consideration: the vertices set when you create a shape are in object space. For example, in a sphere with a radius of 0.5, the north pole is at (0.0, 0.5, 0.0) and the soulth pole is at (0.0, -0.5, 0.0) . They will not necessarily change as you translate, rotate or scale the shape in draw. To change from object space to to world space, you multiply a vertex with the shape’s transform.

0287

import java.util.*;

// A shape's points can be retrieved with getVertex . However, a shape
// may return multiple copies of the same point -- for example -- the
// same two points at the north and south pole of a sphere. A set
// prevents duplicates.
Set<PVector> vertices = new HashSet<PVector>();

// A shape's transform can only be indirectly set with resetMatrix
// and applyMatrix
PMatrix3D shapeMat = new PMatrix3D();

// To show how the screen function works, the renderer is needed.
PGraphics3D rndr;

// To avoid the sqrt incurred by calculating the distance,
// square the radius.
float radius = 3.0;
float radsq = radius * radius;

void setup() {
  size(256, 256, P3D);
  ellipseMode(RADIUS);
  perspective();
  camera(
    0.0, 0.0, height * 0.86602,
    0.0, 0.0, 0.0,
    0.0, 1.0, 0.0);

  // The number of latitudes and longitudes in a sphere,
  // even one stored in a PShape, is determined by sphereDetail .
  sphereDetail(27, 9);
  PShape shape = createShape(SPHERE, 0.5);

  // Acquire vertices from the PShape.
  int len = shape.getVertexCount();
  for (int i = 0; i < len; ++i) {
    vertices.add(shape.getVertex(i));
  }

  // The set has fewer vertices than the shape.
  println(len, vertices.size());

  rndr = (PGraphics3D)getGraphics();
  shapeMat.scale(200.0);
}

void draw() {
  shapeMat.rotate(0.002, 0.7071, 0.7071, 0.0);
  background(#fff7d5);

  // The vertex as transformed by shapeMat.
  PVector trvert = new PVector();

  // The transformed vertex in screen space.
  PVector scvert = new PVector();

  // The mouse input.
  PVector mouse = new PVector(mouseX, mouseY);

  // The difference from subtracting the mouse from scvert.
  PVector diff = new PVector();

  // Loop over all the vertices.
  for (PVector vert : vertices) {

    // Two transformations of the points: from object
    // space to world space, then to screen space.
    shapeMat.mult(vert, trvert);
    screen(rndr, trvert, scvert);

    // Find the distance between the mouse and the point.
    // Euclidean distance squared is used, not distance, to
    // avoid the square-root.
    PVector.sub(scvert, mouse, diff);
    float distsq = PVector.dot(diff, diff);

    // If the point is close by, draw red.
    // If not, black.
    if (distsq < radsq) {
      stroke(#ff2828);
      strokeWeight(10.0);
    } else {
      strokeWeight(4.0);
      stroke(#202020);
    }

    point(trvert.x, trvert.y, trvert.z);
  }
}

It may help to do some research on how screen and model functions work. I’ve never had any luck with those two functions, but I think others have worked on the issue you’re exploring now. This forum thread is one place to start P3D Formula : opposite of screenX and screenY is ...? . The source code for screen is opaque; just know that – just like changing vertices from object space to world space above – multiplying points and transforms is involved.

static Vec4 promote(PVector src, float w, Vec4 trg) {

  // Promote a 3D vector to a 4D vector.
  return trg.set(src.x, src.y, src.z, w);
}

static Vec4 mul(PMatrix3D m, Vec4 src, Vec4 trg) {

  // [ m00, m01, m02, m03,     [ x,        [ dot(m0c, x),
  //   m10, m11, m12, m13,  x    y,    :=    dot(m1c, y),
  //   m20, m21, m22, m23,       z,          dot(m2c, z),
  //   m30, m31, m32, m33 ]      w ]         dot(m3c, w) ]
  //
  // where 'c' is a matrix column index.

  return trg.set(
    m.m00 * src.x + m.m01 * src.y + m.m02 * src.z + m.m03 * src.w,
    m.m10 * src.x + m.m11 * src.y + m.m12 * src.z + m.m13 * src.w,
    m.m20 * src.x + m.m21 * src.y + m.m22 * src.z + m.m23 * src.w,
    m.m30 * src.x + m.m31 * src.y + m.m32 * src.z + m.m33 * src.w);
}

static PVector demote(Vec4 source, PVector target) {

  // Demote a homogenous coordinate to a point by dividing
  // the x, y and z components by w.
  if (source.w == 0.0) {
    return target.set(0.0, 0.0, 0.0);
  }
  float wInv = 1.0 / source.w;
  return target.set(
    source.x * wInv,
    source.y * wInv,
    source.z * wInv);
}

static PVector screen(
  PGraphicsOpenGL rndr,
  PVector src,
  PVector trg) {

  Vec4 point = promote(src, 1.0, new Vec4());

  // Multiply by the modelview, then by the projection.
  Vec4 mvmul = mul(rndr.modelview, point, new Vec4());
  Vec4 projmul = mul(rndr.projection, mvmul, new Vec4());

  demote(projmul, trg);

  // Shift range from [-1.0, 1.0] to [0.0, 1.0] .
  trg.add(1.0, 1.0, 1.0).mult(0.5);  

  // Multiply by screen dimensions.
  trg.x *= rndr.width;
  trg.y *= rndr.height;

  // Flip y-axis because Processing.
  trg.y = rndr.height - trg.y;
  return trg;
}

static class Vec4 {
  float x, y, z, w;

  Vec4() { }

  Vec4(float x, float y, float z, float w) {
    set(x, y, z, w);
  }

  Vec4 set(float x, float y, float z, float w) {
    this.x = x; this.y = y; this.z = z; this.w = w;
    return this;
  }
}

Regards,

1 Like

Just a note that you might also be interested in looking at the Picking Library implementation of picking. It is P2D/P3D only – not Java2D – but since you are using P3D anyway, it might be a good fit. Previous discussion:

Courtesy update.
The project had gotten like a library of libraries in a garden of forking paths. So, I guess I was asking for directions having started in the wrong place.
To make things more manageable I have retraced my steps and started again. This time without the tori. It is much easier to track the rotation of a sphere and when it comes time to have three orthogonal rings I can do this with a texture, or a height map, or both!
Here is the code for rotation of a sphere around three axes. Now to replace the key presses with a mouse event…

PShape sphera;


// Axis around which sphera 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(600, 600, 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);
  //
  //https://processing.org/reference/createShape_.html
  //sphereDetail() //maybe
  sphera = createShape(SPHERE, 150);
}

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


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

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

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;
}
2 Likes