C++/ Unreal Need help with Rotation, getting screwed by Gimbal Lock

Ok, so I am trying to make expanded code for the Mover component that has 4 UPROPERTYS(EditAnywhere) which are: LocationOffSet, TimeToMove, RotatorOffSet, TimeToRotate. Now, the location portion works like a charm, and I had no issues, but the rotation portion has been screwing me for days. I have tried just using regular Rotators and RInterpConstantTo which works like a charm, UNLESS the start rotation + RotationOffSet > 90 degrees because at 90 degrees pitch, I get good ol’ Gimbal Lock. So, I then converted my rotators to Quaternions. This works like a charm, except the timetorotate doesn’t work correctly. It starts off correct, but the closer it gets to it’s target, the more it slows down until the final degree takes several minutes to complete when the whole rotation is supposed to take whatever seconds = TimeToRotate.

The issue is Lerp and Slerp with deltatime does not work the same as RInterpConstantTo or VInterpConstantTo. Can someone please help me with this, as I even tried asking every coding A.I. there was, and they either spit my code back at me, or just plain wrong code. I am only including the portion of code related to Rotation and everything works fine, except the rotation completing in the TimeToRotate specification. Thanks for your time! Code:

void UMover::BeginPlay()

{
Super::BeginPlay();

AActor* Owner = GetOwner();
FRotator ActorStartRotationThrowAway = Owner->GetActorRotation();
ActorStartRotation = ActorStartRotationThrowAway;
ActorStartRotation.Normalize();
    ActorStartRotationQuat = FQuat(ActorStartRotation);
TargetRotation.Pitch = ActorStartRotation.Pitch + RotatorOffSet.Pitch;
TargetRotation.Yaw = ActorStartRotation.Yaw + RotatorOffSet.Yaw;
TargetRotation.Roll = ActorStartRotation.Roll + RotatorOffSet.Roll;
TargetRotation.Normalize();
TargetRotationQuat = FQuat(TargetRotation);
TargetRotationQuat = TargetRotationQuat * TargetRotationQuat.Inverse();
RotationSpeedQuat = ActorStartRotationQuat.AngularDistance(TargetRotationQuat) / TimeToRotate;
RotationSpeedBackQuat = TargetRotationQuat.AngularDistance(ActorStartRotationQuat) / TimeToRotate;

}

// Called every frame
void UMover::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

AActor* Owner = GetOwner();
FRotator CurrentRotation = Owner->GetActorRotation();
FQuat CurrentRotationQuat = FQuat(CurrentRotation);
FRotator TargetRotationBack = ActorStartRotation;

if (ActorStartRotation != TargetRotation) {

if (AreRotatorsApproximatelyEqual(CurrentRotation, ActorStartRotation, Epsilon)) {

	AtTargetRotation = false;
	AtStartRotation = true;

}

else if (AreRotatorsApproximatelyEqual(CurrentRotation, TargetRotation, Epsilon)) {
	
	AtStartRotation = false;
	AtTargetRotation = true;

}

}

if ((Swinger) && (!ShouldMove) && (ActorStartRotation != TargetRotation)) {

if ((AtStartRotation) && (!AtTargetRotation)) {
	
	FQuat NewRotationQuat = FQuat::Slerp(CurrentRotationQuat, TargetRotationQuat, RotationSpeedQuat * DeltaTime);


	if (CurrentRotation != ActorStartRotation) {
		Owner->SetActorRotation(ActorStartRotation);
	}

	Owner->SetActorRotation(NewRotationQuat);

}

}

	else if ((!AtStartRotation) && (AtTargetRotation)) {


		ActorStartRotationQuat = ActorStartRotationQuat * ActorStartRotationQuat.Inverse();
		TargetRotationQuat = FQuat(TargetRotation);
		FQuat NewRotationBackQuat = FQuat::Slerp(CurrentRotationQuat, ActorStartRotationQuat, RotationSpeedBackQuat * DeltaTime);

		Owner->SetActorRotation(NewRotationBackQuat);
	
	}

}

}

