diff --git a/jme3-core/src/main/java/com/jme3/math/Quaternion.java b/jme3-core/src/main/java/com/jme3/math/Quaternion.java index 40936652c9..16faed48be 100644 --- a/jme3-core/src/main/java/com/jme3/math/Quaternion.java +++ b/jme3-core/src/main/java/com/jme3/math/Quaternion.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009-2020 jMonkeyEngine + * Copyright (c) 2009-2021 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -185,12 +185,13 @@ public Quaternion set(Quaternion q) { } /** - * Constructor instantiates a new Quaternion object from a - * collection of rotation angles. + * Instantiate a new Quaternion from Tait-Bryan angles, + * applying the rotations in x-z-y extrinsic order or y-z'-x" intrinsic + * order. * - * @param angles - * the angles of rotation (x, y, z) that will define the - * Quaternion. + * @param angles an array of Tait-Bryan angles (in radians, exactly 3 + * elements, the X angle in angles[0], the Y angle in angles[1], and the Z + * angle in angles[2], unaffected) */ public Quaternion(float[] angles) { fromAngles(angles); @@ -244,11 +245,13 @@ public boolean isIdentity() { } /** - * fromAngles builds a quaternion from the Euler rotation - * angles (x,y,z) aka (pitch, yaw, roll). + * Reconfigure this Quaternion based on Tait-Bryan angles, + * applying the rotations in x-z-y extrinsic order or y-z'-x" intrinsic + * order. * - * @param angles - * the Euler angles of rotation (in radians). + * @param angles an array of Tait-Bryan angles (in radians, exactly 3 + * elements, the X angle in angles[0], the Y angle in angles[1], and the Z + * angle in angles[2], unaffected) * @return this */ public Quaternion fromAngles(float[] angles) { @@ -261,22 +264,15 @@ public Quaternion fromAngles(float[] angles) { } /** - * fromAngles builds a Quaternion from the Euler rotation - * angles (x,y,z) aka (pitch, yaw, roll)). - * Note that we are applying in order: (y, x, z) aka (yaw, pitch, roll) - * but we've ordered them in x, y, and z for convenience. + * Reconfigure this Quaternion based on Tait-Bryan rotations, applying the + * rotations in x-z-y extrinsic order or y-z'-x" intrinsic order. * - * @see http://www.euclideanspace.com/maths/geometry/rotations/conversions/eulerToQuaternion/index.htm + * @see + * http://www.euclideanspace.com/maths/geometry/rotations/conversions/eulerToQuaternion/index.htm * - * @param xAngle - * the Euler pitch of rotation (in radians). (aka Attitude, often rot - * around x) - * @param yAngle - * the Euler yaw of rotation (in radians). (aka Heading, often - * rot around y) - * @param zAngle - * the Euler roll of rotation (in radians). (aka Bank, often - * rot around z) + * @param xAngle the X angle (in radians) + * @param yAngle the Y angle (in radians) + * @param zAngle the Z angle (in radians) * @return this */ public Quaternion fromAngles(float xAngle, float yAngle, float zAngle) { @@ -308,16 +304,20 @@ public Quaternion fromAngles(float xAngle, float yAngle, float zAngle) { } /** - * toAngles returns this quaternion converted to Euler rotation - * angles (x,y,z) aka (pitch, yaw, roll). + * Convert this Quaternion to Tait-Bryan angles, to be applied + * in x-z-y intrinsic order or y-z'-x" extrinsic order, for instance by + * {@link #fromAngles(float[])}. * - * 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 + * Note that the result is not always 100% accurate due to the implications + * of Tait-Bryan angles. * - * @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. + * @see + * http://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToEuler/index.htm + * + * @param angles an array of 3 floats in which to store the result, or else + * null (If null, a new array will be allocated.) + * @return an array of 3 angles (in radians, the X angle in angles[0], the Y + * angle in angles[1], and the Z angle in angles[2]) */ public float[] toAngles(float[] angles) { if (angles == null) { diff --git a/jme3-core/src/test/java/com/jme3/math/TestIssue1388.java b/jme3-core/src/test/java/com/jme3/math/TestIssue1388.java new file mode 100644 index 0000000000..23fe86168a --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/math/TestIssue1388.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2020-2021 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.math; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Verify the order in which Tait-Bryan angles are applied by the Quaternion + * class. This was issue #1388 at GitHub. + * + * @author Stephen Gold + */ +public class TestIssue1388 { + + @Test + public void testIssue1388() { + Vector3f in = new Vector3f(4f, 6f, 9f); // test vector, never modified + Vector3f saveIn = in.clone(); + /* + * Three arbitrary rotation angles between -PI/2 and +PI/2 + */ + final float xAngle = 1.23f; + final float yAngle = 0.765f; + final float zAngle = -0.456f; + float[] angles = new float[]{xAngle, yAngle, zAngle}; + float[] saveAngles = new float[]{xAngle, yAngle, zAngle}; + /* + * Part 1: verify that the extrinsic rotation order is x-z-y + * + * Apply extrinsic rotations to the "in" vector in x-z-y order. + */ + Quaternion qx = new Quaternion().fromAngleAxis(xAngle, Vector3f.UNIT_X); + Quaternion qy = new Quaternion().fromAngleAxis(yAngle, Vector3f.UNIT_Y); + Quaternion qz = new Quaternion().fromAngleAxis(zAngle, Vector3f.UNIT_Z); + Vector3f outXZY = qx.mult(in); + qz.mult(outXZY, outXZY); + qy.mult(outXZY, outXZY); + /* + * Construct a Quaternion using fromAngles(float, float, float), + * use it to rotate the "in" vector, and compare. + */ + Quaternion q1 = new Quaternion().fromAngles(xAngle, yAngle, zAngle); + Vector3f out1 = q1.mult(in); + assertEquals(outXZY, out1, 1e-5f); + /* + * Construct a Quaternion using fromAngles(float[]), + * use it to rotate the "in" vector, and compare. + */ + Quaternion q2 = new Quaternion().fromAngles(angles); + Vector3f out2 = q2.mult(in); + assertEquals(outXZY, out2, 1e-5f); + /* + * Construct a Quaternion using only the constructor, + * use it to rotate the "in" vector, and compare. + */ + Quaternion q3 = new Quaternion(angles); + Vector3f out3 = q3.mult(in); + assertEquals(outXZY, out3, 1e-5f); + /* + * Verify that fromAngles() reverses toAngles() for the chosen angles. + */ + float[] out4 = q1.toAngles(null); + assertEquals(angles, out4, 1e-5f); + float[] out5 = q2.toAngles(null); + assertEquals(angles, out5, 1e-5f); + float[] out6 = q3.toAngles(null); + assertEquals(angles, out6, 1e-5f); + /* + * Part 2: verify intrinsic rotation order + * + * Apply intrinsic rotations to the "in" vector in y-z'-x" order. + */ + Quaternion q4 = qy.mult(qz).mult(qx); + Vector3f out7 = q4.mult(in); + assertEquals(outXZY, out7, 1e-5f); + /* + * Verify that the values of "saveAngles" and "in" haven't changed. + */ + assertEquals(saveAngles, angles, 0f); + assertEquals(saveIn, in, 0f); + } + + private void assertEquals(float[] expected, float[] actual, + float tolerance) { + Assert.assertEquals(expected[0], actual[0], tolerance); + Assert.assertEquals(expected[1], actual[1], tolerance); + Assert.assertEquals(expected[2], actual[2], tolerance); + } + + private void assertEquals(Vector3f expected, Vector3f actual, + float tolerance) { + Assert.assertEquals(expected.x, actual.x, tolerance); + Assert.assertEquals(expected.y, actual.y, tolerance); + Assert.assertEquals(expected.z, actual.z, tolerance); + } +}