Rotate a 3D shape so that it follows a PVector's direction

edited December 2017 in Questions about Code

Hello everyone,

I wrote a Processing code that draws two spheres which are initially in contact. As we know, when two spheres are in contact, there is (usually) a circle. [When they are tangent, it is a point, but let's focus on the circle case]. To draw the circle, I have a function that draws a cylinder via beginShape and endShape functions. When the height parameter is zero, there is only the top shape of the cylinder.

I have some calculations to determine the center and the radius of this circle. However, I cannot solve how should I do to rotate the circle to follow the PVector between the two spheres. That is to say, if we consider the vector between the spheres' centers, the circle should follow that direction.

If no rotations are done, the circle's shape is oriented towards the XY plane. What I need is a way to calculate properly the rotations around the 3 axis to achieve the expected result.

I leave here my source code:

float x0, y0, z0, r0;
float x1, y1, z1, r1;

float camX, camY, camZ;
float centerX, centerY, centerZ;
float upX, upY, upZ;

float iniX0, iniY0, iniZ0;
float iniX1, iniY1, iniZ1;
float iniCamX, iniCamY, iniCamZ;

boolean x0Left, x0Right, y0Up, y0Down, z0Out, z0In;
boolean x1Left, x1Right, y1Up, y1Down, z1Out, z1In;
boolean camXLeft, camXRight, camYUp, camYDown, camZOut, camZIn;

boolean calc;

float red, green, blue;
float lightPosX, lightPosY, lightPosZ;

int tipInt;
PVector center;
float radius;
PVector orient;
Object[] calcRes;

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

  x0 = 200;
  y0 = 200;
  z0 = 50;
  r0 = 100;

  x1 = 350;
  y1 = 210;
  z1 = -50;
  r1 = 150;

  camX = width;
  camY = height / 2.0;
  camZ = (height/2.0) / tan(PI / 6.0);

  iniX0 = x0;      iniY0 = y0;      iniZ0 = z0;
  iniX1 = x1;      iniY1 = y1;      iniZ1 = z1;
  iniCamX = camX;  iniCamY = camY;  iniCamZ = camZ;

  centerX = width / 2.0;
  centerY = height / 2.0;
  centerZ = 0;

  upX = 0;
  upY = 1;
  upZ = 0;

  x0Left = false;    x0Right = false;    y0Up = false;    y0Down = false;    z0Out = false;    z0In = false;
  x1Left = false;    x1Right = false;    y1Up = false;    y1Down = false;    z1Out = false;    z1In = false;
  camXLeft = false;  camXRight = false;  camYUp = false;  camYDown = false;  camZOut = false;  camZIn = false;

  calc = true;

  tipInt = -1;
  center = null;
  radius = 0;
  orient = null;
  calcRes = new Object[4];

  red = 51;
  green = 102;
  blue = 126;
  lightPosX = 300;
  lightPosY = 250;
  lightPosZ = 300;
}

void draw()
{
  if (x0Left)    --x0;
  if (x0Right)   ++x0;
  if (y0Up)      --y0;
  if (y0Down)    ++y0;
  if (z0Out)     --z0;
  if (z0In)      ++z0;

  if (x1Left)    --x1;
  if (x1Right)   ++x1;
  if (y1Up)      --y1;
  if (y1Down)    ++y1;
  if (z1Out)     --z1;
  if (z1In)      ++z1;

  if (camXLeft)  camX += 10;
  if (camXRight) camX -= 10;
  if (camYUp)    camY += 10;
  if (camYDown)  camY -= 10;
  if (camZOut)   camZ += 10;
  if (camZIn)    camZ -= 10;

  if (calc)
  {
    calcRes = calcIntersection();
    tipInt = (int)(calcRes[0]);
    center = (PVector)(calcRes[1]);
    radius = (float)(calcRes[2]);
    orient = (PVector)(calcRes[3]);
  }

  camera(camX, camY, camZ, centerX, centerY, centerZ, upX, upY, upZ);
  background(0);

  drawAxis(0, 0, 0);

  stroke(64);
  if (!mousePressed)
  {
    strokeWeight(1);
    fill(color(255, 0, 0));
    pushMatrix();
    translate(x0, y0, z0);
    sphere(r0);
    popMatrix();

    fill(0, 0, 255);
    pushMatrix();
    translate(x1, y1, z1);
    sphere(r1);
    popMatrix();
  }

  if (tipInt == 1 && center != null)
  {
    drawAxis(center.x, center.y, center.z);

    fill(0);
    stroke(0);
    pushMatrix();
    translate(center.x, center.y, center.z);
    sphere(10);
    popMatrix();

    fill(242, 242, 7);
    stroke(242, 242, 7);
    pushMatrix();
    translate(center.x, center.y, center.z);
    cylinder(radius*3, 0, 100);  // radi, alt, detall
    popMatrix();
  }
}

