Math help - Pitch and roll a character in the controller stick direction

Hello,

I’m building a top-down game where my character is an helicopter.

The helicopter always looks to the mouse pointer, it Yaws to face the mouse.
The MoveRIghtAxis and MoveFowardAxis doesn’t care about the direction helicopter is facing in order to move the helicopter around, and helicopter flies in the X,Y plan (fixed Z).
I would like to Pitch and Roll the helicopter mesh to tilt in the exact direction the moveFowardAxis and moveRightAxis (control stick input axis) is facing, at a max of 30 degrees inclination, taking into the formula how far the stick is pushed.

For instance:

If my helicopter is rotated 90 degrees on Z (yaw=90), it would be looking to the “right” (mouse pointer would be there). Then, if I put the control stick all the way to foward (MoveFowardAxis = 1, MoveRightAxis = 0), I know I have to Pitch = 0 and Roll = -30 degrees (or MoveFowardAxis times MaxRoll times -1).
In the other hand, if it’s not rotated on Z (yaw = 0), it would be looking “foward” and if I put the stick all the way foward I have to Pitch = 30, but Roll = 0, and it tilts the way I want to.

In other words, I want my helicopter to tilt in the direction he’s going, not in the direction he’s facing. How would I do that? I would like to understand the Math behind this, if possible.
Since I have a foward vector and a right vector, how much pitch and roll do I add to them, based on MovementInputAxis, in order to tilt it in the right direction and with the right degree?

I’ve come this far until now:

//calculating Pitch 
fPitch = MaxPitch * ((fMoveFowardAxis * fowardVector.X * -1)
	+ (fMoveRightAxis * rightVector.X ));

//calculating Roll
fRoll = MaxRoll * ((fMoveRightAxis * fowardVector.X)
	+ (fMoveFowardAxis * rightVector.X));

It’s pitching and rolling in the right direction, although I didn’t understand clearly why and I would like to. Also, when RightAxis and FowardAxis equals 1, it gives me a value greater than the MaxRoll or MaxPitch allowed. There are dirty ways to fix it (like restricting fRoll or fPitch to a max of MaxRoll and MaxPitch), but I would like to make it in a cleaner mathematic way.

Hi Lobz,

This sounds like the sort of problem where I would want to change how I think of the problem before actually creating a solution.What I mean by that is that your solution seems to be addressing a slightly different problem than the one you’ve stated:

From your description, you want the mesh to tilt up to 30 degrees(proportional to its speed) in the direction it is moving, right ? Your current method seems to be treating the two movement axis separately, which means it uses two 1D axis to simulate what is really a 2D problem(since the mesh moves on a 2D plane).

Therefore, my suggestion would be this :

  1. Determine the speed and direction of travel
  2. Use the speed to determine the proper tilt(up to 30 degrees)
  3. Use the direction of travel to determine the direction of tilt
  4. Adjust the actor’s existing rotation using the calculated values

The resulting code might look something like this:

// Determine the speed and direction of travel
FVector movementDirection;                     // Direction the mesh is moving in
float speedFraction;                           // Speed at which the mesh is moving, as a fraction of "full throttle"

if (fMoveForwardAxis == 0.0f && fMoveRightAxis == 0.0f)
{
    // Guard against divide-by-zero, just in case
    movementDirection = FVector::ZeroVector;
    speedFraction = 0.0f;
}
else
{
    // The movement direction we can get simply by looking at the normal vector in the same direction as the input
    FVector rawMovementVector(fMoveForwardAxis, fMoveRightAxis, 0.0f);
    movementDirection = rawMovementVector;
    movementDirection.Normalize();

    // The speed can be determined by looking at the ratio between the actual input and largest conceiveable input in this direction.
    // The largest input in this direction will be when one of the axis is +/-1.  Thus, by taking the axis that is farther from 0 and
    // scaling the movement direction by 1 / (axis value), we can get the largest possible input in this direction.
    FVector maxInput = movementDirection;
    float largerAxis = FMath::Max(FMath::Abs(maxInput.X), FMath::Abs(maxInput.Y));
    maxInput /= largerAxis;

    speedFraction = rawMovementVector.Size() / maxInput.Size();
}

// Use the speed to determine the proper tilt
FRotator tilt(MaxAngle * speedFraction, 0.0f, 0.0f);

// Use the direction of travel to determine the direction of tilt
FQuat rotation = FQuat::FindBetween(FVector(0, 1, 0), movementDirection);

