-1

I have modified this arcball class so that every call to arcball.rollforward(PI/180); rotates a matrix 1 degree. I have tried to set it up so arcball.rollback() is called with the accumulated float rotatebywithincludedfloaterror but it has had the same degree error as rolling back 360 degrees without the float error. this is how far it is off after 1000 full rotations, it should be a 1:1 reflection of the top cube over x

degree error

here is main function with a loop of 1 * 360 degree rotation and framerate for testing (set framerate to 900 for multiple rotations so it dose not take forever)

Arcball arcball;

int i;

//framecount
int fcount, lastm;
float frate;
int fint = 3;

boolean[] keys = new boolean[13];
    final int w = 0;


void setup() {
  size(900, 700, P3D); 
  frameRate(60);
  noStroke();
  arcball = new Arcball(width/2, height/2, 100);   //100 is radius
}

void draw() {
  lights();
  background(255,160,122);
  
  print(" \n degree = " + i );
  i++;
  if(i <= (360 * 1)) { arcball.rollforward(PI/180); }
  else { print(" break"); }
  
  if(keys[w]) { arcball.rollforward(PI/180); }

  translate(width/2, height/2-100, 0);
  box(50);
   
  translate(0, 200, 0);
  arcball.run();
  box(50);  
  
  
  fcount += 1;
  int m = millis();
  if (m - lastm > 1000 * fint) {
    frate = float(fcount) / fint;
    fcount = 0;
    lastm = m;
    println("fps: " + frate);
  }
                           
}

void keyPressed() {
  switch(key) {
    case 119: 
        keys[w] = true;
        break;
  }
}
void keyReleased() {
  switch(key) {
    case 119: 
        keys[w] = false;
        break;
    } 
}

and the arcball class

// Ariel and V3ga's arcball class with a couple tiny mods by Robert Hodgin and smaller mods by cubesareneat

class Arcball {
  float center_x, center_y, radius;
  Vec3 v_down, v_drag;
  Quat q_now, q_down, q_drag;
  Vec3[] axisSet;
  int axis;
  float mxv, myv;
  float x, y;
  
  float degreeW_count = 0;
  float degreeS_count = 0;
  float rotatebywithincludedfloaterror =0;
  
  Arcball(float center_x, float center_y, float radius){
    this.center_x = center_x;
    this.center_y = center_y;
    this.radius = radius;

    v_down = new Vec3();
    v_drag = new Vec3();

    q_now = new Quat();
    q_down = new Quat();
    q_drag = new Quat();

    axisSet = new Vec3[] {new Vec3(1.0f, 0.0f, 0.0f), new Vec3(0.0f, 1.0f, 0.0f), new Vec3(0.0f, 0.0f, 1.0f)};
    axis = -1;  // no constraints...    
  }

  void rollforward(float radians2turn) { 
    rotatebywithincludedfloaterror = rotatebywithincludedfloaterror + (-1 * (((sin(radians2turn) * radius))/2));
    if(degreeW_count >= 360) {
      arcball.rollback(rotatebywithincludedfloaterror);
      degreeW_count = 0;
      rotatebywithincludedfloaterror = 0;
    }
    rollortilt(0, -1 * (((sin(radians2turn) * radius))/2)); 
    degreeW_count = degreeW_count + 1; // need to edit this later to work with rotations other then 1 degree
  }
  void rollback(float radians2turn) { 
    rollortilt(0, ((sin(radians2turn) * radius))/2);
  }
  
  void rollortilt(float xtra, float ytra){
    q_down.set(q_now);
    v_down = XY_to_sphere(center_x, center_y);
    q_down.set(q_now);
    q_drag.reset();
    
    v_drag = XY_to_sphere(center_x + xtra, center_y + ytra);
    q_drag.set(Vec3.dot(v_down, v_drag), Vec3.cross(v_down, v_drag)); 
  }

/*
  void mousePressed(){
    v_down = XY_to_sphere(mouseX, mouseY);  
    q_down.set(q_now);
    q_drag.reset();
  }

  void mouseDragged(){
    v_drag = XY_to_sphere(mouseX, mouseY);
    q_drag.set(Vec3.dot(v_down, v_drag), Vec3.cross(v_down, v_drag));
  }
*/
  void run(){
    q_now = Quat.mul(q_drag, q_down);
    applyQuat2Matrix(q_now);
    
    x += mxv;
    y += myv;
    mxv -= mxv * .01;
    myv -= myv * .01;
  }
  
  Vec3 XY_to_sphere(float x, float y){
    Vec3 v = new Vec3();
    v.x = (x - center_x) / radius;
    v.y = (y - center_y) / radius;

    float mag = v.x * v.x + v.y * v.y;
    if (mag > 1.0f){
      v.normalize();
    } else {
      v.z = sqrt(1.0f - mag);
    }

    return (axis == -1) ? v : constrain_vector(v, axisSet[axis]);
  }

  Vec3 constrain_vector(Vec3 vector, Vec3 axis){
    Vec3 res = new Vec3();
    res.sub(vector, Vec3.mul(axis, Vec3.dot(axis, vector)));
    res.normalize();
    return res;
  }

  void applyQuat2Matrix(Quat q){
    // instead of transforming q into a matrix and applying it...

    float[] aa = q.getValue();
    rotate(aa[0], aa[1], aa[2], aa[3]);
  }
}

static class Vec3{
  float x, y, z;

  Vec3(){
  }