From the very limited information I could find in days of looking for an answer, the problem is with slerp and delta time and (I might explain it wrong, I am not 100% in understanding of what I was reading, but it’s relatively close) how deltatime basically says to do a percentage of a task within a frame. So if it’s constantly 10%, and the current rotation - target rotation keeps getting smaller while the delta percentage stays the same then thats why it slows down. But I have no idea how to fix it. I’m open to an alternative fix sticking with Rotators or if there is a fix with FQuats and Slerp that’d be great as well, but I def wanna fix it in c++ and stay off blueprints for it.

That’s not how lerp works. The third parameter is for a percentage between the two points.
Modifying the first parameter’s value as you move is going to give you that effect.

Demo: Compiler Explorer

You should keep the first two values constant and update the third

Demo: Compiler Explorer

So would changing all the current, target’s to start/targets and then making the speedback variable (currentrotation.AngularDistance(Target) / TimeToMove) * DeltaTime and then multiplying this answer by .1 to ensure it’s always below 1? This also means the lower the distance gets to the target, the lower the number will be. I tested this in a calculator and it’s always under 1, however I’m thinking the speed will be not correct at all . Is it just pretty impossible/Very hard to find a formula that will work that rotation cycle will be = TimeToSeconds? And you’re better off instead incorporating a more vague variable like “speed” where the higher it is, the faster the cycle completes? (I’ve been trying to avoid this, but each day it’s looking like it may have to be the answer.)

Wait, I think I might get what you’re saying, and what the code you showed is saying… So basically I need to make it Start/Target and then do the Currentangulardistance(target) / TimeToMove * DeltaTime and then somehow make that answer go from a scale to .0-1.0 (with it reaching it’s target at 1.0). This seems complicated, but am I at correct in that’s how you’re saying it works?

I’m sorry for the spam responses, I just happen to think of something else that may work better if the way I’m understanding the way it works now. Would it be far easier to incorporate elapsed time into it and somehow making the elapsedtime the “target time” = TimeToRotate and then making that a scale of 0-1 so that when the elapsed time is = to TimeToRotate then it will be on 1. And it goes up evenly, scaled to how many seconds there are? So for example if TimeToRotate was = 5 then with every elapsedtime second that goes up it would increase the third paramater by .02? If this is correct then I would just have to figure out a formula that no matter what the TimeToRotate is set to it scales correctly

Yes, that’s it.

That formula is 1 / Time. You could keep it in terms of seconds so you can edit as you want it in the editor then in BeginPlay do TimeToRotate = 1 / TimeToRotate

So you would end up having

Current = Lerp(Start, Target, Time);
Time += TimeToRotate * DeltaTime;

You’re a saint. This is working. It doesn’t work fully, but not because of you, it’s because of the way I tried handling gimbal lock so it ends up flipping the object which kind of breaks it, but it works until that point and uelogging the Time variable goes up correctly and smoothly. I really do appreciate it man. I have been struggling with this for going on a week now, and I was so close to just giving up and doing something else entirely that I know would have worked, but would not have been the way I wanted it. (I wanted it to be the same as my location portion since I made it all in the same Mover component instead of making 2 seperate movers.) So I am forever in your debt!

You’re welcome. Also I should probably note the increment should be first otherwise you would do nothing for the first tick, I just wanted to print the full range of numbers (but then failing to do so as whilst I renamed “speed” to “percent” I forgot to change its value 🤦).

So you’re saying the first tick before any movement needs to include the +=.05, so the very first movement would be .06 instead of .01? Or in my example Time += TimeToRotate * DeltaTime should happen once before slerp?

Hey also, I just figured I’d give you an update on what I changed to make this work completely right, no gimbal lock, no object flipping (Neither of which had anything to do with your code but still want to say it just in case anyone has the same problems and sees this post as a solution and trys to use my incorrect code), and a smooth rotation from point a to point b with slerp. I got rid of the inverse() operations. My revised code that works as expected (also note: some things may seem a little redundant, but I also have code for the location portion which I edited out here for the sake of clarity of this issue regarding specifically to rotation. but technically even if redundant, the code im about to post will work for the rotation w/o gimbal lock issues, and with a smooth slerp in the amount of time = TimeToRotate which is set in the editer for each actor you attach a mover component too.) code:

#include "Mover.h"
#include "Math/UnrealMathUtility.h"


