## Euler angles - current bugs + improved Euler Angle Class

Discussion regarding the Managed .Net Wrapper for Ogre, MOGRE (http://sourceforge.net/projects/mogre)

Moderators: OGRE Team, MOGRE Moderators

### Re: Euler angles - current bugs + improved Euler Angle Class

If you are a student of a (German) university, then you have a good chance to get many products of Microsoft for free. One of these is Visual Studio.
(Not only the seperated Express versions with reduced functionality.)
Help to add information to the wiki. Also tiny edits will let it grow ...
Add your country to your profile ... it's interesting to know from where of the world you are.
IRC chat ... Mogre: irc://freenode/#mogre ... Ogre: irc://freenode/#ogre3d

Beauty
OGRE Community Helper

Posts: 1629
Kudos: 36
Joined: 09 May 2007
Location: Germany

### Re: Euler angles - current bugs + improved Euler Angle Class

Today I continued my test.
Here are my improved test results.

Test conditions:
(As postet some days ago.)

My test uses the YPR convention, created by this code:
Code: Select all
``// calculate quaternion from yaw/pitch/roll by help of a SceneNodenode.Orientation = new Quaternion(new Degree(yaw), Vector3.UNIT_Y);node.Pitch(new Degree(pitch), Node.TransformSpace.TS_LOCAL);node.Roll(new Degree(roll), Node.TransformSpace.TS_LOCAL);quat = node.Orientation;``

If somebody knows how to calculate the the Quaternion without help of a SceneNode, please let me know.

To check the result of the Euler class I use this conversion steps:
Code: Select all
``EulerAngles --> Quaternion --> EulerClass --> EulerAngles``

Extended test results:

Everything works fine for these angles:
Code: Select all
``Yaw   range from  -180  to  180  Pitch range from   -89  to   89  Roll  range from  -180  to  180  ``

The output results are equal to the input results.

Note:
When an angle input is minus 180 and the output is plus 180
then the values are different (mathematically), but for Euler Angles usage the meaning is equal.
So my test code assume these cases as "is equal".

The output value range (co-domain) of Kojacks Euler class is:
Code: Select all
``Yaw    -180 .. 180 Pitch  -89.99 .. 89.99Roll   -180 .. 180 ``

For the case that Yaw input is in range -180 .. -90 or in range 90 .. 180,
the output is different to the input.
Reason: The Euler Class flips to an other axis.
Means: The output are large Roll values instead of large Pitch values.

Nevertheless the resulting Orientation should be equal to the input Orientation. (I didn't check that!!)
So the SceneNodes or clamped characters should have the same orientation behaviour.

NaN problem:

When the Pitch of the input angle is +/- 90 degree,
then the output of the Euler Angle class is bad.
The output of Pitch is NaN (not a number / undefined).

It doesn't matter which input values are used for Yaw and Roll.
There is always the NaN problem.
I suppose it's the Gimbal Lock case.

For this special case we should add a workaround.

Reason for the NaN problem:

......... somewhen later ........

The NaN value will be returned from Mogre.Matrix3.ToEulerAnglesYXZ()
which is used in this code:
Code: Select all
``/// <summary>Constructor which calculates the Euler Angles from a quaternion.</summary>public Euler(Quaternion oriantation){    Matrix3 rotMat;    rotMat = oriantation.ToRotationMatrix();    rotMat.ToEulerAnglesYXZ(out mYaw, out mPitch, out mRoll);  // NaN-PROBLEM    mChanged = true;    mCachedQuaternion = Quaternion.IDENTITY;}``

Then I looked to the related Mogre sources for Matrix3::ToEulerAnglesYXZ().
(The 1.6.5 sources, but I think in Mogre 1.7.x it's the same.)
Code: Select all
``bool Matrix3::ToEulerAnglesYXZ (Radian% rfYAngle, Radian% rfPAngle, Radian% rfRAngle){    // rot =  cy*cz+sx*sy*sz  cz*sx*sy-cy*sz  cx*sy    //        cx*sz           cx*cz          -sx    //       -cz*sy+cy*sx*sz  cy*cz*sx+sy*sz  cx*cy    rfPAngle = System::Math::Asin(-m12);    if ( rfPAngle < Radian(Math::HALF_PI) )    {        if ( rfPAngle > Radian(-Math::HALF_PI) )        {            rfYAngle = System::Math::Atan2(m02,m22);            rfRAngle = System::Math::Atan2(m10,m11);            return true;        }        else        {            // WARNING.  Not a unique solution.            Radian fRmY = System::Math::Atan2(-m01,m00);            rfRAngle = Radian(0.0);  // any angle works            rfYAngle = rfRAngle - fRmY;            return false;        }    }    else    {        // WARNING.  Not a unique solution.        Radian fRpY = System::Math::Atan2(-m01,m00);        rfRAngle = Radian(0.0);  // any angle works        rfYAngle = fRpY - rfRAngle;        return false;    }}``

The problem is the same as for the Quaternion.Yaw/Pitch/Roll problems.
Mogre uses System.Math for Asin() and Atan2() calculations.
As wrote somewhere above (or in an other topic) one of these functions (or both) can return NaN values for special cases. (mathematically correct)
So we have to use Mogre.Math instead of System.Math, because for usage with Ogre there are advanced math calculations to avoid problems like this.

By the way:
What means the percent symbol % at the method parameters?

The best solution would be to
• Figure out which math functions are "critical"
• Fix them
. . . . . Either by wrapping them to Ogre::Math
. . . . . Or by porting the related Ogre code and put it into Mogre (could be better for performance)
• Check all Mogre sources for "critical" System::Math calls
. . . . . Especially check members of Mogre.Math and Mogre.Quaternion
• Replace all "critical" calls of System::Math by Mogre::Math
• After all we would need to build (or even wrap?) Mogre again, before we can use it in practice

I could help to find all related calls in the source code.
There I can add a comment like "// CRITICAL CALL, because of ...".
The code I don't want to touch myself, because my C++ knowledge is very bad.

As quick workaround for the Euler class we could create a bugfixed method of
and add it into the Euler class.

Kojack,
can you tell us, which math functions are "critical"?
(Functions which can return NaN values and need special internal calculations)
Help to add information to the wiki. Also tiny edits will let it grow ...
Add your country to your profile ... it's interesting to know from where of the world you are.
IRC chat ... Mogre: irc://freenode/#mogre ... Ogre: irc://freenode/#ogre3d

Beauty
OGRE Community Helper

Posts: 1629
Kudos: 36
Joined: 09 May 2007
Location: Germany

### Re: Euler angles - current bugs + improved Euler Angle Class

Well, I found out that (at least) ASin() is critical:
Kojack wrote:getYaw does an asin operation. This MUST be given a value between -1 to 1. Outside of that range will cause a nan. Even slight floating point errors like 1.000001 will fail. Ogre provides Math::ASin, which protects asin from being called with bad values.

Interesting files for us:
OgreMath.cpp .................................. contains bullet-proof code
MogreMath.h and MogreMath.cpp ........ Mogre code

bullet-proof math methods

Now I found out, that MogreMath.cpp and by this Mogre still contains the bullet-proof code:

ASin()
Code: Select all
``Radian Math::ASin (Real fValue){    if ( -1.0 < fValue )    {        if ( fValue < 1.0 )            return Radian(System::Math::Asin(fValue));        else            return Radian(HALF_PI);    }    else    {        return Radian(-HALF_PI);    }}``

ACos()
Code: Select all
``Radian Math::ACos (Real fValue){    if ( -1.0 < fValue )    {        if ( fValue < 1.0 )            return Radian(acos(fValue));        else            return Radian(0.0);    }    else    {        return Radian(PI);    }}``

TODO

Just replace the System calls
Code: Select all
``System::Math::ASin()System::Math::ACos()``

by Mogre calls in the Mogre source files
Code: Select all
``Mogre::Math::ASin()Mogre::Math::ACos()``

Important:
Do it in a way (how?) that the Mogre autowrapper tool doesn't kill (overwrite) the modifications.
Help to add information to the wiki. Also tiny edits will let it grow ...
Add your country to your profile ... it's interesting to know from where of the world you are.
IRC chat ... Mogre: irc://freenode/#mogre ... Ogre: irc://freenode/#ogre3d

Beauty
OGRE Community Helper

Posts: 1629
Kudos: 36
Joined: 09 May 2007
Location: Germany

### Re: Euler angles - current bugs + improved Euler Angle Class

Ok, I checked all files.
Only these ones have to be updated:

Code: Select all
``MogreMath.cppMogreMatrix3.cppMogreQuaternion.cppMogreVector3.h``
Help to add information to the wiki. Also tiny edits will let it grow ...
Add your country to your profile ... it's interesting to know from where of the world you are.
IRC chat ... Mogre: irc://freenode/#mogre ... Ogre: irc://freenode/#ogre3d

Beauty
OGRE Community Helper

Posts: 1629
Kudos: 36
Joined: 09 May 2007
Location: Germany

### Re: Euler angles - current bugs + improved Euler Angle Class

The basic Mogre utility classes (vector, matrix, etc) do not wrap the Ogre equivalents for performance reasons if I remember correctly. They actually are custom C++/CLI classes included in Mogre itself. This wouldn't affect autowrapping as that tool doesn't overwrite these files.
DirtyHippy
Gnoblar

Posts: 14
Kudos: 5
Joined: 10 Apr 2008

### Re: Euler angles - current bugs + improved Euler Angle Class

Oh yes yes. Thanks for remembering me.
I forgot this fact when I looked to the code so deeply.

By the way - in the Wiki is an overview of all related classes.
The page MOGRE pure .NET classes ist linked directly from the Mogre wiki front page.

In this case the bugfix should be easy.
Help to add information to the wiki. Also tiny edits will let it grow ...
Add your country to your profile ... it's interesting to know from where of the world you are.
IRC chat ... Mogre: irc://freenode/#mogre ... Ogre: irc://freenode/#ogre3d

Beauty
OGRE Community Helper

Posts: 1629
Kudos: 36
Joined: 09 May 2007
Location: Germany

### Re: Euler angles - current bugs + improved Euler Angle Class

Ok, now I made the changes and committed the bugfix to the official repository.
https://bitbucket.org/mogre/mogre/changesets

Mercurial I used the first time.
I hope I did it in a correct way. (I used "distribute" instead of "patch")

My bugfix was applied to the main branch.
I couldn't find out, how to apply it to the TerrainAndPaging branch, too.

Well, nice to have the bugfix in the source code.
For the most people the bug will still exist until we have related binaries. (including updated MogreSDK)

But the first step is done now.
Help to add information to the wiki. Also tiny edits will let it grow ...
Add your country to your profile ... it's interesting to know from where of the world you are.
IRC chat ... Mogre: irc://freenode/#mogre ... Ogre: irc://freenode/#ogre3d

Beauty
OGRE Community Helper

Posts: 1629
Kudos: 36
Joined: 09 May 2007
Location: Germany

### Re: Euler angles - current bugs + improved Euler Angle Class

Now it's in the terrain&paging branch, too.

Perhaps there is a better way to do it (insead of 2 redundant commits).
Help to add information to the wiki. Also tiny edits will let it grow ...
Add your country to your profile ... it's interesting to know from where of the world you are.
IRC chat ... Mogre: irc://freenode/#mogre ... Ogre: irc://freenode/#ogre3d

Beauty
OGRE Community Helper

Posts: 1629
Kudos: 36
Joined: 09 May 2007
Location: Germany

### Re: Euler angles - current bugs + improved Euler Angle Class

Beauty wrote:Perhaps there is a better way to do it (insead of 2 redundant commits).

1) make your changes in the 'default' branch and commit:
Code: Select all
``hg update -C default//change fileshg commit -m "bugfix"``

2) switch to the 'TerrainAndPaging' branch:
Code: Select all
``hg update -C TerrainAndPaging``

3) merge with 'default':
Code: Select all
``hg merge default``

4) commit:
Code: Select all
``hg commit -m "merge with default"``
There was a SIGNATURE here. It's gone now.

