Problem with getLocalRotation().FromAngleAxis()

Hey there,

It seems that jMonkeyEngine is having a bug with the getLocalRotation().toAngles() method. When the Z rotation is between +/- 87 and +/- 93 degrees (after multiplying the radians with FastMath.RAD_TO_DEG) it tells me the rotation is exactly 90 degrees, while it really isn’t. The same problem happens when it’s around 270 degrees…

Here’s a testcase to show you what I mean. Don’t run it on fullscreen because the actual rotation and the rotation getLocalRotation().toAngles() is telling me is posted in the console (using System.out.println()).

[java]
package mygame;

import com.jme3.app.SimpleApplication;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Box;

/**

  • test

  • @author Marco de Zeeuw
    */
    public class Main extends SimpleApplication {

    private Geometry geom;
    private float rotationZ = 0f;

    public static void main(String[] args) {
    Main app = new Main();
    app.start();
    }

    @Override
    public void simpleInitApp() {
    Box b = new Box(Vector3f.ZERO, .2f, 1, .2f);
    geom = new Geometry(“Box”, b);

     Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
     mat.setColor("Color", ColorRGBA.Blue);
     geom.setMaterial(mat);
    
     rootNode.attachChild(geom);
    

    }

    @Override
    public void simpleUpdate(float tpf) {
    rotationZ += .002;
    if(rotationZ >= 360) {
    rotationZ = 0;
    }
    geom.setLocalRotation(geom.getLocalRotation().fromAngleAxis(rotationZ * FastMath.DEG_TO_RAD, Vector3f.UNIT_Z));

     float xRot = geom.getLocalRotation().toAngles(null)[0] * FastMath.RAD_TO_DEG;
     float yRot = geom.getLocalRotation().toAngles(null)[1] * FastMath.RAD_TO_DEG;
     float zRot = geom.getLocalRotation().toAngles(null)[2] * FastMath.RAD_TO_DEG;
     
     if(xRot == 180 && yRot == 180) {
         zRot = 180 - zRot;
     }
     else if(xRot == -180 && yRot == -180) {
         zRot = 180 - zRot;
     }
     else if(zRot < 0) {
         zRot += 360;
     }
     
     float difference = rotationZ - zRot;
     if(difference < 0) {
         difference = 0 - difference;
     }
     float allowedErrorValue = .001f;
     
     if(difference < allowedErrorValue) {
         System.out.println("Actual rotation: " + rotationZ + " getLocalRotation: " + zRot);
     }
     else {
         System.err.println("Actual rotation: " + rotationZ + " getLocalRotation: " + zRot);
     }
    

    }

    @Override
    public void simpleRender(RenderManager rm) {
    //TODO: add render code
    }
    }
    [/java]

[java]
public Quaternion fromAngleNormalAxis(float angle, Vector3f axis) {
if (axis.x == 0 && axis.y == 0 && axis.z == 0) {
loadIdentity();
} else {
float halfAngle = 0.5f * angle;
float sin = FastMath.sin(halfAngle);
w = FastMath.cos(halfAngle);
x = sin * axis.x;
y = sin * axis.y;
z = sin * axis.z;
}
return this;
}[/java]

well the equation we use, seems to be very standard, so idk:

http://www.euclideanspace.com/maths/geometry/rotations/conversions/angleToQuaternion/index.htm
http://content.gpwiki.org/index.php/OpenGL:Tutorials:Using_Quaternions_to_represent_rotation#Quaternion_from_axis-angle

When I use getLocalRotation().toAngleAxis(new Vector3f(x, y, z)); it does work as it’s supposed to.

Here the testcase for that:

[java]
package mygame;

import com.jme3.app.SimpleApplication;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Box;

