如何使用 LookRotation 根据手的位置旋转对象?

How to use LookRotation to rotate an object based on hand positions?

我正在为 VR 游戏创建一个交互系统,但我正在为双手交互而苦苦挣扎。我正在使用 Quaternion.LookRotation() 方法根据手的位置和旋转生成抓取对象的旋转。前向部分非常简单:

Vector3 fwd = (primaryHand.position - secondaryHand.position).normalized;

“向上”部分是我遇到的困难。最初我尝试使用双手的平均向上方向:

Vector3 avgUp = Quaternion.Slerp(primaryHand.rotation, secondaryHand.rotation, 0.5f) * Vector3.up;

这种方法存在一个问题:手的向上向量可能与 fwd 向量对齐,这会导致对象在越过它时翻转。这是问题的简单说明: 浅绿色箭头表示手的向上方向,而深绿色是用作 LookRotation() 方法参数的计算方向。

显而易见的解决方案似乎是选择一个不同的固定向量而不是“向上”,这样一个向量不会很容易与 fwd 向量对齐。在示例中,它可以是与手指对齐的矢量。但请记住,手的初始旋转没有限制,因此无论您选择哪个向量,手总是会发生旋转,以便向量对齐。即使您动态地选择一个最佳矢量(一个垂直于 fwd 的矢量),它仍然最多与 fwd.

对齐 90 度

为了解决这个问题,我尝试将方向限制为不会导致问题的值,但这导致了另一个问题(我很难确定哪些值是正确的,哪些应该被丢弃)。我觉得我在这里做错了,有没有更好的解决方案?

您可以计算手的旋转增量并将其应用于对象的“基础向上”(如果我们只考虑手的位置变化,则新的向上......当然是正交于物体的轴)。然后确定导致每只手向上旋转的角度变化。平均这些角度,然后使用 Quaternion.AngleAxis 将这些角度与 hand-hand 轴应用到之前的“基础向上”。然后你有你的前进和向上 Quaternion.LookRotation).

