Yaw delta from From to To, measured as a heading difference about UpAxis. Forward = (cosP*cosY, cosP*sinY, sinP) is independent of roll, and its world-horizontal heading is exactly Y, so when neither rotation has roll the FRotator yaw component already is the heading (pitch is irrelevant) and a plain yaw subtraction is exact. We only fall back to projection when roll is present, where world-space
| 72 | // the rotation looks nearly straight up/down the up axis, which otherwise made the projected heading swing as the |
| 73 | // up axis moved and spuriously drive turn-in-place. |
| 74 | static float ComputeYawDeltaAroundUp(const FRotator& From, const FRotator& To, const FVector& UpAxis = FVector::UpVector) |
| 75 | { |
| 76 | const bool bWorldUp = UpAxis.Equals(FVector::UpVector); |
| 77 | if (bWorldUp && FMath::IsNearlyZero(From.Roll) && FMath::IsNearlyZero(To.Roll)) |
| 78 | { |
| 79 | return FRotator::NormalizeAxis(To.Yaw - From.Yaw); |
| 80 | } |
| 81 | |
| 82 | const FVector FromH = HeadingInPlane(From, UpAxis); |
| 83 | const FVector ToH = HeadingInPlane(To, UpAxis); |
| 84 | if (FromH.IsNearlyZero() || ToH.IsNearlyZero()) |
| 85 | { |
| 86 | // Unreachable in practice (forward and right can't both be parallel to UpAxis); kept as a safety net. |
| 87 | return FRotator::NormalizeAxis(To.Yaw - From.Yaw); |
| 88 | } |
| 89 | const float Cross = (FromH ^ ToH) | UpAxis; |
| 90 | const float Dot = FromH | ToH; |
| 91 | return FMath::RadiansToDegrees(FMath::Atan2(Cross, Dot)); |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | namespace TurnInPlaceCvars |
no test coverage detected