// Sets default values for this component's properties
UMover::UMover()
{
    // Set this component to be initialized when the game starts, and to be ticked every frame.  You can turn these features
    // off to improve performance if you don't need them.
    PrimaryComponentTick.bCanEverTick = true;

    // ...
}


// Called when the game starts
void UMover::BeginPlay()

{


    Super::BeginPlay();

    AActor* Owner = GetOwner();
    FRotator ActorStartRotationThrowAway = Owner->GetActorRotation();
    ActorStartLocation = Owner->GetActorLocation();

    timetorotate = TimeToRotate;
    ActorStartRotation = ActorStartRotationThrowAway;
     ActorStartRotationQuat = FQuat(ActorStartRotation);
    TargetRotation.Pitch = ActorStartRotation.Pitch + RotatorOffSet.Pitch;
    TargetRotation.Yaw = ActorStartRotation.Yaw + RotatorOffSet.Yaw;
    TargetRotation.Roll = ActorStartRotation.Roll + RotatorOffSet.Roll;
    TargetRotationQuat = FQuat(TargetRotation);


    timetorotate = 1 / TimeToRotate;



}


// Called every frame
void UMover::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);



    AActor* Owner = GetOwner();
    FRotator CurrentRotation = Owner->GetActorRotation();
    FQuat CurrentRotationQuat = FQuat(CurrentRotation);
    RotationSpeedQuat = CurrentRotationQuat.AngularDistance(TargetRotationQuat) / TimeToRotate;
    RotationSpeedBackQuat = CurrentRotationQuat.AngularDistance(ActorStartRotationQuat) / TimeToRotate;


    FRotator TargetRotationBack = ActorStartRotation;





    if (ActorStartLocation != TargetLocation) {


        if(CurrentLocation == ActorStartLocation) {
            AtTargetLocation = false;
            AtStartLocation = true;

        }


        else if(CurrentLocation == TargetLocation) {
            AtStartLocation = false;
            AtTargetLocation = true;

        }
    }


    if (CurrentRotation == ActorStartRotation || Time > 1) {

        if (AtStartRotation) {
            Owner->SetActorRotation(TargetRotation);
            AtStartRotation = false;
            AtTargetRotation = true;
            Time = timetorotate * DeltaTime;
        }

        else if (AtTargetRotation) {
            Owner->SetActorRotation(ActorStartRotation);
            AtTargetRotation = false;
            AtStartRotation = true;
            Time = timetorotate * DeltaTime;
        }
    }




    if ((ShouldMove)) {



        if (ActorStartRotation != TargetRotation) {

            FQuat NewQuat = FQuat::Slerp(ActorStartRotationQuat, TargetRotationQuat, Time);
            Time += timetorotate * DeltaTime;


            Owner->SetActorRotation(NewQuat);

        }
    }

    else if ((!ShouldMove) && (!Swinger) && (CurrentRotation != ActorStartRotation)) {


        FQuat NewRotationBackQuat = FQuat::Slerp(TargetRotationQuat, ActorStartRotationQuat, Time);
        Time += timetorotate * DeltaTime;


        UE_LOG(LogTemp, Display, TEXT("DeltaTime = %f, Time = %f"), DeltaTime, Time);


        if (CurrentRotation != ActorStartRotation) {

            Owner->SetActorRotation(NewRotationBackQuat);


        }


    }





    if ((Swinger) && (!ShouldMove) && (ActorStartRotation != TargetRotation)) {

        UE_LOG(LogTemp, Display, TEXT("In main rotation if statement"));

        if ((AtStartRotation) && (!AtTargetRotation)) {


            FQuat NewRotationQuat = FQuat::Slerp(ActorStartRotationQuat, TargetRotationQuat, Time);
            Time += timetorotate * DeltaTime;
            Owner->SetActorRotation(NewRotationQuat);



            FString CurrentRotationString = CurrentRotation.ToCompactString();
            FString TargetRotationString = TargetRotation.ToCompactString();
            //FString CurrentRotationQuatString = FRotator(CurrentRotationQuat).ToCompactString();
            //FString TargetRotationQuatString = FRotator(TargetRotationQuat).ToCompactString();

            //UE_LOG(LogTemp, Display, TEXT("Current RotationQuat = %s, Target rotationQuat = %s"), *CurrentRotationString, *TargetRotationString);

            //UE_LOG(LogTemp, Display, TEXT("In AtStartRotation && !AtTargetRotation if statemnet. AtStartRotation = %hs, AtTargetRotation = %hs"), AtStartRotation ? "true" : "false", AtTargetRotation ? "true" : "false")

        }


        else if ((!AtStartRotation) && (AtTargetRotation)) {


            FQuat NewRotationBackQuat = FQuat::Slerp(TargetRotationQuat, ActorStartRotationQuat, Time);
            Time += timetorotate * DeltaTime;


            Owner->SetActorRotation(NewRotationBackQuat);


            //UE_LOG(LogTemp, Display, TEXT("In !AtStartRotation && AtTargetRotation if statemnet. AtStartRotation = %hs, AtTargetRotation = %hs"), AtStartRotation ? "true" : "false", AtTargetRotation ? "true" : "false")

        }
    }
}

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

