PICO Unity空间锚点管理

PICO Unity空间锚点管理

锚点创建、删除、持久化、应用、轴向锁定等功能;

视频展示

PICOXR锚点管理

代码

csharp 复制代码
using UnityEngine;
using Unity.XR.PXR;

/// <summary>
/// 使能PICO视频透视
/// </summary>
public class EnableVideoSeeThrough : MonoBehaviour
{
    private void Start()
    {
        // 监听透视功能状态
        PXR_Manager.VstDisplayStatusChanged += VstDisplayStatusChanged;
        // 开启视频透视
        EnableVideoSeeThroughOrNo(true);
    }

    //Re-enable seethrough after the app resumes
    private void OnApplicationPause(bool pause)
    {
        if (!pause)
            PXR_Manager.EnableVideoSeeThrough = true;
    }

    private void OnDestroy()
    {
        // 取消监听
        PXR_Manager.VstDisplayStatusChanged -= VstDisplayStatusChanged;
    }

    /// <summary>
    /// 开启或关闭视频透视功能
    /// </summary>
    /// <param name="enable"></param>
    void EnableVideoSeeThroughOrNo(bool enable)
    {
        PXR_Manager.EnableVideoSeeThrough = enable;
    }

    private void VstDisplayStatusChanged(PxrVstStatus status)
    {
        switch (status)
        {
            case PxrVstStatus.Disabled: // 已关闭
                Debug.Log("视频透视已关闭");
                break;
            case PxrVstStatus.Enabling: // 开启中
                Debug.Log("视频透视正在开启...");
                break;
            case PxrVstStatus.Enabled: // 已开启
                Debug.Log("视频透视已开启");
                break;
            case PxrVstStatus.Disabling: // 关闭中
                Debug.Log("视频透视正在关闭...");
                break;
        }
    }
}
csharp 复制代码
using UnityEngine;
using UnityEngine.UI;
using Unity.XR.PXR;
using UnityEngine.XR.Interaction.Toolkit;
using System.Collections.Generic;
using UnityEngine.XR.Interaction.Toolkit.Transformers;

public class PXRSample_SpatialAnchor : MonoBehaviour
{
    // XR 基础交互对象
    private XRBaseInteractable interactable;
    private XRGrabInteractable grabInteractable;
    private PXRSample_SpatialAnchorRotationConstraintGrabTransformer rotationConstraintGrabTransformer;

    // 锚点句柄,唯一标识一个空间锚点
    [HideInInspector]
    public ulong anchorHandle;

    // UI 显示锚点 ID 的文本
    [SerializeField]
    private Text txtAnchorID, txtCreatedIsOrNo, txtPersistedIsOrNo, txtAppliedIsOrNo;
    

    [SerializeField] private GameObject previewObject; // 预览对象
    [SerializeField] private GameObject toggleGroupLock; // 锁定旋转轴的 Toggle 组
    
    // UI 画布
    [SerializeField]
    private GameObject uiCanvas, uiAnchorPanel;

    // UI 按钮
    [SerializeField] private Button btnApply;          // 应用锚点的按钮
    [SerializeField] private Button btnPersist;        // 持久化锚点的按钮
    [SerializeField] private Button btnDestroyAnchor;  // 销毁锚点的按钮
    [SerializeField] private Button btnDeleteAnchor;   // 删除锚点的按钮
    
    private void Awake()
    {
        toggleGroupLock.SetActive(false);
        uiAnchorPanel.SetActive(false);
        // 设置画布使用主摄像头
        uiCanvas.GetComponent<Canvas>().worldCamera = Camera.main;

        // 为按钮添加点击事件监听器
        btnApply.onClick.AddListener(OnBtnApply);
        btnPersist.onClick.AddListener(OnBtnPressedPersist);
        btnDestroyAnchor.onClick.AddListener(OnBtnPressedDestroy);
        btnDeleteAnchor.onClick.AddListener(OnBtnPressedUnPersist);
    }

    protected void OnEnable()
    {
        // 获取 XRBaseInteractable 组件并添加事件监听器
        interactable = GetComponent<XRBaseInteractable>();
        grabInteractable = GetComponent<XRGrabInteractable>();
        interactable.firstHoverEntered.AddListener(OnFirstHoverEntered);
        interactable.lastHoverExited.AddListener(OnLastHoverExited);
        // interactable.firstSelectEntered.AddListener(OnFirstSelectEntered);
        // interactable.lastSelectExited.AddListener(OnLastSelectExited);
    }

    // protected void OnDisable()
    // {
    //     // 在禁用时可以执行清理操作(当前未实现)
    // }
    //
    // private void Start()
    // {
    //     // 初始化时的操作(当前未实现)
    // }

    private void Update()
    {
        // 如果 UI 画布处于活动状态,使其始终面向摄像头
        if (uiCanvas.activeSelf)
        {
            uiCanvas.transform.LookAt(new Vector3(uiCanvas.transform.position.x * 2 - Camera.main.transform.position.x, 
                                                  uiCanvas.transform.position.y * 2 - Camera.main.transform.position.y, 
                                                  uiCanvas.transform.position.z * 2 - Camera.main.transform.position.z), 
                                                  Vector3.up);
        }
    }

    // private void LateUpdate()
    // {
    //     // 尝试定位空间锚点(获取空间锚点的实时位置)
    //     var result = PXR_MixedReality.LocateAnchor(anchorHandle, out var position, out var rotation);
    //     if (result == PxrResult.SUCCESS)
    //     {
    //         // 如果成功,更新当前对象的位置和旋转
    //         transform.position = position;
    //         transform.rotation = rotation;
    //     }
    //     else
    //     {
    //         // 记录定位锚点的结果
    //         PXRSample_SpatialAnchorManager.Instance.SetLogInfo("LocateSpatialAnchor:" + result.ToString());
    //     }
    // }
    
    /// <summary>
    /// 获取空间锚点的实时位置
    /// </summary>
    public bool LocateAnchor(Transform targetTf)
    {
        bool ret = false;
        // 尝试定位空间锚点
        var result = PXR_MixedReality.LocateAnchor(anchorHandle, out var position, out var rotation);
        if (result == PxrResult.SUCCESS)
        {
            // 如果成功,更新当前对象的位置和旋转
            targetTf.position = position;
            targetTf.rotation = rotation;
            ret = true;
        }
        else
        {
            // 记录定位锚点的结果
            PXRSample_SpatialAnchorManager.Instance.SetLogInfo("LocateSpatialAnchor:" + result.ToString());
        }
    
        return ret;
    }
    
    // 处理第一次悬停进入事件
    protected virtual void OnFirstHoverEntered(HoverEnterEventArgs args) => UpdateColor();

    // 处理最后一次悬停退出事件
    protected virtual void OnLastHoverExited(HoverExitEventArgs args) => UpdateColor();

