旋转/四元数约束

4

我正在研究VR交互,希望能够旋转物体,但我遇到了问题。

例如,我想在VR中使用手打开/关闭笔记本电脑的上部。为了实现这一点,我将前置摆放如下:

laptopForward

我正在使用位置、前向和上方创建一个飞机。然后获取与我的VR控制器对应的平面上最接近的点,然后使用transform.LookAt。这个方法很好用,但我想能够限制旋转,以便不能旋转太多(请参见视频结尾)。我尝试了一切,使用eulersAngle和Quaternion,但我无法做到。我制作了一些助手(用于显示localEulerAngles的文本和LookAt的变换,因此我不必使用VR头戴式耳机,因为它变得非常乏味)。这里是一个视频展示了正在发生的事情: https://www.youtube.com/watch?v=UfN97OpYElk。这是我的代码:
using UnityEngine;

public class JVRLookAtRotation : MonoBehaviour, IJVRControllerInteract
{

    [SerializeField] private Transform toRotate;

    [SerializeField] private Vector3 minRotation;
    [SerializeField] private Vector3 maxRotation;

    [Header("Rotation contraints")]
    [SerializeField] private bool lockX;
    [SerializeField] private bool lockY;
    [SerializeField] private bool lockZ;

    private JVRController _jvrController;
    private bool _isGrabbed;
    private Vector3 _targetPosition;
    private Vector3 _tmp;

    public Transform followTransform;

    private void LateUpdate()
    {

        /*
        if (!_isGrabbed) return;

        if (_jvrController.Grip + _jvrController.Trigger < Rules.GrabbingThreshold)
        {
            _isGrabbed = false;
            _jvrController.StopGrabbing();
            _jvrController = null;
            return;
        }

        */

        Vector3 up = toRotate.up;
        Vector3 forward = toRotate.forward;
        Vector3 pos0 = toRotate.position;
        Vector3 pos1 = pos0 + up;
        Vector3 pos2 = pos0 + forward;
        Plane p = new Plane(pos0, pos1, pos2);

        // Using followTransform just to no have to use VR, otherwise it's the controller pos
        _targetPosition = p.ClosestPointOnPlane(followTransform.position);

        toRotate.LookAt(_targetPosition, up);

        /*
        _tmp = toRotate.localEulerAngles;
        _tmp.x = Mathf.Clamp(WrapAngle(_tmp.x), minRotation.x, maxRotation.x);
        _tmp.y = WrapAngle(_tmp.y);
        _tmp.z = WrapAngle(_tmp.z);
        toRotate.localRotation = Quaternion.Euler(_tmp);
        */

    }

    public void JVRControllerInteract(JVRController jvrController)
    {
        if (_isGrabbed) return;
        if (!(jvrController.Grip + jvrController.Trigger > Rules.GrabbingThreshold)) return;

        _jvrController = jvrController;
        _jvrController.SetGrabbedObject(this);
        _isGrabbed = true;
    }

    private static float WrapAngle(float angle)
    {
        angle%=360;
        if(angle >180)
            return angle - 360;

        return angle;
    }

    private static float UnwrapAngle(float angle)
    {
        if(angle >=0)
            return angle;

        angle = -angle%360;

        return 360-angle;
    }
}

是的,我尝试了很多类似于两个答案中所述的方法。最大的问题是(您可以在我的视频中看到),当我到达“顶部”(前方指向Vector3.up时),Y和Z都从0变为180,X按相反的顺序变化,这破坏了我的夹紧。四元数并不容易理解 :x - Jichael
也许我只需要做一个Rotate()或其他什么,而不是先LookAt再尝试夹紧。但我不知道如何实现这一点,以便我可以按照自己的需求进行操作。(理想情况下,我可以使用3个布尔值“Lock”,以便我可以选择在哪个轴上旋转/不能旋转,但是如果我找到单个轴的解决方案,那仍然很好。) - Jichael
2个回答

2
假设显示器的父变换是笔记本电脑的主体/键盘。下面显示了父对象的本地轴:

local axes of parent

描述运动范围时,可以定义一个相对于父级的“旋转中心”向量(例如,标记为C的灰色向量),以及一个角度(例如,每个紫色向量与灰色向量之间的110度)。例如:
[SerializeField] private Vector3 LocalRotationRangeCenter = new Vector3(0f, 0.94f, 0.342f);
[SerializeField] private float RotationRangeExtent = 110f;

然后,您可以获取它“想要”前往的向量,并找到RotationRangeCenter的世界方向与该点之间的有符号角度,然后将其限制在±RotationRangeExtent之间。
Vector3 worldRotationRangeCenter = toRotate.parent.TransformDirection(RotationRangeCenter);

Vector3 targetForward = _targetPosition - toRotate.position;

float targetAngle = Vector3.SignedAngle(worldRotationRangeCenter, targetForward, 
        toRotate.right);

float clampedAngle = Mathf.Clamp(targetAngle, -RotationRangeExtent, RotationRangeExtent);

首先,找到对应于该角度的方向。最后,旋转显示器,使其前方与夹紧前方对齐,而右侧不变。您可以使用叉积找到显示器的上方向,然后使用Quaternion.LookRotation查找相应的旋转:
Vector3 clampedForward = Quaternion.AngleAxis(clampedAngle, toRotate.right)
        * worldRotationRangeCenter;

