Trouble working with Spherical World gravity

We’re trying to make a character walk around a spherical planet. We disabled the world gravity and are attracting the Character in it’s Tick function. The attract function adds a force to the character toward the planet’s center and also orients him according to his position on the planet. We’re facing a weird issue when the character reaches the planet’s equator. The FindBetween function keeps returning rotation values and the character’s forward vector keeps rotating along the Z axis. So if the “forward” button is held, the character keeps moving in circles.

The attract function of the PlanetGravity class is as below:

void APlanetGravity::Attract(ACharacter* characterToAttract)
{
      FVector gravityUp = this->GetActorLocation() - characterToAttract->GetActorLocation();

      //if successfully normalized, attract the body
      if (gravityUp.Normalize())
      {
            FVector bodyUp = characterToAttract->GetActorUpVector();
            characterToAttract->GetCapsuleComponent()->AddForce(gravityUp*GravityForce);
            FQuat findBetween = FQuat::FindBetween(bodyUp, gravityUp);
            FQuat targetRotation = 
                            findBetween*characterToAttract->GetActorRotation().Quaternion();

            FQuat currentRotation = characterToAttract->GetActorRotation().Quaternion();
            characterToAttract->SetActorRotation(targetRotation.Rotator());
      }    
}

The character calls the PlanetGravity’s Attract function on every Tick.

No one? On further research, we found out that we might be facing a phenomenon called a Gimbal Lock. Anyone knows how to circumvent that?

We tried using just Quaternions. But it still didn’t work.

FVector gravityUp = (characterToAttract->GetActorLocation() - this->GetActorLocation());
		//if successfully normalized, attract the body

		if (gravityUp.Normalize())
		{
		FVector bodyUp = characterToAttract->GetActorUpVector();
		characterToAttract->GetCapsuleComponent()->AddForce(gravityUp*GravityForce);
		FQuat findBetween = FQuat::FindBetween(bodyUp, gravityUp);
		logWarning(characterToAttract->GetActorRotation().Euler().ToString());

		FTransform transform = characterToAttract->CapsuleComponent->GetRelativeTransform();
		transform.ConcatenateRotation(findBetween);
		characterToAttract->SetActorTransform(transform);
		}

We also tried finding the cross product and calculated the Right and Front vectors:

		FVector characterRight = characterToAttract->GetActorRightVector();
		FVector crossProductRightAndUp = FVector::CrossProduct(characterRight, gravityUp);
		FRotator rotationMatrix = FRotationMatrix::MakeFromZX(gravityUp, crossProductRightAndUp).Rotator();
		///* Get the Z-UnRotated Cross Product of The Actors Right Vector and the Normal */
		FVector UnrotatedRCrossN = FRotator(rotationMatrix).UnrotateVector(characterRight);
		///* Get the Z-Orientated Rotation Of the Cross Product Of the Z-Unrotated Vector, and the Normal */
		FRotator FinalRotation = FRotationMatrix::MakeFromZX(gravityUp, FVector::CrossProduct(UnrotatedRCrossN, gravityUp)).Rotator();
		///* Get the Right and Front Axis Vectors of the Rotation */
		FVector Right = FRotationMatrix(FinalRotation).GetScaledAxis(EAxis::Y);
		FVector Front = FRotationMatrix(FinalRotation).GetScaledAxis(EAxis::X);

and this:

FQuat localRotation = FQuat::FindBetween(characterToAttract->GetActorUpVector(), gravityUp)*characterToAttract->GetActorQuat();
		characterToAttract->GetCapsuleComponent()->AddForce(gravityUp*GravityForce);
		FTransform transform = FTransform();
		transform.SetLocation(characterToAttract->GetActorLocation());
		transform.SetRotation(localRotation);
		characterToAttract->SetActorTransform(transform);

In Unity I just had to do this:

    public float gravity;

    public void Attract(Transform body)
    {
        Vector3 gravityUp = (body.position - transform.position).normalized;
        Vector3 bodyUp = body.up;

        body.rigidbody.AddForce(gravityUp * gravity);
        Quaternion targetRotation = Quaternion.FromToRotation(bodyUp, gravityUp) * body.rotation;
        body.rotation = Quaternion.Slerp(body.rotation, targetRotation, 50 * Time.deltaTime);
    }

And call it from my Player controller:

    private float moveSpeed = 15;
    private Vector3 moveDirection;


    void Update()
    {
        moveDirection = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical")).normalized;
    }

    void FixedUpdate()
    {
        rigidbody.MovePosition(rigidbody.position + transform.TransformDirection(moveDirection) * moveSpeed * Time.deltaTime);
    }

public Gravity gravity;
private Transform objectTransform;

// Use this for initialization
void Start()
{
    rigidbody.constraints = RigidbodyConstraints.FreezeRotation;
    rigidbody.useGravity = false;
    objectTransform = this.transform;
}

// Update is called once per frame
void Update()
{
    gravity.Attract(objectTransform);
}

Hello,

I apologize for delay. I’m wondering if you could try this fix.

Replace AxisA with AxisB here - FQuat FQuat::FindBetween(const FVector& vec1, const FVector& vec2)

			Vec.FindBestAxisVectors(AxisA, AxisB);

====>>			return FQuat(AxisA.X, AxisA.Y, AxisA.Z, 0.f); // (axis*sin(pi/2), cos(pi/2)) = (axis, 0)
TO

====>> 			return FQuat(AxisB.X, AxisB.Y, AxisB.Z, 0.f); // (axis*sin(pi/2), cos(pi/2)) = (axis, 0)