    // // 处理第一次选择进入事件
    // protected virtual void OnFirstSelectEntered(SelectEnterEventArgs args) => UpdateCanvas();
    //
    // // 处理最后一次选择退出事件
    // protected virtual void OnLastSelectExited(SelectExitEventArgs args) => UpdateCanvas();

    // 更新对象的颜色以指示悬停状态
    protected void UpdateColor()
    {
        if (interactable.isHovered)
        {
            foreach (var renderer in GetComponentsInChildren<Renderer>())
            {
                // 如果被悬停,设置发光颜色为黄色
                renderer.material.SetColor("_EmissionColor", Color.yellow);
            }
        }
        else
        {
            foreach (var renderer in GetComponentsInChildren<Renderer>())
            {
                // 如果未被悬停,清除发光颜色
                renderer.material.SetColor("_EmissionColor", Color.clear);
            }
        }
    }

    // // 更新 UI 画布的显示状态
    // protected void UpdateCanvas()
    // {
    //     uiCanvas.SetActive(interactable.isSelected);
    // }
    
    /// <summary>
    /// 应用锚点
    /// </summary>
    private void OnBtnApply()
    {
        PXRSample_SpatialAnchorManager.Instance.ApplyAnchor(this);
    }
    
    /// <summary>
    /// 持久化锚点会将锚点数据保存到设备中,以便在应用程序关闭后仍然存在。
    /// 持久化后,锚点会分配一个唯一的 ID,可以通过该 ID 来重新定位和管理锚点。
    /// 需要注意的是,持久化锚点可能会占用设备存储空间,因此请根据实际需求合理使用。
    /// </summary>
    private async void OnBtnPressedPersist()
    {
        var result = await PXR_MixedReality.PersistSpatialAnchorAsync(anchorHandle);
        PXRSample_SpatialAnchorManager.Instance.SetLogInfo("PersistSpatialAnchorAsync:" + result.ToString());
        if (result == PxrResult.SUCCESS)
        {
            // 如果成功,显示保存图标
            ShowPersistedTip();
        }
    }
    
    /// <summary>
    /// 销毁锚点会直接销毁锚点对象,但不会取消持久化(如果已持久化)。如果需要删除锚点,请先取消持久化,然后销毁锚点对象。
    /// </summary>
    private void OnBtnPressedDestroy()
    {
        PXRSample_SpatialAnchorManager.Instance.DestroySpatialAnchor(anchorHandle);
    }
    
    /// <summary>
    /// 删除锚点
    /// </summary>
    private void OnBtnPressedUnPersist()
    {
        UnPersistAndDestoryAnchor();
    }
    
    /// <summary>
    /// 先去消持久化(如果已持久化),然后销毁锚点对象
    /// </summary>
    public async void UnPersistAndDestoryAnchor()
    {
        var result = await PXR_MixedReality.UnPersistSpatialAnchorAsync(anchorHandle);
        PXRSample_SpatialAnchorManager.Instance.SetLogInfo("UnPersistSpatialAnchorAsync:" + result.ToString());
        OnBtnPressedDestroy();
        // if (result == PxrResult.SUCCESS)
        // {
        //     // 如果成功,销毁锚点
        //     OnBtnPressedDestroy();
        // }
    }

    // 设置锚点句柄并更新 UI 显示
    public void SetAnchorHandle(ulong handle)
    {
        anchorHandle = handle;
        txtAnchorID.text = "锚点ID:" + anchorHandle;
    }
    
    public void ShowPersistedTip()
    {
        txtPersistedIsOrNo.text = $"是否已持久化:是";
    }
    
    public void ShowCreatedTip(bool isTrue = true)
    {
        txtCreatedIsOrNo.text = $"是否已创建:{(isTrue ? "是" : "否")}";
    }
    
    public void ShowAppliedTip(bool isTrue = true)
    {
        txtAppliedIsOrNo.text = $"是否已应用:{(isTrue ? "是" : "否")}";
    }
    
    /// <summary>
    /// 显隐锁定旋转轴的 Toggle 组。该组包含用于配置锚点旋转约束的 UI 元素
    /// </summary>
    /// <param name="isTrue"></param>
    public void ShowLockToggleGroup(bool isTrue)
    {
        toggleGroupLock.SetActive(isTrue);
    }
    
    // 显隐锚点设置面板和预览对象
    public void ShowPreviewAndUICavas(bool isTrue)
    {
        uiAnchorPanel.SetActive(isTrue);
        previewObject.SetActive(isTrue);
    }

    public void SetMovable(bool isMovable)
    {
        if (grabInteractable == null)
        {
            grabInteractable = GetComponent<XRGrabInteractable>();
        }

        if (grabInteractable != null)
        {
            grabInteractable.enabled = isMovable;
        }
    }

    /// <summary>
    /// 为当前锚点配置抓取时的旋转轴锁定规则。
    /// </summary>
    /// <remarks>
    /// 约束会直接插入 XRGrabInteractable 的 Grab Transformer 链中,
    /// 因此限制会在拖动过程中实时生效,而不是等用户放手后再回正。
    /// </remarks>
    public void ConfigureRotationConstraints(bool lockX, bool lockY, bool lockZ, Quaternion referenceRotation)
    {
        EnsureRotationConstraintTransformerRegistered();
        if (rotationConstraintGrabTransformer != null)
        {
            rotationConstraintGrabTransformer.Configure(lockX, lockY, lockZ, referenceRotation);
        }
    }

    /// <summary>
    /// 确保自定义旋转约束器已经注册到抓取处理链的最后。
    /// </summary>
    private void EnsureRotationConstraintTransformerRegistered()
    {
        if (grabInteractable == null)
        {
            grabInteractable = GetComponent<XRGrabInteractable>();
        }

        if (grabInteractable == null)
        {
            return;
        }

        if (rotationConstraintGrabTransformer == null)
        {
            rotationConstraintGrabTransformer = GetComponent<PXRSample_SpatialAnchorRotationConstraintGrabTransformer>();
            if (rotationConstraintGrabTransformer == null)
            {
                rotationConstraintGrabTransformer = gameObject.AddComponent<PXRSample_SpatialAnchorRotationConstraintGrabTransformer>();
            }
        }

        List<IXRGrabTransformer> singleGrabTransformers = new List<IXRGrabTransformer>();
        grabInteractable.GetSingleGrabTransformers(singleGrabTransformers);
        if (!singleGrabTransformers.Contains(rotationConstraintGrabTransformer))
        {
            grabInteractable.AddSingleGrabTransformer(rotationConstraintGrabTransformer);
            grabInteractable.MoveSingleGrabTransformerTo(rotationConstraintGrabTransformer, grabInteractable.singleGrabTransformersCount - 1);
        }

        List<IXRGrabTransformer> multipleGrabTransformers = new List<IXRGrabTransformer>();
        grabInteractable.GetMultipleGrabTransformers(multipleGrabTransformers);
        if (!multipleGrabTransformers.Contains(rotationConstraintGrabTransformer))
        {
            grabInteractable.AddMultipleGrabTransformer(rotationConstraintGrabTransformer);
            grabInteractable.MoveMultipleGrabTransformerTo(rotationConstraintGrabTransformer, grabInteractable.multipleGrabTransformersCount - 1);
        }
    }
}
csharp 复制代码
using System.Collections.Generic; 
using System.Linq;
using System.Threading.Tasks;
using UnityEngine.UI; 
using Unity.XR.PXR; 
using UnityEngine;

