Quaternion Lerp and Gimbal Lock(?)

Hi, I’m having some trouble with quaternion lerp.

I’m simply drawing 2 rotation gizmos, which the user can rotate, and then drawing the resulting rotation in the middle.

I want the interpolated rotation to always be half way between the two (so t = 0.5). Here’s the code:

_interpolatedRot = Quaternion.Lerp(_fromRot, _toRot, 0.5f);

When an axis from _fromRot lines up with one from _toRot, it gimbal locks and the interpolated rotation flips. I think understand why this happens (although I’m not sure why people say quaternion lerp doesn’t have this problem, maybe I’m misunderstanding some terminology).

I’ve tried doing the rotations with quaternions, euler angles, rotation towards, LerpAngle and everything in between, but I just can’t seem to get it to work. I think the problem lies in knowing which direction the handle was rotated and rotating about an axis, but I just can’t seem to make it work. Can someone help explain how to get this right?

using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public class GizmoRenderer : EditorWindow
{
    static float _handleSize = 1.0f;
    static float _t = 0.5f;

    static Vector3 _fromPos = new Vector3(-5.0f, 0.0f, 0.0f);
    static Quaternion _fromRot = Quaternion.identity;

    static Vector3 _toPos = new Vector3(5.0f, 0.0f, 0.0f);
    static Quaternion _toRot = Quaternion.identity;

    static Vector3 _interpolatedPos = new Vector3(0.0f, 0.0f, 0.0f);
    static Quaternion _interpolatedRot = Quaternion.identity;

    [UnityEditor.Callbacks.DidReloadScripts]
    static GizmoRenderer()
    {
#if UNITY_2019_1_OR_NEWER
        SceneView.duringSceneGui -= OnScene;
        SceneView.duringSceneGui += OnScene;
#else
        SceneView.onSceneGUIDelegate -= OnScene;
        SceneView.onSceneGUIDelegate += OnScene;
#endif
    }

    [MenuItem("Tools/Gizmo Renderer")]
    public static void ShowWindow()
    {
        GizmoRenderer window = (GizmoRenderer)EditorWindow.GetWindow(typeof(GizmoRenderer));
        window.titleContent = new GUIContent("Gizmo Renderer");
        window.Show();
    }

    //------------------------------------------------------
    private static void OnScene(SceneView sceneView)
    {
        _fromRot = RotationHandle(_fromPos, _fromRot, _handleSize);
        DrawAxis(_fromPos, _fromRot, _handleSize); // Just debug, helps vis orientation

        _toRot = RotationHandle(_toPos, _toRot, _handleSize);
        DrawAxis(_toPos, _toRot, _handleSize); // Just debug, helps vis orientation

        // Interpolate

        _interpolatedRot = Quaternion.Lerp(_fromRot, _toRot, 0.5f);    // ** Requirement ** t must remain constant
        DrawAxis(_interpolatedPos, _interpolatedRot, _handleSize);
    }

    //--------------------------------------------------------------
    public static void DrawAxis(Vector3 pos, Quaternion rot, float handleSize)
    {
        Color prevColor = Handles.color;
        Handles.color = Color.green;
        Handles.ArrowHandleCap(0, pos, Quaternion.LookRotation(rot * Vector3.up), handleSize, EventType.Repaint);
        Handles.color = Color.red;
        Handles.ArrowHandleCap(0, pos, Quaternion.LookRotation(rot * Vector3.right), handleSize, EventType.Repaint);
        Handles.color = Color.blue;
        Handles.ArrowHandleCap(0, pos, Quaternion.LookRotation(rot * Vector3.forward), handleSize, EventType.Repaint);
        Handles.color = prevColor;
    }

    //--------------------------------------------------------------
    public static Quaternion RotationHandle(Vector3 position, Quaternion rotation, float handleSize)
    {
        Color color = Handles.color;
        Handles.color = Handles.xAxisColor;
        rotation = Handles.Disc(rotation, position, rotation * Vector3.right, handleSize, true, EditorPrefs.GetFloat("RotationSnap"));
        Handles.color = Handles.yAxisColor;
        rotation = Handles.Disc(rotation, position, rotation * Vector3.up, handleSize, true, EditorPrefs.GetFloat("RotationSnap"));
        Handles.color = Handles.zAxisColor;
        rotation = Handles.Disc(rotation, position, rotation * Vector3.forward, handleSize, true, EditorPrefs.GetFloat("RotationSnap"));
        Handles.color = Handles.centerColor;
        rotation = Handles.Disc(rotation, position, Camera.current.transform.forward, handleSize * 1.1f, false, 0.0f);

        rotation = Handles.FreeRotateHandle(rotation, position, handleSize);
        Handles.color = color;

        return rotation;
    }

    //------------------------------------------------------
    void OnGUI()
    {
        EditorGUI.BeginChangeCheck();
        _t = EditorGUILayout.Slider("t", _t, 0.0f, 1.0f);
        _handleSize = EditorGUILayout.Slider("Handle Size", _handleSize, 1.0f, 10.0f);

        if (EditorGUI.EndChangeCheck())
            SceneView.RepaintAll();
    }
}

Try

_interpolatedRot = Quaternion.Slerp(_fromRot, _toRot, 0.5f);

Unfortunately Slerp has the same effect

I’m not quite sure what your question is. A quaternion essentially has a max angle of 180°. So your rotation looks right. There can not be any kind of gimbal lock with pure quaternions since quaterions do not have any gimbals. You would only introduce such issues when you use euler angles at some point. Euler angles fundamentally represent 3 gimbals. Quaternions define a 3d rotation as one single rotation around a single axis.

It’s possible to do a slerp the “long way” when you slerp manually because Unity’s Slerp will always rotate the shortest path. See my implementation over here.

1 Like

I agree, the rotations are right, just not what I wanted. The behavior I wanted is what you pointed me to, god bless you sir