Having various problems with rotating things over time without gimbal lock, in c++

I’m working on a lockstep networked movement system, after implementing arbitrary gravity direction I thought it might be a nice, quick improvement to add in angular velocity and acceleration. Make things ‘stand up’ against gravity (defined as a unit vector direction) at a rate I can control with properties per rotation axis. I’m not proud to say I’ve been tearing my hair out for days trying to make this work.

What I have is a rotator of degrees for the current angular velocity of an agent, and another for the angular acceleration. I put in a simple braking behaviour that reduces angular velocity above angular acceleration towards 0 by the acceleration, per axis, this appears to work. The next 2 parts are defeating me.

If the angular velocity is low enough that the angular acceleration can stop it in 1 frame it has to find the delta between the current and desired rotation and set velocity on that axis to the delta angle or angular acceleration, whichever is lower. I am finding it impossible to reliably get Euler angles back out of my transform once pitch hits 90 or -90.

Next I need to take the angular velocities per axis and make the thing rotate that much for that frame on each of those axis. No matter how I do this I can’t seem to get it to rotate where I want it to.

Below is the part of the mass processor that is handling the setting of angular velocities. It basically worked for gravity pointing straight down, but not up.

//once gimbal lock kicks in this goes wild
FRotator rot = velocities[i].CurrentTransform.GetRotation().Rotator();
FRotator desiredrot = FRotator(gravangle.Pitch, lookinputs[i].Input.Yaw, gravangle.Yaw);
FRotator angularvelocity = velocities[i].AngularVelocity;
FRotator angularacceleration = movecoreparams[i].AngularAcceleration;
double testpitch = FMath::Abs(angularvelocity.Pitch) - FMath::Abs(angularacceleration.Pitch);
double testyaw = FMath::Abs(angularvelocity.Yaw) - FMath::Abs(angularacceleration.Yaw);
double testroll = FMath::Abs(angularvelocity.Roll) - FMath::Abs(angularacceleration.Roll);
//if less than or equal 0 (we can stop our current angular velocity) we apply it to get a new one towards goal
//if greater than 0 we apply our angular acceleration to current velocity towards 0
double deltapitch = FMath::FindDeltaAngleDegrees(rot.Pitch, desiredrot.Pitch);
double deltayaw = FMath::FindDeltaAngleDegrees(rot.Yaw, desiredrot.Yaw);
double deltaroll = FMath::FindDeltaAngleDegrees(rot.Roll, desiredrot.Roll);
testpitch = testpitch <= 0.0 ?
    FMath::Min(FMath::Abs(deltapitch), angularacceleration.Pitch) * FMath::Sign(deltapitch) :
    FMath::FixedTurn(angularvelocity.Pitch, 0.0, angularacceleration.Pitch);
testyaw = testyaw <= 0.0 ?
    FMath::Min(FMath::Abs(deltayaw), angularacceleration.Yaw) * FMath::Sign(deltayaw) :
    FMath::FixedTurn(angularvelocity.Yaw, 0.0, angularacceleration.Yaw);
testroll = testroll <= 0.0 ?
    FMath::Min(FMath::Abs(deltaroll), angularacceleration.Roll) * FMath::Sign(deltaroll) :
    FMath::FixedTurn(angularvelocity.Roll, 0.0, angularacceleration.Roll);

velocities[i].AngularVelocity = FRotator(testpitch, testyaw, testroll);

In a later processor I do collision and find walkable floors etc and set the new transform on the actor and in my entity data. This seemed like an appropriate place to rotate too. Commented out sections kept in to show the sort of things I’ve tried. I don’t think I really get quaternion composition and multiplication yet.

/*FQuat rotationquat = testtransform.GetRotation();
FQuat pitchquat = FQuat(FVector::UpVector, FMath::DegreesToRadians(AirMoveCommitsThisFrame[i].movefragment.AngularVelocity.Pitch));
FQuat yawquat = FQuat(FVector::ForwardVector, FMath::DegreesToRadians(AirMoveCommitsThisFrame[i].movefragment.AngularVelocity.Yaw));
FQuat rollquat = FQuat(FVector::RightVector, FMath::DegreesToRadians(AirMoveCommitsThisFrame[i].movefragment.AngularVelocity.Roll));*/
FQuat rotationquat = testtransform.GetRotation();
FQuat yawquat = FQuat(FVector::UpVector, FMath::DegreesToRadians(AirMoveCommitsThisFrame[i].movefragment.AngularVelocity.Yaw));
FRotator yawrotator = yawquat.Rotator();//yaw
FQuat rollquat = FQuat(FVector::BackwardVector, FMath::DegreesToRadians(AirMoveCommitsThisFrame[i].movefragment.AngularVelocity.Roll));
FRotator rollrotator = rollquat.Rotator();//roll, swap
FQuat pitchquat = FQuat(FVector::LeftVector, FMath::DegreesToRadians(AirMoveCommitsThisFrame[i].movefragment.AngularVelocity.Pitch));
FRotator pitchrotator = pitchquat.Rotator();//pitch, swap
/*rotationquat *= pitchquat;
rotationquat.Normalize();
rotationquat *= yawquat;
rotationquat.Normalize();
rotationquat *= rollquat;
rotationquat.Normalize();*/
rotationquat = pitchquat * rotationquat;
rotationquat.Normalize();
rotationquat = yawquat * rotationquat;
rotationquat.Normalize();
rotationquat = rollquat * rotationquat;
rotationquat.Normalize();