// 定义空间锚点管理器类
public class PXRSample_SpatialAnchorManager : MonoBehaviour
{
    // 单例实例
    private static PXRSample_SpatialAnchorManager instance = null;
    // 单例属性
    public static PXRSample_SpatialAnchorManager Instance
    {
        get
        {
            // 如果实例为空,则查找场景中的实例
            if (instance == null)
            {
                instance = FindObjectOfType<PXRSample_SpatialAnchorManager>();
            }
            return instance; // 返回实例
        }
    }
    
    // 存储锚点的字典
    private Dictionary<ulong, PXRSample_SpatialAnchor> anchorList = new Dictionary<ulong, PXRSample_SpatialAnchor>();
    
    [SerializeField] private Button btnStartAnchorCreateMode; // 进入创建锚点模式按钮
    [SerializeField] private Button btnCreateAnchor; // 创建锚点按钮
    [SerializeField] private Button btnLoadAnchors; // 显示/隐藏所有锚点按钮
    [SerializeField] private Button btnDeleteAllAnchors; // 删除所有锚点
    
    [SerializeField] private Toggle toggleLockRotationX; // 锁定X轴旋转的Toggle
    [SerializeField] private Toggle toggleLockRotationY; // 锁定Y轴旋转的Toggle
    [SerializeField] private Toggle toggleLockRotationZ; // 锁定Z轴旋转的Toggle
    
    [SerializeField] private GameObject anchorPrefab; // 锚点预制体
    [SerializeField] private GameObject GameObjectRoot; // 锚点对象的根节点
    [SerializeField] private Text headerText; // 顶部标题文本,用于显示"空间锚点管理 N"
    
    [SerializeField] private Text tipsText; // 提示文本
    [SerializeField] private bool IsSyncRealtime = true; // 创建模式下拖动预览锚点时,目标根节点是否实时跟随
    [SerializeField] private bool lockAnchorRotationX; // 是否锁定预览锚点的 X 轴旋转
    [SerializeField] private bool lockAnchorRotationY; // 是否锁定预览锚点的 Y 轴旋转
    [SerializeField] private bool lockAnchorRotationZ; // 是否锁定预览锚点的 Z 轴旋转
    
    private int maxLogCount = 16; // 最大日志条数
    private Queue<string> logQueue = new Queue<string>(); // 日志队列
    private bool isLoadingAnchors; // 防止重复加载锚点
    private bool hasStartedSpatialAnchorProvider; // 记录锚点服务是否已启动
    private bool isStartingSpatialAnchorProvider; // 防止重复启动锚点服务
    private bool isShowingLoadedAnchorVisuals; // 当前是否显示已加载锚点的预览和 UI
    private bool isAnchorCreateMode; // 是否处于锚点创建模式
    private bool isCreatingAnchorFromPreview; // 是否正在根据预览锚点创建空间锚点
    private Vector3 initialRootPosition; // 根节点初始位置
    private Quaternion initialRootRotation; // 根节点初始旋转
    private Vector3 pendingAnchorPosition; // 正在创建中的锚点位置
    private Quaternion pendingAnchorRotation; // 正在创建中的锚点旋转
    private PXRSample_SpatialAnchor appliedAnchor; // 当前被应用到根节点的锚点
    private Quaternion previewRotationReference; // 进入创建模式时的标准朝向,用于做轴向锁定基准

    private const float AnchorPreviewDistance = 0.5f;
    private const string HeaderTextPrefix = "空间锚点管理";
    private const string StartAnchorCreateModeText = "开启空间锚点创建";
    private const string CancelAnchorCreateModeText = "取消空间锚点创建";
    private const string ShowAllAnchorsText = "显示所有空间锚点";
    private const string HideAllAnchorsText = "隐藏所有空间锚点";

    // 初始化
    void Start()
    {
        // PXR_Manager.EnableVideoSeeThrough = true; // 启用视频透视

        if (GameObjectRoot != null)
        {
            initialRootPosition = GameObjectRoot.transform.position;
            initialRootRotation = GameObjectRoot.transform.rotation;
        }

        if (anchorPrefab != null)
        {
            anchorPrefab.SetActive(false);
            PXRSample_SpatialAnchor previewAnchor = anchorPrefab.GetComponent<PXRSample_SpatialAnchor>();
            if (previewAnchor != null)
            {
                previewAnchor.ShowCreatedTip(false);
                previewAnchor.ShowAppliedTip(false);
                previewAnchor.ShowLockToggleGroup(false);
                previewAnchor.SetMovable(true);
                previewAnchor.ConfigureRotationConstraints(lockAnchorRotationX, lockAnchorRotationY, lockAnchorRotationZ, anchorPrefab.transform.rotation);
            }
        }
        
        //更新顶部标题文本,显示当前锚点数量
        UpdateHeaderText(anchorList.Count);

        // 添加按钮点击事件
        btnStartAnchorCreateMode.onClick.AddListener(OnBtnStartAnchorCreateMode);
        btnCreateAnchor.onClick.AddListener(OnBtnPressedCreateAnchor);
        btnLoadAnchors.onClick.AddListener(OnBtnPressedLoadAllAnchors);
        btnDeleteAllAnchors.onClick.AddListener(OnBtnDeleteAllAnchors);

        // 添加旋转轴锁定Toggle事件
        toggleLockRotationX.onValueChanged.AddListener((value) => { lockAnchorRotationX = value; });
        toggleLockRotationY.onValueChanged.AddListener((value) => { lockAnchorRotationY = value; });
        toggleLockRotationZ.onValueChanged.AddListener((value) => { lockAnchorRotationZ = value; });
        toggleLockRotationX.isOn = lockAnchorRotationX;
        toggleLockRotationY.isOn = lockAnchorRotationY;
        toggleLockRotationZ.isOn = lockAnchorRotationZ;
        
        // 显示按钮
        btnCreateAnchor.gameObject.SetActive(true);
        btnLoadAnchors.gameObject.SetActive(true);
        SetButtonText(btnLoadAnchors, ShowAllAnchorsText);

        // // 获取右手控制器
        // rightController = InputDevices.GetDeviceAtXRNode(XRNode.RightHand);

        //初始化空间锚点服务,确保服务已启动后加载锚点数据
        _ = InitializeSpatialAnchorsAsync();
    }

