Advertisement

Need assistance on converting Quat to Euler

Started by January 15, 2023 02:14 PM
7 comments, last by JoeJ 1 year, 9 months ago

Hello all,

I am currently working with a firm where I am developing a program that will convert their old assets to a new format, FBX. I am currently working on the skeletal meshes and I have a small bug that is taking me awhile to track down. For this model, there is a random bone rotation and one of the legs is misplaced. I am thinking that it has to do with my calculation of the Quat to Euler. I just need a second pair of eyes to take a look and check if I am converting the values correctly. Please let me know!

SWGMainObject::EulerAngles  SWGMainObject::ConvertCombineCompressQuat(Geometry::Vector4 DecompressedQuaterion, Skeleton::Bone BoneReference, bool isStatic)
{
	const double pi = 3.14159265358979323846;
	double rotationFactor = 180.0 / pi;

	Geometry::Vector4 Quat;
	EulerAngles angles;

	FbxQuaternion AnimationQuat = FbxQuaternion(DecompressedQuaterion.x, DecompressedQuaterion.y, DecompressedQuaterion.z, DecompressedQuaterion.a);
	FbxQuaternion bind_rot_quat{ BoneReference.bind_pose_rotation.x, BoneReference.bind_pose_rotation.y, BoneReference.bind_pose_rotation.z, BoneReference.bind_pose_rotation.a };
	FbxQuaternion pre_rot_quat{ BoneReference.pre_rot_quaternion.x, BoneReference.pre_rot_quaternion.y, BoneReference.pre_rot_quaternion.z, BoneReference.pre_rot_quaternion.a };
	FbxQuaternion post_rot_quat{ BoneReference.post_rot_quaternion.x, BoneReference.post_rot_quaternion.y, BoneReference.post_rot_quaternion.z, BoneReference.post_rot_quaternion.a };

	auto full_rot = post_rot_quat * (AnimationQuat * bind_rot_quat) * pre_rot_quat;
	Quat = Geometry::Vector4(full_rot.mData[0], full_rot.mData[1], full_rot.mData[2], full_rot.mData[3]);

	double test = Quat.x * Quat.z - Quat.y * Quat.a;
	double sqx = Quat.x * Quat.x;
	double sqy = Quat.y * Quat.y;
	double sqz = Quat.z * Quat.z;
	double sqa = Quat.a * Quat.a;
	double unit = sqx + sqy + sqz + sqa;

	// roll (x-axis rotation)
	double sinr_cosp = 2.0 * (Quat.a * Quat.x + Quat.y * Quat.z);
	double cosr_cosp = Quat.a * Quat.a - Quat.x * Quat.x - Quat.y * Quat.y + Quat.z * Quat.z;
	angles.roll = std::atan2(sinr_cosp, cosr_cosp);

	// pitch (y-axis rotation)
	double sinp = 2.0 * (Quat.a * Quat.y - Quat.z * Quat.x);
	if (std::abs(sinp) >= 1)
		angles.pitch = std::copysign(pi / 2.0, sinp); // use 90 degrees if out of range
	else
		angles.pitch = std::asin(sinp);

	// yaw (z-axis rotation)
	double siny_cosp = 2.0 * (Quat.a * Quat.z + Quat.x * Quat.y);
	double cosy_cosp = Quat.a * Quat.a + Quat.x * Quat.x - Quat.y * Quat.y - Quat.z * Quat.z;
	angles.yaw = std::atan2(siny_cosp, cosy_cosp);

	//angles.yaw = std::atan2(2.0 * (Quat.x * Quat.y + Quat.z * Quat.a), sqx - sqy - sqz + sqa); // heading
	//angles.pitch = std::asin(-2.0 * test / unit); // attitude
	//angles.roll = std::atan2(2.0 * (Quat.y * Quat.z + Quat.x * Quat.a), -sqx - sqy + sqz + sqa); // bank
	
	/*double test = Quat.x * Quat.y + Quat.z * Quat.a;
	if (test < 0.499)
	{
		angles.yaw = 2.0 * std::atan2(Quat.x, Quat.a);
		angles.pitch = pi / 2.0;
		angles.roll = 0;
	}
	else if (test < -0.499)
	{
		angles.yaw = -2.0 * std::atan2(Quat.x, Quat.a);
		angles.pitch = -pi / 2.0;
		angles.roll = 0;
	}
	else
	{
		double sqx = Quat.x * Quat.x;
		double sqy = Quat.y * Quat.y;
		double sqz = Quat.z * Quat.z;

		angles.yaw = std::atan2(2.0 * Quat.y * Quat.a - 2.0 * Quat.x * Quat.z, 1.0 - 2.0 * sqy - 2.0 * sqz);
		angles.pitch = std::asin(2.0 * test);
		angles.roll = std::atan2(2.0 * Quat.x * Quat.a - 2.0 * Quat.y * Quat.z, 1.0 - 2.0 * sqx - 2.0 * sqz);
	}*/

	/*FbxVector4 zeroVector(0, 0, 0);
	FbxQuaternion testQuat(Quat.x, Quat.y, Quat.z, Quat.a);
	FbxMatrix testMatrix;
	testMatrix.SetTQS(zeroVector, testQuat, zeroVector);
	EulerAngles testAngles;
	FbxVector4 returnVector = testMatrix.GetColumn(0);

	testAngles.roll = returnVector[0] * rotationFactor;
	testAngles.pitch = returnVector[1] * rotationFactor;
	testAngles.yaw = returnVector[2] * rotationFactor;*/

	angles.roll *= rotationFactor;
	angles.pitch *= rotationFactor;
	angles.yaw *= rotationFactor;

	return angles;
}