  Vec3(float x, float y, float z){
    this.x = x;
    this.y = y;
    this.z = z;
  }

  void normalize(){
    float length = length();
    x /= length;
    y /= length;
    z /= length;
  }

  float length(){
    return (float) Math.sqrt(x * x + y * y + z * z);
  }

  static Vec3 cross(Vec3 v1, Vec3 v2){
    Vec3 res = new Vec3();
    res.x = v1.y * v2.z - v1.z * v2.y;
    res.y = v1.z * v2.x - v1.x * v2.z;
    res.z = v1.x * v2.y - v1.y * v2.x;
    return res;
  }

  static float dot(Vec3 v1, Vec3 v2){
    return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
  }
  
  static Vec3 mul(Vec3 v, float d){
    Vec3 res = new Vec3();
    res.x = v.x * d;
    res.y = v.y * d;
    res.z = v.z * d;
    return res;
  }

  void sub(Vec3 v1, Vec3 v2){
    x = v1.x - v2.x;
    y = v1.y - v2.y;
    z = v1.z - v2.z;
  }
}

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

  Quat(){
    reset();
  }

  Quat(float w, float x, float y, float z){
    this.w = w;
    this.x = x;
    this.y = y;
    this.z = z;
  }

  void reset(){
    w = 1.0f;
    x = 0.0f;
    y = 0.0f;
    z = 0.0f;
  }

  void set(float w, Vec3 v){
    this.w = w;
    x = v.x;
    y = v.y;
    z = v.z;
  }

  void set(Quat q){
    w = q.w;
    x = q.x;
    y = q.y;
    z = q.z;
  }

  static Quat mul(Quat q1, Quat q2){
    Quat res = new Quat();
    res.w = q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z;
    res.x = q1.w * q2.x + q1.x * q2.w + q1.y * q2.z - q1.z * q2.y;
    res.y = q1.w * q2.y + q1.y * q2.w + q1.z * q2.x - q1.x * q2.z;
    res.z = q1.w * q2.z + q1.z * q2.w + q1.x * q2.y - q1.y * q2.x;
    return res;
  }
  
  float[] getValue(){
    // transforming this quat into an angle and an axis vector...

    float[] res = new float[4];

    float sa = (float) Math.sqrt(1.0f - w * w);
    if (sa < EPSILON){
      sa = 1.0f;
    }

    res[0] = (float) Math.acos(w) * 2.0f;
    res[1] = x / sa;
    res[2] = y / sa;
    res[3] = z / sa;
    return res;
  }
}

keep track of the floating error margin to return same number of degrees arcball.rollforward()

  void rollforward(float radians2turn) { 
    rotatebywithincludedfloaterror = rotatebywithincludedfloaterror + (-1 * (((sin(radians2turn) * radius))/2));
    if(degreeW_count >= 360) {
      arcball.rollback(rotatebywithincludedfloaterror);
      degreeW_count = 0;
      rotatebywithincludedfloaterror = 0;
    }
    rollortilt(0, -1 * (((sin(radians2turn) * radius))/2)); 
    degreeW_count = degreeW_count + 1; // need to edit this later to work with rotations other then 1 degree
  }
cubesareneat
  • 302
  • 2
  • 14
  • You will keep more precision if you use `double` instead of `float` (as in `void rollforward(double radians2turn) {`). Considering the trig operations you do, it's widened to double anyway. – Andy Turner Jan 11 '21 at 15:55
  • I figured I needed a more mathematical fix, as that would just make the error smaller not eradicate it – cubesareneat Jan 11 '21 at 15:58
  • Yes. Such is the nature of the floating-point beast. Numerical errors essentially always exist, so they will accumulate. – Andy Turner Jan 11 '21 at 15:59
  • what about keeping track of the accumulated error so it can reset with the same error margin, say it rotates 359.9 degrees it would then reset by rolling back 359.9 – cubesareneat Jan 11 '21 at 16:10
  • you might be able to represent the error *more* accurately than the value, but you're still limited by the precision of the thing you're using to store the error. – Andy Turner Jan 11 '21 at 16:12
  • if the degrees forward are all added together then applied in reverse when it is ~360 degrees should it not just go back to 0? for example if it rounds pi/180 when rolling forward it should also round to the same number to roll back to 0. if the error is 0.000001 when rolling forward it should have a 0.000001 error rolling back to = 0 – cubesareneat Jan 11 '21 at 16:24
  • its unique to rotational problems because sin(0) = sin(2PI) – cubesareneat Jan 11 '21 at 16:46
  • Try with doubles. If that works, it's the easiest fix. If not, then I do remember seeing a demo of a system that used fixed-point arithmetic that didn't have this problem (it was a version of QuickDraw3d for the Mac, 20+ years ago). – NomadMaker Jan 11 '21 at 17:07

1 Answers1

0

using my idea in the question to reset every 2*PI

  if(keys[w]) { 
    arcball.rollforward(PI/180);
    degreeW_count = degreeW_count + 1;
  }

  if(degreeW_count == 360) {
    arcball = new Arcball(width/2, height/2, 100); // setset to original arcball at 0 degrees
    degreeW_count = 0;
  }

in arcball

  void rollforward(float degrees2turn) { 
    rollortilt(0, -1 * (((sin(degrees2turn) * radius))/2));  // one degree forward 180/PI
  }

this totally circumvents the any rounding error that would accumulate with any data type using irrational numbers and periodic functions!

cubesareneat
  • 302
  • 2
  • 14