For this message the author smiley80 has received kudos
smiley80
Gnoll

Posts: 672
Kudos: 47
Joined: 13 Nov 2008
Location: Germany

### Re: Euler angles - current bugs + improved Euler Angle Class

Kojack,
can you tell us, which math functions are "critical"?
(Functions which can return NaN values and need special internal calculations)

asin and acos are the ones. Slight floating point error can cause them to generate nans, because it's mathematically impossible to use them for certain values.

If somebody knows how to calculate the the Quaternion without help of a SceneNode, please let me know.

Code: Select all
``quat = new Quaternion(yaw, Vector3.UNIT_Y) * new Quaternion(pitch, Vector3.UNIT_X) * new Quaternion(roll, Vector3.UNIT_Z);``

For this message the author kojack has received kudos

kojack
Gnoblar

Posts: 17
Kudos: 2
Joined: 10 Feb 2006

### Re: Euler angles - current bugs + improved Euler Angle Class

Beauty wrote:Ok, now I made the changes and committed the bugfix to the official repository.
https://bitbucket.org/mogre/mogre/changesets
...
Well, nice to have the bugfix in the source code.
For the most people the bug will still exist until we have related binaries. (including updated MogreSDK)

I've just had a look at the source code that I used to create the binaries over in this topic. It appears that they have been built with the changes you made according to the changeset. For example, here's a bit of code from my Main/include/Custom/MogreVector3.h file:

Code: Select all
``      inline bool DirectionEquals(Vector3 rhs, Radian tolerance){   Real dot = DotProduct(rhs);   Radian angle = Mogre::Math::ACos(dot);   return Math::Abs(angle.ValueRadians) <= tolerance.ValueRadians;}``

Someone might like to download and test to see if the changes have fixed the bugs in those binaries.
Craftwork Games - hand crafted entertainment.
http://www.craftworkgames.com/

zarfius
Goblin

Posts: 234
Kudos: 24
Joined: 09 Jul 2008
Location: Brisbane, Australia

### Re: Euler angles - current bugs + improved Euler Angle Class

Today I wrote a workaround for the NaN problem.
So this Euler class can be used successfully, even with buggy Mogre binaries.

The new helper method Matrix3ToEulerAnglesYXZ() I tested extensively.
(For 24 million rotation states, the output is equal to the original method. NaN values will not be returned.)

In the Mogre sources the bugs are fixed now, but I suppose the unfixed Mogre binaries will be used for a longer time. (e.g. the unofficial Cygon build + the unmaintained MogreSDK)

Here is the modificated method of the Euler Class:
Code: Select all
``/// <summary>/// Constructor which calculates the Euler Angles from a quaternion./// </summary>public Euler(Quaternion oriantation){    Matrix3 rotMat;    rotMat = oriantation.ToRotationMatrix();            // BUGGY METHOD (NaN return in some cases)    // rotMat.ToEulerAnglesYXZ(out mYaw, out mPitch, out mRoll);    // WORKAROUND    Boolean success;    Matrix3ToEulerAnglesYXZ(rotMat, out mYaw, out mPitch, out mRoll, out success);    mChanged = true;    mCachedQuaternion = Quaternion.IDENTITY;}``

And here is the added method Matrix3ToEulerAnglesYXZ() as replacement for the buggy method Matrix3::ToEulerAnglesYXZ():

Code: Select all
``/// <summary>/// Port of method <c>Matrix3.ToEulerAnglesYXZ()</c>, from MogreMatrix3.cpp as a workaround for a bug in the Math class. /// (The bug was fixed, but is not present in common used binary files.)/// </summary>/// <param name="matrix">Rotation matrix</param>/// <param name="success">This COULD BE a flag for "success", but it's not documented in the original code.</param>public static void Matrix3ToEulerAnglesYXZ(Matrix3 matrix,    out Radian rfYAngle, out Radian rfPAngle, out Radian rfRAngle, out Boolean success){    rfPAngle = Mogre.Math.ASin(-matrix.m12);    if (rfPAngle < new Radian(Math.HALF_PI))    {        if (rfPAngle > new Radian(-Math.HALF_PI))        {            rfYAngle = Math.ATan2(matrix.m02, matrix.m22);            rfRAngle = Math.ATan2(matrix.m10, matrix.m11);            success = true;            return;        }        else        {            // WARNING.  Not a unique solution.            Radian fRmY = Math.ATan2(-matrix.m01, matrix.m00);            rfRAngle = new Radian(0f);  // any angle works            rfYAngle = rfRAngle - fRmY;            success = false;            return;        }    }    else    {        // WARNING.  Not a unique solution.        Radian fRpY = Math.ATan2(-matrix.m01, matrix.m00);        rfRAngle = new Radian(0f);  // any angle works        rfYAngle = fRpY - rfRAngle;        success = false;        return;    }    // "Original" CODE FROM CLASS Matrix3.ToEulerAnglesYXZ()    //rfPAngle = Mogre::Math::ASin(-m12);    //if ( rfPAngle < Radian(Math::HALF_PI) )    //{    //    if ( rfPAngle > Radian(-Math::HALF_PI) )    //    {    //        rfYAngle = System::Math::Atan2(m02,m22);    //        rfRAngle = System::Math::Atan2(m10,m11);    //        return true;    //    }    //    else    //    {    //        // WARNING.  Not a unique solution.    //        Radian fRmY = System::Math::Atan2(-m01,m00);    //        rfRAngle = Radian(0.0);  // any angle works    //        rfYAngle = rfRAngle - fRmY;    //        return false;    //    }    //}    //else    //{    //    // WARNING.  Not a unique solution.    //    Radian fRpY = System::Math::Atan2(-m01,m00);    //    rfRAngle = Radian(0.0);  // any angle works    //    rfYAngle = fRpY - rfRAngle;    //    return false;    //}} // Matrix3ToEulerAnglesYXZ()``
Help to add information to the wiki. Also tiny edits will let it grow ...
Add your country to your profile ... it's interesting to know from where of the world you are.
IRC chat ... Mogre: irc://freenode/#mogre ... Ogre: irc://freenode/#ogre3d

Beauty
OGRE Community Helper

Posts: 1629
Kudos: 36
Joined: 09 May 2007
Location: Germany

### Re: Euler angles - current bugs + improved Euler Angle Class

Partially wrong values in Gimbal Lock case.
(For the Euler Angle class it means: When Pitch is +/- 90 degree)