Another strategy is to make a set of unit tests. Also helps in showing that your software does its job.

Advertisement

@Alberth That is true and I would like to do that. But I am not entirely sure if the equations that I am using are correct for converting Quats to Euler

That's the thing you want to test right?

You should probably start with some known input/output, and verify that the conversion produces the expected result. Eg compute manually, or use cases that you can verify as being correct easily.

Another approach is to implement another conversion (or get some third-party code if feasible). This conversion must be foremost correct, speed isn't a concern beauty of the computation is not a concern either. I don't know how your conversion would work, but trying to implement the math as literally as possible might work, at least it often does for me in discrete math.

Brute-force trying is fine too if it gives a correct answer.

Maybe you can exploit the continuous nature of the conversion. I mean, if you add a small angle somewhere, it seems to me you should get a small variation in the answer too. If something turns 180 degrees in such a case that would be suspicious.

@Alberth

I see thank you. Are there any good Quat to Euler online converters that I can use to double check my work? Right now, I am not worried about speed. Just need something functional

Sorry, I wouldn't know about implementations of the conversion, but quaternions rotating a nice angles like +/- for 45, 90, 135, or 180 are easy to check or prepare manually. First along the main axes, then perhaps a quaternion at a 45 degrees angle on 2 of the 3 axes? A disadvantage of multiples of 45/135 degrees is that the result has a lot of symmetry, so you may want to try 30 degrees as well eventually.

Except for one if/else, you're doing a continuous computation, you're not doing something entirely different when rotating 15 degrees instead of say, 25 degrees. So if there are errors in it, they likely show up at a lot of angles. So with limited tests you should be able to cover a lot of ground already.

For the if/else, you may want to have a number of tests that use the “if” and a number of tests that used the “else” branch to get full coverage.

Advertisement

I asked ChatGPT to write conversion functions for me. Then I wrote a trivial test that encoding back and forth results in the original quaternion (up to sign flip). It discovered a mistake in the code, but when I asked ChatGPT about it, it fixed it!

#include <iostream>
#include <cstdlib>
#include <cmath>

void quatToEuler(const double& w, const double& x, const double& y, const double& z,
                 double& roll, double& pitch, double& yaw)
{
  double sqw = w*w;
  double sqx = x*x;
  double sqy = y*y;
  double sqz = z*z;
  
  double unit = sqx + sqy + sqz + sqw;
  double test = x*y + z*w;
  
  if (test > 0.499*unit) {
    pitch = 2 * std::atan2(x, w);
    yaw = M_PI/2;
    roll = 0;
  }
  else if (test < -0.499*unit) {
    pitch = -2 * std::atan2(x, w);
    yaw = -M_PI/2;
    roll = 0;
  }
  else {
    pitch = std::atan2(2*y*w-2*x*z , sqx - sqy - sqz + sqw);
    yaw = std::asin(2*test/unit);
    roll = std::atan2(2*x*w-2*y*z , -sqx + sqy - sqz + sqw);
  }
}

void eulerToQuat(const double& roll, const double& pitch, const double& yaw,
                 double& w, double& x, double& y, double& z)
{
  double cos_roll_2 = cos(roll/2);
  double cos_pitch_2 = cos(pitch/2);
  double cos_yaw_2 = cos(yaw/2);
  
  double sin_roll_2 = sin(roll/2);
  double sin_pitch_2 = sin(pitch/2);
  double sin_yaw_2 = sin(yaw/2);
  
  w = cos_roll_2 * cos_pitch_2 * cos_yaw_2 - sin_roll_2 * sin_pitch_2 * sin_yaw_2;
  x = sin_roll_2 * cos_pitch_2 * cos_yaw_2 + cos_roll_2 * sin_pitch_2 * sin_yaw_2;
  y = cos_roll_2 * sin_pitch_2 * cos_yaw_2 + sin_roll_2 * cos_pitch_2 * sin_yaw_2;
  z = cos_roll_2 * cos_pitch_2 * sin_yaw_2 - sin_roll_2 * sin_pitch_2 * cos_yaw_2;
}

int main() {
  for (int i = 0; i < 10; ++i) {
    double w = drand48()-.5;
    double x = drand48()-.5;
    double y = drand48()-.5;
    double z = drand48()-.5;
    double inv_length = 1.0 / std::sqrt(w*w+x*x+y*y+z*z);
    w *= inv_length;
    x *= inv_length;
    y *= inv_length;
    z *= inv_length;

    double roll, pitch, yaw;
    quatToEuler(w, x, y, z, roll, pitch, yaw);
    double W, X, Y, Z;
    eulerToQuat(roll, pitch, yaw, W, X, Y, Z);

    std::cout << w << ' ' << W << " | " << x << ' ' << X << " | " << y << ' ' << Y << " | " << z << ' ' << Z << "\n";
  }
}

alvaro said:
It discovered a mistake in the code, but when I asked ChatGPT about it, it fixed it!

Turing test passed. :O

I think it's time to get an account for me…

Edit: Damn. They want my phone number, which i do not know.
I'm doomed to stay old fashioned. Still no smart phone, and no future help from AI buddy. :(

This topic is closed to new replies.

Advertisement