    /// <summary>
    /// 初始化空间锚点服务,确保服务已启动后加载锚点数据
    /// </summary>
    private async Task InitializeSpatialAnchorsAsync()
    {
        if (await EnsureSpatialAnchorProviderReadyAsync())
        {
            // 启动时做"静默加载":
            // 1. 把持久化锚点实例化到场景中;
            // 2. 默认隐藏所有锚点的 UI 和预览;
            // 3. 如果存在锚点,则自动将找到的第一个锚点应用到目标根节点。
            await LoadAnchorsAsync(false, true);
        }
    }

    /// <summary>
    /// 确保空间锚点服务提供者已准备就绪,防止重复启动和加载
    /// </summary>
    /// <returns></returns>
    private async Task<bool> EnsureSpatialAnchorProviderReadyAsync()
    {
        if (hasStartedSpatialAnchorProvider)
        {
            return true;
        }

        if (isStartingSpatialAnchorProvider)
        {
            while (isStartingSpatialAnchorProvider)
            {
                await Task.Delay(100);
            }

            return hasStartedSpatialAnchorProvider;
        }

        isStartingSpatialAnchorProvider = true;
        try
        {
            var result = await PXR_MixedReality.StartSenseDataProvider(PxrSenseDataProviderType.SpatialAnchor); // 启动空间锚点感知数据提供者
            SetLogInfo("StartSenseDataProvider:" + result); // 记录日志
            hasStartedSpatialAnchorProvider = result == PxrResult.SUCCESS;
            return hasStartedSpatialAnchorProvider;
        }
        finally
        {
            isStartingSpatialAnchorProvider = false;
        }
    }

    // 启用时注册事件
    void OnEnable()
    {
        PXR_Manager.SpatialAnchorDataUpdated += SpatialAnchorDataUpdated; // 注册锚点数据更新事件
    }

    // 禁用时注销事件
    void OnDisable()
    {
        PXR_Manager.SpatialAnchorDataUpdated -= SpatialAnchorDataUpdated; // 注销锚点数据更新事件
    }

    private void LateUpdate()
    {
        if (GameObjectRoot == null)
        {
            return;
        }

        // 预览锚点在创建模式下可以被拖拽,拖拽后先按配置做旋转轴锁定,
        // 再决定是否把结果实时同步到目标根节点。
        // 正常抓取时,实时锁定由自定义 Grab Transformer 处理;
        // 这里保留一次兜底修正,兼容非抓取代码改动 transform 的情况。
        if (isAnchorCreateMode && anchorPrefab != null && anchorPrefab.activeInHierarchy)
        {
            ApplyPreviewRotationConstraints();
        }

        if (isAnchorCreateMode && IsSyncRealtime && anchorPrefab != null && anchorPrefab.activeInHierarchy)
        {
            GameObjectRoot.transform.SetPositionAndRotation(anchorPrefab.transform.position, anchorPrefab.transform.rotation);
            return;
        }

        if (isCreatingAnchorFromPreview)
        {
            GameObjectRoot.transform.SetPositionAndRotation(pendingAnchorPosition, pendingAnchorRotation);
            return;
        }

        if (appliedAnchor != null)
        {
            appliedAnchor.LocateAnchor(GameObjectRoot.transform);
        }
    }

    /// <summary>
    /// 锚点数据更新事件
    /// </summary>
    private void SpatialAnchorDataUpdated()
    {
        if (!hasStartedSpatialAnchorProvider || isLoadingAnchors)
        {
            return;
        }

        // 系统通知锚点数据发生变化时,保持静默加载策略,避免自动把所有锚点 UI 打开。
        _ = LoadAnchorsAsync(isShowingLoadedAnchorVisuals, false);
    }

    /// <summary>
    /// 开启创建锚点模式
    /// </summary>
    private void OnBtnStartAnchorCreateMode()
    {
        if (anchorPrefab == null)
        {
            return;
        }

        if (isCreatingAnchorFromPreview)
        {
            return;
        }

        if (isAnchorCreateMode)
        {
            CancelAnchorCreateMode();
            return;
        }

        Transform cameraTransform = Camera.main != null ? Camera.main.transform : null;
        if (cameraTransform == null)
        {
            SetLogInfo("OnBtnStartAnchorCreateMode:Main camera not found");
            return;
        }

        isAnchorCreateMode = true;
        anchorPrefab.SetActive(true);
        anchorPrefab.transform.position = cameraTransform.position + cameraTransform.forward * AnchorPreviewDistance;

        Vector3 lookDirection = cameraTransform.position - anchorPrefab.transform.position;
        if (lookDirection.sqrMagnitude < 0.0001f)
        {
            lookDirection = -cameraTransform.forward;
        }

        anchorPrefab.transform.rotation = Quaternion.LookRotation(lookDirection.normalized, Vector3.up);
        previewRotationReference = anchorPrefab.transform.rotation;
        ApplyPreviewRotationConstraints();

        PXRSample_SpatialAnchor previewAnchor = anchorPrefab.GetComponent<PXRSample_SpatialAnchor>();
        if (previewAnchor != null)
        {
            previewAnchor.ShowCreatedTip(false);
            previewAnchor.ShowAppliedTip(false);
            previewAnchor.ShowLockToggleGroup(true);
            previewAnchor.SetMovable(true);
            previewAnchor.ConfigureRotationConstraints(lockAnchorRotationX, lockAnchorRotationY, lockAnchorRotationZ, previewRotationReference);
        }

        SetButtonText(btnStartAnchorCreateMode, CancelAnchorCreateModeText);
    }
    
    /// <summary>
    /// 删除所有锚点
    /// </summary>
    private void OnBtnDeleteAllAnchors()
    {
        //先将锚点去持久化,再销毁,最后清空列表
        foreach (var anchor in anchorList.Values.ToList())
        {
            if (anchor != null)
            {
                anchor.UnPersistAndDestoryAnchor();
            }
        }
        appliedAnchor = null;

        if (!isAnchorCreateMode && !isCreatingAnchorFromPreview)
        {
            RestoreRootToAppliedAnchorOrInitial();
        }
    }
    
    /// <summary>
    /// 创建锚点按钮事件
    /// </summary>
    private async void OnBtnPressedCreateAnchor()
    {
        if (!isAnchorCreateMode || anchorPrefab == null)
        {
            SetLogInfo("OnBtnPressedCreateAnchor:Anchor create mode is not active");
            return;
        }

        if (!await EnsureSpatialAnchorProviderReadyAsync())
        {
            return;
        }

        // 用户点击创建按钮时,再执行一次旋转轴锁定,
        // 避免最后一次拖拽与按钮点击之间还没来得及经过 LateUpdate,导致创建结果带入未约束的旋转值。
        ApplyPreviewRotationConstraints();

        isAnchorCreateMode = false;
        isCreatingAnchorFromPreview = true;
        pendingAnchorPosition = anchorPrefab.transform.position;
        pendingAnchorRotation = anchorPrefab.transform.rotation;
        PXRSample_SpatialAnchor previewAnchor = anchorPrefab.GetComponent<PXRSample_SpatialAnchor>();
        if (previewAnchor != null)
        {
            previewAnchor.ShowLockToggleGroup(false);
        }

        // 创建空间锚点
        CreateSpatialAnchor(anchorPrefab.transform); 
        //隐藏
        anchorPrefab.SetActive(false);
        SetButtonText(btnStartAnchorCreateMode, StartAnchorCreateModeText);
    }

