Unity OpenXR 快速入门及实例

Unity OpenXR 快速入门及实例

一、OpenXR 概述

什么是 OpenXR?

OpenXR 是由 Khronos Group 开发的开放标准和跨平台 API,用于虚拟现实(VR)和增强现实(AR)应用开发。它的主要目标是简化 XR 开发,让一套代码可以在多种硬件平台上运行,包括 Oculus、HTC Vive、Windows Mixed Reality、Pico 等。

OpenXR 的优势

  1. 跨平台兼容性:一次开发,多平台部署
  2. 简化开发流程:减少学习多个 SDK 的时间成本
  3. 前沿 XR 技术支持:持续更新支持最新的 XR 功能
  4. 未来兼容性:随着新设备的发布,只需更新 OpenXR 运行时即可支持
  5. 性能优化:提供直接访问 XR 硬件的接口,最小化开销

二、在 Unity 中设置 OpenXR

系统要求

  • Unity 2020.3 LTS 或更高版本(推荐 Unity 2021.3 LTS 及以上)
  • Unity XR 插件管理系统
  • 对应 XR 设备的驱动程序

安装必要组件

  1. 通过 Package Manager 安装 OpenXR 插件:

    • 打开 Window > Package Manager
    • 切换到 "Unity Registry"
    • 搜索并安装以下包:
      • XR Plugin Management
      • OpenXR Plugin
  2. 通过 Player Settings 配置 OpenXR:

    • 打开 Edit > Project Settings > XR Plugin Management
    • 切换到目标平台标签(例如 PC, Android)
    • 勾选 "OpenXR" 插件
    • 点击 OpenXR 下方的设置图标并配置:
      • 在 "Interaction Profiles" 中勾选你需要支持的设备
      • 在 "Features" 中启用所需功能(如手部追踪、眼动追踪等)

// 编程方式检查 OpenXR 是否启用
using UnityEngine;
using UnityEngine.XR.Management;

public class OpenXRChecker : MonoBehaviour
{
    void Start()
    {
        var xrSettings = XRGeneralSettings.Instance;
        if (xrSettings == null)
        {
            Debug.LogError("XR 插件管理器未正确安装");
            return;
        }

        var xrManager = xrSettings.Manager;
        if (xrManager == null)
        {
            Debug.LogError("XR 管理器未初始化");
            return;
        }

        if (xrManager.activeLoader == null)
        {
            Debug.LogError("没有 XR 加载器处于活动状态");
            return;
        }

        Debug.Log($"当前 XR 加载器: {xrManager.activeLoader.name}");
        
        // 如果需要明确检查是否为 OpenXR
        if (xrManager.activeLoader.name.Contains("OpenXR"))
        {
            Debug.Log("OpenXR 已成功加载");
        }
    }
}

三、创建基础 OpenXR 项目

场景设置

  1. 设置 XR 原点:

    • 在场景中创建一个空的 GameObject 命名为 "XR Origin"
    • 添加 "XR Origin" 组件
    • 添加 "Input Action Manager" 组件
  2. 设置相机:

    • 在 XR Origin 下创建一个 "Camera Offset" 对象
    • 在 Camera Offset 下添加一个带有 "Camera" 组件的 GameObject
    • 确保相机具有 "Audio Listener" 组件
    • 设置相机的 Clear Flags, Background 及其他参数
  3. 基本场景设置:

    • 添加地面平面以提供参考点
    • 添加简单的环境对象,例如墙、桌子等

OpenXR 交互设置

  1. 手部射线设置:

    • 在 XR Origin 下创建两个空物体,分别为 "LeftHand Controller" 和 "RightHand Controller"
    • 向两个控制器添加以下组件:
      • "XR Controller (Action-based)"
      • "XR Ray Interactor"
      • "XR Interactor Line Visual"
    • 配置每个控制器的输入动作引用
  2. 交互管理器:

    • 在场景中添加空物体命名为 "XR Interaction Manager"
    • 添加 "XR Interaction Manager" 组件
    • 此组件将自动管理所有交互器和交互对象

// 基础 XR 设置脚本
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;

public class XRSetupHelper : MonoBehaviour
{
    public void SetupXROrigin()
    {
        // 创建 XR 原点
        GameObject xrOrigin = new GameObject("XR Origin");
        xrOrigin.AddComponent<XROrigin>();
        
        // 创建摄像机偏移
        GameObject cameraOffset = new GameObject("Camera Offset");
        cameraOffset.transform.SetParent(xrOrigin.transform);
        
        // 创建主摄像机
        GameObject mainCamera = new GameObject("Main Camera");
        mainCamera.transform.SetParent(cameraOffset.transform);
        mainCamera.tag = "MainCamera";
        
        Camera camera = mainCamera.AddComponent<Camera>();
        mainCamera.AddComponent<AudioListener>();
        
        // 设置 XR 原点的摄像机
        XROrigin origin = xrOrigin.GetComponent<XROrigin>();
        origin.Camera = camera;
        
        Debug.Log("XR Origin setup completed!");
    }
}

四、输入交互示例

手柄输入配置

  1. 创建输入动作资源:

    • 在 Project 窗口中右键点击 > Create > Input Actions
    • 创建以下动作映射:
      • 握持按钮 (grip)
      • 扳机按钮 (trigger)
      • 主按钮 (primary button)
      • 摇杆 (thumbstick)
    • 为每个动作设置适当的控制类型和绑定
  2. 连接输入动作:

    • 选择控制器对象
    • 在 XR Controller 组件中分配相应的输入动作
    • 确保 "Enable Input Tracking" 和 "Enable Input Actions" 选项被勾选

手部追踪与交互实例

using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;

public class GrabInteractableExample : MonoBehaviour
{
    private XRGrabInteractable grabInteractable;
    private MeshRenderer meshRenderer;
    private Color originalColor;
    
    void Start()
    {
        // 获取或添加交互组件
        grabInteractable = GetComponent<XRGrabInteractable>();
        if (grabInteractable == null)
            grabInteractable = gameObject.AddComponent<XRGrabInteractable>();
            
        // 获取渲染器组件
        meshRenderer = GetComponent<MeshRenderer>();
        if (meshRenderer != null)
            originalColor = meshRenderer.material.color;
            
        // 注册事件
        grabInteractable.selectEntered.AddListener(OnGrab);
        grabInteractable.selectExited.AddListener(OnRelease);
        grabInteractable.hoverEntered.AddListener(OnHoverStart);
        grabInteractable.hoverExited.AddListener(OnHoverEnd);
    }
    
    private void OnGrab(SelectEnterEventArgs args)
    {
        // 当物体被抓取时
        Debug.Log($"物体 {gameObject.name} 被抓取");
        
        // 可以添加抓取时的效果,如振动反馈
        if (args.interactorObject is XRBaseControllerInteractor controllerInteractor)
        {
            OpenXRControllerHelper.SendHapticImpulse(controllerInteractor.xrController, 0.5f, 0.2f);
        }
    }
    
    private void OnRelease(SelectExitEventArgs args)
    {
        Debug.Log($"物体 {gameObject.name} 被释放");
    }
    
    private void OnHoverStart(HoverEnterEventArgs args)
    {
        // 当交互器悬停在物体上时
        if (meshRenderer != null)
            meshRenderer.material.color = Color.yellow;
    }
    
    private void OnHoverEnd(HoverExitEventArgs args)
    {
        // 当交互器离开物体时
        if (meshRenderer != null)
            meshRenderer.material.color = originalColor;
    }
}

// 控制器帮助类
public static class OpenXRControllerHelper
{
    public static void SendHapticImpulse(XRBaseController controller, float amplitude, float duration)
    {
        if (controller != null)
            controller.SendHapticImpulse(amplitude, duration);
    }
}

五、OpenXR 高级功能

手部追踪

手部追踪允许用户无需控制器直接用手与虚拟对象交互:

using UnityEngine;
using UnityEngine.XR.Hands;
using UnityEngine.XR.OpenXR.Features.Interactions;

public class HandTrackingExample : MonoBehaviour
{
    public GameObject handPrefab; // 手的视觉表现
    private GameObject leftHandObject;
    private GameObject rightHandObject;
    
    private XRHandSubsystem handSubsystem;
    