//testtransform.SetRotation(rotationquat);
//FRotator rot = rotationquat.Rotator();
//FRotator newrot = rot + AirMoveCommitsThisFrame[i].movefragment.AngularVelocity;
//newrot.Normalize();
//FQuat newquat = newrot.Quaternion();
//testtransform.SetRotation(rotationquat);
//testtransform.SetRotation((testtransform.GetRotation().Rotator() + AirMoveCommitsThisFrame[i].movefragment.AngularVelocity).Quaternion());
		//testtransform.ConcatenateRotation(AirMoveCommitsThisFrame[i].movefragment.AngularVelocity.Quaternion());
//testtransform.NormalizeRotation();
		AirMoveCommitsThisFrame[i].simroot->SetWorldLocation(testtransform.GetTranslation());
AirMoveCommitsThisFrame[i].simroot->AddLocalRotation(rotationquat);
testtransform = AirMoveCommitsThisFrame[i].simroot->GetComponentTransform();
AirMoveCommitsThisFrame[i].simroot->ComponentVelocity = AirMoveCommitsThisFrame[i].movefragment.Velocity;
AirMoveCommitsThisFrame[i].movefragment.CurrentTransform = testtransform;

Any pointers on how to get this to work would be greatly appreciated. Both with how to actually apply the angular velocity and how to apply acceleration and decelleration on the angular velocity over time by arbitrary rates per axis.

honestly this is above my paygrade but I do remember a video published by unreal where they got this one guy which have the job to implement the math part in unreal and he talked about these topics.

I’ve understood nothing but maybe you will be more lucky.

the one thing I’ve got out of it is that you need to use quaternions and not rotators to avoid the gimbal lock

here is the video

Yeah, this is basically the answer to most questions about this, but I have no idea what I am doing with quats. I tried using them for the part where I change the rotation, and got something stable, but it was not the rotation I expected, so I must be doing it wrong.

Other than that the specific problems are:
-Reliably getting the shortest delta rotation between a current rotation and desired rotation
-Getting the pitch/yaw/roll out of that so I can see how far I need to turn and in what direction
-Turning that into a rotation to apply to my actor’s root component
-Recording this new rotation and storing it outside the actor system for Mass Entity to work off

with quaternians you need to add a new quaternian to an existing one and not set them directly. so you use the functions provided by the engine which will take care of the math with out you caring about. the math is already there no need to reinvent higher level mathematics

The answer ended up being extremely simple.

First, rotation had to be stored in Mass frame to frame as a FRotator, representing the relative rotation, instead of inside a FTransform of the component transform. This does not gimbal lock. Gimbal lock is a world rotation thing. World rotation was what was stored in the FTransform and so it would gimbal lock.

In the actor/component system you can get this with get relative rotation on the root component, or get local rotation on the actor. This is actually stored in the component object as a FRotator, setting this by FQuat costs a conversion to FRotator anyway, which takes a bunch of trig functions. So if you want some sort of PID controller style turning you’re better off going straight to that rotator.

Second, applying angular velocity as a change to rotation per frame is just adding the rotators together and normalizing. No need for quats there either. The unwinding it does is enough to cross the 360/0 degree point. My problem stemmed entirely from this world/local rotation stuff.

for (int32 i = 0; i < RotationCommitsThisFrame.Num(); i++)
	{
		RotationCommitsThisFrame[i].Rotation.Rotation += RotationCommitsThisFrame[i].Velocity.AngularVelocity;
		RotationCommitsThisFrame[i].Rotation.Rotation.Normalize();
		RotationCommitsThisFrame[i].SimRoot->SetRelativeRotation(RotationCommitsThisFrame[i].Rotation.Rotation);
	}

Simple as.

However, the one use I can see still for quats in this particular problem is finding the shortest arc delta rotation between the current and desired rotations. Still working on that, but my agents now rotate to stand up against gravity and look towards a desired yaw by accelerating an angular velocity. They just turn like Zoolander to do it. But I’m just going to mark this post as the answer.