    /// <summary>
    /// 异步加载所有锚点
    /// </summary>
    private async void OnBtnPressedLoadAllAnchors()
    {
        if (!await EnsureSpatialAnchorProviderReadyAsync())
        {
            return;
        }

        if (isShowingLoadedAnchorVisuals)
        {
            SetLoadedAnchorVisuals(false);
            return;
        }

        // 点击"显示所有空间锚点"时,显式显示所有锚点 UI/预览;
        // 再次点击同一个按钮时,则改为统一隐藏。
        await LoadAnchorsAsync(true, false);
    }

    /// <summary>
    /// 加载所有锚点,查询设备中所有持久化的锚点,并在场景中实例化对应的锚点对象进行显示和管理。
    /// "showVisualsAfterLoad"
    /// true:显式显示所有锚点的 Canvas 和 Preview,适用于用户点击"加载所有空间锚点"按钮。
    /// false:静默加载,仅保证锚点存在于场景中,但默认隐藏 Canvas 和 Preview。
    /// "applyFirstAnchorIfNeeded"
    /// true:如果当前还没有已应用锚点,则把找到的第一个锚点应用到目标根节点。
    /// false:不自动切换当前应用锚点。
    /// </summary>
    private async Task LoadAnchorsAsync(bool showVisualsAfterLoad, bool applyFirstAnchorIfNeeded)
    {
        if (!hasStartedSpatialAnchorProvider)
        {
            return;
        }

        if (isLoadingAnchors)
        {
            return;
        }

        isLoadingAnchors = true;

        try
        {
            var result = await PXR_MixedReality.QuerySpatialAnchorAsync(); // 查询所有空间锚点
            if (result.result == PxrResult.ERROR_HANDLE_INVALID)
            {
                SetLogInfo("LoadSpatialAnchorAsync:No persisted anchors");
                return;
            }

            SetLogInfo("LoadSpatialAnchorAsync:" + result.result.ToString() + result.anchorHandleList.Count); // 记录日志
            if (result.result != PxrResult.SUCCESS)
            {
                return;
            }

            // 记录本次查询结果里的第一个锚点。
            // 启动静默加载时,如果还没有当前应用锚点,就使用它来恢复目标根节点的位置和旋转。
            PXRSample_SpatialAnchor firstResolvedAnchor = null;

            foreach (var key in result.anchorHandleList) // 遍历锚点句柄
            {
                PXRSample_SpatialAnchor anchor = null;

                if (!anchorList.ContainsKey(key)) // 如果锚点列表中不存在该锚点
                {
                    GameObject anchorObject = Instantiate(anchorPrefab); // 实例化锚点预制体
                    anchorObject.SetActive(true);
                    anchor = anchorObject.GetComponent<PXRSample_SpatialAnchor>(); // 获取锚点组件
                    anchor.SetAnchorHandle(key); // 设置锚点句柄

                    // 定位锚点
                    var locateResult = PXR_MixedReality.LocateAnchor(key, out var position, out var orientation);
                    if (locateResult != PxrResult.SUCCESS)
                    {
                        SetLogInfo("LocateSpatialAnchor:" + locateResult.ToString());
                        Destroy(anchorObject);
                        continue;
                    }

                    anchor.transform.position = position; // 设置位置
                    anchor.transform.rotation = orientation; // 设置旋转
                    anchor.SetMovable(false);
                    anchorList.Add(key, anchor); // 添加到锚点列表
                    anchor.ShowPersistedTip(); // 显示保存图标
                    anchor.ShowCreatedTip();
                    anchor.ShowLockToggleGroup(false);
                }

                anchor = anchorList[key];
                if (firstResolvedAnchor == null)
                {
                    firstResolvedAnchor = anchor;
                }

                // 根据加载入口决定是否显示锚点的可视化 UI。
                anchor.ShowAppliedTip(appliedAnchor == anchor);
                anchor.ShowPreviewAndUICavas(showVisualsAfterLoad);
                anchor.ShowLockToggleGroup(false);
            }

            // 启动时若尚未有当前应用锚点,则自动应用设备中查询到的第一个锚点。
            if (applyFirstAnchorIfNeeded && appliedAnchor == null && firstResolvedAnchor != null)
            {
                ApplyAnchor(firstResolvedAnchor);
            }

            SetLoadedAnchorVisuals(showVisualsAfterLoad, false);
            UpdateHeaderText(anchorList.Count);
        }
        finally
        {
            isLoadingAnchors = false;
        }
    }

    /// <summary>
    /// 异步创建空间锚点
    /// </summary>
    /// <param name="transform"></param>
    private async void CreateSpatialAnchor(Transform transform)
    {
        var result = await PXR_MixedReality.CreateSpatialAnchorAsync(transform.position, transform.rotation); // 创建锚点
        SetLogInfo("CreateSpatialAnchorAsync:" + result.ToString()); // 记录日志
        isCreatingAnchorFromPreview = false;

        if (result.result == PxrResult.SUCCESS) // 成功创建
        {
            GameObject anchorObject = Instantiate(anchorPrefab); // 实例化锚点预制体
            anchorObject.SetActive(true);
            PXRSample_SpatialAnchor anchor = anchorObject.GetComponent<PXRSample_SpatialAnchor>(); // 获取锚点组件
            if (anchor == null) // 如果锚点组件不存在
            {
                anchor = anchorObject.AddComponent<PXRSample_SpatialAnchor>(); // 添加锚点组件
            }

            anchorObject.transform.SetPositionAndRotation(transform.position, transform.rotation);
            anchor.SetAnchorHandle(result.anchorHandle); // 设置锚点句柄
            anchor.ShowCreatedTip();
            anchor.ShowAppliedTip(false);
            anchor.ShowPreviewAndUICavas(true);
            anchor.ShowLockToggleGroup(false);
            anchor.SetMovable(false);

            anchorList.Add(result.anchorHandle, anchor); // 添加到锚点列表
            UpdateHeaderText(anchorList.Count);
            ApplyAnchor(anchor);

            // 获取锚点的UUID
            var result1 = PXR_MixedReality.GetAnchorUuid(result.anchorHandle, out var uuid);
            SetLogInfo("GetUuid:" + result1.ToString() + "  " + (result.uuid.Equals(uuid)) + "Uuid:" + uuid); // 记录UUID信息
            return;
        }

        RestoreRootToAppliedAnchorOrInitial();
    }