    void Start()
    {
        // 检查手部追踪功能是否可用
        var handSubsystems = new List<XRHandSubsystem>();
        SubsystemManager.GetSubsystems(handSubsystems);
        
        if (handSubsystems.Count > 0)
        {
            handSubsystem = handSubsystems[0];
            Debug.Log("手部追踪功能已找到");
            
            // 创建手部可视化对象
            leftHandObject = Instantiate(handPrefab);
            rightHandObject = Instantiate(handPrefab);
            
            // 订阅更新事件
            handSubsystem.updatedHands += OnHandsUpdated;
        }
        else
        {
            Debug.LogWarning("未找到手部追踪子系统");
        }
    }
    
    void OnDestroy()
    {
        if (handSubsystem != null)
        {
            handSubsystem.updatedHands -= OnHandsUpdated;
        }
    }
    
    private void OnHandsUpdated(XRHandSubsystem subsystem, XRHandSubsystem.UpdateSuccessFlags updateSuccessFlags)
    {
        // 更新左手
        if ((updateSuccessFlags & XRHandSubsystem.UpdateSuccessFlags.LeftHandRootPose) != 0)
        {
            XRHand leftHand = subsystem.leftHand;
            UpdateHandVisualization(leftHandObject, leftHand);
        }
        
        // 更新右手
        if ((updateSuccessFlags & XRHandSubsystem.UpdateSuccessFlags.RightHandRootPose) != 0)
        {
            XRHand rightHand = subsystem.rightHand;
            UpdateHandVisualization(rightHandObject, rightHand);
        }
    }
    
    private void UpdateHandVisualization(GameObject handObject, XRHand hand)
    {
        if (!hand.isTracked)
        {
            handObject.SetActive(false);
            return;
        }
        
        handObject.SetActive(true);
        
        // 更新手的根部位置和旋转
        var rootPose = hand.rootPose;
        handObject.transform.position = rootPose.position;
        handObject.transform.rotation = rootPose.rotation;
        
        // 更新每个手指关节
        // 注意:这需要手部模型有相应的骨骼结构
        UpdateHandJoints(handObject, hand);
    }
    
    private void UpdateHandJoints(GameObject handObject, XRHand hand)
    {
        // 这里应该根据你的手部模型结构来更新每个关节
        // 下面是示例代码,需要根据实际骨骼结构调整
        
        var joints = handObject.GetComponentsInChildren<Transform>();
        
        foreach (var joint in hand.joints)
        {
            if (joint.trackingState.HasFlag(XRHandJointTrackingState.Pose))
            {
                // 找到对应的骨骼并更新
                // 这里需要根据你的命名约定来匹配关节
                string jointName = GetJointName(joint.id);
                Transform jointTransform = Array.Find(joints, t => t.name == jointName);
                
                if (jointTransform != null)
                {
                    jointTransform.localPosition = joint.pose.localPosition;
                    jointTransform.localRotation = joint.pose.localRotation;
                }
            }
        }
    }
    
    private string GetJointName(XRHandJointID id)
    {
        // 将 XRHandJointID 转换为你模型中使用的关节名称
        // 这需要根据你的手部模型来自定义
        switch (id)
        {
            case XRHandJointID.ThumbMetacarpal: return "thumb_metacarpal";
            case XRHandJointID.ThumbProximal: return "thumb_proximal";
            case XRHandJointID.ThumbDistal: return "thumb_distal";
            case XRHandJointID.ThumbTip: return "thumb_tip";
            // 添加其它手指关节...
            default: return "unknown";
        }
    }
}

眼动跟踪

眼动跟踪允许应用捕获用户注视的位置,用于焦点交互或分析:

using UnityEngine;
using UnityEngine.XR.OpenXR;
using UnityEngine.XR.OpenXR.Features;

public class EyeGazeExample : MonoBehaviour
{
    public Transform gazeIndicator; // 视线指示器
    public float maxDistance = 10f; // 最大检测距离
    public LayerMask hitMask; // 检测层级
    
    private OpenXREyeGazeFeature eyeGazeFeature;
    private bool eyeTrackingSupported = false;
    
    void Start()
    {
        // 检查眼动追踪功能是否可用
        OpenXRFeature[] features = OpenXRSettings.Instance.features;
        foreach (OpenXRFeature feature in features)
        {
            if (feature is OpenXREyeGazeFeature && feature.enabled)
            {
                eyeGazeFeature = feature as OpenXREyeGazeFeature;
                eyeTrackingSupported = true;
                Debug.Log("眼动追踪功能已启用");
                break;
            }
        }
        
        if (!eyeTrackingSupported)
        {
            Debug.LogWarning("眼动追踪功能不可用或未启用");
        }
    }
    
    void Update()
    {
        if (!eyeTrackingSupported) return;
        
        // 获取视线数据
        if (TryGetEyeGaze(out Vector3 origin, out Vector3 direction))
        {
            // 执行视线射线检测
            RaycastHit hit;
            bool hitSomething = Physics.Raycast(origin, direction, out hit, maxDistance, hitMask);
            
            if (hitSomething)
            {
                // 更新视线指示器位置
                if (gazeIndicator != null)
                {
                    gazeIndicator.position = hit.point;
                    gazeIndicator.rotation = Quaternion.LookRotation(-direction);
                    gazeIndicator.gameObject.SetActive(true);
                }
                
                // 处理视线交互
                HandleGazeInteraction(hit);
            }
            else if (gazeIndicator != null)
            {
                gazeIndicator.gameObject.SetActive(false);
            }
        }
    }
    
    bool TryGetEyeGaze(out Vector3 origin, out Vector3 direction)
    {
        // 这里的实现取决于具体的 OpenXR 运行时和 Unity 版本
        // 下面是一个示例,需要根据具体 API 调整
        
        // 假设 eyeGazeFeature 提供了获取视线的方法
        if (eyeGazeFeature != null && eyeGazeFeature.TryGetEyeGaze(out Pose eyeGazePose))
        {
            origin = eyeGazePose.position;
            direction = eyeGazePose.forward;
            return true;
        }
        
        // 如果无法获取,可以尝试使用头部朝向代替
        Camera xrCamera = Camera.main;
        if (xrCamera != null)
        {
            origin = xrCamera.transform.position;
            direction = xrCamera.transform.forward;
            return true;
        }
        
        origin = Vector3.zero;
        direction = Vector3.forward;
        return false;
    }
    
    void HandleGazeInteraction(RaycastHit hit)
    {
        // 获取被注视物体
        GameObject gazedObject = hit.collider.gameObject;
        
        // 可以在这里添加视线交互逻辑
        // 例如:注视时间累积、高亮显示等
        
        IGazeInteractable gazeInteractable = gazedObject.GetComponent<IGazeInteractable>();
        if (gazeInteractable != null)
        {
            gazeInteractable.OnGaze();
        }
    }
}

// 视线交互接口
public interface IGazeInteractable
{
    void OnGaze();
}

// 视线交互示例组件
public class GazeInteractableObject : MonoBehaviour, IGazeInteractable
{
    public float gazeActivationTime = 2.0f; // 激活所需注视时间
    
    private float gazeTimer = 0f;
    private bool isGazed = false;
    private Material material;
    private Color originalColor;
    
    void Start()
    {
        material = GetComponent<Renderer>()?.material;
        if (material != null)
            originalColor = material.color;
    }
    
    void Update()
    {
        if (isGazed)
        {
            gazeTimer += Time.deltaTime;
            
            // 视觉反馈
            if (material != null)
            {
                float t = Mathf.Clamp01(gazeTimer / gazeActivationTime);
                material.color = Color.Lerp(originalColor, Color.green, t);
            }
            
            // 激活条件检查
            if (gazeTimer >= gazeActivationTime)
            {
                OnGazeActivated();
                gazeTimer = 0f;
            }
        }
        else
        {
            // 重置计时器
            gazeTimer = 0f;
            
            // 重置视觉状态
            if (material != null)
                material.color = originalColor;
        }
        
        // 每帧重置注视状态,在 OnGaze 中设置
        isGazed = false;
    }
    
    public void OnGaze()
    {
        isGazed = true;
    }
    
    private void OnGazeActivated()
    {
        Debug.Log($"物体 {gameObject.name} 被注视激活");
        
        // 在这里添加激活逻辑
        // 例如:播放动画、触发事件等
    }
}

六、实用 XR 互动组件

可抓取物体

using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;