/**

  • test

  • @author Marco de Zeeuw
    */
    public class Main extends SimpleApplication {

    private Geometry geom;
    private float rotationZ = 0f;

    public static void main(String[] args) {
    Main app = new Main();
    app.start();
    }

    @Override
    public void simpleInitApp() {
    Box b = new Box(Vector3f.ZERO, .2f, 1, .2f);
    geom = new Geometry(“Box”, b);

     Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
     mat.setColor("Color", ColorRGBA.Blue);
     geom.setMaterial(mat);
    
     rootNode.attachChild(geom);
    

    }

    @Override
    public void simpleUpdate(float tpf) {
    rotationZ += .002;
    if(rotationZ >= 360) {
    rotationZ = 0;
    }
    geom.setLocalRotation(geom.getLocalRotation().fromAngleAxis(rotationZ * FastMath.DEG_TO_RAD, Vector3f.UNIT_Z));

     float[] rot = new float[3];
     rot[0] = geom.getLocalRotation().toAngleAxis(new Vector3f(1, 0, 0));
     rot[1] = geom.getLocalRotation().toAngleAxis(new Vector3f(0, 1, 0));
     rot[2] = geom.getLocalRotation().toAngleAxis(new Vector3f(0, 0, 1));
     
     float xRot = rot[0] * FastMath.RAD_TO_DEG;
     float yRot = rot[1] * FastMath.RAD_TO_DEG;
     float zRot = rot[2] * FastMath.RAD_TO_DEG;
     
     if(xRot == 180 && yRot == 180) {
         zRot = 180 - zRot;
     }
     else if(xRot == -180 && yRot == -180) {
         zRot = 180 - zRot;
     }
     else if(zRot < 0) {
         zRot += 360;
     }
     
     float difference = rotationZ - zRot;
     if(difference < 0) {
         difference = 0 - difference;
     }
     float allowedErrorValue = .001f;
     
     if(difference < allowedErrorValue) {
         System.out.println("Actual rotation: " + rotationZ + " getLocalRotation: " + zRot);
     }
     else {
         System.err.println("Actual rotation: " + rotationZ + " getLocalRotation: " + zRot);
     }
    

    }

    @Override
    public void simpleRender(RenderManager rm) {
    //TODO: add render code
    }
    }
    [/java]

1 Like

How is it supposed to work according to you? :slight_smile: Are you sure you have a full grasp on euler angles, gimbal lock etc? Its much easier to use the quaternions directly for rotation in combination with direction vectors as described here.

right ok I found the problem, and its to do with accuracy in the toAngles function. As per:

http://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToEuler/index.htm

Note2: The cutoff point for singulaties is set to 0.499 and -0.499 which corresponds to 86.3 degrees, this can be set closer to 90 if required but we have to accept some inaccuracy around the singulaties.

[java]

This patch file was generated by NetBeans IDE

It uses platform neutral UTF-8 encoding and \n newlines.