Now for several cases I get correct results.
But for several other cases I also get incorrect results.
Well, at least there are no more NaN values, which can cause crashes in further calculations.

Examples:
Code: Select all
``        Yaw Pitch RollBefore:  -095  090  -060After:    000  090   000Before:   175  090  -065After:    120  090   000Before:   165  090  -095After:    100  090   000``

So be distrustfully in the cases that Euler returns Pitch = +/- 90
In this case the represented pitch value is correct, but Yaw and Roll can be wrong.

All test results are correct for the case that the input value of Pitch is greater than -90 and smaller than +90 degree.
(This statement is related to my test converter chain, as I told above.)
For input pitch values out of the range, I can't say if the Euler class gets back the correct orientation from the quaternion.

Perhaps Kojack can test my current code with his Sinbad model test rotation application. To have a visual result for the input and output rotation.

The NaN problem is solved.

Here is the current state of my Euler Angle source code.
Code: Select all
``using System;using System.Collections.Generic;using System.Text;using System.Text.RegularExpressions;using Mogre;using Math = Mogre.Math;namespace MogreEulerAngles{// ABOUT THIS CODE:// // Developed by user Kojack in January 2012// Extended by Beauty and Tubulii//// Detailed information about usage:// http://www.ogre3d.org/tikiwiki/Euler+Angle+Class// // "official place" of C# version:// http://www.ogre3d.org/tikiwiki/Euler+Angle+Class+Mogre// // For questions and bug reports use this forum topic:// viewtopic.php?f=8&t=29262// /// <summary>/// This struct manages rotations by use of Euler Angles. /// The rotation will be applied in the order Yaw-Pitch-Roll, which are related to the axis order Y-X-Z. /// It's fully compatible with Ogre::Matrix3::FromEulerAnglesYXZ and Ogre::Matrix3::ToEulerAnglesYXZ. /// For the Yaw angle the standard anticlockwise right handed rotation is used (as common in Ogre)./// The Yaw-Pitch-Roll ordering is most convenient for upright character controllers and cameras./// </summary>public struct Euler{    /// <summary>Get or set the Yaw angle.</summary>    public Radian Yaw    {        get { return mYaw; }        set        {            mYaw = value;            mChanged = true;        }    }    /// <summary>Get or set the Pitch angle.</summary>    public Radian Pitch    {        get { return mPitch; }        set        {            mPitch = value;            mChanged = true;        }    }    /// <summary>Get or set the Roll angle.</summary>    public Radian Roll    {        get { return mRoll; }        set        {            mRoll = value;            mChanged = true;        }    }     /// <summary>    /// Constructor to create a new Euler Angle struct from angle values.     /// The rotation will be applied in the order Yaw-Pitch- Roll, which are related to the axis order Y-X-Z.     /// </summary>    public Euler(Radian yaw, Radian pitch, Radian roll)    {        mYaw = yaw;        mPitch = pitch;        mRoll = roll;        mChanged = true;        mCachedQuaternion = Quaternion.IDENTITY;    }     /// <summary>    /// Constructor which calculates the Euler Angles from a quaternion.    /// </summary>    public Euler(Quaternion oriantation)    {        Matrix3 rotMat;        rotMat = oriantation.ToRotationMatrix();                // BUGGY METHOD (NaN return in some cases)        // rotMat.ToEulerAnglesYXZ(out mYaw, out mPitch, out mRoll);        // WORKAROUND        Boolean success;        Matrix3ToEulerAnglesYXZ(rotMat, out mYaw, out mPitch, out mRoll, out success);        mChanged = true;        mCachedQuaternion = Quaternion.IDENTITY;    }        /// <summary>    /// Apply a relative yaw. (Adds the angle to the current yaw value)    /// </summary>    /// <param name="yaw">Yaw value as radian</param>    public void AddYaw(Radian yaw)    {        mYaw += yaw;        mChanged = true;    }     /// <summary>    /// Apply a relative pitch. (Adds the angle to the current pitch value)    /// </summary>    /// <param name="pitch">Pitch value as radian</param>    public void AddPitch(Radian pitch)    {        mPitch += pitch;        mChanged = true;    }     /// <summary>    /// Apply a relative roll. (Adds the angle to the current roll value)    /// </summary>    /// <param name="roll">Roll value as radian</param>    public void AddRoll(Radian roll)    {        mRoll += roll;        mChanged = true;    }    /// <summary>    /// Apply a relative yaw. (Adds the angle to the current yaw value)    /// </summary>    /// <param name="yaw">Yaw value as degree</param>    public void AddYaw(Degree yaw)    {        mYaw += yaw;        mChanged = true;    }    /// <summary>    /// Apply a relative pitch. (Adds the angle to the current pitch value)    /// </summary>    /// <param name="pitch">Pitch value as degree</param>    public void AddPitch(Degree pitch)    {        mPitch += pitch;        mChanged = true;    }    /// <summary>    /// Apply a relative roll. (Adds the angle to the current roll value)    /// </summary>    /// <param name="roll">Roll value as degree</param>    public void AddRoll(Degree roll)    {        mRoll += roll;        mChanged = true;    }    /// <summary>Get a vector pointing forwards. </summary>    public Vector3 Forward    {        get { return ToQuaternion() * Vector3.NEGATIVE_UNIT_Z; }    }    /// <summary>Get a vector pointing to the right.</summary>   public Vector3 Right   {        get { return ToQuaternion() * Vector3.UNIT_X; }   }    /// <summary> Get a vector pointing up.</summary>   public Vector3 Up    {        get { return ToQuaternion() * Vector3.UNIT_Y; }   }             /// <summary>    /// Calculate the quaternion of the euler object.     /// The result is cached. It will only be recalculated when the component euler angles are changed.    /// </summary>   public Quaternion ToQuaternion()    {      if(mChanged)       {         mCachedQuaternion = new Quaternion(mYaw, Vector3.UNIT_Y) * new Quaternion(mPitch, Vector3.UNIT_X) * new Quaternion(mRoll, Vector3.UNIT_Z);          mChanged = false;      }      return mCachedQuaternion;   }     /// <summary>    /// Return a String with degree values of the axis rotations (human readable style).     /// For example "X-axis: 0°   Y-axis: 36°   Z-axis: 90°"    /// </summary>    public String ToAxisString()    {        return String.Format("X: {0:00}°   Y: {1:00}°   Z: {2:00}°",             Pitch.ValueDegrees, Yaw.ValueDegrees, Roll.ValueDegrees);    }    /// <summary>    /// Return a String with degree values in the applied rotation order (human readable style).    /// For example "Yaw: 0°   Pitch: 36°   Roll: 90°"    /// </summary>    public String ToYawPitchRollString()    {        return String.Format("Yaw: {0:00}°   Pitch: {1:00}°   Roll: {2:00}°",             Yaw.ValueDegrees, Pitch.ValueDegrees, Roll.ValueDegrees);    }    /// <summary>    /// Try to parse 3 floating point values from a string, which are seperated by spaces or tabulators.     /// Input order for rotation values:: Yaw, Pitch, Roll (all in degree)    /// If success, an Euler struct will be returned.     /// If parsing failed, a FormatException will be thrown.     /// Example: "111  .99   -66"     /// </summary>    /// <param name="valueString">String which contains 3 floating point values</param>    /// <remarks>    /// Multiple and mixed usage of space/tabulator/commas are possible.     /// As decimal seperator a dot "." is expected.     /// </remarks>    /// <returns>Returns an Euler struct or a FormatException</returns>    public static Euler ParseStringYawPitchRoll(String valueString)    {        Single[] values = Parse3Params(valueString);        if (values == null)            throw new FormatException(String.Format("Can't parse floating point values of string '{0}'", valueString));        return new Euler(new Degree(values[0]), new Degree(values[1]), new Degree(values[2]));    }    /// <summary>    /// Try to parse 3 floating point values from a string, which are seperated by spaces or tabulators or comma.     /// Input order for rotation values: X-axis, Y-axis, Z-axis (all in degree)    /// If success, an Euler struct will be returned.     /// If parsing failed, a FormatException will be thrown.     /// Example: "111  .99   -66"     /// </summary>    /// <param name="valueString">String which contains 3 floating point values</param>    /// <remarks>    /// Multiple and mixed usage of space/tabulator/commas are possible.     /// As decimal seperator a dot "." is expected.     /// </remarks>    /// <returns>Returns an Euler struct or a FormatException</returns>    public static Euler ParseStringAxisXYZ(String valueString)    {        Single[] values = Parse3Params(valueString);        if (values == null)            throw new FormatException(String.Format("Can't parse floating point values of string '{0}'", valueString));        return new Euler(new Degree(values[1]), new Degree(values[0]), new Degree(values[2]));    }    /// <summary>    /// Try to parse 3 floating point values from a string, which are seperated by spaces or tabulators or comma.     /// If parsing failed, null will be returned.     /// Example: "111  .99   -66"     /// </summary>    /// <param name="valueString">String which contains 3 floating point values</param>    /// <remarks>    /// Multiple and mixed usage of space/tabulator/commas are possible.     /// As decimal seperator a dot "." is expected.     /// </remarks>    /// <returns>Returns 3 Single values or null</returns>    private static Single[] Parse3Params(String valueString)    {        // Some Regex explanation:        //        // The "@" prefix in front of the String means:         //         Backslash are processed as Text instead of special symbols.        //         Advantage: Just write "\" instead of "\\" for each backslash        //         // "^" at first position means:  No text is allowed before        // "\$" at the end means:         No text is allowed after that        //         // Floating point values are matched        //         Expression: "-?\d*\.?\d+"        //         Examples:   "111",  "0.111",  ".99",  "-66"        //        // Seperator can be tabs or spaces or commas (at least one symbol; mixing is possible)        //         Expression: "[, \t]+"        String val = @"[-\d\.]+";     // simplified (faster) floating point pattern (exact pattern would be @"-?\d*\.?\d+" )        String sep = @"[, \t]+";      // seperator pattern        // build target pattern        String searchPattern = "^(" + val + ")" + sep + "(" + val + ")" + sep + "(" + val + ")\$";        Match match = Regex.Match(valueString, searchPattern);        try        {            if (match.Success)            {                // Force to parse "." as decimal char.  (Can be different with other culture settings. E.g. German culture expect "," instad of ".")                System.Globalization.CultureInfo englishCulture = new System.Globalization.CultureInfo("en-US");                 Single[] result = new Single[3];                result[0] = Convert.ToSingle(match.Groups[1].Value, englishCulture);                result[1] = Convert.ToSingle(match.Groups[2].Value, englishCulture);                result[2] = Convert.ToSingle(match.Groups[3].Value, englishCulture);                return result;            }            else                 return null;        }        catch (FormatException) { return null; }        catch (OverflowException) { return null; }    } // Parse3Params()    /// <summary>    /// Return the Euler rotation state as quaternion.    /// </summary>    /// <param name="e">Euler Angle state</param>    /// <returns>Rotation state as Quaternion</returns>    public static implicit operator Quaternion(Euler e)    {        return e.ToQuaternion();    }     /// <summary>    /// Set the yaw and pitch to face in the given direction.     /// The direction doesn't need to be normalised.     /// Roll is always unaffected.    /// </summary>    /// <param name="directionVector">Vector which points to the wanted direction</param>    /// <param name="setYaw">if false, the yaw isn't changed.</param>    /// <param name="setPitch">if false, the pitch isn't changed.</param>   public void SetDirection(Vector3 directionVector, Boolean setYaw, Boolean setPitch)   {      Vector3 d = directionVector.NormalisedCopy;      if(setPitch)         mPitch = Math.ASin(d.y);      if(setYaw)         mYaw = Math.ATan2(-d.x, -d.z);//+Math.PI/2.0;      mChanged = setYaw || setPitch;   }     /// <summary>    /// Normalise the selected rotations to be within the +/-180 degree range.     /// The normalise uses a wrap around, so for example a yaw of 360 degrees becomes 0 degrees,     /// and -190 degrees becomes 170.     /// By the parameters it's possible to choose which angles should be normalised.    /// </summary>    /// <param name="normYaw">If true, the angle will be normalised.</param>    /// <param name="normPitch">If true, the angle will be normalised.</param>    /// <param name="normRoll">If true, the angle will be normalised.</param>    /// <remarks></remarks>    public void Normalise(Boolean normYaw, Boolean normPitch, Boolean normRoll)   {      if(normYaw)      {         Single yaw = mYaw.ValueRadians;         if(yaw < -Math.PI)         {            yaw = (Single)System.Math.IEEERemainder(yaw, Math.PI * 2.0);            if(yaw < -Math.PI)            {               yaw += Math.PI * 2.0f;            }            mYaw = yaw;            mChanged = true;         }         else if(yaw > Math.PI)         {                yaw = (Single)System.Math.IEEERemainder(yaw, Math.PI * 2.0f);            if(yaw > Math.PI)            {               yaw -= Math.PI * 2.0f;            }            mYaw = yaw;            mChanged = true;         }      }      if(normPitch)      {            Single pitch = mPitch.ValueRadians;         if(pitch < -Math.PI)         {                pitch = (Single)System.Math.IEEERemainder(pitch, Math.PI * 2.0f);            if(pitch < -Math.PI)            {               pitch += Math.PI * 2.0f;            }            mPitch = pitch;            mChanged = true;                            if (Single.IsNaN(mPitch.ValueDegrees)) // DEBUGGING                {                 }  // add breakpoint here            }         else if(pitch > Math.PI)         {                pitch = (Single)System.Math.IEEERemainder(pitch, Math.PI * 2.0f);            if(pitch > Math.PI)            {               pitch -= Math.PI * 2.0f;            }            mPitch = pitch;            mChanged = true;                if (Single.IsNaN(mPitch.ValueDegrees)) // DEBUGGING                {                 }  // add breakpoint here         }      }      if(normRoll)      {         Single roll= mRoll.ValueRadians;         if(roll < -Math.PI)         {                roll = (Single)System.Math.IEEERemainder(roll, Math.PI * 2.0f);            if(roll < -Math.PI)            {               roll += Math.PI * 2.0f;            }            mRoll = roll;            mChanged = true;         }         else if(roll > Math.PI)         {                roll = (Single)System.Math.IEEERemainder(roll, Math.PI * 2.0f);            if(roll > Math.PI)            {               roll -= Math.PI * 2.0f;            }            mRoll = roll;            mChanged = true;         }      }   } // Normalise()     /// <summary>    /// Return the relative euler angles required to rotate from the current forward direction to the specified direction vector.     /// The result euler can then be added to the current euler to immediately face dir.     /// Rotation is found to face the correct direction. For example, when false a yaw of 1000 degrees and a dir of    /// (0,0,-1) will return a -1000 degree yaw. When true, the same yaw and dir would give 80 degrees (1080 degrees faces    /// the same way as (0,0,-1).    /// The rotation won't flip upside down then roll instead of a 180 degree yaw.    /// </summary>    /// <param name="direction">...TODO...</param>    /// <param name="shortest">If false, the full value of each angle is used. If true, the angles are normalised and the shortest rotation is found to face the correct direction.</param>    /// <param name="setYaw">If true the angles are calculated. If false, the angle is set to 0. </param>    /// <param name="setPitch">If true the angles are calculated. If false, the angle is set to 0. </param>    public Euler GetRotationTo(Vector3 direction, Boolean setYaw, Boolean setPitch, Boolean shortest)   {      Euler t1 = Euler.IDENTITY;      Euler t2;      t1.SetDirection(direction, setYaw, setPitch);      t2 = t1 - this;      if(shortest && setYaw)      {         t2.Normalise(true, true, true);      }      return t2;   }     /// <summary>    /// Clamp the yaw angle to a range of +/-limit.    /// </summary>    /// <param name="limit">Wanted co-domain for the Yaw angle.</param>    public void LimitYaw(Radian limit)    {        if (mYaw > limit)        {            mYaw = limit;            mChanged = true;        }        else if (mYaw < -limit)        {            mYaw = -limit;            mChanged = true;        }    }     /// <summary>    /// Clamp the pitch angle to a range of +/-limit.    /// </summary>    /// <param name="limit">Wanted co-domain for the Pitch angle.</param>    public void LimitPitch(Radian limit)    {        if (mPitch > limit)        {            mPitch = limit;            mChanged = true;        }        else if (mPitch < -limit)        {            mPitch = -limit;            mChanged = true;        }    }     /// <summary>    /// Clamp the roll angle to a range of +/-limit.    /// </summary>    /// <param name="limit">Wanted co-domain for the Roll angle.</param>    public void LimitRoll(Radian limit)    {        if (mRoll > limit)        {            mRoll = limit;            mChanged = true;        }        else if (mRoll < -limit)        {            mRoll = -limit;            mChanged = true;        }    }    /// <summary>    /// Port of method <c>Matrix3.ToEulerAnglesYXZ()</c>, from MogreMatrix3.cpp as a workaround for a bug in the Math class.     /// (The bug was fixed, but is not present in common used binary files.)    /// </summary>    /// <param name="matrix">Rotation matrix</param>    /// <param name="success">This COULD BE a flag for "success", but it's not documented in the original code.</param>    public static void Matrix3ToEulerAnglesYXZ(Matrix3 matrix,        out Radian rfYAngle, out Radian rfPAngle, out Radian rfRAngle, out Boolean success)    {        rfPAngle = Mogre.Math.ASin(-matrix.m12);        if (rfPAngle < new Radian(Math.HALF_PI))        {            if (rfPAngle > new Radian(-Math.HALF_PI))            {                rfYAngle = Math.ATan2(matrix.m02, matrix.m22);                rfRAngle = Math.ATan2(matrix.m10, matrix.m11);                success = true;                return;            }            else            {                // WARNING.  Not a unique solution.                Radian fRmY = Math.ATan2(-matrix.m01, matrix.m00);                rfRAngle = new Radian(0f);  // any angle works                rfYAngle = rfRAngle - fRmY;                success = false;                return;            }        }        else        {            // WARNING.  Not a unique solution.            Radian fRpY = Math.ATan2(-matrix.m01, matrix.m00);            rfRAngle = new Radian(0f);  // any angle works            rfYAngle = fRpY - rfRAngle;            success = false;            return;        }        // "Original" CODE FROM CLASS Matrix3.ToEulerAnglesYXZ()        //rfPAngle = Mogre::Math::ASin(-m12);        //if ( rfPAngle < Radian(Math::HALF_PI) )        //{        //    if ( rfPAngle > Radian(-Math::HALF_PI) )        //    {        //        rfYAngle = System::Math::Atan2(m02,m22);        //        rfRAngle = System::Math::Atan2(m10,m11);        //        return true;        //    }        //    else        //    {        //        // WARNING.  Not a unique solution.        //        Radian fRmY = System::Math::Atan2(-m01,m00);        //        rfRAngle = Radian(0.0);  // any angle works        //        rfYAngle = rfRAngle - fRmY;        //        return false;        //    }        //}        //else        //{        //    // WARNING.  Not a unique solution.        //    Radian fRpY = System::Math::Atan2(-m01,m00);        //    rfRAngle = Radian(0.0);  // any angle works        //    rfYAngle = fRpY - rfRAngle;        //    return false;        //}    } // Matrix3ToEulerAnglesYXZ()    /// <summary>    /// Add two euler objects.    /// </summary>    /// <returns>Calculation result</returns>    public static Euler operator +(Euler lhs, Euler rhs)    {        return new Euler(lhs.Yaw + rhs.Yaw, lhs.Pitch + rhs.Pitch, lhs.Roll + rhs.Roll);    }     /// <summary>    /// Subtract two euler objects. This finds the difference as relative angles.    /// </summary>    /// <returns>Calculation result</returns>   public static Euler operator-(Euler lhs, Euler rhs)   {        return new Euler(lhs.Yaw - rhs.Yaw, lhs.Pitch - rhs.Pitch, lhs.Roll - rhs.Roll);   }     /// <summary>    /// Interpolate each euler angle by the given factor.     /// (Each angle will be multiplied with the factor.)    /// </summary>    /// <returns>Calculation result</returns>    public static Euler operator *(Euler lhs, Single factor)    {        return new Euler(lhs.Yaw * factor, lhs.Pitch * factor, lhs.Roll * factor);    }     /// <summary>    /// Interpolate the euler angles by lhs.     /// (Each angle will be multiplied with the factor.)    /// </summary>    /// <returns>Calculation result</returns>    public static Euler operator *(Single factor, Euler rhs)    {        return new Euler(factor * rhs.Yaw, factor * rhs.Pitch, factor * rhs.Roll);    }     /// <summary>    /// Apply the euler rotation to the vector rhs.     /// The calculation is equal to: quaternion*vector    /// </summary>    /// <returns>Calculation result</returns>    public static Vector3 operator *(Euler lhs, Vector3 rhs)    {        return lhs.ToQuaternion() * rhs;    }     /// <summary>Base settings (all angles are 0)</summary>    public static Euler IDENTITY = new Euler(new Radian(0), new Radian(0), new Radian(0));     /// <summary>Rotation around the Y axis.</summary>    private Radian mYaw;    /// <summary>Rotation around the X axis.</summary>    private Radian mPitch;    /// <summary>Rotation around the Z axis.</summary>    private Radian mRoll;    /// <summary>Is the cached quaternion out of date?</summary>    private bool mChanged;    /// <summary>Cached quaternion equivalent of this euler object.</summary>    private Quaternion mCachedQuaternion;  }  // struct Euler``

Here is my latest testing code:

Code: Select all
``public class EulerTesting{    /// <summary>    /// To check the result of the Euler class this method uses these conversion steps:    /// EulerAngles --> Quaternion --> EulerClass --> EulerAngles    /// Then the input angles and output angles are compared.    /// </summary>    public static void Test()     {        //-- TEST PARAMETERS --        Boolean printDivergences = false;  // print all problematic angle combinations to console                                            // Note: Will need much more process time!!        Boolean printProblemsNAN = false;  // print angles which returns an NaN value                                           // Note: Will also print if "printProblems" is false        Boolean printProblems = false;      // print angles, which causes a different quaterion result than input angles        Single stepSize = 10; // in degree        Boolean customTestRange = false;  // in normal case Pitch greater than -90 and lower than +90        Single yawStart, yawEnd, pitchStart, pitchEnd, rollStart, rollEnd;        if (customTestRange == false)        {            // "normal" test range            yawStart   = -180;            yawEnd     =  180;            pitchStart = -90 + stepSize;            pitchEnd   =  90 - stepSize;            rollStart  = -180;            rollEnd    =  180;        }        else        {            // custom test range            yawStart   = -180;            yawEnd     =  180;            pitchStart = -180;            pitchEnd   = -180;            rollStart  = -180;            rollEnd    =  180;        }        //--------------        Single yaw = 0;        Single pitch = 0;        Single roll = 0;        Single ymin = 0;        Single ymax = 0;        Single pmin = 0;        Single pmax = 0;        Single rmin = 0;        Single rmax = 0;        Single yy, pp, rr;  // new values        Int32 equalCounter = 0;        Int32 differentCounter = 0;        Int32 problemCounter = 0;        Quaternion quat;        Console.WriteLine("Start calculation ...\n");        for (yaw = yawStart;   yaw <= yawEnd;   yaw += stepSize)        {            for (pitch = pitchStart;   pitch <= pitchEnd;   pitch += stepSize)            {                for (roll = rollStart;   roll <= rollEnd;   roll += stepSize)                {                    //-- calculate quaternion from yaw/pitch/roll --                    quat = new Quaternion(yaw, Vector3.UNIT_Y) * new Quaternion(pitch, Vector3.UNIT_X) * new Quaternion(roll, Vector3.UNIT_Z);                    //-- convert quaternion back to y/p/r by Kojacks Euler Angle Class --                    Euler euler = new Euler(quat);                    yy = euler.Yaw.ValueDegrees;                    pp = euler.Pitch.ValueDegrees;                    rr = euler.Roll.ValueDegrees;                    //-- check co-domain --                    if (yy < ymin)                        ymin = yy;                    if (yy > ymax)                        ymax = yy;                    if (pp < pmin)                        pmin = pp;                    if (pp > pmax)                        pmax = pp;                    if (rr < rmin)                        rmin = rr;                    if (rr > rmax)                        rmax = rr;                    //-- compare Euler values before/after  --                    Boolean isDifferent = (!IsEqual(yy, yaw) || !IsEqual(pp, pitch) || !IsEqual(rr, roll));                    Boolean isProblem = false;                    if (isDifferent)                    {                        differentCounter++;                        if (IsEqualQuaternion(yaw, pitch, roll, yy, pp, rr) == false)                        {                            problemCounter++;                            isProblem = true;                        }                    }                    else                        equalCounter++;                    // NaN test                    Boolean hasNAN = false;                    if (isDifferent && (Single.IsNaN(yy) || Single.IsNaN(pp) || Single.IsNaN(rr)))                        hasNAN = true;                    // debug print (different or NaN)                    if (isDifferent &&                        (printDivergences || (printProblemsNAN && hasNAN)))                    {                        Console.WriteLine(String.Format(                            "Yaw Pitch Roll:  {0:000}  {1:000}  {2:000}\n" +                            "            -->  {3:000}  {4:000}  {5:000}\n",                            yaw, pitch, roll,                            yy, pp, rr                            ));                    }                    // debug print (rotation problem)                    if (isProblem && printProblems)                    {                        Console.WriteLine(String.Format(                            "Yaw Pitch Roll:  {0:000}  {1:000}  {2:000}\n" +                            "            -->  {3:000}  {4:000}  {5:000}\n",                            yaw, pitch, roll,                            yy, pp, rr                            ));                    }                                        //if (isDifferent)                    //    Console.WriteLine("                                -->  DIFFERENT !!");                    //Vector3 yprNew = EulerAngles.GetAsDegrees_3(quat);                    //Single yawOg  = EulerAngles.GetYawAngle(ref pubVar, ref quat, Mogre.Math.AngleUnit.AU_DEGREE, EulerAngles.YawConvention.Ogre);                    //Single yawNED = EulerAngles.GetYawAngle(ref pubVar, ref quat, Mogre.Math.AngleUnit.AU_DEGREE, EulerAngles.YawConvention.NED);                } // for roll            } // for pitch            //Console.Write(".");            if ((yaw % 10) == 0)                Console.WriteLine("Current calculation state in yaw loop: " + yaw); // show current state of calculation            if (equalCounter % 10000 == 0)                System.Windows.Forms.Application.DoEvents();        } // for yaw        Console.WriteLine("\n\nCalculation ready.\n");        Console.WriteLine("==================================\n\n");        Console.WriteLine(String.Format(            "INPUT TEST CONDITIONS:  \n\n" +             "Yaw   range from  {0}  to  {1}  \n" +            "Pitch range from  {2}  to  {3}  \n" +            "Roll  range from  {4}  to  {5}  \n" +            "Step size:  {6} \n\n",            yawStart, yawEnd, pitchStart, pitchEnd, rollStart, rollEnd, stepSize            ));        Console.WriteLine("RESULTS: \n");        Console.Write("co-domains:\n");        Console.Write(String.Format("Yaw    {0} .. {1} \n", ymin, ymax));        Console.Write(String.Format("Pitch  {0} .. {1} \n", pmin, pmax));        Console.Write(String.Format("Roll   {0} .. {1} \n", rmin, rmax));        Console.WriteLine();        Console.WriteLine("Equality counter:    " + equalCounter.ToString());        Console.WriteLine("Differeces counter:  " + differentCounter.ToString() + "    (Other angle values, but same resulting orientation)");        Console.WriteLine("Problem counter:     " + problemCounter.ToString() + "    (Unknown test result - possibly wrong)");        Console.ReadLine();    }    /// <summary>check if equal Euler angle values</summary>    private static Boolean IsEqual(Single val1, Single val2)    {        Single test = val1 - val2;                // make positive        if (test < 0)            test *= 1;        if (test < 0.05f)            return true;        // special case where  val1 == 180   and   val2 == -180  (second is negative)        if ((test > 359.9) && (test < 360.1))            return true;        return false;    }    /// <summary>Simple check, based on quaternion calculation.</summary>    private static Boolean IsEqualQuaternion(Degree yaw1, Degree pitch1, Degree roll1, Degree yaw2, Degree pitch2, Degree roll2)    {        Single pBak = pitch1.ValueDegrees;        Quaternion q1 = new Quaternion(yaw1, Vector3.UNIT_Y)                        * new Quaternion(pitch1, Vector3.UNIT_X)                        * new Quaternion(roll1, Vector3.UNIT_Z);        Quaternion q2 = new Quaternion(yaw2, Vector3.UNIT_Y)                        * new Quaternion(pitch2, Vector3.UNIT_X)                        * new Quaternion(roll2, Vector3.UNIT_Z);        Boolean possiblyWrong = false;        // compare        if (System.Math.Abs(q1.w - q2.w) > 0.1f)            possiblyWrong = true;        if (System.Math.Abs(q1.x - q2.x) > 0.1f)            possiblyWrong = true;        if (System.Math.Abs(q1.y - q2.y) > 0.1f)            possiblyWrong = true;        if (System.Math.Abs(q1.z - q2.z) > 0.1f)            possiblyWrong = true;        if (possiblyWrong)        {            // Gimbal Lock case            if (System.Math.Abs(pitch1.ValueDegrees) == 90f &&                System.Math.Abs(pitch2.ValueDegrees) == 90f)            {                Single diff1 = System.Math.Abs(yaw1.ValueDegrees + roll1.ValueDegrees);                Single diff2 = System.Math.Abs(yaw2.ValueDegrees + roll2.ValueDegrees);                Single diff11 = System.Math.Abs(yaw1.ValueDegrees - roll1.ValueDegrees);                Single diff22 = System.Math.Abs(yaw2.ValueDegrees - roll2.ValueDegrees);                if (System.Math.Abs(diff1 - diff2) < 0.1f  ||                    (System.Math.Abs(diff11 - diff22) < 0.1f))                    return true;                else                    return false;            }            // ... TODO: further checks (compare quaternion divergence) ...            return false;        }                return true;    }    //private Single AngleBetweenQuaternions(Quaternion q1, Quaternion q2)    //{        //// compare rotation divergence - code from        //// http://www.ogre3d.org/forums/viewtopic.php?t=44704        //Single lenProduct = q1.XAxis.Length * q2.XAxis.Length;;        //// Divide by zero check        //if(lenProduct < 1e-6f)        //lenProduct = 1e-6f;        //Single f = dotProduct(dest) / lenProduct;  // ??        //f = Math::Clamp(f, (Real)-1.0, (Real)1.0);        //return Math::ACos(f);                            // ORIGINAL        //Real lenProduct = length() * dest.length();        //// Divide by zero check        //if(lenProduct < 1e-6f)        //   lenProduct = 1e-6f;        //Real f = dotProduct(dest) / lenProduct;        //f = Math::Clamp(f, (Real)-1.0, (Real)1.0);        //return Math::ACos(f);    //}} // Euler struct} // namespace``

Now I want to stop my work on this.
I spent much time. My diploma thesis have a higher priority.
But feedback is still welcome.
Last edited by Beauty on Wed Jan 25, 2012 2:09 am, edited 2 times in total.
Reason: added "}" to close the namespace (-;
Help to add information to the wiki. Also tiny edits will let it grow ...
Add your country to your profile ... it's interesting to know from where of the world you are.
IRC chat ... Mogre: irc://freenode/#mogre ... Ogre: irc://freenode/#ogre3d

Beauty
OGRE Community Helper

Posts: 1629
Kudos: 36
Joined: 09 May 2007
Location: Germany

### Re: Euler angles - current bugs + improved Euler Angle Class

Thank you very much for your effort!
And good luck

The VB.Net Code:
Code: Select all
``Imports System.Collections.GenericImports System.TextImports System.Text.RegularExpressionsImports MogreImports Math = Mogre.MathNamespace MogreEulerAngles   ' ABOUT THIS CODE:   '    ' Developed by user Kojack in January 2012   ' Extended by Beauty and Tubulii   '   ' Detailed information about usage:   ' http://www.ogre3d.org/tikiwiki/Euler+Angle+Class   '    ' "official place" of C# version:   ' http://www.ogre3d.org/tikiwiki/Euler+Angle+Class+Mogre   '    ' For questions and bug reports use this forum topic:   ' viewtopic.php?f=8&t=29262   '   ''' <summary>   ''' This struct manages rotations by use of Euler Angles.    ''' The rotation will be applied in the order Yaw-Pitch-Roll, which are related to the axis order Y-X-Z.    ''' It's fully compatible with Ogre::Matrix3::FromEulerAnglesYXZ and Ogre::Matrix3::ToEulerAnglesYXZ.    ''' For the Yaw angle the standard anticlockwise right handed rotation is used (as common in Ogre).   ''' The Yaw-Pitch-Roll ordering is most convenient for upright character controllers and cameras.   ''' </summary>   Public Structure Euler      ''' <summary>Get or set the Yaw angle.</summary>      Public Property Yaw() As Radian         Get            Return mYaw         End Get         Set            mYaw = value            mChanged = True         End Set      End Property      ''' <summary>Get or set the Pitch angle.</summary>      Public Property Pitch() As Radian         Get            Return mPitch         End Get         Set            mPitch = value            mChanged = True         End Set      End Property      ''' <summary>Get or set the Roll angle.</summary>      Public Property Roll() As Radian         Get            Return mRoll         End Get         Set            mRoll = value            mChanged = True         End Set      End Property      ''' <summary>      ''' Constructor to create a new Euler Angle struct from angle values.       ''' The rotation will be applied in the order Yaw-Pitch- Roll, which are related to the axis order Y-X-Z.       ''' </summary>      Public Sub New(yaw As Radian, pitch As Radian, roll As Radian)         mYaw = yaw         mPitch = pitch         mRoll = roll         mChanged = True         mCachedQuaternion = Quaternion.IDENTITY      End Sub      ''' <summary>      ''' Constructor which calculates the Euler Angles from a quaternion.      ''' </summary>      Public Sub New(oriantation As Quaternion)         Dim rotMat As Matrix3         rotMat = oriantation.ToRotationMatrix()         ' BUGGY METHOD (NaN return in some cases)         ' rotMat.ToEulerAnglesYXZ(out mYaw, out mPitch, out mRoll);         ' WORKAROUND         Dim success As [Boolean]         Matrix3ToEulerAnglesYXZ(rotMat, mYaw, mPitch, mRoll, success)         mChanged = True         mCachedQuaternion = Quaternion.IDENTITY      End Sub      ''' <summary>      ''' Apply a relative yaw. (Adds the angle to the current yaw value)      ''' </summary>      ''' <param name="yaw">Yaw value as radian</param>      Public Sub AddYaw(yaw As Radian)         mYaw += yaw         mChanged = True      End Sub      ''' <summary>      ''' Apply a relative pitch. (Adds the angle to the current pitch value)      ''' </summary>      ''' <param name="pitch">Pitch value as radian</param>      Public Sub AddPitch(pitch As Radian)         mPitch += pitch         mChanged = True      End Sub      ''' <summary>      ''' Apply a relative roll. (Adds the angle to the current roll value)      ''' </summary>      ''' <param name="roll">Roll value as radian</param>      Public Sub AddRoll(roll As Radian)         mRoll += roll         mChanged = True      End Sub      ''' <summary>      ''' Apply a relative yaw. (Adds the angle to the current yaw value)      ''' </summary>      ''' <param name="yaw">Yaw value as degree</param>      Public Sub AddYaw(yaw As Degree)         mYaw += yaw         mChanged = True      End Sub      ''' <summary>      ''' Apply a relative pitch. (Adds the angle to the current pitch value)      ''' </summary>      ''' <param name="pitch">Pitch value as degree</param>      Public Sub AddPitch(pitch As Degree)         mPitch += pitch         mChanged = True      End Sub      ''' <summary>      ''' Apply a relative roll. (Adds the angle to the current roll value)      ''' </summary>      ''' <param name="roll">Roll value as degree</param>      Public Sub AddRoll(roll As Degree)         mRoll += roll         mChanged = True      End Sub      ''' <summary>Get a vector pointing forwards. </summary>      Public ReadOnly Property Forward() As Vector3         Get            Return ToQuaternion() * Vector3.NEGATIVE_UNIT_Z         End Get      End Property      ''' <summary>Get a vector pointing to the right.</summary>      Public ReadOnly Property Right() As Vector3         Get            Return ToQuaternion() * Vector3.UNIT_X         End Get      End Property      ''' <summary> Get a vector pointing up.</summary>      Public ReadOnly Property Up() As Vector3         Get            Return ToQuaternion() * Vector3.UNIT_Y         End Get      End Property      ''' <summary>      ''' Calculate the quaternion of the euler object.       ''' The result is cached. It will only be recalculated when the component euler angles are changed.      ''' </summary>      Public Function ToQuaternion() As Quaternion         If mChanged Then            mCachedQuaternion = New Quaternion(mYaw, Vector3.UNIT_Y) * New Quaternion(mPitch, Vector3.UNIT_X) * New Quaternion(mRoll, Vector3.UNIT_Z)            mChanged = False         End If         Return mCachedQuaternion      End Function      ''' <summary>      ''' Return a String with degree values of the axis rotations (human readable style).       ''' For example "X-axis: 0°   Y-axis: 36°   Z-axis: 90°"      ''' </summary>      Public Function ToAxisString() As [String]         Return [String].Format("X: {0:00}°   Y: {1:00}°   Z: {2:00}°", Pitch.ValueDegrees, Yaw.ValueDegrees, Roll.ValueDegrees)      End Function      ''' <summary>      ''' Return a String with degree values in the applied rotation order (human readable style).      ''' For example "Yaw: 0°   Pitch: 36°   Roll: 90°"      ''' </summary>      Public Function ToYawPitchRollString() As [String]         Return [String].Format("Yaw: {0:00}°   Pitch: {1:00}°   Roll: {2:00}°", Yaw.ValueDegrees, Pitch.ValueDegrees, Roll.ValueDegrees)      End Function      ''' <summary>      ''' Try to parse 3 floating point values from a string, which are seperated by spaces or tabulators.       ''' Input order for rotation values:: Yaw, Pitch, Roll (all in degree)      ''' If success, an Euler struct will be returned.       ''' If parsing failed, a FormatException will be thrown.       ''' Example: "111  .99   -66"       ''' </summary>      ''' <param name="valueString">String which contains 3 floating point values</param>      ''' <remarks>      ''' Multiple and mixed usage of space/tabulator/commas are possible.       ''' As decimal seperator a dot "." is expected.       ''' </remarks>      ''' <returns>Returns an Euler struct or a FormatException</returns>      Public Shared Function ParseStringYawPitchRoll(valueString As [String]) As Euler         Dim values As [Single]() = Parse3Params(valueString)         If values Is Nothing Then            Throw New FormatException([String].Format("Can't parse floating point values of string '{0}'", valueString))         End If         Return New Euler(New Degree(values(0)), New Degree(values(1)), New Degree(values(2)))      End Function      ''' <summary>      ''' Try to parse 3 floating point values from a string, which are seperated by spaces or tabulators or comma.       ''' Input order for rotation values: X-axis, Y-axis, Z-axis (all in degree)      ''' If success, an Euler struct will be returned.       ''' If parsing failed, a FormatException will be thrown.       ''' Example: "111  .99   -66"       ''' </summary>      ''' <param name="valueString">String which contains 3 floating point values</param>      ''' <remarks>      ''' Multiple and mixed usage of space/tabulator/commas are possible.       ''' As decimal seperator a dot "." is expected.       ''' </remarks>      ''' <returns>Returns an Euler struct or a FormatException</returns>      Public Shared Function ParseStringAxisXYZ(valueString As [String]) As Euler         Dim values As [Single]() = Parse3Params(valueString)         If values Is Nothing Then            Throw New FormatException([String].Format("Can't parse floating point values of string '{0}'", valueString))         End If         Return New Euler(New Degree(values(1)), New Degree(values(0)), New Degree(values(2)))      End Function      ''' <summary>      ''' Try to parse 3 floating point values from a string, which are seperated by spaces or tabulators or comma.       ''' If parsing failed, null will be returned.       ''' Example: "111  .99   -66"       ''' </summary>      ''' <param name="valueString">String which contains 3 floating point values</param>      ''' <remarks>      ''' Multiple and mixed usage of space/tabulator/commas are possible.       ''' As decimal seperator a dot "." is expected.       ''' </remarks>      ''' <returns>Returns 3 Single values or null</returns>      Private Shared Function Parse3Params(valueString As [String]) As [Single]()         ' Some Regex explanation:         '         ' The "@" prefix in front of the String means:          '         Backslash are processed as Text instead of special symbols.         '         Advantage: Just write "\" instead of "\\" for each backslash         '          ' "^" at first position means:  No text is allowed before         ' "\$" at the end means:         No text is allowed after that         '          ' Floating point values are matched         '         Expression: "-?\d*\.?\d+"         '         Examples:   "111",  "0.111",  ".99",  "-66"         '         ' Seperator can be tabs or spaces or commas (at least one symbol; mixing is possible)         '         Expression: "[, \t]+"         Dim val As [String] = "[-\d\.]+"         ' simplified (faster) floating point pattern (exact pattern would be @"-?\d*\.?\d+" )         Dim sep As [String] = "[, \t]+"         ' seperator pattern         ' build target pattern         Dim searchPattern As [String] = "^(" & val & ")" & sep & "(" & val & ")" & sep & "(" & val & ")\$"         Dim match As Match = Regex.Match(valueString, searchPattern)         Try            If match.Success Then               ' Force to parse "." as decimal char.  (Can be different with other culture settings. E.g. German culture expect "," instad of ".")               Dim englishCulture As New System.Globalization.CultureInfo("en-US")               Dim result As [Single]() = New [Single](2) {}               result(0) = Convert.ToSingle(match.Groups(1).Value, englishCulture)               result(1) = Convert.ToSingle(match.Groups(2).Value, englishCulture)               result(2) = Convert.ToSingle(match.Groups(3).Value, englishCulture)               Return result            Else               Return Nothing            End If         Catch generatedExceptionName As FormatException            Return Nothing         Catch generatedExceptionName As OverflowException            Return Nothing         End Try      End Function      ' Parse3Params()      ''' <summary>      ''' Return the Euler rotation state as quaternion.      ''' </summary>      ''' <param name="e">Euler Angle state</param>      ''' <returns>Rotation state as Quaternion</returns>      Public Shared Widening Operator CType(e As Euler) As Quaternion         Return e.ToQuaternion()      End Operator      ''' <summary>      ''' Set the yaw and pitch to face in the given direction.       ''' The direction doesn't need to be normalised.       ''' Roll is always unaffected.      ''' </summary>      ''' <param name="directionVector">Vector which points to the wanted direction</param>      ''' <param name="setYaw">if false, the yaw isn't changed.</param>      ''' <param name="setPitch">if false, the pitch isn't changed.</param>      Public Sub SetDirection(directionVector As Vector3, setYaw As [Boolean], setPitch As [Boolean])         Dim d As Vector3 = directionVector.NormalisedCopy         If setPitch Then            mPitch = Math.ASin(d.y)         End If         If setYaw Then            mYaw = Math.ATan2(-d.x, -d.z)         End If         '+Math.PI/2.0;         mChanged = setYaw OrElse setPitch      End Sub      ''' <summary>      ''' Normalise the selected rotations to be within the +/-180 degree range.       ''' The normalise uses a wrap around, so for example a yaw of 360 degrees becomes 0 degrees,       ''' and -190 degrees becomes 170.       ''' By the parameters it's possible to choose which angles should be normalised.      ''' </summary>      ''' <param name="normYaw">If true, the angle will be normalised.</param>      ''' <param name="normPitch">If true, the angle will be normalised.</param>      ''' <param name="normRoll">If true, the angle will be normalised.</param>      ''' <remarks></remarks>      Public Sub Normalise(normYaw As [Boolean], normPitch As [Boolean], normRoll As [Boolean])         If normYaw Then            Dim yaw As [Single] = mYaw.ValueRadians            If yaw < -Math.PI Then               yaw = CType(System.Math.IEEERemainder(yaw, Math.PI * 2.0), [Single])               If yaw < -Math.PI Then                  yaw += Math.PI * 2F               End If               mYaw = yaw               mChanged = True            ElseIf yaw > Math.PI Then               yaw = CType(System.Math.IEEERemainder(yaw, Math.PI * 2F), [Single])               If yaw > Math.PI Then                  yaw -= Math.PI * 2F               End If               mYaw = yaw               mChanged = True            End If         End If         If normPitch Then            Dim pitch As [Single] = mPitch.ValueRadians            If pitch < -Math.PI Then               pitch = CType(System.Math.IEEERemainder(pitch, Math.PI * 2F), [Single])               If pitch < -Math.PI Then                  pitch += Math.PI * 2F               End If               mPitch = pitch               mChanged = True               If [Single].IsNaN(mPitch.ValueDegrees) Then                  ' DEBUGGING                  ' add breakpoint here               End If            ElseIf pitch > Math.PI Then               pitch = CType(System.Math.IEEERemainder(pitch, Math.PI * 2F), [Single])               If pitch > Math.PI Then                  pitch -= Math.PI * 2F               End If               mPitch = pitch               mChanged = True               If [Single].IsNaN(mPitch.ValueDegrees) Then                  ' DEBUGGING                  ' add breakpoint here               End If            End If         End If         If normRoll Then            Dim roll As [Single] = mRoll.ValueRadians            If roll < -Math.PI Then               roll = CType(System.Math.IEEERemainder(roll, Math.PI * 2F), [Single])               If roll < -Math.PI Then                  roll += Math.PI * 2F               End If               mRoll = roll               mChanged = True            ElseIf roll > Math.PI Then               roll = CType(System.Math.IEEERemainder(roll, Math.PI * 2F), [Single])               If roll > Math.PI Then                  roll -= Math.PI * 2F               End If               mRoll = roll               mChanged = True            End If         End If      End Sub      ' Normalise()      ''' <summary>      ''' Return the relative euler angles required to rotate from the current forward direction to the specified direction vector.       ''' The result euler can then be added to the current euler to immediately face dir.       ''' Rotation is found to face the correct direction. For example, when false a yaw of 1000 degrees and a dir of      ''' (0,0,-1) will return a -1000 degree yaw. When true, the same yaw and dir would give 80 degrees (1080 degrees faces      ''' the same way as (0,0,-1).      ''' The rotation won't flip upside down then roll instead of a 180 degree yaw.      ''' </summary>      ''' <param name="direction">...TODO...</param>      ''' <param name="shortest">If false, the full value of each angle is used. If true, the angles are normalised and the shortest rotation is found to face the correct direction.</param>      ''' <param name="setYaw">If true the angles are calculated. If false, the angle is set to 0. </param>      ''' <param name="setPitch">If true the angles are calculated. If false, the angle is set to 0. </param>      Public Function GetRotationTo(direction As Vector3, setYaw As [Boolean], setPitch As [Boolean], shortest As [Boolean]) As Euler         Dim t1 As Euler = Euler.IDENTITY         Dim t2 As Euler         t1.SetDirection(direction, setYaw, setPitch)         t2 = t1 - Me         If shortest AndAlso setYaw Then            t2.Normalise(True, True, True)         End If         Return t2      End Function      ''' <summary>      ''' Clamp the yaw angle to a range of +/-limit.      ''' </summary>      ''' <param name="limit">Wanted co-domain for the Yaw angle.</param>      Public Sub LimitYaw(limit As Radian)         If mYaw > limit Then            mYaw = limit            mChanged = True         ElseIf mYaw < -limit Then            mYaw = -limit            mChanged = True         End If      End Sub      ''' <summary>      ''' Clamp the pitch angle to a range of +/-limit.      ''' </summary>      ''' <param name="limit">Wanted co-domain for the Pitch angle.</param>      Public Sub LimitPitch(limit As Radian)         If mPitch > limit Then            mPitch = limit            mChanged = True         ElseIf mPitch < -limit Then            mPitch = -limit            mChanged = True         End If      End Sub      ''' <summary>      ''' Clamp the roll angle to a range of +/-limit.      ''' </summary>      ''' <param name="limit">Wanted co-domain for the Roll angle.</param>      Public Sub LimitRoll(limit As Radian)         If mRoll > limit Then            mRoll = limit            mChanged = True         ElseIf mRoll < -limit Then            mRoll = -limit            mChanged = True         End If      End Sub      ''' <summary>      ''' Port of method <c>Matrix3.ToEulerAnglesYXZ()</c>, from MogreMatrix3.cpp as a workaround for a bug in the Math class.       ''' (The bug was fixed, but is not present in common used binary files.)      ''' </summary>      ''' <param name="matrix">Rotation matrix</param>      ''' <param name="success">This COULD BE a flag for "success", but it's not documented in the original code.</param>      Public Shared Sub Matrix3ToEulerAnglesYXZ(matrix As Matrix3, ByRef rfYAngle As Radian, ByRef rfPAngle As Radian, ByRef rfRAngle As Radian, ByRef success As [Boolean])         rfPAngle = Mogre.Math.ASin(-matrix.m12)         If rfPAngle < New Radian(Math.HALF_PI) Then            If rfPAngle > New Radian(-Math.HALF_PI) Then               rfYAngle = Math.ATan2(matrix.m02, matrix.m22)               rfRAngle = Math.ATan2(matrix.m10, matrix.m11)               success = True               Return            Else               ' WARNING.  Not a unique solution.               Dim fRmY As Radian = Math.ATan2(-matrix.m01, matrix.m00)               rfRAngle = New Radian(0F)               ' any angle works               rfYAngle = rfRAngle - fRmY               success = False               Return            End If         Else            ' WARNING.  Not a unique solution.            Dim fRpY As Radian = Math.ATan2(-matrix.m01, matrix.m00)            rfRAngle = New Radian(0F)            ' any angle works            rfYAngle = fRpY - rfRAngle            success = False            Return         End If         ' "Original" CODE FROM CLASS Matrix3.ToEulerAnglesYXZ()         'rfPAngle = Mogre::Math::ASin(-m12);         'if ( rfPAngle < Radian(Math::HALF_PI) )         '{         '    if ( rfPAngle > Radian(-Math::HALF_PI) )         '    {         '        rfYAngle = System::Math::Atan2(m02,m22);         '        rfRAngle = System::Math::Atan2(m10,m11);         '        return true;         '    }         '    else         '    {         '        // WARNING.  Not a unique solution.         '        Radian fRmY = System::Math::Atan2(-m01,m00);         '        rfRAngle = Radian(0.0);  // any angle works         '        rfYAngle = rfRAngle - fRmY;         '        return false;         '    }         '}         'else         '{         '    // WARNING.  Not a unique solution.         '    Radian fRpY = System::Math::Atan2(-m01,m00);         '    rfRAngle = Radian(0.0);  // any angle works         '    rfYAngle = fRpY - rfRAngle;         '    return false;         '}      End Sub      ' Matrix3ToEulerAnglesYXZ()      ''' <summary>      ''' Add two euler objects.      ''' </summary>      ''' <returns>Calculation result</returns>      Public Shared Operator +(lhs As Euler, rhs As Euler) As Euler         Return New Euler(lhs.Yaw + rhs.Yaw, lhs.Pitch + rhs.Pitch, lhs.Roll + rhs.Roll)      End Operator      ''' <summary>      ''' Subtract two euler objects. This finds the difference as relative angles.      ''' </summary>      ''' <returns>Calculation result</returns>      Public Shared Operator -(lhs As Euler, rhs As Euler) As Euler         Return New Euler(lhs.Yaw - rhs.Yaw, lhs.Pitch - rhs.Pitch, lhs.Roll - rhs.Roll)      End Operator      ''' <summary>      ''' Interpolate each euler angle by the given factor.       ''' (Each angle will be multiplied with the factor.)      ''' </summary>      ''' <returns>Calculation result</returns>      Public Shared Operator *(lhs As Euler, factor As [Single]) As Euler         Return New Euler(lhs.Yaw * factor, lhs.Pitch * factor, lhs.Roll * factor)      End Operator      ''' <summary>      ''' Interpolate the euler angles by lhs.       ''' (Each angle will be multiplied with the factor.)      ''' </summary>      ''' <returns>Calculation result</returns>      Public Shared Operator *(factor As [Single], rhs As Euler) As Euler         Return New Euler(factor * rhs.Yaw, factor * rhs.Pitch, factor * rhs.Roll)      End Operator      ''' <summary>      ''' Apply the euler rotation to the vector rhs.       ''' The calculation is equal to: quaternion*vector      ''' </summary>      ''' <returns>Calculation result</returns>      Public Shared Operator *(lhs As Euler, rhs As Vector3) As Vector3         Return lhs.ToQuaternion() * rhs      End Operator      ''' <summary>Base settings (all angles are 0)</summary>      Public Shared IDENTITY As New Euler(New Radian(0), New Radian(0), New Radian(0))      ''' <summary>Rotation around the Y axis.</summary>      Private mYaw As Radian      ''' <summary>Rotation around the X axis.</summary>      Private mPitch As Radian      ''' <summary>Rotation around the Z axis.</summary>      Private mRoll As Radian      ''' <summary>Is the cached quaternion out of date?</summary>      Private mChanged As Boolean      ''' <summary>Cached quaternion equivalent of this euler object.</summary>      Private mCachedQuaternion As Quaternion   End Structure   ' struct EulerEnd Namespace``

And there is missing "}" in the last line of the c# code
Last edited by Beauty on Mon Jan 23, 2012 11:49 pm, edited 1 time in total.
Reason: renamed namespace name

Tubulii
Goblin

Posts: 222
Kudos: 15
Joined: 24 Jan 2010
Location: Germany

### Re: Euler angles - current bugs + improved Euler Angle Class

@smiley80

@Kojack
Thanks for sharing your specialist knowledge.

@All
Thanks to all, who are following this topic and took time to read my long posts.

zarifus wrote:binaries [...] have been built with the changes you made according to the changeset

Nice to know.

Tubulii wrote:And there is missing "}" in the last line of the c# code

Oh, I killed the namespace

If there is no bug report in the next time, I will update the code in the wiki.
Help to add information to the wiki. Also tiny edits will let it grow ...
Add your country to your profile ... it's interesting to know from where of the world you are.
IRC chat ... Mogre: irc://freenode/#mogre ... Ogre: irc://freenode/#ogre3d

Beauty
OGRE Community Helper

Posts: 1629
Kudos: 36
Joined: 09 May 2007
Location: Germany

PreviousNext