public class EnhancedGrabbable : MonoBehaviour
{
    public AudioClip grabSound;
    public AudioClip releaseSound;
    public ParticleSystem grabEffect;
    public float hapticAmplitude = 0.5f;
    public float hapticDuration = 0.2f;
    public bool useTwoHandScaling = true;
    
    private AudioSource audioSource;
    private XRGrabInteractable grabInteractable;
    private Vector3 originalScale;
    private float initialGrabDistance = 0f;
    
    void Start()
    {
        // 初始化组件
        audioSource = gameObject.AddComponent<AudioSource>();
        audioSource.playOnAwake = false;
        audioSource.spatialBlend = 1.0f; // 3D音效
        
        grabInteractable = GetComponent<XRGrabInteractable>();
        if (grabInteractable == null)
            grabInteractable = gameObject.AddComponent<XRGrabInteractable>();
        
        originalScale = transform.localScale;
        
        // 订阅事件
        grabInteractable.selectEntered.AddListener(OnGrab);
        grabInteractable.selectExited.AddListener(OnRelease);
        grabInteractable.firstSelectEntered.AddListener(OnFirstGrab);
        grabInteractable.lastSelectExited.AddListener(OnLastRelease);
    }
    
    private void OnGrab(SelectEnterEventArgs args)
    {
        // 播放抓取音效
        if (grabSound != null)
        {
            audioSource.clip = grabSound;
            audioSource.Play();
        }
        
        // 触发粒子效果
        if (grabEffect != null)
        {
            grabEffect.Play();
        }
        
        // 触觉反馈
        if (args.interactorObject is XRBaseControllerInteractor controllerInteractor)
        {
            if (controllerInteractor.xrController != null)
            {
                controllerInteractor.xrController.SendHapticImpulse(hapticAmplitude, hapticDuration);
            }
        }
        
        if (useTwoHandScaling && grabInteractable.interactorsSelecting.Count == 2)
        {
            // 获取两个交互器
            var interactor1 = grabInteractable.interactorsSelecting[0] as XRBaseInteractor;
            var interactor2 = grabInteractable.interactorsSelecting[1] as XRBaseInteractor;
            
            // 计算初始距离
            initialGrabDistance = Vector3.Distance(
                interactor1.transform.position, 
                interactor2.transform.position);
        }
    }
    
    private void OnRelease(SelectExitEventArgs args)
    {
        // 播放释放音效
        if (releaseSound != null)
        {
            audioSource.clip = releaseSound;
            audioSource.Play();
        }
    }
    
    private void OnFirstGrab(SelectEnterEventArgs args)
    {
        // 第一次被抓取时的特殊处理
        Debug.Log($"物体 {gameObject.name} 开始被抓取");
    }
    
    private void OnLastRelease(SelectExitEventArgs args)
    {
        // 最后一个交互器释放时的处理
        Debug.Log($"物体 {gameObject.name} 完全释放");
    }
    
    void Update()
    {
        // 双手缩放逻辑
        if (useTwoHandScaling && grabInteractable.interactorsSelecting.Count == 2)
        {
            var interactor1 = grabInteractable.interactorsSelecting[0] as XRBaseInteractor;
            var interactor2 = grabInteractable.interactorsSelecting[1] as XRBaseInteractor;
            
            float currentDistance = Vector3.Distance(
                interactor1.transform.position, 
                interactor2.transform.position);
            
            float scaleFactor = currentDistance / initialGrabDistance;
            
            // 计算新的缩放值
            Vector3 newScale = originalScale * scaleFactor;
            
            // 限制缩放范围
            newScale = Vector3.ClampMagnitude(newScale, originalScale.magnitude * 3f);
            newScale = Vector3.Max(newScale, originalScale * 0.5f);
            
            // 应用缩放
            transform.localScale = newScale;
        }
    }
}

传送系统

using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.XR.Interaction.Toolkit;

public class TeleportationController : MonoBehaviour
{
    public InputActionReference teleportActivateAction;
    public InputActionReference teleportCancelAction;
    public XRRayInteractor rayInteractor;
    public TeleportationProvider teleportationProvider;
    public GameObject teleportReticle;
    
    private bool isTeleportActive = false;
    private bool wasInteractorRayActive = false;
    private LayerMask originalInteractionLayerMask;
    public LayerMask teleportLayerMask;
    
    void Start()
    {
        // 保存原始设置
        originalInteractionLayerMask = rayInteractor.interactionLayers;
        
        // 初始化传送准星
        if (teleportReticle != null)
            teleportReticle.SetActive(false);
        
        // 订阅输入事件
        teleportActivateAction.action.started += OnTeleportActivate;
        teleportCancelAction.action.performed += OnTeleportCancel;
        
        // 确保传送系统已初始化
        if (teleportationProvider == null)
            teleportationProvider = FindObjectOfType<TeleportationProvider>();
    }
    
    void OnDestroy()
    {
        // 取消订阅输入事件
        teleportActivateAction.action.started -= OnTeleportActivate;
        teleportCancelAction.action.performed -= OnTeleportCancel;
    }
    
    private void OnTeleportActivate(InputAction.CallbackContext context)
    {
        if (!isTeleportActive)
        {
            // 激活传送模式
            wasInteractorRayActive = rayInteractor.enabled;
            
            // 确保射线交互器启用
            rayInteractor.enabled = true;
            
            // 切换到传送层
            rayInteractor.interactionLayers = teleportLayerMask;
            
            // 显示传送准星
            if (teleportReticle != null)
                teleportReticle.SetActive(true);
            
            isTeleportActive = true;
        }
    }
    
    private void OnTeleportCancel(InputAction.CallbackContext context)
    {
        if (isTeleportActive)
        {
            // 关闭传送模式
            rayInteractor.enabled = wasInteractorRayActive;
            
            // 恢复原始层设置
            rayInteractor.interactionLayers = originalInteractionLayerMask;
            
            // 隐藏传送准星
            if (teleportReticle != null)
                teleportReticle.SetActive(false);
            
            isTeleportActive = false;
        }
    }
    
    void Update()
    {
        if (isTeleportActive)
        {
            // 检查是否有有效的传送目标
            if (rayInteractor.TryGetCurrent3DRaycastHit(out RaycastHit hit))
            {
                // 检查命中表面是否可传送
                TeleportArea teleportArea = hit.collider.GetComponent<TeleportArea>();
                TeleportAnchor teleportAnchor = hit.collider.GetComponent<TeleportAnchor>();
                
                if (teleportArea != null || teleportAnchor != null)
                {
                    // 更新准星位置
                    if (teleportReticle != null)
                    {
                        teleportReticle.transform.position = hit.point + hit.normal * 0.01f;
                        teleportReticle.transform.up = hit.normal;
                        teleportReticle.SetActive(true);
                    }
                    
                    // 如果松开按钮,执行传送
                    if (teleportActivateAction.action.phase == InputActionPhase.Waiting)
                    {
                        // 创建传送请求
                        TeleportRequest request = new TeleportRequest
                        {
                            destinationPosition = hit.point,
                            destinationRotation = Quaternion.LookRotation(Vector3.ProjectOnPlane(rayInteractor.transform.forward, Vector3.up)),
                            matchOrientation = MatchOrientation.WorldSpaceUp
                        };
                        
                        // 执行传送
                        teleportationProvider.QueueTeleportRequest(request);
                        
                        // 重置传送模式
                        OnTeleportCancel(new InputAction.CallbackContext());
                    }
                }
                else if (teleportReticle != null)
                {
                    // 如果不是可传送表面,隐藏准星
                    teleportReticle.SetActive(false);
                }
            }
            else if (teleportReticle != null)
            {
                // 如果没有命中任何物体,隐藏准星
                teleportReticle.SetActive(false);
            }
        }
    }
}

UI 交互

using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;
using UnityEngine.UI;
using TMPro;

public class XRUIInteractionExample : MonoBehaviour
{
    public Canvas mainCanvas;
    public GraphicRaycaster graphicRaycaster;
    public TMP_Text statusText;
    public Button[] interactiveButtons;
    public Slider valueSlider;
    
    private XRRayInteractor rayInteractor;
    private XRInteractionManager interactionManager;
    