— Base (BASE)
+++ Locally Modified (Based On LOCAL)
@@ -299,11 +299,11 @@
float unit = sqx + sqy + sqz + sqw; // if normalized is one, otherwise
// is correction factor
float test = x * y + z * w;

  •    if (test > 0.499 * unit) { // singularity at north pole
    
  •    if (test > 0.49999 * unit) { // singularity at north pole
           angles[1] = 2 * FastMath.atan2(x, w);
           angles[2] = FastMath.HALF_PI;
           angles[0] = 0;
    
  •    } else if (test < -0.499 * unit) { // singularity at south pole
    
  •    } else if (test < -0.49999 * unit) { // singularity at south pole
           angles[1] = -2 * FastMath.atan2(x, w);
           angles[2] = -FastMath.HALF_PI;
           angles[0] = 0; [/java]
    

Using these values passes your test.

1 Like

…which is why going via the euler angles is not a good idea ^^

Well if I ask for the current z-rotation (in degrees) of an object (which I need to check if a player has been spinning it’s rods in the foosball-simulator I’m making) using getLocalRotation().toAngles(null)[2] * FastMath.RAD_TO_DEG I expect to get the right z-rotation (in degrees) at all times, and not always except when it’s between approx 87 and 93 and except when it’s between approx 267 and 273.

I just thought it was strange that it’s doing what it’s doing at this moment, which is why I posted it here… So it can be fixed in case it’s recognized as a bug (not only by me).

Whoops I see I’ve made a mistake in the title and in the explanation - anyway this doesn’t change that the testcase speaks for it’sself =)

The singularities are inherent with the method you apply. Theres only an approximation, not a solution. Like, mathematically, no opinions needed. If you stay within the quaternion realm you won’t get this issue.

…which is why going via the euler angles is not a good idea

Wouldn’t it be an idea to add that to the JavaDoc then? To avoid that people think they’re using the right method while they’re actually using an inaccurate method…

1 Like

You mean the implications of the euler angle transform itself? I guess it could be added to the javadoc but they are not really math training docs. As for using this instead of converting quaternions to angles and back… We can’t really cover all the ways users can possibly decide to do things the complicated way. The math tutorial I linked shows the easy way and its one of the first docs we link…

Well, it’s a valid point that the javadoc should include limitations of methods - particularly things like if getting the angles from a quaternion doesn’t always give the results you might expect.

1 Like
You mean the implications of the euler angle transform itself? I guess it could be added to the javadoc but they are not really math training docs. As for using this instead of converting quaternions to angles and back.. We can’t really cover all the ways users can possibly decide to do things the complicated way. The math tutorial I linked shows the easy way and its one of the first docs we link..

Nahh I mean that the method is not 100% accurate by adding something like “Use toAngleAxis(Vector3f axisStore) if you need an accurate result”. Or perhaps simply change the method to something like this:

[java]
public void toAngles(float angles) {
float returnValue = new float[3];
returnValue[0] = this.toAngleAxis(new Vector3f(1, 0, 0));
returnValue[1] = this.toAngleAxis(new Vector3f(0, 1, 0));
returnValue[2] = this.toAngleAxis(new Vector3f(0, 0, 1));
return returnValue;
[/java]

So it doesn’t lose accuracy.

Or am I totally telling rubbish now? As in this would only work if the corresponding object/quaternion hasn’t been rotated around certain axes yet?

Yes, I updated the javadoc to note that:
[java] /**
* toAngles returns this quaternion converted to Euler
* rotation angles (yaw,roll,pitch).

* Note that the result is not always 100% accurate due to the implications of euler angles.
* @see http://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToEuler/index.htm
*
* @param angles
* the float[] in which the angles should be stored, or null if
* you want a new float[] to be created
* @return the float[] in which the angles are stored.
*/
[/java]

3 Likes
Yes, I updated the javadoc to note that

Who’s the man? You the man! =)

Accuracy is a funny word here.

First, toAngleAxis() does not work like you think. You cannot pass a parameter that is the axis you want the rotation around. The passed vector is ONLY to store the result. A quaternion can be represented by an axis and an angle but the axis and the angle are 100% up to the quaternion.

So when you do this:
[java]
returnValue[0] = this.toAngleAxis(new Vector3f(1, 0, 0));
returnValue[1] = this.toAngleAxis(new Vector3f(0, 1, 0));
returnValue[2] = this.toAngleAxis(new Vector3f(0, 0, 1));
[/java]

The results should be the same for each of those… since the passed parameter’s values are ignored.

It’s also not that Quaternion is not accurate but that Euler angles aren’t a proper representation of an object’s rotation. A Quaternion represents a unique rotation in space but can be converted to an infinite number of Euler angle combinations. So some convention is developed that tries to keep yaw, pitch, and roll within some ideal range but it can’t be perfect. Near the “singularities” are places where the number of Euler angles that represent that Quaternion explodes and it becomes difficult to decide if it’s better to have a larger yaw or a relatively small pitch and roll.

For example, a 180 degree turn can be represented by yaw =180 or as pitch = 90, roll=180. Quaternion tries to detect this case, I think but if there is already pitch and roll in addition to yaw it can easily pick a different set of angles than they ones you might arbitrarily think it should.

If you only want to track a specific axis then track that axis and do all of your math on that axis and then apply it to the object as a Quaternion when you want the result. Don’t bother trying to extract that value out of the Quaternion again. The Euler angles you put into a Quaternion are definitely not guaranteed to come out again the same way. That’s not because Quaternion is inaccurate but because Euler angles are not a unique representation of rotation.

2 Likes

Hmm seems it’s a coïncidence then that it returned the exact value I needed =) They all return the exact same value indeed (I only need the Z-axis, which it coïncidentally seemed to return, even though it wasn’t actually the Z-axis).

I’m not saying Quaternion is inaccurate, I’m just saying that the method toAngles() is inaccurate (which is probably because of the Euler angle transformation, but that’s not something I (want to) know when I use a method, otherwise I probably wouldn’t have been able to use jME by now because I was still trying to figure out how each single method was working).

Here some return-values of the toAngles() method:
toAnglesX: 0.0 toAnglesY 0.0 toAnglesZ: 86.373825 actual Z: 86.37379 -> accurate
toAnglesX: 0.0 toAnglesY 0.0 toAnglesZ: 90.0 actual Z: 86.375786 -> inaccurate

As you can see, when it gets too close to 90 (and 270) the return value is saying it’s exactly at 90 (or 270) degrees while the X and Y axis still return the same value as when it was accurate.

Nevertheless, I agree that I should store the value I use to change the Quaternion, and use this for my other math. This is probably a lot faster/cheaper for the CPU and the value will always be the value I expect, because I use that to change the Quaternion as well. So thanks for that xD

right, in this case the only issue was because it is near the poles, where an approximation is made in toAngles, and after that it just snaps to 90. The patch I posted gives these improved results:

Before (Invalid for ~ 86.39 - 93.55 degrees (with 4.99)):
[java]Actual rotation: 86.38667 getLocalRotation: 90.0
Actual rotation: 86.553474 getLocalRotation: 90.0
Actual rotation: 86.71995 getLocalRotation: 90.0
Actual rotation: 86.88657 getLocalRotation: 90.0
Actual rotation: 87.05341 getLocalRotation: 90.0
Actual rotation: 87.21987 getLocalRotation: 90.0
Actual rotation: 87.38671 getLocalRotation: 90.0
Actual rotation: 87.553215 getLocalRotation: 90.0
Actual rotation: 87.71968 getLocalRotation: 90.0
Actual rotation: 87.886566 getLocalRotation: 90.0
Actual rotation: 88.053185 getLocalRotation: 90.0
Actual rotation: 88.219696 getLocalRotation: 90.0
Actual rotation: 88.38642 getLocalRotation: 90.0
Actual rotation: 88.55323 getLocalRotation: 90.0
Actual rotation: 88.71951 getLocalRotation: 90.0
Actual rotation: 88.88625 getLocalRotation: 90.0
Actual rotation: 89.05322 getLocalRotation: 90.0
Actual rotation: 89.21956 getLocalRotation: 90.0
Actual rotation: 89.38655 getLocalRotation: 90.0
Actual rotation: 89.55136 getLocalRotation: 90.0
Actual rotation: 89.71783 getLocalRotation: 90.0
Actual rotation: 89.886086 getLocalRotation: 90.0
Actual rotation: 90.05295 getLocalRotation: 90.0
Actual rotation: 90.21948 getLocalRotation: 90.0
Actual rotation: 90.386154 getLocalRotation: 90.0
Actual rotation: 90.55285 getLocalRotation: 90.0
Actual rotation: 90.71926 getLocalRotation: 90.0
Actual rotation: 90.885925 getLocalRotation: 90.0
Actual rotation: 91.05283 getLocalRotation: 90.0
Actual rotation: 91.219315 getLocalRotation: 90.0
Actual rotation: 91.38633 getLocalRotation: 90.0
Actual rotation: 91.5526 getLocalRotation: 90.0
Actual rotation: 91.71922 getLocalRotation: 90.0
Actual rotation: 91.88593 getLocalRotation: 90.0
Actual rotation: 92.052956 getLocalRotation: 90.0
Actual rotation: 92.217606 getLocalRotation: 90.0
Actual rotation: 92.38432 getLocalRotation: 90.0
Actual rotation: 92.552635 getLocalRotation: 90.0
Actual rotation: 92.719025 getLocalRotation: 90.0
Actual rotation: 92.88589 getLocalRotation: 90.0
Actual rotation: 93.052574 getLocalRotation: 90.0
Actual rotation: 93.21911 getLocalRotation: 90.0
Actual rotation: 93.38432 getLocalRotation: 90.0
Actual rotation: 93.55086 getLocalRotation: 90.0[/java]

After (Invalid for ~ 89.535 - 90.36 degrees (with 4.9999)):
[java]Actual rotation: 89.36958 getLocalRotation: 89.36974
Actual rotation: 89.53483 getLocalRotation: 89.53479
Actual rotation: 89.77631 getLocalRotation: 90.0
Actual rotation: 89.86569 getLocalRotation: 90.0
Actual rotation: 90.03214 getLocalRotation: 90.0
Actual rotation: 90.19847 getLocalRotation: 90.0
Actual rotation: 90.36553 getLocalRotation: 90.365845
Actual rotation: 90.531586 getLocalRotation: 90.53156[/java]

But it’s still not perfect. Would there be any side effects of making the “4.99” very small (within floating point limits). That almost completely solves the issue.

No “snapping” results, although there is still some slight deviations from the actual value (with 4.9999999)):
[java]Actual rotation: 89.31569 getLocalRotation: 89.31557
Actual rotation: 89.48157 getLocalRotation: 89.48186
Actual rotation: 89.64877 getLocalRotation: 89.648895
Actual rotation: 89.815605 getLocalRotation: 89.816536
Actual rotation: 89.98162 getLocalRotation: 89.98021
Actual rotation: 90.14866 getLocalRotation: 90.14804
Actual rotation: 90.315475 getLocalRotation: 90.3159
Actual rotation: 90.481476 getLocalRotation: 90.48133
Actual rotation: 90.648636 getLocalRotation: 90.64892
Actual rotation: 90.81583 getLocalRotation: 90.815895
Actual rotation: 90.98152 getLocalRotation: 90.98159
Actual rotation: 91.148766 getLocalRotation: 91.148766[/java]

There will always be an approximation, but I think we can improve on what we have currently at least.

1 Like