Unity OpenXR 快速入门及实例
一、OpenXR 概述
什么是 OpenXR?
OpenXR 是由 Khronos Group 开发的开放标准和跨平台 API,用于虚拟现实(VR)和增强现实(AR)应用开发。它的主要目标是简化 XR 开发,让一套代码可以在多种硬件平台上运行,包括 Oculus、HTC Vive、Windows Mixed Reality、Pico 等。
OpenXR 的优势
- 跨平台兼容性:一次开发,多平台部署
- 简化开发流程:减少学习多个 SDK 的时间成本
- 前沿 XR 技术支持:持续更新支持最新的 XR 功能
- 未来兼容性:随着新设备的发布,只需更新 OpenXR 运行时即可支持
- 性能优化:提供直接访问 XR 硬件的接口,最小化开销
二、在 Unity 中设置 OpenXR
系统要求
- Unity 2020.3 LTS 或更高版本(推荐 Unity 2021.3 LTS 及以上)
- Unity XR 插件管理系统
- 对应 XR 设备的驱动程序
安装必要组件
-
通过 Package Manager 安装 OpenXR 插件:
- 打开 Window > Package Manager
- 切换到 "Unity Registry"
- 搜索并安装以下包:
- XR Plugin Management
- OpenXR Plugin
-
通过 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 项目
场景设置
-
设置 XR 原点:
- 在场景中创建一个空的 GameObject 命名为 "XR Origin"
- 添加 "XR Origin" 组件
- 添加 "Input Action Manager" 组件
-
设置相机:
- 在 XR Origin 下创建一个 "Camera Offset" 对象
- 在 Camera Offset 下添加一个带有 "Camera" 组件的 GameObject
- 确保相机具有 "Audio Listener" 组件
- 设置相机的 Clear Flags, Background 及其他参数
-
基本场景设置:
- 添加地面平面以提供参考点
- 添加简单的环境对象,例如墙、桌子等
OpenXR 交互设置
-
手部射线设置:
- 在 XR Origin 下创建两个空物体,分别为 "LeftHand Controller" 和 "RightHand Controller"
- 向两个控制器添加以下组件:
- "XR Controller (Action-based)"
- "XR Ray Interactor"
- "XR Interactor Line Visual"
- 配置每个控制器的输入动作引用
-
交互管理器:
- 在场景中添加空物体命名为 "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!");
}
}
四、输入交互示例
手柄输入配置
-
创建输入动作资源:
- 在 Project 窗口中右键点击 > Create > Input Actions
- 创建以下动作映射:
- 握持按钮 (grip)
- 扳机按钮 (trigger)
- 主按钮 (primary button)
- 摇杆 (thumbstick)
- 为每个动作设置适当的控制类型和绑定
-
连接输入动作:
- 选择控制器对象
- 在 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,创建基本交互,实现高级功能,以及针对不同设备进行优化。
开发建议
- 从简单开始 - 使用基础的 XR 互动套件构建核心功能,然后逐步添加复杂元素
- 频繁测试 - 如果可能,在不同的 XR 设备上测试你的应用
- 注重性能 - 特别是在移动 VR 平台上,优化性能至关重要
- 遵循人体工程学 - 为用户设计舒适的交互体验,避免长时间的不自然姿势
- 利用 OpenXR 特性 - 关注并使用新的 OpenXR 功能和扩展
常见陷阱
- 过度依赖特定平台 API - 这会降低跨平台兼容性
- 忽视性能优化 - 特别是在移动 VR 设备上,帧率下降会导致晕动症
- 缺乏友好的错误处理 - 例如,当手部追踪不可用时没有回退方案
- 控制器输入映射错误 - 不同设备的控制器布局可能不同
扩展阅读建议
- Unity XR Interaction Toolkit 文档
- OpenXR 规范和最佳实践
- Unity 性能优化指南
- 设备特定的开发者文档(Oculus, SteamVR, Pico 等)
通过本指南和提供的示例代码,你应该能够开始使用 Unity 和 OpenXR 创建高质量的 XR 应用程序。随着你的技能不断提高,你可以探索更多高级功能,创造出更加身临其境和交互性强的体验。