    void Start()
    {
        // 查找交互器和交互管理器
        rayInteractor = FindObjectOfType<XRRayInteractor>();
        interactionManager = FindObjectOfType<XRInteractionManager>();
        
        if (rayInteractor == null || interactionManager == null)
        {
            Debug.LogError("找不到必要的XR交互组件");
            return;
        }
        
        // 确保Canvas设置正确
        if (mainCanvas != null)
        {
            // 可选:设置Canvas为世界空间
            mainCanvas.renderMode = RenderMode.WorldSpace;
            
            // 确保拥有GraphicRaycaster
            if (graphicRaycaster == null)
                graphicRaycaster = mainCanvas.GetComponent<GraphicRaycaster>();
            
            if (graphicRaycaster == null)
                graphicRaycaster = mainCanvas.gameObject.AddComponent<GraphicRaycaster>();
        }
        
        // 设置UI交互
        SetupUIInteractions();
    }
    
    void SetupUIInteractions()
    {
        // 为按钮添加事件监听
        for (int i = 0; i < interactiveButtons.Length; i++)
        {
            Button button = interactiveButtons[i];
            int buttonIndex = i;
            
            button.onClick.AddListener(() => {
                OnButtonClicked(buttonIndex);
            });
        }
        
        // 为滑动条添加事件监听
        if (valueSlider != null)
        {
            valueSlider.onValueChanged.AddListener(OnSliderValueChanged);
        }
    }
    
    void OnButtonClicked(int buttonIndex)
    {
        if (statusText != null)
        {
            statusText.text = $"按钮 {buttonIndex + 1} 被点击了!";
        }
        
        // 触觉反馈
        if (rayInteractor != null && rayInteractor.TryGetComponent<XRBaseController>(out var controller))
        {
            controller.SendHapticImpulse(0.5f, 0.1f);
        }
    }
    
    void OnSliderValueChanged(float value)
    {
        if (statusText != null)
        {
            statusText.text = $"滑动值: {value:F2}";
        }
    }
    
    // 创建XR友好的UI元素的辅助方法
    public Button CreateXRButton(string text, Vector3 position, Vector2 size)
    {
        GameObject buttonObj = new GameObject("XR_Button");
        buttonObj.transform.SetParent(mainCanvas.transform, false);
        buttonObj.transform.localPosition = position;
        
        // 添加RectTransform并设置大小
        RectTransform rectTransform = buttonObj.AddComponent<RectTransform>();
        rectTransform.sizeDelta = size;
        
        // 添加图像组件
        Image image = buttonObj.AddComponent<Image>();
        image.color = new Color(0.2f, 0.2f, 0.2f);
        
        // 添加按钮组件
        Button button = buttonObj.AddComponent<Button>();
        ColorBlock colors = button.colors;
        colors.normalColor = new Color(0.2f, 0.2f, 0.2f);
        colors.highlightedColor = new Color(0.3f, 0.3f, 0.3f);
        colors.pressedColor = new Color(0.1f, 0.1f, 0.1f);
        colors.selectedColor = new Color(0.3f, 0.3f, 0.3f);
        button.colors = colors;
        
        // 添加文本
        GameObject textObj = new GameObject("Text");
        textObj.transform.SetParent(buttonObj.transform, false);
        
        RectTransform textRectTransform = textObj.AddComponent<RectTransform>();
        textRectTransform.anchorMin = Vector2.zero;
        textRectTransform.anchorMax = Vector2.one;
        textRectTransform.offsetMin = Vector2.zero;
        textRectTransform.offsetMax = Vector2.zero;
        
        TMP_Text tmpText = textObj.AddComponent<TextMeshProUGUI>();
        tmpText.text = text;
        tmpText.color = Color.white;
        tmpText.fontSize = 24;
        tmpText.alignment = TextAlignmentOptions.Center;
        
        return button;
    }
}

七、性能优化

移动 VR 设备关键优化

using UnityEngine;
using UnityEngine.XR;

public class XRPerformanceOptimizer : MonoBehaviour
{
    [Header("帧率设置")]
    public float targetFrameRate = 90f;
    public bool adaptiveResolution = true;
    
    [Header("LOD 设置")]
    public float lodThresholdNear = 5f;
    public float lodThresholdFar = 20f;
    
    [Header("渲染优化")]
    public bool dynamicBatching = true;
    public bool occlusion = true;
    public Camera xrCamera;
    
    void Start()
    {
        // 设置目标帧率
        Application.targetFrameRate = (int)targetFrameRate;
        
        // 设置自适应分辨率
        if (adaptiveResolution && XRSettings.enabled)
        {
            XRSettings.eyeTextureResolutionScale = 1.0f;
        }
        
        // 配置LOD系统
        ConfigureLOD();
        
        // 配置摄像机
        if (xrCamera != null)
        {
            // 优化裁剪距离
            xrCamera.farClipPlane = 50f; // 根据游戏场景适当调整
            
            // 启用或禁用遮挡剔除
            xrCamera.useOcclusionCulling = occlusion;
        }
        
        // 优化QualitySettings
        QualitySettings.vSyncCount = 0; // 在VR中通常禁用VSync
        QualitySettings.maxQueuedFrames = 1; // 减少延迟
        
        // 启用或禁用动态批处理
        if (dynamicBatching)
        {
            // 注意:某些渲染管线可能不支持动态批处理
            GraphicsSettings.useScriptableRenderPipelineBatching = false;
        }
    }
    
    void Update()
    {
        if (adaptiveResolution)
        {
            // 动态调整分辨率以维持目标帧率
            AdjustResolutionScale();
        }
    }
    
    private void ConfigureLOD()
    {
        // 获取所有LOD组
        LODGroup[] lodGroups = FindObjectsOfType<LODGroup>();
        
        foreach (LODGroup group in lodGroups)
        {
            // 获取现有的LOD级别
            LOD[] lods = group.GetLODs();
            
            if (lods.Length >= 2)
            {
                // 调整LOD阈值以在VR中更快地切换到低细节模型
                lods[0].screenRelativeTransitionHeight = 0.4f; // 高细节
                lods[1].screenRelativeTransitionHeight = 0.1f; // 低细节
                
                // 如果有更多LOD级别,可以继续调整
                
                // 应用更改
                group.SetLODs(lods);
            }
        }
    }
    
    private void AdjustResolutionScale()
    {
        // 这是一个简单的实现,实际应用中应该使用更复杂的算法
        
        // 获取当前帧时间
        float frameTime = Time.deltaTime;
        float currentFPS = 1.0f / frameTime;
        
        // 帧率目标的90%作为阈值
        float fpsThreshold = targetFrameRate * 0.9f;
        
        // 当前分辨率比例
        float currentScale = XRSettings.eyeTextureResolutionScale;
        
        if (currentFPS < fpsThreshold)
        {
            // 如果帧率太低,降低分辨率
            float newScale = currentScale - 0.05f;
            XRSettings.eyeTextureResolutionScale = Mathf.Clamp(newScale, 0.5f, 1.0f);
        }
        else if (currentFPS > targetFrameRate && currentScale < 1.0f)
        {
            // 如果帧率充足且分辨率低于1.0,尝试提高分辨率
            float newScale = currentScale + 0.01f;
            XRSettings.eyeTextureResolutionScale = Mathf.Clamp(newScale, 0.5f, 1.0f);
        }
    }
    
    // 检测性能问题并记录
    public void PerformanceCheck()
    {
        // 记录关键性能指标
        float cpuTime = Time.deltaTime * 1000f;
        string gpuName = SystemInfo.graphicsDeviceName;
        int memory = SystemInfo.graphicsMemorySize;
        
        Debug.Log($"性能检查: CPU帧时间={cpuTime:F2}ms, GPU={gpuName}, 显存={memory}MB");
        
        // 检查潜在问题
        if (cpuTime > (1000f / targetFrameRate) * 0.8f)
        {
            Debug.LogWarning("CPU负载接近阈值,考虑优化游戏逻辑或物理计算");
        }
        
        if (QualitySettings.shadows != ShadowQuality.Disable)
        {
            Debug.Log("阴影已启用,这可能会影响移动VR的性能");
        }
    }
}