0.1 is 10%, I was intending to start at 0 and also print the full range (0 - 5, inclusive) but that would actually go on too long as that would print 21 times instead of 20 (increasing 5% each step).


I haven’t fully read your code but it looks rather complicated (I also edited it to be in a code block to be readable). Here’s what I have (not fully tested):

Header:

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class BLANK_API URotatorer : public UActorComponent
{
    GENERATED_BODY()

public:
    URotatorer();

protected:
    virtual void BeginPlay() override;

public:
    virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
    void SetOpenState(bool bState);

private:
    FQuat StartRotation;
    FQuat TargetRotation;
    float Time = 0.f;
    bool bToOpen = false;

    UPROPERTY(EditAnywhere, meta=(DisplayName = "TimeToRotate"))
    float TimeStep = 0.f;

    UPROPERTY(EditAnywhere)
    FRotator RotationOffset;
};

cpp:

#include "Rotatorer.h"

URotatorer::URotatorer()
{
    PrimaryComponentTick.bCanEverTick = true;
}

void URotatorer::BeginPlay()
{
    Super::BeginPlay();

    TimeStep = 1 / TimeStep;
    TimeStep *= bToOpen ? 1 : -1;
    StartRotation = GetOwner()->GetActorQuat();
    TargetRotation = StartRotation * RotationOffset.Quaternion();
}

void URotatorer::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

    bool bIsAtTarget = bToOpen ? Time == 1.f : Time == 0.f;
    if (bIsAtTarget)
    {
        return;
    }

    Time = FMath::Clamp(Time + TimeStep * DeltaTime, 0.f, 1.f);
    FQuat NewRot = FQuat::Slerp(StartRotation, TargetRotation, Time);
    GetOwner()->SetActorRotation(NewRot);
}

void URotatorer::SetOpenState(bool bState)
{
    if (bToOpen != bState)
    {
        bToOpen = bState;
        TimeStep *= -1;
    }
}
1 Like

Yes my code is def complicated, but it’s because I have several extra things going on that I edited out as to not confuse anyone with the non related code specifically to the rotation portions. Also it looks quite messy because I’d like to have a lot more of this stuff as functions, but for some reason when trying to make anything that requires Delta Time as a function is giving me issues and not working correctly. This is true even when I try using a reference to DeltaTime as the parameter. Something I’m sure I can work out, but just haven’t made it there yet. So ty for making a simplified version, as I am sure someone will run into this same issue in the future, and it was causing my hair to fall out. Thanks again, Dan for helping me figure this out. You saved my innocent keyboard from getting smashed out of frustration haha.

1 Like

Even via the world? i.e.

GetWorld()->GetDeltaSeconds();
1 Like

I have not tried that. I just attempted it as a parameter to the function, and then also attempted a refrence as the paramter. But considering DeltaTime is already a pointer I shouldn’t have to do a reference, but I figured it was worth a shot. I will give your suggestion a go today and get back to you on if it worked.

Just giving you an update, I put everything into functions, and GetWorld()->GetDeltaSeconds() did indeed work. Thanks for the suggestion.

DeltaTime is not a pointer. If you were taking a reference/address to the parameter then that was likely the culprit.

Privacy & Terms