下面是如何使用它的示例,包括 VR 手部噪音模拟。要查看测试,请统一创建一个新场景并将其附加到相机,它将在播放开始时构建场景。有一个 grip/release gui 将出现在播放视图中。您可以在场景视图中调整手部旋转

    Vector3 leftHandPosCurrent;
    Vector3 rightHandPosCurrent;

    Vector3 worldAxisPrev;

    Quaternion leftHandRotPrev;
    Quaternion leftHandRotCurrent;

    Quaternion rightHandRotPrev;
    Quaternion rightHandRotCurrent;

    bool isGripping;
    bool firstFrameGripping;

    Rigidbody grippedRB;

    Transform leftHand;
    Transform rightHand;

    Quaternion targetRot;

    /* 
     * On subsequent frames of gripping, calculate deltas in positions and
     * rotations, average out the hand's effects, then apply them to the gripped
     * object
     */
    void HandleGrippedRot()
    {
        Vector3 worldAxisCurrent = rightHandPosCurrent - leftHandPosCurrent;

        if (!firstFrameGripping)
        {
            Vector3 prevUp = targetRot * Vector3.up;
            // we haven't moved the transform based on the hands yet, so 
            // find the new up would be ignoring hand rotations
            Vector3 newUp = Quaternion.FromToRotation(worldAxisPrev,
                    worldAxisCurrent) * prevUp;

            float leftHandAngle = GetDegRot(newUp, leftHandRotPrev,
                    leftHandRotCurrent, worldAxisCurrent);
            float rightHandAngle = GetDegRot(newUp, rightHandRotPrev,
                    rightHandRotCurrent, worldAxisCurrent);
            float avgAngle = (leftHandAngle + rightHandAngle) * 0.5f;

            newUp = Quaternion.AngleAxis(avgAngle, worldAxisCurrent) * prevUp;

            targetRot = Quaternion.LookRotation(worldAxisCurrent,
                    newUp);
        }
        else
        {
            firstFrameGripping = false;
        }

        leftHandRotPrev = leftHandRotCurrent;
        rightHandRotPrev = rightHandRotCurrent;

        worldAxisPrev = worldAxisCurrent;
    }

    /*
     * Given the "up" of the object without taking hand rotations into account
     * and the axis, determine how a hand's delta rotation affects that up 
     * around the axis and return the angle of that rotation 
     */
    float GetDegRot(Vector3 baseUp, Quaternion prevHandRot, Quaternion curHandRot,
            Vector3 axis)
    {
        Vector3 adjUp = (curHandRot * Quaternion.Inverse(prevHandRot)) * baseUp;
        adjUp = Vector3.ProjectOnPlane(adjUp, axis);

        return Vector3.SignedAngle(baseUp, adjUp, axis);
    }

    void Update()
    {
        AddVRNoise(leftHand);
        AddVRNoise(rightHand);

        leftHandPosCurrent = leftHand.position;
        rightHandPosCurrent = rightHand.position;

        leftHandRotCurrent = leftHand.rotation;
        rightHandRotCurrent = rightHand.rotation;


        if (isGripping)
        {
            HandleGrippedRot();
        }
    }

    void StartGrip()
    {
        if (isGripping) return;
        isGripping = true;
        firstFrameGripping = true;
        // grippedTransform is set accordingly at some point
    }

    void EndGrip()
    {
        if (!isGripping) return;
        isGripping = false;
    }

    /*
     * example of using targetRot to move rb
     */
    private void FixedUpdate()
    {
        if (!isGripping) return;

        Quaternion delta = targetRot
                * Quaternion.Inverse(grippedRB.transform.rotation);

        delta.ToAngleAxis(out float angle, out Vector3 axis);

        // convert to shortest angle form
        if (angle > 180f)
        {
            axis = -axis; angle = 360f - angle;
        }

        grippedRB.angularVelocity = angle * 0.25f * axis;
    }

    /*
     * just for testing purposes
     */
    void Start()
    {
        leftHand = CreateHand(true);
        leftHand.position = Vector3.left;

        rightHand = CreateHand(false);
        rightHand.position = Vector3.right;

        CreateArrow();
    }

    /*
     * just for testing purposes
     */
    void AddVRNoise(Transform hand)
    {
        Quaternion noise = Random.rotation;
        noise.ToAngleAxis(out float angle, out Vector3 axis);
        angle = 100f * Time.deltaTime;
        noise = Quaternion.AngleAxis(angle, axis);

        Quaternion noisyRot = hand.rotation * noise;
        hand.rotation = noisyRot;
    }

    /*
     * just for testing purposes
     */
    void OnGUI()
    {
        if (GUI.Button(new Rect(0, 0, 100, 50), "Grip"))
        {
            StartGrip();
        }

        if (GUI.Button(new Rect(100, 0, 100, 50), "Release"))
        {
            EndGrip();
        }
    }


    /*
     * just for testing purposes
     */
    Transform CreateHand(bool isLeft)
    {
        string handName = isLeft ? "Left" : "Right";
        GameObject hand = new GameObject($"{handName}hand");
        GameObject palm = GameObject.CreatePrimitive(PrimitiveType.Cube);
        palm.transform.localScale = new Vector3(0.5f, 0.2f, 1f);
        palm.transform.SetParent(hand.transform);
        GameObject thumb = GameObject.CreatePrimitive(PrimitiveType.Cube);
        thumb.transform.localScale = new Vector3(0.2f, 0.1f, 0.1f);
        thumb.transform.SetParent(hand.transform);
        thumb.transform.localPosition = new Vector3(isLeft ? 0.32f : -0.32f,
                0f, -.31f);

        return hand.transform;
    }

    /*
     * just for testing purposes
     */
    void CreateArrow()
    {
        GameObject arrow = new GameObject();
        GameObject body = GameObject.CreatePrimitive(PrimitiveType.Cube);
        body.transform.localScale = new Vector3(1f, 1f, 5f);
        body.transform.SetParent(arrow.transform);
        GameObject head = GameObject.CreatePrimitive(PrimitiveType.Cube);
        head.transform.SetParent(arrow.transform);
        head.transform.localEulerAngles = Vector3.up * 45f;
        head.transform.localPosition = new Vector3(0f, 0f, 2.5f);

        grippedRB = arrow.AddComponent<Rigidbody>();
        grippedRB.useGravity = false;

        arrow.transform.position = 2f * Vector3.up;
    }