资源管理

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class XRResourceManager : MonoBehaviour
{
    [System.Serializable]
    public class LODSettings
    {
        public float distance;
        public GameObject highQualityPrefab;
        public GameObject lowQualityPrefab;
    }
    
    public Transform xrOrigin;
    public List<LODSettings> lodObjects = new List<LODSettings>();
    public float checkInterval = 0.5f;
    
    private List<GameObject> spawnedObjects = new List<GameObject>();
    private Dictionary<LODSettings, GameObject> activeInstances = new Dictionary<LODSettings, GameObject>();
    
    void Start()
    {
        if (xrOrigin == null)
        {
            xrOrigin = Camera.main.transform.parent?.parent;
            if (xrOrigin == null)
            {
                Debug.LogError("无法找到XR原点,请手动分配");
                return;
            }
        }
        
        // 开始定期检查LOD
        StartCoroutine(LODCheckRoutine());
    }
    
    IEnumerator LODCheckRoutine()
    {
        while (true)
        {
            UpdateLODs();
            yield return new WaitForSeconds(checkInterval);
        }
    }
    
    void UpdateLODs()
    {
        if (xrOrigin == null) return;
        
        Vector3 viewerPosition = xrOrigin.position;
        
        foreach (var lodSetting in lodObjects)
        {
            // 检查与设置的距离
            float distance = Vector3.Distance(viewerPosition, lodSetting.highQualityPrefab.transform.position);
            
            // 根据距离决定使用哪个LOD级别
            if (distance <= lodSetting.distance)
            {
                // 使用高质量模型
                SwapToHighQuality(lodSetting);
            }
            else
            {
                // 使用低质量模型
                SwapToLowQuality(lodSetting);
            }
        }
    }
    
    void SwapToHighQuality(LODSettings setting)
    {
        if (!activeInstances.TryGetValue(setting, out GameObject current) || 
            current != setting.highQualityPrefab)
        {
            // 删除旧实例
            if (current != null)
            {
                Destroy(current);
            }
            
            // 创建高质量实例
            GameObject instance = Instantiate(setting.highQualityPrefab, 
                                            setting.highQualityPrefab.transform.position, 
                                            setting.highQualityPrefab.transform.rotation);
            
            activeInstances[setting] = instance;
            spawnedObjects.Add(instance);
        }
    }
    
    void SwapToLowQuality(LODSettings setting)
    {
        if (!activeInstances.TryGetValue(setting, out GameObject current) || 
            current != setting.lowQualityPrefab)
        {
            // 删除旧实例
            if (current != null)
            {
                Destroy(current);
            }
            
            // 创建低质量实例
            GameObject instance = Instantiate(setting.lowQualityPrefab, 
                                            setting.lowQualityPrefab.transform.position, 
                                            setting.lowQualityPrefab.transform.rotation);
            
            activeInstances[setting] = instance;
            spawnedObjects.Add(instance);
        }
    }
    
    // 异步加载资源
    public void LoadResourcesAsync(string sceneName, System.Action onComplete = null)
    {
        StartCoroutine(LoadResourcesRoutine(sceneName, onComplete));
    }
    
    IEnumerator LoadResourcesRoutine(string sceneName, System.Action onComplete)
    {
        // 显示加载屏
        ShowLoadingScreen(true);
        
        // 异步加载场景
        AsyncOperation asyncLoad = UnityEngine.SceneManagement.SceneManager.LoadSceneAsync(sceneName, UnityEngine.SceneManagement.LoadSceneMode.Additive);
        asyncLoad.allowSceneActivation = false;
        
        // 等待加载到90%
        while (asyncLoad.progress < 0.9f)
        {
            UpdateLoadingProgress(asyncLoad.progress);
            yield return null;
        }
        
        // 最终准备
        UpdateLoadingProgress(1.0f);
        yield return new WaitForSeconds(0.5f);
        
        // 激活场景
        asyncLoad.allowSceneActivation = true;
        
        // 隐藏加载屏
        ShowLoadingScreen(false);
        
        // 完成回调
        onComplete?.Invoke();
    }
    
    private void ShowLoadingScreen(bool show)
    {
        // 实现显示/隐藏加载屏的逻辑
        // 这里应该连接到你的UI系统
    }
    
    private void UpdateLoadingProgress(float progress)
    {
        // 更新加载进度条
        // 这里应该连接到你的UI系统
        Debug.Log($"加载进度: {progress * 100:F0}%");
    }
    
    void OnDestroy()
    {
        // 清理所有生成的对象
        foreach (var obj in spawnedObjects)
        {
            if (obj != null)
                Destroy(obj);
        }
    }
}

八、平台特定配置

为不同 XR 设备配置控制器

using UnityEngine;
using UnityEngine.XR;
using UnityEngine.XR.Interaction.Toolkit;
using System.Collections.Generic;

public class XRDeviceConfigurator : MonoBehaviour
{
    [System.Serializable]
    public class ControllerConfig
    {
        public string deviceName;
        public GameObject controllerPrefab;
        public GameObject handPrefab;
        public Vector3 positionOffset;
        public Vector3 rotationOffset;
    }
    
    public List<ControllerConfig> controllerConfigs = new List<ControllerConfig>();
    public Transform leftHandAnchor;
    public Transform rightHandAnchor;
    public bool useHandsWhenAvailable = true;
    
    private GameObject leftControllerInstance;
    private GameObject rightControllerInstance;
    private ControllerConfig activeConfig;
    
    void Start()
    {
        // 检测当前设备
        string deviceName = DetectCurrentDevice();
        Debug.Log($"检测到XR设备: {deviceName}");
        
        // 配置控制器和输入
        ConfigureForDevice(deviceName);
    }
    
    string DetectCurrentDevice()
    {
        // 检查已连接的XR设备
        var devices = new List<InputDevice>();
        InputDevices.GetDevices(devices);
        
        foreach (var device in devices)
        {
            if (device.characteristics.HasFlag(InputDeviceCharacteristics.HeadMounted))
            {
                return device.name;
            }
        }
        
        return "Unknown";
    }
    
    void ConfigureForDevice(string deviceName)
    {
        // 查找匹配的配置
        foreach (var config in controllerConfigs)
        {
            if (deviceName.ToLower().Contains(config.deviceName.ToLower()))
            {
                activeConfig = config;
                ApplyConfiguration(config);
                return;
            }
        }
        
        // 如果没有匹配,使用第一个配置作为默认值
        if (controllerConfigs.Count > 0)
        {
            Debug.LogWarning($"未找到设备 '{deviceName}' 的配置,使用默认配置");
            activeConfig = controllerConfigs[0];
            ApplyConfiguration(activeConfig);
        }
        else
        {
            Debug.LogError("没有可用的控制器配置");
        }
    }
    
    void ApplyConfiguration(ControllerConfig config)
    {
        // 清理现有实例
        if (leftControllerInstance != null) Destroy(leftControllerInstance);
        if (rightControllerInstance != null) Destroy(rightControllerInstance);
        
        // 检查是否有手部追踪
        bool useHands = useHandsWhenAvailable && IsHandTrackingAvailable() && config.handPrefab != null;
        
        // 创建控制器或手的模型
        GameObject prefabToUse = useHands ? config.handPrefab : config.controllerPrefab;
        
        if (prefabToUse != null)
        {
            // 创建左手控制器
            if (leftHandAnchor != null)
            {
                leftControllerInstance = Instantiate(prefabToUse, leftHandAnchor);
                leftControllerInstance.transform.localPosition = config.positionOffset;
                leftControllerInstance.transform.localRotation = Quaternion.Euler(config.rotationOffset);
                
                // 标识为左手
                XRController leftController = leftHandAnchor.GetComponent<XRController>();
                if (leftController != null)
                {
                    leftController.controllerNode = XRNode.LeftHand;
                }
            }
            
            // 创建右手控制器
            if (rightHandAnchor != null)
            {
                rightControllerInstance = Instantiate(prefabToUse, rightHandAnchor);
                rightControllerInstance.transform.localPosition = config.positionOffset;
                rightControllerInstance.transform.localRotation = Quaternion.Euler(config.rotationOffset);
                
                // 标识为右手
                XRController rightController = rightHandAnchor.GetComponent<XRController>();
                if (rightController != null)
                {
                    rightController.controllerNode = XRNode.RightHand;
                }
            }
        }
        
        // 配置输入系统
        ConfigureInputActions(useHands);
    }
    
    bool IsHandTrackingAvailable()
    {
        // 检查是否有手部追踪子系统
        var handSubsystems = new List<XRHandSubsystem>();
        SubsystemManager.GetSubsystems(handSubsystems);
        
        if (handSubsystems.Count > 0)
        {
            XRHandSubsystem handSubsystem = handSubsystems[0];
            return handSubsystem.trackingAcquired;
        }
        
        return false;
    }
    
