0

I want to lerp between two rotations with different velocities on three different axis (yaw/pitch/roll) in unity3d, and tried to achieve that with Quaternion.LookRotation().

Quaternion.LookRotation() takes a direction Vector as first parameter, so i thought that i could lerp the direction first and then look at it with a lerped up-vector.

It should be no problem with Vector3.lerp(), but in this case i need to lerp the direction with different velocities on two axis (X and Y) relative to the initial direction.

So for example i have a camera facing a target, then the target moves up and right a bit, and now i want the camera to tilt slowly to the right too, but a bit faster up to the targets position (keeping its own position).

How to lerp the direction vector with different speeds on both axis to use it in Quaternion.LookRotation()?

EDIT: Changed the title from "Lerp between Vector3 with different velocities on X/Y" to "Quaternion lerp with different velocities for yaw/pitch/roll" and modified the question to match the topic.

sonority
  • 1
  • 4
  • Have you tried Quaternion.slerp? – Leo Bartkus Oct 07 '18 at 02:07
  • Yes, but Quaternion.Slerp() takes a float as third parameter, so all three axis are lerped at the same "speed". I also thought about splitting those axis, doing the lerp, and sum them to a new rotation, but either i have done it completely wrong, or it did not really work well. I also tried it with Vector.Slerp(), but in each case when i tried to lerp on different axis, the world X/Y/Z got lerped and not the local rotation axis. – sonority Oct 07 '18 at 08:32
  • 1
    Try this one http://allenchou.net/2018/05/game-math-swing-twist-interpolation-sterp/ – minorlogic Oct 08 '18 at 07:32
  • At first glance, the CjLib (including this wonderful swing/twist-approach) looked like a perfect solution to me, but it is not exactly what i am searching for. In my case it want to smoothly "look at" a target, by lerping all three axis relative to the viewing orientation. I am not a mathematician, and i am searching since more than five weeks, trying this and that. Another approach wich worked (somehow) was projecting the axis of the target onto the viewers plane, but this worked only in worldspace. ...i am really stuck. – sonority Oct 08 '18 at 18:27
  • Didi you tried to use different "t" lerp parameters in STERP? One for swing and other foe twist. It should do exactly as you describe. – minorlogic Oct 09 '18 at 09:10

3 Answers3

0

Thanks to minorlogic and the CjLib, i tried the following:

public Quaternion QuaternionLerpOn3Axis(
    Quaternion rot1,
    Quaternion rot2,
    Vector3 lerpSpeed
) {
    if (rot1 != rot2) {
        float lerpSpeedPitch = lerpSpeed.x * Time.deltaTime;
        float lerpSpeedYaw = lerpSpeed.y * Time.deltaTime;
        float lerpSpeedRoll = lerpSpeed.z * Time.deltaTime;
        // Lerp up direction
        Vector3 vecUp = Vector3.Slerp(
            rot1 * Vector3.up,
            rot2 * Vector3.up,
            lerpSpeedRoll
        );
        // Get new rotation with lerped yaw/pitch
        Quaternion rotation = QuaternionUtil.Sterp(
            rot1,
            rot2,
            rot1 * Vector3.right,
            lerpSpeedYaw,
            lerpSpeedPitch,
            QuaternionUtil.SterpMode.Slerp
        );
        // Look at new direction and return rotation
        return Quaternion.LookRotation(
            rotation * rot1 * Vector3.forward,
            vecUp
        );
    } else {
        return rot1;
    }
}

To try this without downloading CjLib, here is the whole code including the relevant parts for decoding the swing/twist:

public Quaternion QuaternionLerpOn3Axis(
    Quaternion rot1,
    Quaternion rot2,
    Vector3 lerpSpeed
) {
    if (rot1 != rot2) {
        float lerpSpeedPitch = lerpSpeed.x * Time.deltaTime;
        float lerpSpeedYaw = lerpSpeed.y * Time.deltaTime;
        float lerpSpeedRoll = lerpSpeed.z * Time.deltaTime;
        // Lerp up direction
        Vector3 vecUp = Vector3.Slerp(
            rot1 * Vector3.up,
            rot2 * Vector3.up,
            lerpSpeedRoll
        );
        // Get difference between two rotations
        Quaternion q = rot2 * Quaternion.Inverse(rot1);
        // Decompose quaternion into two axis
        Quaternion rotYaw;
        Quaternion rotPitch;
        DecomposeSwingTwist(
            q,
            rot1 * Vector3.right,
            out rotYaw,
            out rotPitch
        );
        // Lerp yaw & pitch
        rotYaw = Quaternion.Slerp(Quaternion.identity, rotYaw, lerpSpeedYaw);
        rotPitch = Quaternion.Slerp(Quaternion.identity, rotPitch, lerpSpeedPitch);
        // Look at new direction and return rotation
        return Quaternion.LookRotation(
            rotPitch * rotYaw * rot1 * Vector3.forward,
            vecUp
        );
    } else {
        return rot1;
    }
}
public static void DecomposeSwingTwist(
    Quaternion q,
    Vector3 twistAxis,
    out Quaternion swing,
    out Quaternion twist
) {
    Vector3 r = new Vector3(q.x, q.y, q.z); // (rotation axis) * cos(angle / 2)
    float Epsilon = 1.0e-16f;

    // Singularity: rotation by 180 degree
    if (r.sqrMagnitude < Epsilon) {
        Vector3 rotatedTwistAxis = q * twistAxis;
        Vector3 swingAxis = Vector3.Cross(twistAxis, rotatedTwistAxis);

        if (swingAxis.sqrMagnitude > Epsilon) {
            float swingAngle = Vector3.Angle(twistAxis, rotatedTwistAxis);
            swing = Quaternion.AngleAxis(swingAngle, swingAxis);
        } else {
            // More singularity: rotation axis parallel to twist axis
            swing = Quaternion.identity; // no swing
        }

        // Always twist 180 degree on singularity
        twist = Quaternion.AngleAxis(180.0f, twistAxis);
        return;
    }

    // Formula & proof: 
    // http://www.euclideanspace.com/maths/geometry/rotations/for/decomposition/
    Vector3 p = Vector3.Project(r, twistAxis);
    twist = new Quaternion(p.x, p.y, p.z, q.w);
    twist = Normalize(twist);
    swing = q * Quaternion.Inverse(twist);
}
public static Quaternion Normalize(Quaternion q) {
    float magInv = 1.0f / Magnitude(q);
    return new Quaternion(magInv * q.x, magInv * q.y, magInv * q.z, magInv * q.w);
}
public static float Magnitude(Quaternion q) {
    return Mathf.Sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w);
}

By now this is the only way i could achieve a quaternion (s)lerp with different velocities on three different axis with a reasonably acceptable result.

But in my opinion it is not a real mathematical solution, it does not work really well if the lerp values are below ~1.5f (especially the Z/Roll-axis), and it has much overhead.

Any ideas how to solve this puzzle with less/better code?

sonority
  • 1
  • 4
0

...another approach:

Now i tried to extend the concept of decomposing the swing/twist to decomposing yaw/pitch/roll.

This works fine (?) if the target does not flip over 180°, and it still needs some input/feedback from someone who really knows how to deal with quaternion rotations.