// Use the tilt and rotation to determine the actual rotation we need to apply
FQuat tiltRotation = rotation * tilt.Quaternion();

// Apply this rotation to our mesh
FRotator previousAngle;
// ...

this->SetActorRotation(tiltRotation * previousAngle.Quaternion());

Does this help?

Hi ,

The SpeedFraction variable will always be 1 with that math (if I did the math right).

look:

    // testing with inputFowardAxis = 0.5 and inputRightAxis = 0.5
    RawMovementVector = (1/2, 1/2, 0);
    movementDIrection = 1/2,1/2,0);

   // || movementDirection ||  = sqrt ( (1/2)² + (1/2)² ) = sqrt ( 1/4 + 1/4 ) = sqrt ( 2/4 ) = sqrt (1 /2 ) = 1/sqrt (2);
   // movementDIrection.Normalize() = movementDIrection / ||movementDirection|| = ( sqrt(2) / 2 , sqrt(2) / 2 , 0 );

    movementDIrection.Normalize() => ( sqrt(2) / 2 , sqrt(2) / 2 , 0 );

    maxInput = movementDirection =  ( sqrt(2) / 2 , sqrt(2) / 2 , 0 ) ;
    
    largerAxis = sqrt (2)/ 2;
    
    maxInput = maxInput/largerAxis  = ( sqrt(2) / 2 , sqrt(2) / 2 , 0 )  /    sqrt (2)/ 2   = (1, 1, 0);
    
    speedFraction = rawMovementVector.size() / maxInput.size() = sqrt ( 1² + 1² )  /  sqrt (1² + 1² )  = 1;

But with 0.5 FowardAxis and 0.5 RightAxis, the speed should be 0.5 (we’re pushing just half of the controller stick in both axis).

Besides, I didn’t understand pretty well how to work with quaternions… This tilt calculation is just for cosmetics, it’s not important for the gameplay (the rotation in Z is important for gameplay, but it’s been calculated right from mouse and in a different function)… I have a Rotator variable set in the Mesh anim instance for the tilt, and when I update it, it update the mesh rotation.

THe code to set the “tilt” is this one bellow. FRRinterpRotation were the FRotator with the previous pitch and yaw calculated, but How can I pass it using quaterions?

if (!Animation) return;
Animation->SkelMeshPitchRoll.Pitch = FRRinterpRotation.Pitch;
Animation->SkelMeshPitchRoll.Roll = FRRinterpRotation.Roll;
Animation->SkelMeshPitchRoll.Yaw = 0.0f;

Thanks a lot for your help!

Hello again!!!

I’m almost fixing it. I wasted my entire day trying to understand the math of a quaternion (I just gave up, I accepted I don’t need to know how the ■■■■ this is calculated, lets just use it as a black box), and I ended up with this code bellow.
It’s “almost” working, but I don’t know why the tilt is rotated in 90 degrees (in the WSAD axis, W tilts to “right” instead of forward, A tilts foward, S tilts left and D tilts to the back).
The Yaw is ok. Why is this happening???

//movement vectors like the controller stick
	FVector movementVector = fMoveFowardAxis * FVector::ForwardVector + fMoveRightAxis * FVector::RightVector;
	movementVector.Normalize();

	//this value will be multiplied by the MaxTiltAngle. from 0 to 1.
	float tiltMultiplier = FMath::Max(FMath::Abs(fMoveFowardAxis), FMath::Abs(fMoveRightAxis));

	//Calculates the tilt based on movement vector (pitch+ roll inside this quat)
	FQuat tilt = FQuat::MakeFromEuler(tiltMultiplier * MaxTiltAngle * movementVector);

	//rotation Yaw only. Yaw comes from lookat cursor position
	FRotator rotation = FRotator(0, fYaw, 0);

        //Add tilt (pitch and roll) to rotation (yaw) to get the complete rotation
	tilt = tilt * rotation.Quaternion();
	fPitch = tilt.Rotator().Pitch;
	fRoll = tilt.Rotator().Roll;

      //controller rotation for Pitch Yaw and Roll (actual)
    	if (!Controller) return FRRinterpRotation;
    	originRotation = Controller->GetControlRotation();

        //rinterp from actual to new rotation
        FRRinterpRotation = FMath::RInterpTo(originRotation, FRotator(fPitch, fYaw, fRoll), fpDeltaTime, TurnSpeed);

       //set rinterp rot to controller
       Controller->SetControlRotation(FRRinterpRotation);

Hi Lobz!

Sorry for the delay!