    void ConfigureInputActions(bool useHands)
    {
        // 根据是使用控制器还是手部追踪来配置输入动作
        // 这里的实现取决于你的项目结构
        
        Debug.Log($"配置输入动作,使用手部追踪: {useHands}");
        
        // 查找所有使用输入的组件
        var controllers = FindObjectsOfType<XRBaseController>();
        foreach (var controller in controllers)
        {
            // 更新控制器设置
            controller.useDirectVelocity = useHands; // 手部追踪通常需要直接速度
            controller.enableInputTracking = true;
            controller.enableInputActions = true;
        }
    }
}

设备特定的交互优化

using UnityEngine;
using UnityEngine.XR;
using UnityEngine.XR.Interaction.Toolkit;
using System.Collections.Generic;

public class DeviceSpecificInteractions : MonoBehaviour
{
    [System.Serializable]
    public class DeviceSettings
    {
        public string deviceType;
        public float grabDistance = 0.1f;
        public float throwVelocityScale = 1.5f;
        public float hapticIntensity = 0.5f;
        public float rayLength = 5.0f;
    }
    
    public List<DeviceSettings> deviceSettingsList = new List<DeviceSettings>();
    public XRRayInteractor leftRayInteractor;
    public XRRayInteractor rightRayInteractor;
    public XRDirectInteractor leftDirectInteractor;
    public XRDirectInteractor rightDirectInteractor;
    
    private DeviceSettings currentSettings;
    
    void Start()
    {
        // 检测设备类型并应用设置
        string deviceType = DetectDeviceType();
        ApplyDeviceSettings(deviceType);
    }
    
    string DetectDeviceType()
    {
        // 检查已连接的XR设备
        var devices = new List<InputDevice>();
        InputDevices.GetDevices(devices);
        
        foreach (var device in devices)
        {
            if (device.characteristics.HasFlag(InputDeviceCharacteristics.HeadMounted))
            {
                if (device.name.ToLower().Contains("quest"))
                    return "Oculus";
                else if (device.name.ToLower().Contains("pico"))
                    return "Pico";
                else if (device.name.ToLower().Contains("vive"))
                    return "Vive";
                else if (device.name.ToLower().Contains("index"))
                    return "Index";
                else if (device.name.ToLower().Contains("windows"))
                    return "WMR";
            }
        }
        
        return "Generic";
    }
    
    void ApplyDeviceSettings(string deviceType)
    {
        // 查找匹配的设备设置
        foreach (var settings in deviceSettingsList)
        {
            if (settings.deviceType.ToLower() == deviceType.ToLower())
            {
                currentSettings = settings;
                break;
            }
        }
        
        // 如果未找到匹配,使用通用设置
        if (currentSettings == null && deviceSettingsList.Count > 0)
        {
            foreach (var settings in deviceSettingsList)
            {
                if (settings.deviceType.ToLower() == "generic")
                {
                    currentSettings = settings;
                    break;
                }
            }
        }
        
        // 如果仍未找到,创建默认设置
        if (currentSettings == null)
        {
            currentSettings = new DeviceSettings
            {
                deviceType = "Generic",
                grabDistance = 0.1f,
                throwVelocityScale = 1.5f,
                hapticIntensity = 0.5f,
                rayLength = 5.0f
            };
        }
        
        // 应用设置到交互器
        ConfigureInteractors();
        
        Debug.Log($"已应用 {currentSettings.deviceType} 设备的交互设置");
    }
    
    void ConfigureInteractors()
    {
        // 配置射线交互器
        if (leftRayInteractor != null)
        {
            leftRayInteractor.maxRaycastDistance = currentSettings.rayLength;
        }
        
        if (rightRayInteractor != null)
        {
            rightRayInteractor.maxRaycastDistance = currentSettings.rayLength;
        }
        
        // 配置直接交互器
        if (leftDirectInteractor != null)
        {
            // 设置抓取距离
            SphereCollider leftSphere = leftDirectInteractor.GetComponent<SphereCollider>();
            if (leftSphere != null)
            {
                leftSphere.radius = currentSettings.grabDistance;
            }
        }
        
        if (rightDirectInteractor != null)
        {
            SphereCollider rightSphere = rightDirectInteractor.GetComponent<SphereCollider>();
            if (rightSphere != null)
            {
                rightSphere.radius = currentSettings.grabDistance;
            }
        }
        
        // 配置可抓取物体的投掷力度
        ConfigureGrabbableObjects();
    }
    
    void ConfigureGrabbableObjects()
    {
        // 查找所有可抓取物体并应用设备特定设置
        XRGrabInteractable[] grabObjects = FindObjectsOfType<XRGrabInteractable>();
        
        foreach (var grabObject in grabObjects)
        {
            // 应用投掷力度
            grabObject.throwVelocityScale = currentSettings.throwVelocityScale;
            
            // 其他可能的设置...
        }
    }
    
    // 设备特定的触觉反馈
    public void SendDeviceSpecificHaptics(XRBaseController controller, float intensity, float duration)
    {
        if (controller != null)
        {
            // 应用设备特定的强度缩放
            float scaledIntensity = intensity * currentSettings.hapticIntensity;
            controller.SendHapticImpulse(scaledIntensity, duration);
        }
    }
    
    // 根据设备调整交互逻辑
    public bool ShouldUseVelocityTeleport()
    {
        // 例如:Quest 和 WMR 使用运动传送,其他设备使用点击传送
        return currentSettings.deviceType == "Oculus" || currentSettings.deviceType == "WMR";
    }
}

九、实用工具类与扩展

XR 调试工具

using UnityEngine;
using UnityEngine.XR;
using TMPro;
using System.Collections.Generic;
using UnityEngine.XR.Interaction.Toolkit;

public class XRDebugTool : MonoBehaviour
{
    [Header("UI 组件")]
    public Canvas debugCanvas;
    public TMP_Text statusText;
    public TMP_Text positionText;
    public TMP_Text frameRateText;
    
    [Header("设置")]
    public bool showControllerInfo = true;
    public bool showPositionInfo = true;
    public bool showFrameRate = true;
    public float updateInterval = 0.5f;
    
    private XROrigin xrOrigin;
    private float deltaTime = 0.0f;
    private float updateTimer = 0.0f;
    
    void Start()
    {
        // 寻找XR Origin
        xrOrigin = FindObjectOfType<XROrigin>();
        
        // 初始化调试UI
        if (debugCanvas != null)
        {
            debugCanvas.renderMode = RenderMode.WorldSpace;
            
            // 确保画布跟随相机但保持一定距离
            debugCanvas.transform.localScale = Vector3.one * 0.005f; // 缩放到合适大小
            
            // 将画布定位到相机前方
            Camera xrCamera = xrOrigin?.Camera ?? Camera.main;
            if (xrCamera != null)
            {
                debugCanvas.transform.SetParent(xrCamera.transform);
                debugCanvas.transform.localPosition = new Vector3(0, 0, 0.5f);
                debugCanvas.transform.localRotation = Quaternion.identity;
            }
        }
        
        // 初始化文本字段
        if (statusText != null)
            statusText.text = $"OpenXR调试工具\n设备: {XRSettings.loadedDeviceName}";
    }
    
    void Update()
    {
        // 更新帧率计算
        deltaTime += (Time.unscaledDeltaTime - deltaTime) * 0.1f;
        
        // 定时更新UI信息
        updateTimer += Time.deltaTime;
        if (updateTimer >= updateInterval)
        {
            updateTimer = 0;
            UpdateDebugInfo();
        }
        
        // 处理输入切换调试画布
        CheckDebugToggleInput();
    }
    
