3D Rotations: Rotating around the screen axes

Hi all,

These are great solutions… but am I missing something here? Seems like there’s ambiguity between (1.) making a ‘look at’ function and (2.) rotating the sphere from a given orientation to a new orientation based on the mouse (assuming a fixed camera and and a centered sphere)? From @raetrakt 's initial description, I thought I’d want the ability to spin the sphere all the way around? There also seems to be some muddying of the waters as to whether we have the option of rotating the camera rather than the sphere (in which case, try peasycam)… Anyway, here’s my go at it in Java using quaternions.

First, a quaternion class:

// Should be normalized at all times when used to represent a rotation
// in 3D space.
class Quaternion {
  float x = 0.0;
  float y = 0.0;
  float z = 0.0;
  float w = 1.0;

  Quaternion() {
  }

  Quaternion(Quaternion q) {
    set(q);
  }

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

  Quaternion div(float scalar) {
    return mult(1.0 / scalar);
  }

  Quaternion mult(float scalar) {
    x *= scalar; 
    y *= scalar; 
    z *= scalar; 
    w *= scalar;
    return this;
  }

  Quaternion normalize() {
    float m = magSq();
    if (m != 0.0 && m != 1.0) {
      div(sqrt(m));
    }
    return this;
  }

  Quaternion mult(Quaternion b) {
    
    // Caveat: quaternion multiplication is not commutative.
    
    float tx = x;
    float ty = y;
    float tz = z;
    x = x * b.w + w * b.x + y * b.z - z * b.y;
    y = y * b.w + w * b.y + z * b.x - tx * b.z;
    z = z * b.w + w * b.z + tx * b.y - ty * b.x;
    w = w * b.w - tx * b.x - ty * b.y - tz * b.z;
    return this;
  }

  Quaternion set(float angle, PVector axis) {
    
    // Assumes that the axis is of unit length, i.e.,
    // has a magnitude of 1.
    
    float halfangle = 0.5 * angle;
    float sinhalf = sin(halfangle);
    x = axis.x * sinhalf; 
    y = axis.y * sinhalf;
    z = axis.z * sinhalf; 
    w = cos(halfangle);
    return this;
  }

  Quaternion set(Quaternion q) {
    return set(q.x, q.y, q.z, q.w);
  }

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

  float magSq() {
    
    // The quaternion's dot product with itself, i.e., dot(q, q).
    
    return x * x + y * y + z * z + w * w;
  }

  PMatrix3D toMatrix(PMatrix3D out) {
    float x2 = x + x; 
    float y2 = y + y; 
    float z2 = z + z;
    float xsq2 = x * x2; 
    float ysq2 = y * y2; 
    float zsq2 = z * z2;
    float xy2 = x * y2; 
    float xz2 = x * z2; 
    float yz2 = y * z2;
    float wx2 = w * x2; 
    float wy2 = w * y2; 
    float wz2 = w * z2;
    out.set(
      1.0 - ysq2 - zsq2, xy2 - wz2, xz2 + wy2, 0.0, 
      xy2 + wz2, 1.0 - xsq2 - zsq2, yz2 - wx2, 0.0, 
      xz2 - wy2, yz2 + wx2, 1.0 - xsq2 - ysq2, 0.0, 
      0.0, 0.0, 0.0, 1.0);
    return out;
  }

  Quaternion ease(Quaternion b, float t) {
    
    // This is a simplistic form of easing. For situations
    // where you need torque minimization, do not use this.
    // Use spherical linear interpolation instead.
    
    if (t <= 0.0) {
      return normalize();
    }

    if (t>=1.0) {
      return set(b).normalize();
    }

    float u = t * t * (3.0 - 2.0 * t);
    float v = 1.0 - u;
    x = v * x + u * b.x; 
    y = v * y + u * b.y; 
    z = v * z + u * b.z; 
    w = v * w + u * b.w;
    return normalize();
  }
}

Then the main class:

float rotSpeed = 1.25;
float easeSpeed = 0.125;

PVector ref = new PVector(0.0, 0.0, 1.0);
PVector axis = new PVector();
PVector mouse = new PVector();

PMatrix3D sphereMat = new PMatrix3D();

Quaternion rot = new Quaternion();
Quaternion rotMouse = new Quaternion();
Quaternion rotWorld = new Quaternion();
Quaternion identity = new Quaternion(0.0, 0.0, 0.0, 1.0);

PShape sphere;

void setup() {
  size(512, 512, P3D);
  smooth(8);
  
  // Set the camera to the center of the screen.
  ortho();
  camera(
    0.0, 0.0, -height * sqrt(3.0) * 0.5, 
    0.0, 0.0, 0.0, 
    0.0, 1.0, 0.0);

  // Create a sphere PShape of unit size.
  sphereDetail(32, 16);
  sphere = createShape(SPHERE, 0.5);
  //sphere = createShape(BOX, 0.5);
  
  // Just in case the sphere has unnecessary children...
  // Might not be necessary.
  sphere = sphere.getTessellation();

  // Let Processing style tags dictate how the shape looks in draw.
  sphere.disableStyle();
}

void draw() {

  float sphereScale = min(width, height) * 0.75;
  float strokeWeight = 0.0025 / sphereScale;

  if (mousePressed) {

    // If the mouse is pressed, set a mouse vector
    // to coordinates in the range [-1, 1].
    // Invert the y-axis.
    float  wInv = 1.0 / width;
    float hInv = 1.0 / height;
    mouse.set(
      -1.0 + 2.0 * mouseX * wInv, 
      1.0 - 2.0 * mouseY * hInv, 0.0);

    // Capture the mouse's magnitude so that 
    // more dramatic mouse movements will increase
    // the speed of rotation.
    float mag = mouse.mag();

    if (mag > 0.0) {

      // If the mouse is not in the center, then create
      // an angle of rotation.
      float ang = rotSpeed * mag;

      // Cross a reference vector against the mouse vector.
      PVector.cross(ref, mouse, axis);

      // Normalize the axis.
      axis.normalize();
      rotMouse.set(ang, axis);
    }
  } else {

    // When the mouse is not pressed, decelerate the
    // rotation to the identity rotation.
    rotMouse.ease(identity, easeSpeed);
  }

  // The rotation created by the mouse axis is multiplied with
  // the sphere's current rotation.
  rotWorld.set(rotMouse).mult(rot);

  // The rotation is eased toward the target rotation.
  rot.ease(rotWorld, easeSpeed);

  // Since Processing doesn't support quaternions, we need to convert.
  rot.toMatrix(sphereMat);
  
  // The resultant matrix is only a rotation matrix, so we scale
  // up to the size we want.
  sphereMat.scale(sphereScale);
  
  // Apply the matrix to the shape.
  sphere.resetMatrix();
  sphere.applyMatrix(sphereMat);

  // Draw.
  directionalLight(
    255.0, 255.0, 255.0, 
    0.0, 0.6, 0.8);
  background(255.0);
  strokeWeight(strokeWeight);
  stroke(#202020);
  fill(#fff7d5);
  shape(sphere);

  // Update the information for reference.
  surface.setTitle(String.format("%.1f, %.2f, %.2f", 
    frameRate, mouse.x, mouse.y));
}

Cheers,
Jeremy

5 Likes