void keyPressed(KeyEvent evt)
{
  switch (key)
  {
    case 'a':  
    case 'A':  x0Left = true;
               calc = true;
               break;
    case 'd':  
    case 'D':  x0Right = true;
               calc = true;
               break;
    case 'w':  
    case 'W':  y0Up = true;
               calc = true;
               break;
    case 's':  
    case 'S':  y0Down = true;
               calc = true;
               break;
    case 'e':  
    case 'E':  z0Out = true;
               calc = true;
               break;
    case 'q':  
    case 'Q':  z0In = true;
               calc = true;
               break;
    case 'k':  
    case 'K':  camXLeft = true;
               break;
    case 'ñ':  
    case 'Ñ':  camXRight = true;
               break;
    case 'o':  
    case 'O':  camYUp = true;
               break;
    case 'l':  
    case 'L':  camYDown = true;
               break;
    case 'p':  
    case 'P':  camZOut = true;
               break;
    case 'i':  
    case 'I':  camZIn = true;
               break;
    default:   switch (keyCode)
               {
                 case 32:   x0 = iniX0;      y0 = iniY0;      z0 = iniZ0;
                            x1 = iniX1;      y1 = iniY1;      z1 = iniZ1;
                            camX = iniCamX;  camY = iniCamY;  camZ = iniCamZ;
                            calc = true;
                            break;
                 case 132:  x1Left = true;
                            calc = true;
                            break;
                 case 134:  x1Right = true;
                            calc = true;
                            break;
                 case 136:  y1Up = true;
                            calc = true;
                            break;
                 case 133:  y1Down = true;
                            calc = true;
                            break;
                 case 137:  z1Out = true;
                            calc = true;
                            break;
                 case 135:  z1In = true;
                            calc = true;
                            break;
               }
               break;
  }
}

void keyReleased(KeyEvent evt)
{
  switch (key)
  {
    case 'a':  
    case 'A':  x0Left = false;
               calc = true;
               break;
    case 'd':  
    case 'D':  x0Right = false;
               calc = true;
               break;
    case 'w':  
    case 'W':  y0Up = false;
               calc = true;
               break;
    case 's':  
    case 'S':  y0Down = false;
               calc = true;
               break;
    case 'e':  
    case 'E':  z0Out = false;
               calc = true;
               break;
    case 'q':  
    case 'Q':  z0In = false;
               calc = true;
               break;
    case 'k':  
    case 'K':  camXLeft = false;
               break;
    case 'ñ':  
    case 'Ñ':  camXRight = false;
               break;
    case 'o':  
    case 'O':  camYUp = false;
               break;
    case 'l':  
    case 'L':  camYDown = false;
               break;
    case 'p':  
    case 'P':  camZOut = false;
               break;
    case 'i':  
    case 'I':  camZIn = false;
               break;
    default:   switch (keyCode)
               {
                 case 32:   x0 = iniX0;      y0 = iniY0;      z0 = iniZ0;
                            x1 = iniX1;      y1 = iniY1;      z1 = iniZ1;
                            camX = iniCamX;  camY = iniCamY;  camZ = iniCamZ;
                            calc = true;
                            break;
                 case 132:  x1Left = false;
                            calc = true;
                            break;
                 case 134:  x1Right = false;
                            calc = true;
                            break;
                 case 136:  y1Up = false;
                            calc = true;
                            break;
                 case 133:  y1Down = false;
                            calc = true;
                            break;
                 case 137:  z1Out = false;
                            calc = true;
                            break;
                 case 135:  z1In = false;
                            calc = true;
                            break;
               }
               break;
  }
}