toRotate.rotation = Quaternion.LookRotation(clampedForward, 
        Vector3.Cross(clampedForward, toRotate.right));

如果有人试图将显示器拖得太远超出“边界”,它将从一个极限瞬间传送到另一个极限。如果这不是期望的行为,您可以考虑在限制之间进行插值运算,从 SignedAngle(worldRotationRangecenter, targetForward, toRotate.right)clampedAngle 进行移动。
private float angleChangeLimit = 90f; // max angular speed

// ...

Vector3 worldRotationRangeCenter = toRotate.parent.TransformDirection(RotationRangeCenter);

Vector3 targetForward = _targetPosition - toRotate.position;

float targetAngle = Vector3.SignedAngle(worldRotationRangeCenter, targetForward, 
        toRotate.right);

float clampedAngle = Mathf.Clamp(targetAngle, -RotationRangeExtent, RotationRangeExtent);

float currentAngle = Vector3.SignedAngle(worldRotationRangeCenter, toRotate.forward, 
        toRotate.right);

clampedAngle = Mathf.MoveTowards(currentAngle, clampedAngle, 
        angleChangeLimit * Time.deltaTime);

Vector3 clampedForward = Quaternion.AngleAxis(clampedAngle, toRotate.right)
        * worldRotationRangeCenter;

toRotate.rotation = Quaternion.LookRotation(clampedForward, 
        Vector3.Cross(clampedForward, toRotate.right));

非常感谢你的回答!我尝试实现了一下,但完全不起作用。我会继续研究并在这里报告。只有一个问题:这些是从哪里来的:private Vector3 LocalRotationRangeCenter = new Vector3(0f, 0.999f, 0.044f);私有浮点数RotationRangeExtent=110f;?当您使用TransformDirection时,它是否只是拼写错误,应该使用LocalRotationRangeCenter? - Jichael
好的,现在它可以工作了!但我仍然不知道LocalRotationRangeCenter是从哪里来的。 - Jichael
你必须找到适合你目的的一个。它应该是相对于父变换局部运动范围的中间方向。 - Ruzihm
@jichael,我的示例中的值来自一个错误。它应该是LocalRotationRangeCenter = new Vector3(0f, 0.94f, 0.342f);,这是(0,cos(20),sin(20))的近似值,因为我希望中间方向位于+y,+z象限,并且距离y轴20度。我已经编辑了答案以反映这个变化。 - Ruzihm
@Jichael 如果它被固定在桌子上,一个好的值可能是 Vector3.up,并且 RotationRangeExtent = 90f; - Ruzihm
不,它运行得很好,我只是微调了minRotation/maxRotation,以便它符合我的要求。总之一切都很好,再次感谢! - Jichael

0

@Ruzihm的答案只需要稍微调整一下就可以运行了!老实说,我自己不可能做到。

以下是为VR更新的完整代码,如有兴趣,请参考:

using UnityEngine;

public class JVRLookAtRotation : MonoBehaviour, IJVRControllerInteract
{

    [SerializeField] private Transform toRotate;

    [SerializeField] private Vector3 minRotationDelta;
    [SerializeField] private Vector3 maxRotationDelta;

    private JVRController _jvrController;
    private bool _isGrabbed;
    private Vector3 _targetPosition;

    // No clue where does this come from
    private Vector3 _localRotationRangeCenter = new Vector3(0, 0.999f, 0.044f);

    private void LateUpdate()
    {

        if (!_isGrabbed) return;

        if (_jvrController.Grip + _jvrController.Trigger < Rules.GrabbingThreshold)
        {
            _isGrabbed = false;
            _jvrController.StopGrabbing();
            _jvrController = null;
            return;
        }

        Vector3 up = toRotate.up;
        Vector3 forward = toRotate.forward;
        Vector3 right = toRotate.right;
        Vector3 rotatePosition = toRotate.position;
        Vector3 pos1 = rotatePosition + up;
        Vector3 pos2 = rotatePosition + forward;
        Plane p = new Plane(rotatePosition, pos1, pos2);
        _targetPosition = p.ClosestPointOnPlane(_jvrController.CurrentPositionWorld);

        Vector3 worldRotationRangeCenter = toRotate.parent.TransformDirection(_localRotationRangeCenter);
        Vector3 targetForward = _targetPosition - rotatePosition;

        float targetAngle = Vector3.SignedAngle(worldRotationRangeCenter, targetForward, right);

        float clampedAngle = Mathf.Clamp(targetAngle, minRotationDelta.x, maxRotationDelta.x);

        Vector3 clampedForward = Quaternion.AngleAxis(clampedAngle, right) * worldRotationRangeCenter;

        toRotate.rotation = Quaternion.LookRotation(clampedForward, Vector3.Cross(clampedForward, right));

    }

    public void JVRControllerInteract(JVRController jvrController)
    {
        if (_isGrabbed) return;
        if (!(jvrController.Grip + jvrController.Trigger > Rules.GrabbingThreshold)) return;

        _jvrController = jvrController;
        _jvrController.SetGrabbedObject(this);
        _isGrabbed = true;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接