public Quaternion QuaternionLerpYawPitchRoll(
    Quaternion rot1,
    Quaternion rot2,
    Vector3 lerpSpeed
) {
    if (rot1 != rot2) {
        float lerpSpeedPitch = lerpSpeed.x * Time.deltaTime;
        float lerpSpeedYaw = lerpSpeed.y * Time.deltaTime;
        float lerpSpeedRoll = lerpSpeed.z * Time.deltaTime;
        // Decompose quaternion into yaw/pitch/roll
        Quaternion rotYaw;
        Quaternion rotPitch;
        Quaternion rotRoll;
        DecomposeYawPitchRoll(rot1, rot2, out rotYaw, out rotPitch, out rotRoll);
        // Lerp swing & twist
        rotYaw = Quaternion.Slerp(Quaternion.identity, rotYaw, lerpSpeedYaw);
        rotPitch = Quaternion.Slerp(Quaternion.identity, rotPitch, lerpSpeedPitch);
        rotRoll = Quaternion.Slerp(Quaternion.identity, rotRoll, lerpSpeedRoll);
        // Combine yaw/pitch/roll with current rotation
        return Quaternion.LookRotation(
            rotPitch * rotYaw * rot1 * Vector3.forward,
            rotRoll * rot1 * Vector3.up
        );
    } else {
        return rot1;
    }
}
public static void DecomposeYawPitchRoll(
    Quaternion rot1,
    Quaternion rot2,
    out Quaternion yaw,
    out Quaternion pitch,
    out Quaternion roll
) {
    Vector3 pitchAxis = rot1 * Vector3.right;
    Vector3 rollAxis = rot1 * Vector3.forward;
    Vector3 yawAxis = rot1 * Vector3.up;
    // Get difference between two rotations
    Quaternion diffQ = rot2 * Quaternion.Inverse(rot1);

    Vector3 r = new Vector3(diffQ.x, diffQ.y, diffQ.z); // (rotation axis) * cos(angle / 2)
    float Epsilon = 1.0e-16f;

    // Singularity: rotation by 180 degree
    if (r.sqrMagnitude < Epsilon) {
        Vector3 rotatedPitchAxis = diffQ * pitchAxis;
        Vector3 rotatedYawAxis = Vector3.Cross(pitchAxis, rotatedPitchAxis);
        Vector3 rotatedRollAxis = diffQ * rollAxis;

        if (rotatedYawAxis.sqrMagnitude > Epsilon) {
            float yawAngle = Vector3.Angle(pitchAxis, rotatedPitchAxis);
            yaw = Quaternion.AngleAxis(yawAngle, rotatedYawAxis);
        } else {
            // More singularity: yaw axis parallel to pitch axis
            yaw = Quaternion.identity; // No yaw
        }
        if (rotatedRollAxis.sqrMagnitude > Epsilon) {
            float rollAngle = Vector3.Angle(yawAxis, rotatedYawAxis);
            roll = Quaternion.AngleAxis(rollAngle, rotatedRollAxis);
        } else {
            // More singularity: roll axis parallel to yaw axis
            roll = Quaternion.identity; // No roll
        }

        // Always twist 180 degree on singularity
        pitch = Quaternion.AngleAxis(180.0f, pitchAxis);
    } else {
        // Formula & proof: 
        // http://www.euclideanspace.com/maths/geometry/rotations/for/decomposition/
        pitch = GetProjectedRotation(diffQ, pitchAxis);
        roll = GetProjectedRotation(diffQ, rollAxis);
        yaw = diffQ * Quaternion.Inverse(pitch);
    }
}
public static Quaternion GetProjectedRotation(Quaternion rotation, Vector3 direction) {
    Vector3 r = new Vector3(rotation.x, rotation.y, rotation.z);
    Vector3 proj = Vector3.Project(r, direction);
    rotation = new Quaternion(proj.x, proj.y, proj.z, rotation.w);
    return Normalize(rotation);
}
public static Quaternion Normalize(Quaternion q) {
    float magInv = 1.0f / Magnitude(q);
    return new Quaternion(magInv * q.x, magInv * q.y, magInv * q.z, magInv * q.w);
}
public static float Magnitude(Quaternion q) {
    return Mathf.Sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w);
}
sonority
  • 1
  • 4
0

Author of CjLib here.

It sounds like you actually don't need swing-twist decomposition. I'd say just get the decomposed yaw/pitch/row for the current quaternion and desired quaternion. And then update the yaw/pitch/row values depending on how fast you want them to individually track the target value, and generate an updated quaternion from that set of yaw/pitch/row values.

Lerping with a maximum speed cap (which I refer to as "seeking") might be fine, but it would not look smooth. I recommend using critically-damped numeric springing. And here's a shameless placement of a 3-part series I wrote on this topic.

