Unrotate vector into XY plane without projection

Hi,

Sorry in advance for the long post, but I’ve been struggling with this for a few days now. This is regarding player input and a custom pawn movement component. I intentionally avoid using the Character implementation of this.
I have the following pawns, input & movement setup:

class AMovementPawn : public ACustomPawn
{
public:
	virtual void MoveForward(const float Axis) override;
	virtual void MoveRight(const float Axis) override;
	virtual FVector GetControlForwardVector() const override;
	virtual FVector GetControlRightVector() const override;

protected:
	UCustomMovementComponent* Movement;
};

This pawn enables movement with the “MoveForward” and “MoveRight” methods being called by the PlayerController through input axis mapping. Note that this one does not have a camera. Here’s the implementation:

void AMovementPawn::MoveForward(const float Axis)
{
	Movement->AddInputVector(GetControlForwardVector().GetSafeNormal() * Axis);
}

void AMovementPawn::MoveRight(const float Axis)
{
	Movement->AddInputVector(GetControlRightVector().GetSafeNormal() * Axis);
}

FVector AMovementPawn::GetControlForwardVector() const
{
	return GetActorForwardVector();
}

FVector AMovementPawn::GetControlRightVector() const
{
	return GetActorRightVector();
}

Now I have another pawn which derives from AMovementPawn:

class AThirdPersonPawn : public AMovementPawn
{
public:
	virtual void PitchCamera(const float Axis) override;
	virtual void YawCamera(const float Axis) override;
	virtual FVector GetControlForwardVector() const override;
	virtual FVector GetControlRightVector() const override;

protected:
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	UThirdPersonSpringArmComponent* CameraSpringArm;

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	UCameraComponent* Camera;
};

It overrides “GetControlForwardVector” and “GetControlRightVector” to provide a vector based on the PlayerController control rotation (or the camera, if you will). This allows me to move forward and right in relation to my camera and not the pawn rotation. Here’s the implementation:

void AThirdPersonPawn::PitchCamera(const float Axis)
{
	AddControllerPitchInput(Axis * GetWorld()->GetDeltaSeconds());
}

void AThirdPersonPawn::YawCamera(const float Axis)
{
	AddControllerYawInput(Axis * GetWorld()->GetDeltaSeconds());
}

FVector AThirdPersonPawn::GetControlForwardVector() const
{
	return FRotationMatrix(Controller->GetControlRotation()).GetScaledAxis(EAxis::X);
}

FVector AThirdPersonPawn::GetControlRightVector() const
{
	return FRotationMatrix(Controller->GetControlRotation()).GetScaledAxis(EAxis::Y);
}

Here’s the tough part:
I am consuming the input vector in my movement component TickComponent:

	const FVector InputVector = ConsumeInputVector().GetClampedToMaxSize(1.0f);
	const FVector DeltaVelocity = InputVector * MovementSpeed * DeltaTime;	
	if (!DeltaVelocity .IsNearlyZero())
	{
		FHitResult Hit;
		SafeMoveUpdatedComponent(DeltaVelocity, InputVector.ToOrientationQuat(), true, Hit);
	}

This input vector I am getting here is in relation to my camera’s rotation. Meaning it is also affected by my camera’s pitch, which makes my pawn fly around in whatever direction the camera is facing. I am trying to rotate my input vector’s pitch such that it always lies within the XY plane with its original yaw and magnitude.

I know I could simply project it onto XY, get a normal of that and multiply by the original input vector’s magnitude, but this is giving me issues when my camera has pitch close to 90 or -90 (e.g. a projection of Z axis vector onto XY is [0,0,0], similar problem here). So when my camera points down (pitch -90) and I press forward, my pawn does not move because the projection of my camera’s forward vector onto XY is pretty much [0,0,0].

Another solution would be to project the vector onto XY in the “GetControlForwardVector” method to get rid of the pitch all together, and that works! However, that would be functionally wrong since a derived movement component might care about the pitch of the input vector (for example swimming, flying movement etc.).

In short: How do I unrotate my input vector inside the movement component tick so that it aligns with the XY plane and keeps its original magnitude and yaw without projecting it? Or am I missing something simple here?

Thanks for taking the time to read.

Maybe just use the camera yaw and ignore pitch & roll to figure out forward + right vectors?

Thanks for the reply!

Attempted that as well, like this:
const FVector InputVector = ConsumeInputVector().GetClampedToMaxSize(1.0f);
const FRotator YawRotation = FRotator(0.0f, InputVector.Rotation().Yaw, 0.0f);
const FVector InputDirection = YawRotation.RotateVector(FVector::XAxisVector);
const FVector InputTranslateDelta = InputDirection * InputVector.Size() * MovementSpeed * DeltaTime;
if (!InputTranslateDelta.IsNearlyZero())
{
FHitResult Hit;
SafeMoveUpdatedComponent(InputTranslateDelta, YawRotation.Quaternion(), true, Hit);
}
UE_LOG(LogTemp, Warning, TEXT("%s"), *YawRotation.ToCompactString());
But this is still giving me issues when moving diagonally.
When the camera aligns with world coordinates, it works nicely, I get a perfect 45° diagonal movement:

But as soon as you tilt the camera, yaw approximates to 90°:

I’m pressing forward + right in all of these images.

It makes sense why this would happen: The higher the camera’s pitch, the closer the InputVector rotates into the YZ plane. And the yaw between X and Y is 90°.

Sorry, somehow my code formatting disappeared in that previous comment:

const FVector InputVector = ConsumeInputVector().GetClampedToMaxSize(1.0f);
const FRotator YawRotation = FRotator(0.0f, InputVector.Rotation().Yaw, 0.0f);
const FVector InputDirection = YawRotation.RotateVector(FVector::XAxisVector);
const FVector InputTranslateDelta = InputDirection * InputVector.Size() * MovementSpeed * DeltaTime;	
if (!InputTranslateDelta.IsNearlyZero())
{
	FHitResult Hit;
	SafeMoveUpdatedComponent(InputTranslateDelta, YawRotation.Quaternion(), true, Hit);
}
UE_LOG(LogTemp, Warning, TEXT("%s"), *YawRotation.ToCompactString());

I meant use the current yaw of the camera, not of the inputvector.

Hi GrumbleBunny, thanks for the tip I did not think of directly accessing the controller from my movement component. However, even with that I did not manage to wrap my head around the calculation required to get the correct vector.

I finally managed to find the issue though and will post here in case someone makes the same mistake.
Whenever I tried to “unrotate” the pitch of my InputVector I was doing the following:

	const FVector InputVector = ConsumeInputVector().GetClampedToMaxSize(1.0f);
	const FRotator InputRotation = InputVector.Rotation();
	const FVector UnpitchedInputVector = FRotator(InputRotation.Pitch, 0.0f, 0.0f).UnrotateVector(InputVector);

Which I expected would cause UnpitchedInputVector to align with the XY plane. But it did not. Diagonal movement would result in a vector with a non zero Z component.

Now I found that GetClampedToMaxSize is the perpetrator. I wasn’t aware, but it seems like the size of a vector also plays into its rotation. After removing that, I receive the exact vector I wanted: Unrotated and aligned with XY plane with its original length intact and no projection needed.

	const FVector InputVector = ConsumeInputVector();
	const FRotator InputRotation = InputVector.Rotation();
	const FVector UnpitchedInputVector = FRotator(InputRotation.Pitch, 0.0f, 0.0f).UnrotateVector(InputVector);

Funny I didn’t notice earlier, such a tiny issue…
Thanks for the help though!