Object[] calcIntersection()
{
  Object[] o = new Object[4];

  PVector A = new PVector(x0, y0, z0);
  PVector B = new PVector(x1, y1, z1);
  float d = PVector.dist(A, B);

  int codIntersection = -1;  // separate spheres
  PVector I = null;
  float r = 0;
  PVector diff = null;

  if (d > (r0 + r1))                        codIntersection = -1; // separate spheres
  else if (d == (r0 + r1))                  codIntersection = 0;  // exterior tangency
  else if (d < r0 + r1 || d > abs(r0 - r1)) codIntersection = 1;  // intersection
  else if (d == abs(r0 - r1))               codIntersection = 2;  // interior tangency
  else if (d < abs(r0 - r1))                codIntersection = 3;  // inclusion

  if (codIntersection == 1)
  {
    float k = (sq(r0) - sq(r1) + sq(d)) / (2*sq(d));

    diff = PVector.sub(B, A);
    I = A.copy().add(diff.mult(k));
    r = sqrt(sq(r0) - sq(k*d));

    println("circle's center: " + I + ", circle's radii: " + r);
  }

  calc = false;

  o[0] = codIntersection;
  o[1] = I;
  o[2] = r;
  o[3] = PVector.sub(B, A);

  return o;
}

void drawAxis(float posX, float posY, float posZ)
{
  // X axis
  stroke(255, 0, 0);
  fill(255, 0, 0);
  line(posX, posY, posZ, posX+200, posY, posZ);
  text("X-axis", posX+150, posY-20, posZ);

  // Y axis
  stroke(0, 128, 0);
  fill(0, 255, 0);
  line(posX, posY, posZ, posX, posY-150, posZ);
  text("Y-axis", posX, posY-170, posZ);

  // Z axis
  stroke(0, 255, 255);
  fill(0, 255, 255);
  line(posX, posY, posZ, posX, posY, posZ+240);
  text("Z-axis", posX, posY, posZ+240);
}

void cylinder(float cylRadius, float cylHeight, int cylDetail)
{
  float x, y, theta;

  beginShape();
  for (int i = 0; i < cylDetail; i++)
  {
    theta = 360 * i / cylDetail;

    x = cylRadius * cos(radians(theta));
    y = cylRadius * sin(radians(theta));
    vertex(x, y,  (cylHeight > 0) ? -cylHeight/2 : -1.0/1000000.0);
  }
  endShape(CLOSE);

  if (cylHeight > 0)
  {
    beginShape(TRIANGLE_STRIP);
    for (int i = 0; i <= cylDetail; i++)
    {
      theta = 360 * i / cylDetail;
      x = cylRadius * cos(radians(theta));
      y = cylRadius * sin(radians(theta));
      vertex( x, y, cylHeight/2);
      vertex( x, y, -cylHeight/2);
    }
    endShape(CLOSE);

    beginShape();
    for (int i = 0; i < cylDetail; i++)
    {
      theta = 360 * i / cylDetail;
      x = cylRadius * cos(radians(theta));
      y = cylRadius * sin(radians(theta));
      vertex(x, y, cylHeight/2);
    }
    endShape(CLOSE);
  }
}

Thanks in advance.

