PVector.rotate() rounding bug?

I was rotating some vectors and got some strange behaviour.It seems like the PVector.rotate() implementation has some rounding error bug in it, as it scales the vector as it rotates it.

I wrote a simple program/snippet to show its behaviour since it’s faster to show it visually than numericaly prove it.

The method rotate1() is my “manual” rotation and method rotate2() uses the PVector.rotate().The vector rotated by rotate1() is drawn in red and the vector rotated by rotate2() is drawn as green.The white circle behind is only for visual reference of the unwanted scaling taking place.

What do you guys n’ girls think of it?

class particle {
  PVector location;
  
  particle(float x, float y){
    location = new PVector(x,y);
  }
  
  void rotate1(float angle) {
    location.x = location.x * cos(angle) - location.y * sin(angle);
    location.y = location.x * sin(angle) + location.y * cos(angle);
  }
  
  void rotate2(float angle) {
    location.rotate(angle);
  }
  
  void display(float x, float y) {
    line(x, y, x + location.x, y + location.y);
  }
}

particle p1;
particle p2;

void setup(){
  size(800,800);
  p1 = new particle(350, 0);
  p2 = new particle(-350, 0);
}

void draw(){ 
  background(0);
  
  stroke(255);
  text("p1.location.mag(): " + p1.location.mag(), 20,20);
  text("p2.location.mag(): " + p2.location.mag(), 20,30);
    
  stroke(255);
  noFill();
  ellipse(width/2, height/2, 700,700);
  
  stroke(0,255,0);
  p1.display(width/2, height/2);
  
  stroke(255,0,0);
  p2.display(width/2, height/2);
  
  p1.rotate1(PI/180);
  p2.rotate2(PI/180);
}
1 Like

Yes, that is true. PVector is stored internally as an x y z with float precision. Every rotation operation can cause drift on magnitude, as it is derived from xy.

If you want to preserve high precision magnitude no matter what then there are lots of potential workarounds. You could create your own Vector that stores angle + mag and derive xy. You could also use a hack to (periodically) reassert the magnitude after rotation - for example, if you have a 2D sketch then you can store mag in z – x y mag – and after a rotation reapply the magnitude to eliminate drift.

1 Like

Thx for reply!

I follow your point, however, i looked in the processing source and found this.

  public PVector rotate(float theta) {
    float temp = x;
    // Might need to check for rounding errors like with angleBetween function?
    x = x*PApplet.cos(theta) - y*PApplet.sin(theta);
    y = temp*PApplet.sin(theta) + y*PApplet.cos(theta);
    return this;
  }

Which is pretty much exactly how i did it.
And everything in PVector.java appears to be in float.
So i’m not exactly sure to why PVector.rotate() behaves differently.

So, the bottomline if precision is required, dont rely on the x,y,x coordinates, go for angle and mag, i get that.
But i’m not able to see (at this point) where the rounding goes “bad” in PVector.java.

I see now that i got it backwards…
Its actually my rotation that is doing the scaling/rounding error…
:sweat:

It doesn’t go bad differently – it behaves like all other rounding. The issue emerges under conditions of cumulative error – so rotating many, many times gets further away from the original value. This breaks the expectation that the vec is a fixed object, like a ruler, when it is a series of jumps in xy space.

This happens with rotation, random walks, color shifting – pretty much anytime you accumulate lots of float operations rather than using a fixed origin and one transformation. So, for a clock, don’t rotate the second hand each frame. Instead, each frame start with the second hand at 3 (or 12) and then turn it where it should be.

1 Like

Indeed… your implementation mutates the PVector::x field w/o caching it in some temp local variable. :roll_eyes:

:+1:

I improved the code a bit, added some compensation.
Which multiplies/scale the vector to the same magnitude as before the rotation.

1/ (currentmag / prevmag)

And now im accumulating less error than PVector.rotate().
But still, it is accumulating, so I am buying your argument that it will never be a foolproof concept unless you fix/exchange the prevmag to a fixed desiredmag.
Then it will scale the vector to the proper/correct length (to the extent that the number of decimals allows) for each call to rotate1().

  void rotate1(float angle) {
    float prevmag = location.mag();
    location.x = location.x * cos(angle) - location.y * sin(angle);
    location.y = location.x * sin(angle) + location.y * cos(angle);
    location.mult(1.0/(location.mag()/prevmag));
  }

Yupp… one of those nights i guess.

This is much better: :sunglasses:

  void rotate1(float angle) {
    float prevmag = location.mag();
    float tempx = location.x;
    location.x = location.x * cos(angle) - location.y * sin(angle);
    location.y = tempx * sin(angle) + location.y * cos(angle);
    location.mult(1.0/(location.mag()/prevmag));
  }