    void UpdateDebugInfo()
    {
        if (!debugCanvas.gameObject.activeSelf) return;
        
        StringBuilder info = new StringBuilder();
        
        // 显示帧率
        if (showFrameRate && frameRateText != null)
        {
            float fps = 1.0f / deltaTime;
            string fpsColor = fps >= 80 ? "green" : (fps >= 60 ? "yellow" : "red");
            frameRateText.text = $"FPS: <color={fpsColor}>{fps:0.0}</color>";
        }
        
        // 显示位置信息
        if (showPositionInfo && positionText != null && xrOrigin != null)
        {
            Vector3 position = xrOrigin.transform.position;
            Quaternion rotation = xrOrigin.Camera.transform.rotation;
            positionText.text = $"位置: X:{position.x:F2} Y:{position.y:F2} Z:{position.z:F2}\n" +
                               $"旋转: X:{rotation.eulerAngles.x:F1} Y:{rotation.eulerAngles.y:F1} Z:{rotation.eulerAngles.z:F1}";
        }
        
        // 显示控制器信息
        if (showControllerInfo && statusText != null)
        {
            info.Append("控制器状态:\n");
            
            // 获取左手控制器
            InputDevice leftHandDevice = GetController(InputDeviceCharacteristics.Left);
            if (leftHandDevice.isValid)
            {
                info.Append("左手: 已连接\n");
                AppendControllerFeatures(leftHandDevice, info);
            }
            else
            {
                info.Append("左手: 未连接\n");
            }
            
            // 获取右手控制器
            InputDevice rightHandDevice = GetController(InputDeviceCharacteristics.Right);
            if (rightHandDevice.isValid)
            {
                info.Append("右手: 已连接\n");
                AppendControllerFeatures(rightHandDevice, info);
            }
            else
            {
                info.Append("右手: 未连接\n");
            }
            
            // 手部追踪状态
            info.Append($"手部追踪: {(IsHandTrackingAvailable() ? "可用" : "不可用")}\n");
            
            // 显示XR系统信息
            info.Append($"设备: {XRSettings.loadedDeviceName}\n");
            info.Append($"视野: {XRSettings.eyeTextureWidth}x{XRSettings.eyeTextureHeight}\n");
            info.Append($"渲染比例: {XRSettings.eyeTextureResolutionScale:F2}\n");
            
            statusText.text = info.ToString();
        }
    }
    
    void AppendControllerFeatures(InputDevice device, StringBuilder info)
    {
        // 检查主要按钮状态
        if (device.TryGetFeatureValue(CommonUsages.primaryButton, out bool primaryButton))
            info.Append($"  A/X: {(primaryButton ? "按下" : "释放")}\n");
            
        // 检查触发键状态
        if (device.TryGetFeatureValue(CommonUsages.trigger, out float trigger))
            info.Append($"  扳机: {trigger:F2}\n");
            
        // 检查握持键状态
        if (device.TryGetFeatureValue(CommonUsages.grip, out float grip))
            info.Append($"  握持: {grip:F2}\n");
    }
    
    InputDevice GetController(InputDeviceCharacteristics characteristics)
    {
        var devices = new List<InputDevice>();
        InputDevices.GetDevicesWithCharacteristics(
            InputDeviceCharacteristics.Controller | characteristics, 
            devices);
            
        if (devices.Count > 0)
            return devices[0];
            
        return new InputDevice();
    }
    
    bool IsHandTrackingAvailable()
    {
        var handSubsystems = new List<XRHandSubsystem>();
        SubsystemManager.GetSubsystems(handSubsystems);
        return handSubsystems.Count > 0;
    }
    
    void CheckDebugToggleInput()
    {
        // 检测同时按下双手控制器的特定按钮组合以切换调试面板
        // 例如:同时按下左右手的二级按钮
        
        bool leftSecondaryPressed = false;
        bool rightSecondaryPressed = false;
        
        InputDevice leftController = GetController(InputDeviceCharacteristics.Left);
        if (leftController.isValid)
        {
            if (leftController.TryGetFeatureValue(CommonUsages.secondaryButton, out bool pressed))
                leftSecondaryPressed = pressed;
        }
        
        InputDevice rightController = GetController(InputDeviceCharacteristics.Right);
        if (rightController.isValid)
        {
            if (rightController.TryGetFeatureValue(CommonUsages.secondaryButton, out bool pressed))
                rightSecondaryPressed = pressed;
        }
        
        // 当两个控制器的按钮同时按下时切换调试面板
        if (leftSecondaryPressed && rightSecondaryPressed)
        {
            // 防止连续切换,添加简单的防抖
            if (!isToggling)
            {
                isToggling = true;
                
                if (debugCanvas != null)
                    debugCanvas.gameObject.SetActive(!debugCanvas.gameObject.activeSelf);
                    
                // 在下一帧重置切换状态
                StartCoroutine(ResetToggleState());
            }
        }
    }
    
    private bool isToggling = false;
    
    System.Collections.IEnumerator ResetToggleState()
    {
        yield return new WaitForSeconds(0.5f);
        isToggling = false;
    }
}

手势识别系统

using UnityEngine;
using UnityEngine.XR;
using UnityEngine.XR.Hands;
using System;
using System.Collections.Generic;

public class XRHandGestureRecognizer : MonoBehaviour
{
    [Serializable]
    public class GestureDefinition
    {
        public string gestureName;
        [Range(0f, 1f)]
        public float confidenceThreshold = 0.8f;
        public bool thumbExtended;
        public bool indexExtended;
        public bool middleExtended;
        public bool ringExtended;
        public bool pinkyExtended;
        
        [Header("可选 - 高级配置")]
        public bool checkPinch = false;
        public bool checkFist = false;
        public bool checkPalm = false;
    }
    
    [SerializeField]
    private List<GestureDefinition> gestures = new List<GestureDefinition>();
    
    [SerializeField]
    private float gestureHoldTime = 0.5f;
    
    public event Action<string, XRNode> OnGestureRecognized;
    
    private XRHandSubsystem handSubsystem;
    private Dictionary<string, float> gestureHoldTimers = new Dictionary<string, float>();
    private Dictionary<XRNode, string> lastRecognizedGesture = new Dictionary<XRNode, string>();
    
    void Start()
    {
        // 初始化手部追踪
        InitializeHandTracking();
    }
    
    void InitializeHandTracking()
    {
        // 获取手部追踪子系统
        var handSubsystems = new List<XRHandSubsystem>();
        SubsystemManager.GetSubsystems(handSubsystems);
        
        if (handSubsystems.Count > 0)
        {
            handSubsystem = handSubsystems[0];
            handSubsystem.updatedHands += OnHandsUpdated;
            Debug.Log("手部追踪初始化成功");
        }
        else
        {
            Debug.LogWarning("找不到手部追踪子系统");
        }
    }
    
    void OnDestroy()
    {
        if (handSubsystem != null)
        {
            handSubsystem.updatedHands -= OnHandsUpdated;
        }
    }
    
    private void OnHandsUpdated(XRHandSubsystem subsystem, XRHandSubsystem.UpdateSuccessFlags updateSuccessFlags)
    {
        // 处理左手
        if ((updateSuccessFlags & XRHandSubsystem.UpdateSuccessFlags.LeftHandRootPose) != 0 &&
            (updateSuccessFlags & XRHandSubsystem.UpdateSuccessFlags.LeftHandJoints) != 0)
        {
            RecognizeHandGesture(subsystem.leftHand, XRNode.LeftHand);
        }
        
        // 处理右手
        if ((updateSuccessFlags & XRHandSubsystem.UpdateSuccessFlags.RightHandRootPose) != 0 &&
            (updateSuccessFlags & XRHandSubsystem.UpdateSuccessFlags.RightHandJoints) != 0)
        {
            RecognizeHandGesture(subsystem.rightHand, XRNode.RightHand);
        }
    }
    
    private void RecognizeHandGesture(XRHand hand, XRNode handNode)
    {
        if (!hand.isTracked) return;
        
        // 分析手指状态
        bool thumbExtended = IsFingerExtended(hand, XRHandFingerID.Thumb);
        bool indexExtended = IsFingerExtended(hand, XRHandFingerID.Index);
        bool middleExtended = IsFingerExtended(hand, XRHandFingerID.Middle);
        bool ringExtended = IsFingerExtended(hand, XRHandFingerID.Ring);
        bool pinkyExtended = IsFingerExtended(hand, XRHandFingerID.Little);
        
        bool isPinching = IsHandPinching(hand);
        bool isFist = IsFist(hand);
        bool isPalmOpen = IsPalmOpen(hand);
        
        string currentGesture = null;
        float highestConfidence = 0f;
        
        // 检查每个手势定义
        foreach (var gesture in gestures)
        {
            float confidence = CalculateGestureConfidence(
                gesture,
                thumbExtended, indexExtended, middleExtended, ringExtended, pinkyExtended,
                isPinching, isFist, isPalmOpen
            );
            
            // 如果置信度超过阈值且高于之前的最佳匹配
            if (confidence >= gesture.confidenceThreshold && confidence > highestConfidence)
            {
                highestConfidence = confidence;
                currentGesture = gesture.gestureName;
            }
        }
        
        // 处理手势计时器
        ManageGestureTimers(currentGesture, handNode);
    }
    