Allen Chou
  • 48
  • 3
  • wow, a major honour! it seems that you are "someone who really knows how to deal with quaternion rotations". i really like the idea with "seeking" and "numeric springing", but to be honest, right now i don't know how to solve the first puzzle with those quaternion rotations... – sonority Oct 19 '18 at 08:51
  • The idea to decompose all three axis from both rotations and then lerp them on different speeds sounds pretty simple and i have tried that already in many ways. But as i told, i am not a mathematician, and until now i have done all this by trial-and-error ...with moderate success. The only way i could get the axis angle from the quaternion was to extract it as euler, wich leads to flipping axis at 180° ...and some other weird results. So could you please outline the correct approach for decomposing yaw/pitch/roll for dummies, or give a more precise hint? – sonority Oct 19 '18 at 08:52
  • Actually, you don't need quaternions at all to find the Euler angles between two vectors. Also, from what it sounds like you only need to modify the relative yaw (vertical angle) and pitch(horizontal angle). You can find those relative angles by using the current camera local coordinate (X = camera's right, Y = camera's up, Z = camera's forward), and the desired camera facing direction converted into the camera's current local coordinate (let's call it V). – Allen Chou Oct 19 '18 at 17:54
  • Let projection of V onto XZ plane be Vxz = (V.x, 0.0f, V.z), and V onto YZ plane be Vyz = (0.0f, V.y, V.z). The relative pitch angle will be the angle between Vyz and the Z axis, and the relative pitch angle will be the angle between Vxz and the Z axis. Make sure to use Unity's signed angle functions and provide a rotational axis (X axis for pitch and Y axis for yaw); otherwise, you'll always get positive relative pitch and yaw, which always rotate the camera towards the first octant, i.e. up and right. – Allen Chou Oct 19 '18 at 17:54
  • You meant that the relative angle between Vxz and Z will be the yaw, right? ...i have tried projecting those axis before, but maybe i have done something wrong. As far as i remember i also had problems with flipping axis. But i love this puzzle, i will try again. Many thanks for pushing me into the right direction again ;-) – sonority Oct 20 '18 at 09:37
  • OK, i have tried that again with projecting the direction vectors. Exactly as described (and many other variations too) ...with no success, same as before. – sonority Oct 20 '18 at 18:15
  • The Results are: Rotation around Y-axis (yaw) flips the X-axis (pitch) by 180°, if the angle is above 90° or below -90°. Rotation around X-axis (pitch) flips the Y- and Z-axis (yaw/roll) by 180°, if the angle is above 90° or below -90°. And if i sum the rotations (which i get from `Quaternion.AngleAxis()`) the result is really weird. The strangest thing with those angles is, that the flipping does not really follow a fixed scheme. Sometimes it is 180° and sometime -180° (spontaneously changing between positive/negative) Is that a bug in Unity, or am i missing something important? – sonority Oct 20 '18 at 18:15
  • How did you get the angles? I would do all the calculations myself, but Unity also provides a `Vector3.SignedAngle` function that does the same thing. Are you using that function? Quaternions don't need to be involved until you finally convert the relative yaw/pitch to quaternion and update the camera rotation with it. – Allen Chou Oct 20 '18 at 19:27
  • I used the diff of both rotations (`diff = targetRot * Quaternion.Inverse(camRot)`) multiplied by direction to get the relative vector. Then i used `Vector3.ProjectOnPlane()` to project the appropriate vector onto one of the cameras plane (tried your suggested way too). With `Vector3.SignedAngle()` i got the angel, which works perfect on all three axis from -180° to 180°, but with the described flipping on the other axis. If i would lerp on only one axis, the puzzle would be solved, but i want to lerp all three axis at the same time with different velocities and sum them to a new rotation. – sonority Oct 20 '18 at 23:32
  • ...Is that generally possible with small/inexpensive code, or do i need a more complex design (which i don't know right now, and which i think i would not find without help). – sonority Oct 20 '18 at 23:33
  • Sorry. My mistake for saying using the relative pitch and yaw in regards to the camera's local space. I should have said computing the relative yaw by finding the signed angle between the current & desired camera directions projected onto the ABSOLUTE XZ plane, and computing the relative pitch by finding the difference between the pitch angles of the current & desired camera direction, also relative to the ABSOLUTE XZ plane. – Allen Chou Oct 21 '18 at 04:26
  • To find the pitch angle of a vector (V), project it onto the ABSOLUTE XZ plane (Vxz), and take the arc-tangent of V's y component divided by the Vxz's magnitude. Using this method, the relative yaw should always be between -180° and 180°, the relative pitch should always be between -90° and 90°, and there should be no flipping when applying the relative pitch first and then relative yaw on the camera rotation. – Allen Chou Oct 21 '18 at 04:29
  • Please bear with me, i don't get it. I started a fiddle with what i think you meant in your last comment: https://dotnetfiddle.net/zosl5X – sonority Oct 21 '18 at 11:11