    /// <summary>
    /// 销毁空间锚点
    /// </summary>
    /// <param name="anchorHandle"></param>
    public void DestroySpatialAnchor(ulong anchorHandle)
    {
        var result = PXR_MixedReality.DestroyAnchor(anchorHandle); // 销毁锚点
        SetLogInfo("DestroySpatialAnchor:" + result.ToString()); // 记录日志
        if (result == PxrResult.SUCCESS) // 如果成功
        {
            if (anchorList.ContainsKey(anchorHandle)) // 如果锚点列表中存在该锚点
            {
                PXRSample_SpatialAnchor anchor = anchorList[anchorHandle];
                if (appliedAnchor == anchor)
                {
                    appliedAnchor = null;
                }

                Destroy(anchorList[anchorHandle].gameObject); // 销毁锚点对象
                anchorList.Remove(anchorHandle); // 从列表中移除锚点
                UpdateHeaderText(anchorList.Count);
                if (anchorList.Count == 0)
                {
                    SetLoadedAnchorVisuals(false, false);
                }
                RefreshAppliedTips();
                RestoreRootToAppliedAnchorOrInitial();
            }
        }
    }

    /// <summary>
    /// 应用指定锚点到目标根节点。
    /// </summary>
    public void ApplyAnchor(PXRSample_SpatialAnchor anchor)
    {
        if (anchor == null || GameObjectRoot == null)
        {
            return;
        }

        appliedAnchor = anchor;
        RefreshAppliedTips();

        if (!isAnchorCreateMode && !isCreatingAnchorFromPreview)
        {
            bool locateResult = appliedAnchor.LocateAnchor(GameObjectRoot.transform);
            if (!locateResult)
            {
                GameObjectRoot.transform.SetPositionAndRotation(anchor.transform.position, anchor.transform.rotation);
            }
        }
    }

    private void RefreshAppliedTips()
    {
        foreach (var anchor in anchorList.Values)
        {
            if (anchor != null)
            {
                anchor.ShowAppliedTip(anchor == appliedAnchor);
            }
        }
    }

    private void CancelAnchorCreateMode()
    {
        isAnchorCreateMode = false;
        PXRSample_SpatialAnchor previewAnchor = anchorPrefab != null ? anchorPrefab.GetComponent<PXRSample_SpatialAnchor>() : null;
        if (previewAnchor != null)
        {
            previewAnchor.ShowLockToggleGroup(false);
        }
        anchorPrefab.SetActive(false);
        SetButtonText(btnStartAnchorCreateMode, StartAnchorCreateModeText);
        RestoreRootToAppliedAnchorOrInitial();
    }

    private void RestoreRootToAppliedAnchorOrInitial()
    {
        if (GameObjectRoot == null)
        {
            return;
        }

        if (appliedAnchor != null)
        {
            bool locateResult = appliedAnchor.LocateAnchor(GameObjectRoot.transform);
            if (locateResult)
            {
                return;
            }

            GameObjectRoot.transform.SetPositionAndRotation(appliedAnchor.transform.position, appliedAnchor.transform.rotation);
            return;
        }

        GameObjectRoot.transform.SetPositionAndRotation(initialRootPosition, initialRootRotation);
    }

    /// <summary>
    /// 对创建模式下的预览锚点做旋转轴约束。
    /// </summary>
    /// <remarks>
    /// 约束只影响当前可拖拽的预览锚点,不会修改已经创建完成的持久化锚点。
    /// 锁定后的轴会保持"进入创建模式时"的标准朝向值,从而让放置结果更统一。
    /// </remarks>
    private void ApplyPreviewRotationConstraints()
    {
        if (anchorPrefab == null)
        {
            return;
        }

        anchorPrefab.transform.rotation = PXRSample_SpatialAnchorRotationConstraintGrabTransformer.ConstrainRotation(
            anchorPrefab.transform.rotation,
            previewRotationReference,
            lockAnchorRotationX,
            lockAnchorRotationY,
            lockAnchorRotationZ);
    }
    
    /// <summary>
    /// 更新顶部标题文字。
    /// </summary>
    private void UpdateHeaderText(int anchorCount)
    {
        if (headerText == null)
        {
            return;
        }

        headerText.text = anchorCount > 0 ? $"{HeaderTextPrefix} {anchorCount}" : HeaderTextPrefix;
    }

    /// <summary>
    /// 统一更新已加载锚点的可视化状态,并同步刷新"显示/隐藏所有空间锚点"按钮文案。
    /// </summary>
    private void SetLoadedAnchorVisuals(bool isVisible, bool updateAnchorObjects = true)
    {
        isShowingLoadedAnchorVisuals = isVisible && anchorList.Count > 0;

        if (updateAnchorObjects)
        {
            foreach (var anchor in anchorList.Values)
            {
                if (anchor != null)
                {
                    anchor.ShowPreviewAndUICavas(isShowingLoadedAnchorVisuals);
                    anchor.ShowLockToggleGroup(false);
                }
            }
        }

        SetButtonText(btnLoadAnchors, isShowingLoadedAnchorVisuals ? HideAllAnchorsText : ShowAllAnchorsText);
    }

    private void SetButtonText(Button button, string text)
    {
        if (button == null)
        {
            return;
        }

        Text buttonText = button.GetComponentInChildren<Text>(true);
        if (buttonText != null)
        {
            buttonText.text = text;
        }
    }

    // 设置日志信息
    public void SetLogInfo(string log)
    {
        if (logQueue.Count > 0 && logQueue.Last() == log)
        {
            return;
        }

        if (logQueue.Count >= maxLogCount) // 如果日志队列达到最大条数
        {
            logQueue.Dequeue(); // 移除最旧的日志
        }
        logQueue.Enqueue(log); // 添加新的日志

        Debug.Log("PXRSample_SpatialAnchorManager" + log); // 输出到控制台

        if (tipsText != null)
        {
            tipsText.text = string.Join("\n", logQueue.ToArray()); // 更新提示文本显示日志
        }
    }
}
csharp 复制代码
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;
using UnityEngine.XR.Interaction.Toolkit.Transformers;

/// <summary>
/// 在抓取交互处理阶段直接约束 SpatialAnchor 的旋转。
/// </summary>
/// <remarks>
/// 这个约束器和管理器里最后兜底的旋转修正不同:
/// 它会在 XRGrabInteractable 计算目标位姿时立即生效,
/// 因此用户拖拽过程中就无法把被锁定的轴真正旋转出去。
/// </remarks>
public class PXRSample_SpatialAnchorRotationConstraintGrabTransformer : XRBaseGrabTransformer
{
    private const float AxisEpsilon = 0.0001f;

