RotateAround with deltaRotation and rotation snapping causes angle drift over time

Hello.

I’ve been bashing my brain against this issue for about four hours now and I’m not making any progress, so I thought I would see if there is an obvious solution here. I have a method I wrote to RotateAround a transform:

private Vector3 SnappedRotationDelta(DragData drag, Vector3 rotationDelta) {
        if (!settingsController.SnapEnabled()) return rotationDelta;
        Vector3 start = drag.rotationDelta;
        Vector3 total = start + rotationDelta;
        float rotationSnapIncrements = settingsController.rotationSnapIncrements;

        Vector3 totalSnapped = Vector3.zero;
        totalSnapped.x = Mathf.Round(total.x / rotationSnapIncrements) * rotationSnapIncrements;
        totalSnapped.y = Mathf.Round(total.y / rotationSnapIncrements) * rotationSnapIncrements;
        totalSnapped.z = Mathf.Round(total.z / rotationSnapIncrements) * rotationSnapIncrements;

        Vector3 snappedRotationDelta = Vector3.zero;
        snappedRotationDelta.x = totalSnapped.x - drag.snappedRotationDelta.x;
        snappedRotationDelta.y = totalSnapped.y - drag.snappedRotationDelta.y;
        snappedRotationDelta.z = totalSnapped.z - drag.snappedRotationDelta.z;

        drag.snappedRotationDelta = totalSnapped;
        drag.rotationDelta += rotationDelta;
        return snappedRotationDelta;
    }

private void RotateAroundTransform(DragData drag) {
        // In order to add two quaternion rotations correctly you multiply them.
        // To subtract you need to multiply by the inverse.
        Vector3 rotationDelta = (drag.transform.rotation * Quaternion.Inverse(drag.lastControllerRotation)).eulerAngles;
        Vector3 oldPosition = transform.position;

        Vector3 snappedRotationDelta = SnappedRotationDelta(drag, rotationDelta);
        transform.RotateAround(drag.transform.position, Vector3.right, snappedRotationDelta.x);
        transform.RotateAround(drag.transform.position, Vector3.up, snappedRotationDelta.y);
        transform.RotateAround(drag.transform.position, Vector3.forward, snappedRotationDelta.z);

        drag.preDragPosition = transform.position + drag.preDragPosition - oldPosition;
        drag.lastControllerRotation = drag.transform.rotation;
    }

In this case, drag.transform is a VR touch controller and transform.position is some object I’m dragging and rotating. I expect this object to maintain it’s distance and position in relation to the controller while being dragged and rotated.

This works fine for a little while, but if you rotate the object enough, it seems like errors accumulate and the snapped rotations are a few degrees off.

I tried to figure out how to do this by recording the initial position and rotate of the controller and object, in an attempt to get around the drift issue, but I think I ran into gimbal lock or something because my rotations ended up being constrained.

Any ideas?

Yes, errors will accumulate with the way you’re doing it here, because you have no independent measure of the object’s rotation. You’re just telling it to rotate, based on its current orientation.

Instead what you should do is keep your own rotation properties — cumulative Euler angles or whatever. Update these, snap them however you need to, and then set your object’s rotation directly from this data. In this, way, there is nowhere for errors to accumulate — you get exactly what your own data says you should get, no matter how long you do it.

Ok, that’s what I thought too, so I tried caching the position and rotation at the start of the drag and building my new position and rotation from there. This is what I came up with:

    private Vector3 SnappedRotationDelta(Vector3 rotationDelta) {
        if (!settingsController.SnapEnabled()) return rotationDelta;
        float rotationSnapIncrements = settingsController.rotationSnapIncrements;

        Vector3 snappedRotationDelta = Vector3.zero;
        snappedRotationDelta.x = Mathf.Round(rotationDelta.x / rotationSnapIncrements) * rotationSnapIncrements;
        snappedRotationDelta.y = Mathf.Round(rotationDelta.y / rotationSnapIncrements) * rotationSnapIncrements;
        snappedRotationDelta.z = Mathf.Round(rotationDelta.z / rotationSnapIncrements) * rotationSnapIncrements;
        return snappedRotationDelta;
    }

    private void RotateAroundTransform(DragData drag) {
        // In order to add two quaternion rotations correctly you multiply them.
        // To subtract you need to multiply by the inverse.
        Vector3 rotationDelta = (drag.transform.rotation * Quaternion.Inverse(drag.startTransformRotation)).eulerAngles;

        transform.rotation = drag.startRotation;
        Vector3 snappedRotationDelta = SnappedRotationDelta(rotationDelta);
        transform.RotateAround(drag.transform.position, Vector3.right, snappedRotationDelta.x);
        transform.RotateAround(drag.transform.position, Vector3.up, snappedRotationDelta.y);
        transform.RotateAround(drag.transform.position, Vector3.forward, snappedRotationDelta.z);
    }

This code works ok for the Z and Y axis, but I can’t rotate past a certain number of degrees in either direction on the X axis. Everything I read seems to indicate this is Gimbal Lock, but I thought RotateAround was immune to Gimbal Lock?

Edit:

Hmmm… switching the order so Y is performed first seems to work around the gimbal lock:

        transform.RotateAround(drag.transform.position, Vector3.up, snappedRotationDelta.y);
        transform.RotateAround(drag.transform.position, Vector3.right, snappedRotationDelta.x);
        transform.RotateAround(drag.transform.position, Vector3.forward, snappedRotationDelta.z);

But you’re still calling transform.RotateAround. Technically it should work the way you’re doing it (since you reset to drag.startRotation every frame), but still… I would recommend instead just setting the rotation to Quaternion.Euler (or if you really must change the standard order of rotations, then a product of Quaternion.Eulers).

This would require that you don’t just “cache” the rotation at the start of the drag… you actually keep the xRot, yRot, and zRot values, all the time, forever and ever (since Start). This is no big deal, and is (in my experience, anyway) the standard way of handling this.

What you’re doing now is better because you’re only accumulating a little error once per drag instead of once per frame… but eventually, it will still add up (unless I misunderstand how your snapping works, which is entirely possible).

I understand what you’re saying about errors accumulating. However, I’m not sure how to perform a rotate around without rotate around.

The object I’m rotating is a vertex, so it’s initial rotation is always Quaternion.identity in local space. However, I’m currently doing these transforms in world space. In world space, the rotation is probably the rotation of the mesh. I’m going to have to think about this and see if I can reproduce the error accumulation with the current code. This is complex.

I think you’re making harder than it is. All I’m saying is, you keep track of the rotation yourself, update your own rotation properties, and then just set the rotation of the object. Something like this:

public class RotDemo : MonoBehaviour {
    public float xRot = 0;

    void Update() {
        if (Input.GetKeyDown(KeyCode.R)) xRot += 60 * Time.deltaTime;  // or whatever

        transform.localRotation = Quaternion.Euler(xRot, 0, 0);
    }
}

That’s only one rotation axis, but you could extend it to all three (and add whatever snapping behavior you need) in the obvious way.