Pitch Past 90: Normal to Quat

I’m working on a simple spider physics type movement. I had the following coded up in Blueprints using rotators and things work fine until the pawn reaches 90º pitch. As I understand it, rotators have gimbal lock when working at > 90º of pitch. I translated the Blueprint to C++ in order to use quaternions. However, I realized I do not know how to use quaternions.

I’ve poked at this for a bit using the various related threads but haven’t come to a simple method of taking the impact normal from a trace and rotating the pawn to “stand” flat on that point while respecting the pawn’s yaw using only quaternions.

Here is my rotator based code called on tick:

AKJ_RotatingPlayerController::PositionOnSurface(float DeltaSeconds)
	APawn * Pawn = GetControlledPawn();
	if (!Pawn)
		return false;

	FTransform PawnTransform = Pawn->GetTransform();
	FQuat PawnQuat = PawnTransform.GetRotation();
	FVector PawnUpVector = PawnQuat.RotateVector(FVector(0, 0, 1));
	FVector PawnRightVector = PawnQuat.RotateVector(FVector(0, 1, 0));
	FVector PawnForwardVector = PawnQuat.RotateVector(FVector(1, 0, 0));
	FVector PawnLocation = PawnTransform.GetLocation();
	FVector Start = PawnLocation + PawnUpVector * TraceStartFactor;
	FVector End = PawnLocation + PawnUpVector * TraceLength;
	FCollisionQueryParams Params(true);
	FHitResult OutHit;

	GetWorld()->LineTraceSingle(OutHit, Start, End, ECC_Visibility, Params);
	if (OutHit.bBlockingHit)
		FVector ImpactNormal = OutHit.ImpactNormal;
		FVector ImpactPoint = OutHit.ImpactPoint;

		DrawDebugDirectionalArrow(GetWorld(), Start, ImpactPoint, 40, FColor::Green, true, 5);

		FRotator FromYZ(FRotationMatrix::MakeFromYZ(PawnRightVector, ImpactNormal).Rotator());
		FRotator FromXZ(FRotationMatrix::MakeFromXZ(PawnForwardVector, ImpactNormal).Rotator());
		FRotator PawnRotator = PawnQuat.Rotator();

		FRotator FinalRot = FRotator(FromYZ.Pitch, PawnRotator.Yaw, FromXZ.Roll);
		FVector NewLocation = ImpactPoint + ImpactNormal * 100.0;


		return true;
	return false;

This code and the blueprint behaved the same way. Movement works as expected < 90º and I can move straight up 90º and rotate in-place, but any other sort of movement will tend to send it all a bit haywire. Quite frequently a drift will be introduced and the pawn will mis-trace/mis-align itself with no control input until it reaches the ground (I turn on gravity and zero out the rotation if a trace fails) where it will stop drifting but will have a slight jitter. At this point it will start drifting again on any º of incline, so something gets permanently tweaked.

Can anyone shed light on what the problem is? If this is gimbal lock shenanigans, how would I best take that impact normal and rotate my pawn accordingly using only FQuat?


The problem is likely that you’re using Frotators at all.

If you are using quaternions for true “free” movement, you want to each frame do:

  1. Get “up” axis
  2. rotate by right-movement around “up” axis (this is viewer-relative left/right)
  3. Get “right” axis
  4. Rotate by down-movement around “right” axis (this is viewer-relative up/down)
  5. The quaternion you end up with is your new actor orientation
  6. re-normalize this quaternion

This will always rotate exactly the way you expect by looking at the camera ahead. However, you will accumulate roll compared to the world up axis unless you exactly back-trace your mouse path to look back forward again. This is how relative rotations work – Euler angles get around this by mapping mouse movement to world-absolute values and re-generating the rotation each frame.

If there is a “gravity” or “up” vector that you want to favor, you may need to add a separate kind of rotation that procedurally drives the derived quaternion rotation towards that up vector.

Thank you for the reply. The following snippet seems to work perfectly and my pawn can now spider walk anywhere.

FVector c = FVector::CrossProduct(PawnUpVector, ImpactNormal);
float d = FVector::DotProduct(PawnUpVector, ImpactNormal);
float s = FMath::Sqrt((1.0 + d) * 2.0);
float rs = 1.0 / s;

FQuat q(c.X * rs, c.Y * rs, c.Z * rs, s * 0.5);

q = q * PawnQuat;