You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity橱柜门交互:全局坐标转局部坐标代码适配问题

橱柜门VR交互适配任意橱柜朝向的解决方案

在Unity(C#)VR厨房场景中实现橱柜门跟随手部开合的交互时,原脚本仅在橱柜坐标轴与世界坐标轴完全对齐时生效——抽屉需要橱柜Forward与世界Forward一致,柜门需要橱柜Y轴无倾斜才能正常工作。

原问题代码

public class DoorPull : XRBaseInteractable
{
    [Header("Required variables")]
    public GameObject door;
    public float openDegree = 90f;
    public float startingOpenDegree;
    public float handOffset = 90f;
    public bool hingeRight;

    private Vector3 closedRotation;
    private Vector3 openRotation;
    private IXRSelectInteractor hand;

    // Start is called before the first frame update
    void Start()
    { 
        closedRotation = door.transform.localRotation.eulerAngles;

        //sets the closed and max open positions.
        openRotation = closedRotation;
        if (hingeRight)
        {
            openRotation.y = closedRotation.y - openDegree;
        } 
        else
        {
            openRotation.y = closedRotation.y + openDegree;
        }

        //makes sure the door can't start further open than its max open position.
        if (startingOpenDegree > openDegree)
        {
            startingOpenDegree = openDegree;
        }

        //starts the door open at users starting open degree
        var startingOpenRotation = closedRotation;
        if (hingeRight)
        {
            startingOpenRotation.y -= startingOpenDegree; 
        }
        else
        {
            startingOpenRotation.y += startingOpenDegree;
        }
        door.transform.localRotation = Quaternion.Euler(startingOpenRotation);
    }

//------------------------above this line works no matter the position or rotation of the cabinet, below this line must be aligned with world y axis to work------------------------------

    // Update is called once per frame
    void Update()
    {
        if (interactorsSelecting.Count > 0)
        {
            hand = interactorsSelecting[0];
            Vector3 lookHere = hand.transform.position;
            lookHere.y = door.transform.position.y;
            door.transform.LookAt(lookHere, door.transform.up);
            var temp = door.transform.rotation.eulerAngles;
            
            if (hingeRight)
            {
                temp.y -= handOffset;
                //will have code to stop in open and closed positions
            }
            else
            {
                temp.y += handOffset;
                //will have code to stop in open and closed positions
            }

            door.transform.rotation = Quaternion.Euler(temp);
        }
    }
}

解决方案

核心思路是在橱柜的局部空间内计算手部相对位置与旋转,彻底摆脱世界坐标轴对齐的限制。以下是修改后的Update方法及关键说明:

修改后的Update方法

// Update is called once per frame
void Update()
{
    if (interactorsSelecting.Count > 0)
    {
        hand = interactorsSelecting[0];
        // 获取橱柜(柜门父物体)的Transform,确保计算基于橱柜局部空间
        Transform cabinetTransform = door.transform.parent;
        if (cabinetTransform == null)
        {
            cabinetTransform = door.transform;
        }

        // 将手部世界位置转换为橱柜局部空间位置
        Vector3 localHandPos = cabinetTransform.InverseTransformPoint(hand.transform.position);
        // 忽略局部Y轴高度,仅保留XZ平面位置(对应柜门开合的水平方向)
        localHandPos.y = cabinetTransform.InverseTransformPoint(door.transform.position).y;

        // 计算局部空间内柜门指向手部的水平方向
        Vector3 localLookDir = localHandPos - cabinetTransform.InverseTransformPoint(door.transform.position);
        localLookDir.y = 0;
        if (localLookDir.magnitude < 0.01f) return; // 避免方向为零时的异常

        // 转换为局部空间下的旋转(围绕局部Y轴)
        Quaternion targetLocalRot = Quaternion.LookRotation(localLookDir, cabinetTransform.up);
        // 应用手部偏移,调整旋转对齐
        if (hingeRight)
        {
            targetLocalRot *= Quaternion.Euler(0, -handOffset, 0);
        }
        else
        {
            targetLocalRot *= Quaternion.Euler(0, handOffset, 0);
        }

        // 处理欧拉角0-360循环问题,转换为-180~180范围便于角度限制
        float currentYRot = targetLocalRot.eulerAngles.y;
        if (currentYRot > 180) currentYRot -= 360;

        // 确定旋转角度的上下限(基于闭合状态和最大打开角度)
        float minYRot, maxYRot;
        if (hingeRight)
        {
            minYRot = closedRotation.y - openDegree;
            maxYRot = closedRotation.y;
        }
        else
        {
            minYRot = closedRotation.y;
            maxYRot = closedRotation.y + openDegree;
        }
        // 同样转换为-180~180范围,避免角度循环导致的限制错误
        if (minYRot > 180) minYRot -= 360;
        if (maxYRot > 180) maxYRot -= 360;

        // 限制旋转角度在合法范围内
        float clampedYRot = Mathf.Clamp(currentYRot, Mathf.Min(minYRot, maxYRot), Mathf.Max(minYRot, maxYRot));
        // 构建最终局部旋转,保持X、Z轴与初始闭合状态一致(避免不必要倾斜)
        Quaternion finalLocalRot = Quaternion.Euler(closedRotation.x, clampedYRot, closedRotation.z);

        // 应用局部旋转,适配任意橱柜朝向
        door.transform.localRotation = finalLocalRot;
    }
}

关键修改说明

  • 局部空间转换:使用cabinetTransform.InverseTransformPoint将手部世界坐标转为橱柜局部坐标,确保计算不受世界坐标轴影响,适配任意橱柜旋转状态。
  • 水平方向约束:忽略局部Y轴高度,仅在XZ平面计算手部方向,保证柜门开合始终沿水平方向进行。
  • 欧拉角循环处理:将0-360范围的欧拉角转换为-180~180,避免角度循环导致的限制逻辑错误。
  • 旋转范围限制:严格约束柜门旋转角度在闭合到最大打开角度之间,防止过度旋转。
  • 局部旋转应用:最终使用localRotation设置柜门旋转,确保旋转基于橱柜的局部坐标系,而非世界坐标系。

内容的提问来源于stack exchange,提问作者Jeremy Gillmore

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 22:05:13