You’re doing exactly what it’s supposed to do, so I think the bug is in FindBetween function. Let me know if this changes any behavior.

Thanks,

–Lina,

Thanks a lot for responding. Unfortunately, it’s still not working. To avoid altering engine code, I tried doing the following, which is essentially copying the function body and making the necessary changes as follows:

void APlanetGravity::Attract(ACharacter* characterToAttract)
{

	FVector gravityUp = (characterToAttract->GetActorLocation() - this->GetActorLocation());
	FQuat findBetween;
	if (gravityUp.Normalize())
	{
		FVector vec2 = gravityUp;
		FVector vec1 = characterToAttract->GetActorUpVector();
		const FVector cross = vec1 ^ vec2;
		const float crossMag = cross.Size();
		if (crossMag < KINDA_SMALL_NUMBER)
		{
			const float Dot = vec1 | vec2;
			if (Dot > -KINDA_SMALL_NUMBER)
			{
				findBetween = FQuat::Identity; // no rotation
			}
			else
			{
				FVector Vec = vec1.SizeSquared() > vec2.SizeSquared() ? vec1 : vec2;
				Vec.Normalize();
				FVector AxisA, AxisB;
				Vec.FindBestAxisVectors(AxisA, AxisB);
				findBetween = FQuat(AxisB.X, AxisB.Y, AxisB.Z, 0.f); 
			}
		}

		float angle = FMath::Asin(crossMag);

		const float dot = vec1 | vec2;
		if (dot < 0.0f)
		{
			angle = PI - angle;
		}    
		const float sinHalfAng = FMath::Sin(0.5f * angle);
		const float cosHalfAng = FMath::Cos(0.5f * angle);
		const FVector axis = cross / crossMag;

		findBetween = FQuat(
			sinHalfAng * axis.X,
			sinHalfAng * axis.Y,
			sinHalfAng * axis.Z,
			cosHalfAng);
		FQuat localRotation = findBetween*characterToAttract->GetActorQuat();
		characterToAttract->GetCapsuleComponent()->AddForce(gravityUp*GravityForce);
		FVector bodyUp = characterToAttract->GetActorUpVector();
		FQuat targetRotation = FQuat::FindBetween(bodyUp, gravityUp)*characterToAttract->GetActorRotation().Quaternion();
		FQuat currentRotation = characterToAttract->GetActorRotation().Quaternion();
		characterToAttract->SetActorRotation(FQuat::Slerp(currentRotation, targetRotation, 1.0f).Rotator());
}
}

However, we’re still facing Gimbal Lock. The line that was changed never even executes as the vectors are not parallel and the “normal code” section is executed. The Gimbal Lock happens when the rotation around Y reaches either 90 or -90.

Is there any other suggestion that you might have to get this working?

Gimbal lock is caused by interpolationg with Euler. You’re not using Euler here, but is there any code that changes from quaternion to rotator? If you’re relying on rotator, you’re going to see gimbal lock, but if you’re just using quaternion, you shouldn’t see gimbal lock. FQuat::FindBetween returns Quat, and you use Quat to interpolate and to multiply, so I don’t know where gimbal lock happens.

If you convert quaternion to rotator to see the value it won’t be continuous, but that’s expected.

Is there any code that is relying on rotator(euler)?

–Lina,

Thanks a lot for the reply Lina!

Our code essentially has just an AActor class that acts as the Planet and an ACharacter class that is the default pawn of the project. The character can move, but we are just adding translation to his position. This function is the only code where we deal with Rotations. I updated the code above to remove the remaining Rotators:

	FVector gravityUp = (characterToAttract->GetActorLocation() - this->GetActorLocation());
	if (gravityUp.Normalize())
	{
		characterToAttract->GetCapsuleComponent()->AddForce(gravityUp*GravityForce);
		FVector bodyUp = characterToAttract->GetActorUpVector();
		FQuat findBetween = FQuat::FindBetween(bodyUp, gravityUp);
		FQuat targetRotation = findBetween*characterToAttract->GetActorQuat();
		FQuat currentRotation = characterToAttract->GetActorQuat();
		characterToAttract->SetActorRotation(FQuat::Slerp(currentRotation, targetRotation, 1.0f).Rotator());

But it still doesn’t work. :frowning: Are we missing something here?

This code is the same thing I do with look at control, but pitch of 90 or -90 is the point where it flips, so it is somewhere conversion problem.

Could you explain exactly what’s happening? On the north/south pole of the sphere, you see character is spinning around Z axis? Did you try draw orientation on the world - draw coordinate - to make sure the rotation you’re feeding is correct and it’s not because the way character filters rotation?

The best way to debug this kind of situation is to draw your data as much as possible in the world to figure out where the breakdown happens.

Thanks,

–Lina,

At the poles, the character moves perfectly. It’s at the equator, when the rotation around Y axis reaches -90 or 90 is when it goes haywire. Someone suggested that it might be the character’s MoveComponent as it’s Z is hardcoded to be the world’s Z axis. So I’m gonna try using just a pawn and see where we get. I’ll let you know how it goes!

Thanks for taking the time Lina!

Finally got it working. Used a pawn instead of a character. The character’s movement component was going into the fall state everytime the rotation around Y reached -90 or 90. So now I just have to write all its physics and movement logic. But more power to me!! :slight_smile:

Good to hear it’s working but so that was Character code that filters rotation.

I’m adding Zak here, maybe he can see if we can fix that up.

Thanks,

–Lina,

Arbitrary gravity and orientation aren’t currently supported in CharacterMovementComponent, but is planned for the future. Glad you found something that worked though!

Maybe something you can check it out.