Answers

  • if we consider the vector between the spheres' centers, the circle should follow that direction

    Adding rotateY(90) after line 145 could be a potential partial solution for that case?

    A circle's surface is defined by its normal vector at its center. Do you want this vector to be aligned to the vector defined between the two spheres's center?

    Kf

  • Hi kfrajer,

    The solution goes on the way you suggest. In fact, I realized that rotating 5° in the X axis and 125° in the Y axis is close to what I need. The problem is, how should I calculate this two angles?

    Effectively, I would like to have the normal vector aligned to the vector between the two spheres’ center.

    Thank you so much!!

  • Answer ✓

    Please check the reference under the keyword PVector for some of the functions next.

    The angles you are looking for are theta and phi. What you want is express your vector k (define as the difference between center's position) in spherical coordinates. In Processing, most operations are done in the cartesian coordinate system. Have a look at the following code. Adjust the vector vekBetweenCenters to see the resultant angles. For example, work with the trivial case where the Z component of your vector is zero, then the problem reduces to 2D calculations.

    .........

    //Next in other words, the vectorial difference between the sphere's center 
    PVector vekBetweenCenters=new PVector(1,1,1);  //This is the vector k as I discussed above
    
    //Projection of your vector into the XY plane (aka. only x and y components)
    PVector projXY= new PVector(vekBetweenCenters.x,vekBetweenCenters.y,0);
    //Similarly, projection of your vector into the XZ plane (aka. only x and z components)
    PVector projXZ=new PVector(vekBetweenCenters.x,0,vekBetweenCenters.z);
    
    //Make their magnitudes unity
    projXY.normalize();
    projXZ.normalize();
    
    
    float theta=acos(projXY.dot(Yaxis));
    float phi=acos(projXZ.dot(Zaxis));
    
    println(vekBetweenCenters);
    println(degrees(theta)+" "+degrees(phi));
    

    Kf

  • edited December 2017

    Hi again,

    I have tried with your suggested code, but surely I'm doing something wrong, because the result is almost what I need, but it's not exactly...

    The portion of code is as follows:

    PVector projXY = new PVector(orient.x, orient.y, 0);
    PVector projXZ = new PVector(orient.x, 0, orient.z);
    
    PVector yAxis = new PVector(0, 1, 0);
    PVector zAxis = new PVector(0, 0, 1);
    
    projXY.normalize();
    projXZ.normalize();
    
    float theta = acos(projXY.dot(yAxis));
    float phi   = acos(projXZ.dot(zAxis));
    
    println("direction: " + orient + "\ntheta: " + degrees(theta) + ", phi: " + degrees(phi));
    
    fill(242, 242, 7);
    stroke(242, 242, 7);
    pushMatrix();
    translate(center.x, center.y, center.z);
    rotateX(phi);
    rotateY(theta);
    cylinder(radius*3, 0, 100);  // radi, alt, detall
    popMatrix();
    

    The console shows this information:

    direction: [ 150.0, 10.0, -100.0 ]
    theta: 86.18592, phi: 123.69006
    

    What I seen on the screen is this image: img01

    However, I'd like to have something similar to this: img02

    I don't know if it's a problem with the definition of yAxis and zAxis, the type of rotations (rotateX, rotateY) o its order (I'm aware that it's not the same to do rotateX+rotateY and rotateY+rotateX).

    If you see something wrong, please, let me know. Thanks!!!!

  • Answer ✓

    Order is important. You apply theta first and then phi. Think about a vector in 3D space. Project the vector to the XY plane. This vector will rotate about the Z axis an angle of theta. In Processing, when you do this rotation, your XY also rotates as you know. The next step is to rotate about Y (I think) an angle of phi and then translate.

    I suggest you do a test program using the peasyCam kibrary as it will allow you to interact with the 3D space using the mouse. You can see your results faster, instead of using key inputs.

    I have the feeling you are not drawing the yellow circle at the right distance. Unfortunately I can't test your code atm.

    Kf

  • I suggest you do a test program using the peasyCam kibrary as it will allow you to interact with the 3D space using the mouse. You can see your results faster, instead of using key inputs. A very good idea, I didn't know about this library.

    Now I have tried with:

    rotateZ(theta);
    rotateX(phi);
    

    It's close, but something else is lacking... I'll investigate.

    Thank you for your patience.

Sign In or Register to comment.