    private void ManageGestureTimers(string currentGesture, XRNode handNode)
    {
        string timerKey = handNode.ToString() + "_" + (currentGesture ?? "None");
        
        // 清理其他手势的计时器
        var keysToRemove = new List<string>();
        foreach (var key in gestureHoldTimers.Keys)
        {
            if (key.StartsWith(handNode.ToString() + "_") && key != timerKey)
            {
                keysToRemove.Add(key);
            }
        }
        
        foreach (var key in keysToRemove)
        {
            gestureHoldTimers.Remove(key);
        }
        
        // 如果没有识别手势,则退出
        if (currentGesture == null)
        {
            lastRecognizedGesture.Remove(handNode);
            return;
        }
        
        // 增加当前手势的计时器
        if (!gestureHoldTimers.ContainsKey(timerKey))
        {
            gestureHoldTimers[timerKey] = 0f;
        }
        
        gestureHoldTimers[timerKey] += Time.deltaTime;
        
        // 检查是否超过持续时间阈值
        if (gestureHoldTimers[timerKey] >= gestureHoldTime)
        {
            // 如果这是一个新的手势,则触发事件
            if (!lastRecognizedGesture.ContainsKey(handNode) || 
                lastRecognizedGesture[handNode] != currentGesture)
            {
                lastRecognizedGesture[handNode] = currentGesture;
                OnGestureRecognized?.Invoke(currentGesture, handNode);
                Debug.Log($"识别到手势: {currentGesture} ({handNode})");
            }
        }
    }
    
    private float CalculateGestureConfidence(
        GestureDefinition gesture,
        bool thumbExt, bool indexExt, bool middleExt, bool ringExt, bool pinkyExt,
        bool isPinching, bool isFist, bool isPalmOpen)
    {
        int totalChecks = 5; // 基本的5个手指检查
        int matchedChecks = 0;
        
        // 检查基本手指状态
        if (gesture.thumbExtended == thumbExt) matchedChecks++;
        if (gesture.indexExtended == indexExt) matchedChecks++;
        if (gesture.middleExtended == middleExt) matchedChecks++;
        if (gesture.ringExtended == ringExt) matchedChecks++;
        if (gesture.pinkyExtended == pinkyExt) matchedChecks++;
        
        // 检查高级手势特征
        if (gesture.checkPinch)
        {
            totalChecks++;
            if (isPinching) matchedChecks++;
        }
        
        if (gesture.checkFist)
        {
            totalChecks++;
            if (isFist) matchedChecks++;
        }
        
        if (gesture.checkPalm)
        {
            totalChecks++;
            if (isPalmOpen) matchedChecks++;
        }
        
        // 计算置信度
        return (float)matchedChecks / totalChecks;
    }
    
    private bool IsFingerExtended(XRHand hand, XRHandFingerID fingerID)
    {
        // 获取指尖关节
        XRHandJoint tipJoint = hand.GetJoint(XRHandJointIDUtility.GetTip(fingerID));
        XRHandJoint knuckleJoint = hand.GetJoint(XRHandJointIDUtility.GetKnuckle(fingerID));
        
        if (!tipJoint.TryGetPose(out Pose tipPose) || 
            !knuckleJoint.TryGetPose(out Pose knucklePose))
        {
            return false;
        }
        
        // 计算指尖相对于指关节的方向
        Vector3 fingerDirection = tipPose.position - knucklePose.position;
        
        // 计算手掌法向量
        Vector3 palmNormal = hand.rootPose.up;
        
        // 计算指尖方向与手掌法向量的夹角
        float angle = Vector3.Angle(fingerDirection, palmNormal);
        
        // 如果是大拇指,使用不同的判断逻辑
        if (fingerID == XRHandFingerID.Thumb)
        {
            Vector3 palmDirection = hand.rootPose.right * (hand == handSubsystem.leftHand ? -1 : 1);
            angle = Vector3.Angle(fingerDirection, palmDirection);
            return angle > 45f; // 大拇指伸展时与手掌方向的夹角较大
        }
        
        // 其他手指伸展时与手掌法向量的夹角较大
        return angle > 45f;
    }
    
    private bool IsHandPinching(XRHand hand)
    {
        // 获取拇指和食指的指尖
        XRHandJoint thumbTip = hand.GetJoint(XRHandJointIDUtility.GetTip(XRHandFingerID.Thumb));
        XRHandJoint indexTip = hand.GetJoint(XRHandJointIDUtility.GetTip(XRHandFingerID.Index));
        
        if (!thumbTip.TryGetPose(out Pose thumbPose) || 
            !indexTip.TryGetPose(out Pose indexPose))
        {
            return false;
        }
        
        // 计算拇指和食指之间的距离
        float distance = Vector3.Distance(thumbPose.position, indexPose.position);
        
        // 距离小于阈值时视为捏合
        return distance < 0.03f;
    }
    
    private bool IsFist(XRHand hand)
    {
        // 检查除拇指外的所有手指是否弯曲
        bool indexCurled = !IsFingerExtended(hand, XRHandFingerID.Index);
        bool middleCurled = !IsFingerExtended(hand, XRHandFingerID.Middle);
        bool ringCurled = !IsFingerExtended(hand, XRHandFingerID.Ring);
        bool pinkyCurled = !IsFingerExtended(hand, XRHandFingerID.Little);
        
        return indexCurled && middleCurled && ringCurled && pinkyCurled;
    }
    
    private bool IsPalmOpen(XRHand hand)
    {
        // 检查所有手指是否伸展
        bool indexExt = IsFingerExtended(hand, XRHandFingerID.Index);
        bool middleExt = IsFingerExtended(hand, XRHandFingerID.Middle);
        bool ringExt = IsFingerExtended(hand, XRHandFingerID.Ring);
        bool pinkyExt = IsFingerExtended(hand, XRHandFingerID.Little);
        
        return indexExt && middleExt && ringExt && pinkyExt;
    }
    
    public void AddGestureDefinition(string name, bool thumb, bool index, bool middle, bool ring, bool pinky)
    {
        GestureDefinition newGesture = new GestureDefinition
        {
            gestureName = name,
            thumbExtended = thumb,
            indexExtended = index,
            middleExtended = middle,
            ringExtended = ring,
            pinkyExtended = pinky
        };
        
        gestures.Add(newGesture);
    }
}

十、总结与最佳实践

OpenXR 提供了一个统一的接口,让开发者能够创建跨平台的 XR 应用。通过本指南,你已经学习了如何在 Unity 中设置 OpenXR,创建基本交互,实现高级功能,以及针对不同设备进行优化。

开发建议

  1. 从简单开始 - 使用基础的 XR 互动套件构建核心功能,然后逐步添加复杂元素
  2. 频繁测试 - 如果可能,在不同的 XR 设备上测试你的应用
  3. 注重性能 - 特别是在移动 VR 平台上,优化性能至关重要
  4. 遵循人体工程学 - 为用户设计舒适的交互体验,避免长时间的不自然姿势
  5. 利用 OpenXR 特性 - 关注并使用新的 OpenXR 功能和扩展

常见陷阱

  • 过度依赖特定平台 API - 这会降低跨平台兼容性
  • 忽视性能优化 - 特别是在移动 VR 设备上,帧率下降会导致晕动症
  • 缺乏友好的错误处理 - 例如,当手部追踪不可用时没有回退方案
  • 控制器输入映射错误 - 不同设备的控制器布局可能不同

扩展阅读建议

  • Unity XR Interaction Toolkit 文档
  • OpenXR 规范和最佳实践
  • Unity 性能优化指南
  • 设备特定的开发者文档(Oculus, SteamVR, Pico 等)

通过本指南和提供的示例代码,你应该能够开始使用 Unity 和 OpenXR 创建高质量的 XR 应用程序。随着你的技能不断提高,你可以探索更多高级功能,创造出更加身临其境和交互性强的体验。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小宝哥Code

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值