I treat quaternions as black boxes too – I know they’re another representation of rotation information, but I haven’t wrapped my head around the actual math they use and honestly have never needed to.

I think there is an error in your earlier math (when you calculate speedFraction). The value for rawMovementVector should be (1/2, 1/2, 0), but you seem to have copied in (1, 1, 0). Using (1/2, 1/2, 0) would result in sqrt( 1/4 + 1/4 ) / sqrt (1 + 1) = (1 / sqrt(2)) / sqrt (2) = 1/2, which is the correct value.

As for the rotation issue you’re seeing, it looks like an issue with the difference between Euler angles and yaw, pitch, and roll values. Euler angles are traditionally defined as the rotation around the corresponding axes. Your Euler angles are based on the movement vector. If you take (1/2, 0, 0) as your movement vector, you’ll see the Euler angles come out to (15, 0, 0). Thus, the x=15 here will cause a rotation of 15 degrees around the X-axis, which tilts your object to the right. I believe you’re looking for a pitch of 15 degrees instead, which would be y=-15.

There are a number of possible solutions. You could manually swap the axis of your Euler angles, like this:

//Calculates the tilt based on movement vector (pitch+ roll inside this quat)
FVector euler = tiltMultiplier * MaxTiltAngle * movementVector;
float tempPitch = euler.X;
euler.X = euler.Y;
euler.Y = -tempPitch;
FQuat tilt = FQuat::MakeFromEuler(euler);

A cleaner option might be creating a FRotator with the values you mean and having that create the proper FQuat. My initial suggestion included creating a FRotator called tilt and creates the corresponding FQuat as the last part of calculating tiltRotation.

Please let me know if this helps!

I came up with a solution and it’s working flawless now!
Thanks a lot for you answers, they made me understand a lot more of the math involved.
I realized that I was finding a foward vector (movementDirection) and applying a rotation in degrees to it. But since that movementDirection is sort of a FORWARD vector, applying angle would be a ROLL, not a PITCH. That’s why my entire rotation was rotated in 90 degrees.
All I had to do was to find the Right vector related to that movementDirection (forward), and apply rotation to it (which would make my char tilt in the forward direction, pitching).

So, step-by-step:

1- Create a vector that points “Forward” for the moviment, using the controller stick axis;
2- Cross product this Forward and Up (0 , 0 , 1) to find the “Right” vector related to this forward.
3- calculate your tiltAngle (mine is absolute max value between both input Axis. It will give a value between 0 and 1 - if you’re pulling your controller stick to the edge in any axis it will be 1… Then multiply this by MaxAngle variable).
4- multiply your vector by your tilt angle. Vector will be magnitude of tiltAngle;
5- Turn it in a rotator or quaternion and use it to Tilt;

Working, clean and understood :slight_smile:

The code is this bellow:

//movement direction we're going to. Rotation Yaw doesn't matter yet.
FVector movForwardVector = fMoveFowardAxis * FVector::ForwardVector + fMoveRightAxis * FVector::RightVector;
movForwardVector.Normalize(); // size = 1

//calculates a "right" vector from this movementDirFoward (cross product of  foward and up give us the right vec. 
FVector movRightVector = movForwardVector ^ FVector::UpVector;

//tiltAngle will be something between 0 degrees and MaxTiltAngle, according to how much pulled is the controller movement stick.
float tiltAngle = FMath::Max(FMath::Abs(fMoveFowardAxis), FMath::Abs(fMoveRightAxis))
	* MaxTiltAngle;

//aply tiltAngle to RightVector (like pitching it). Now our vector is tilted in the direction we wanted to.
//movRightVector = movRightVector.RotateAngleAxis(tiltAngle,FVector(1,0,0));

//Calculates the tilt rotation based on movement Right vector
FQuat tilt = FQuat::MakeFromEuler(tiltAngle * movRightVector);

//rotation Yaw only. Yaw comes from lookat cursor position
FRotator rotation = FRotator(0, fYaw, 0);

//Add tilt (pitch + roll) to rotation (yaw) to get the complete rotation
tilt = tilt * rotation.Quaternion();

if (!Controller) return;	
Controller->SetControlRotation(tilt.Rotator());

Hi again, Lobz,

I just stumbled into a presentation from GDC 2012 talking about different types of rotation:

It doesn’t go into crazy detail, but it seems to give a solid overview. The part on Quaternions starts on page 99 if you want to skip the rest.

I hope you find it helpful too!

Thanks for this! It helped me out a lot with a similar problem!