    private bool lockRotationX;
    private bool lockRotationY;
    private bool lockRotationZ;
    private Quaternion referenceRotation = Quaternion.identity;
    private Quaternion initialInteractorRotation = Quaternion.identity;
    private Quaternion initialConstrainedRotation = Quaternion.identity;
    private bool hasGrabReference;

    /// <inheritdoc />
    protected override RegistrationMode registrationMode => RegistrationMode.SingleAndMultiple;

    /// <summary>
    /// 配置当前抓取约束器的旋转锁定规则。
    /// </summary>
    public void Configure(bool lockX, bool lockY, bool lockZ, Quaternion reference)
    {
        lockRotationX = lockX;
        lockRotationY = lockY;
        lockRotationZ = lockZ;
        referenceRotation = reference;
        hasGrabReference = false;
    }

    /// <summary>
    /// 根据锁定规则对输入旋转做"世界坐标轴约束"。
    /// 被锁定的轴不会再去读取或回写任何欧拉角参考值,
    /// 而是直接把对应世界轴的旋转增量清零,只保留未锁定轴的世界轴增量。
    /// </summary>
    public static Quaternion ConstrainRotation(Quaternion inputRotation, Quaternion referenceRotation, bool lockX, bool lockY, bool lockZ)
    {
        if (!lockX && !lockY && !lockZ)
        {
            return inputRotation;
        }

        if (GetUnlockedAxisCount(lockX, lockY, lockZ) == 1)
        {
            return ConstrainSingleFreeWorldAxisRotation(inputRotation, referenceRotation, lockX, lockY, lockZ);
        }

        Quaternion worldDeltaRotation = inputRotation * Quaternion.Inverse(referenceRotation);
        Vector3 worldAxisDelta = GetWorldAxisAngles(worldDeltaRotation);
        return ApplyUnlockedWorldAxisDelta(referenceRotation, worldAxisDelta, lockX, lockY, lockZ);
    }

    /// <inheritdoc />
    public override void OnGrab(XRGrabInteractable grabInteractable)
    {
        base.OnGrab(grabInteractable);
        CaptureGrabReference(grabInteractable);
    }

    /// <inheritdoc />
    public override void OnGrabCountChanged(XRGrabInteractable grabInteractable, Pose targetPose, Vector3 localScale)
    {
        base.OnGrabCountChanged(grabInteractable, targetPose, localScale);
        CaptureGrabReference(grabInteractable);
    }

    /// <inheritdoc />
    public override void Process(XRGrabInteractable grabInteractable, XRInteractionUpdateOrder.UpdatePhase updatePhase, ref Pose targetPose, ref Vector3 localScale)
    {
        if (!hasGrabReference)
        {
            CaptureGrabReference(grabInteractable);
        }

        targetPose.rotation = ComputeConstrainedRotationFromInteractor(grabInteractable, targetPose.rotation);

        // 默认抓取器先根据手部位姿计算出目标位置和目标旋转。
        // 当我们在这里强行改写目标旋转后,如果不同时重算目标位置,
        // 动态 Attach 点与手部 Attach 点之间的空间关系就会被打破,
        // 表现出来就会像"物体几乎拖不动"或者"位置被锁死"。
        //
        // 因此这里要基于"当前交互器 Attach 点位置"以及"物体自身 Attach 点的局部偏移"
        // 重新求一次目标位置,让受限后的旋转和抓取位置保持一致。
        if (grabInteractable.interactorsSelecting.Count <= 0)
        {
            return;
        }

        IXRSelectInteractor interactor = grabInteractable.interactorsSelecting[0];
        Transform interactorAttachTransform = interactor.GetAttachTransform(grabInteractable);
        Transform grabAttachTransform = grabInteractable.GetAttachTransform(interactor);
        if (interactorAttachTransform == null || grabAttachTransform == null)
        {
            return;
        }

        Vector3 localAttachPosition = grabInteractable.transform.InverseTransformPoint(grabAttachTransform.position);
        targetPose.position = interactorAttachTransform.position - targetPose.rotation * localAttachPosition;
    }

    /// <summary>
    /// 将未锁定的世界轴旋转增量按固定世界轴顺序叠加到基准旋转上。
    /// </summary>
    private static Quaternion ApplyUnlockedWorldAxisDelta(Quaternion baseRotation, Vector3 worldAxisDelta, bool lockX, bool lockY, bool lockZ)
    {
        Quaternion constrainedRotation = baseRotation;

        if (!lockX)
        {
            constrainedRotation = Quaternion.AngleAxis(worldAxisDelta.x, Vector3.right) * constrainedRotation;
        }

        if (!lockY)
        {
            constrainedRotation = Quaternion.AngleAxis(worldAxisDelta.y, Vector3.up) * constrainedRotation;
        }

        if (!lockZ)
        {
            constrainedRotation = Quaternion.AngleAxis(worldAxisDelta.z, Vector3.forward) * constrainedRotation;
        }

        return constrainedRotation;
    }

    /// <summary>
    /// 当只放开一个轴时,直接把物体约束为"绕单个固定世界轴"的绝对旋转。
    /// 这样可以确保:
    /// 1. 只放开 Y 时,Y 永远等于地面法线方向;
    /// 2. 只放开 X 时,X 永远等于世界右方向;
    /// 3. 只放开 Z 时,Z 永远等于世界前方向。
    /// </summary>
    private static Quaternion ConstrainSingleFreeWorldAxisRotation(Quaternion inputRotation, Quaternion referenceRotation, bool lockX, bool lockY, bool lockZ)
    {
        if (!lockY)
        {
            return ConstrainAroundWorldUp(inputRotation, referenceRotation);
        }

        if (!lockX)
        {
            return ConstrainAroundWorldRight(inputRotation, referenceRotation);
        }

        return ConstrainAroundWorldForward(inputRotation, referenceRotation);
    }

    private static Quaternion ConstrainAroundWorldUp(Quaternion inputRotation, Quaternion referenceRotation)
    {
        Vector3 referenceForward = GetProjectedAxis(referenceRotation * Vector3.forward, Vector3.up, Vector3.forward);
        Vector3 inputForward = GetProjectedAxis(inputRotation * Vector3.forward, Vector3.up, referenceForward);
        float deltaAngle = Vector3.SignedAngle(referenceForward, inputForward, Vector3.up);
        Quaternion baseRotation = Quaternion.LookRotation(referenceForward, Vector3.up);
        return Quaternion.AngleAxis(deltaAngle, Vector3.up) * baseRotation;
    }

    private static Quaternion ConstrainAroundWorldRight(Quaternion inputRotation, Quaternion referenceRotation)
    {
        Vector3 referenceUp = GetProjectedAxis(referenceRotation * Vector3.up, Vector3.right, Vector3.up);
        Vector3 inputUp = GetProjectedAxis(inputRotation * Vector3.up, Vector3.right, referenceUp);
        float deltaAngle = Vector3.SignedAngle(referenceUp, inputUp, Vector3.right);
        Quaternion baseRotation = Quaternion.LookRotation(Vector3.Cross(Vector3.right, referenceUp).normalized, referenceUp);
        return Quaternion.AngleAxis(deltaAngle, Vector3.right) * baseRotation;
    }

    private static Quaternion ConstrainAroundWorldForward(Quaternion inputRotation, Quaternion referenceRotation)
    {
        Vector3 referenceUp = GetProjectedAxis(referenceRotation * Vector3.up, Vector3.forward, Vector3.up);
        Vector3 inputUp = GetProjectedAxis(inputRotation * Vector3.up, Vector3.forward, referenceUp);
        float deltaAngle = Vector3.SignedAngle(referenceUp, inputUp, Vector3.forward);
        Quaternion baseRotation = Quaternion.LookRotation(Vector3.forward, referenceUp);
        return Quaternion.AngleAxis(deltaAngle, Vector3.forward) * baseRotation;
    }

    /// <summary>
    /// 计算手部当前世界旋转相对抓取开始时的世界轴旋转增量,
    /// 再叠加到抓取开始时缓存的受限旋转上。
    /// </summary>
    private Quaternion ComputeConstrainedRotationFromInteractor(XRGrabInteractable grabInteractable, Quaternion fallbackRotation)
    {
        if (grabInteractable.interactorsSelecting.Count <= 0)
        {
            return ConstrainRotation(fallbackRotation, referenceRotation, lockRotationX, lockRotationY, lockRotationZ);
        }

        Transform interactorAttachTransform = grabInteractable.interactorsSelecting[0].GetAttachTransform(grabInteractable);
        if (interactorAttachTransform == null)
        {
            return ConstrainRotation(fallbackRotation, referenceRotation, lockRotationX, lockRotationY, lockRotationZ);
        }

        Quaternion interactorDeltaRotation = interactorAttachTransform.rotation * Quaternion.Inverse(initialInteractorRotation);
        Vector3 interactorAxisAngles = GetWorldAxisAngles(interactorDeltaRotation);
        return ApplyUnlockedWorldAxisDelta(initialConstrainedRotation, interactorAxisAngles, lockRotationX, lockRotationY, lockRotationZ);
    }

    /// <summary>
    /// 记录抓取开始时的手部世界旋转和物体世界旋转,供后续做统一的世界轴约束。
    /// </summary>
    private void CaptureGrabReference(XRGrabInteractable grabInteractable)
    {
        if (grabInteractable.interactorsSelecting.Count <= 0)
        {
            hasGrabReference = false;
            return;
        }

        Transform interactorAttachTransform = grabInteractable.interactorsSelecting[0].GetAttachTransform(grabInteractable);
        if (interactorAttachTransform == null)
        {
            hasGrabReference = false;
            return;
        }

        initialConstrainedRotation = ConstrainRotation(grabInteractable.transform.rotation, referenceRotation, lockRotationX, lockRotationY, lockRotationZ);
        initialInteractorRotation = interactorAttachTransform.rotation;
        hasGrabReference = true;
    }

    /// <summary>
    /// 将一个相对旋转投影到世界 X/Y/Z 三个轴上,得到更稳定的有符号角度。
    /// </summary>
    /// <remarks>
    /// 这里不再直接读取欧拉角差值,因为欧拉角在某些姿态下会发生耦合和跳变,
    /// 尤其是只放开单个世界轴时,最容易出现"抽动"或"来回回弹"。
    /// 当前实现通过把旋转后的参考向量投影到指定世界轴的法平面上,再求有符号夹角,
    /// 从而得到绕该世界轴的稳定旋转量。
    /// </remarks>
    private static Vector3 GetWorldAxisAngles(Quaternion deltaRotation)
    {
        return new Vector3(
            GetSignedAngleAroundWorldAxis(deltaRotation, Vector3.right),
            GetSignedAngleAroundWorldAxis(deltaRotation, Vector3.up),
            GetSignedAngleAroundWorldAxis(deltaRotation, Vector3.forward));
    }

    /// <summary>
    /// 计算一个相对旋转绕指定世界轴的有符号角度。
    /// </summary>
    private static float GetSignedAngleAroundWorldAxis(Quaternion deltaRotation, Vector3 axis)
    {
        Vector3 referenceDirection = GetStablePerpendicular(axis);
        Vector3 rotatedDirection = deltaRotation * referenceDirection;

        Vector3 projectedReference = Vector3.ProjectOnPlane(referenceDirection, axis);
        Vector3 projectedRotated = Vector3.ProjectOnPlane(rotatedDirection, axis);

        if (projectedReference.sqrMagnitude < AxisEpsilon || projectedRotated.sqrMagnitude < AxisEpsilon)
        {
            return 0f;
        }

        return Vector3.SignedAngle(projectedReference.normalized, projectedRotated.normalized, axis);
    }

    /// <summary>
    /// 获取一个与指定轴稳定垂直的参考向量。
    /// </summary>
    private static Vector3 GetStablePerpendicular(Vector3 axis)
    {
        Vector3 candidate = Mathf.Abs(Vector3.Dot(axis.normalized, Vector3.up)) < 0.99f ? Vector3.up : Vector3.right;
        Vector3 perpendicular = Vector3.ProjectOnPlane(candidate, axis);
        return perpendicular.sqrMagnitude < AxisEpsilon ? Vector3.forward : perpendicular.normalized;
    }

    private static int GetUnlockedAxisCount(bool lockX, bool lockY, bool lockZ)
    {
        int unlockedAxisCount = 0;
        if (!lockX)
        {
            unlockedAxisCount++;
        }

        if (!lockY)
        {
            unlockedAxisCount++;
        }

        if (!lockZ)
        {
            unlockedAxisCount++;
        }

        return unlockedAxisCount;
    }

    private static Vector3 GetProjectedAxis(Vector3 sourceDirection, Vector3 planeNormal, Vector3 fallbackDirection)
    {
        Vector3 projectedDirection = Vector3.ProjectOnPlane(sourceDirection, planeNormal);
        if (projectedDirection.sqrMagnitude >= AxisEpsilon)
        {
            return projectedDirection.normalized;
        }

        projectedDirection = Vector3.ProjectOnPlane(fallbackDirection, planeNormal);
        if (projectedDirection.sqrMagnitude >= AxisEpsilon)
        {
            return projectedDirection.normalized;
        }

        return GetStablePerpendicular(planeNormal);
    }
}
相关推荐
Cool-浩1 年前
Unity 开发Apple Vision Pro空间锚点应用Spatial Anchor
unity·游戏引擎·apple vision pro·空间锚点·spatial anchor·visionpro开发
程序员正茂1 年前
PICO+Unity MR空间锚点
unity·pico·空间锚点