Unity引擎XR平台开发经验总结
一、XR开发概述与平台差异
主要XR平台特性对比
特性 | Meta Quest | Pico | HoloLens |
---|
类型 | VR (沉浸式) | VR (沉浸式) | AR (混合现实) |
定位追踪 | 6DoF 无需外部传感器 | 6DoF 无需外部传感器 | 6DoF 空间映射 |
控制方式 | 手柄控制器+手势追踪 | 手柄控制器+手势追踪 | 手势识别+语音+眼动追踪 |
处理能力 | 中等 (骁龙XR2) | 中等 (骁龙XR2) | 中等 (Holographic Processing Unit) |
开发特色 | 社交功能、应用商店成熟 | 国内生态、B端应用 | 全息投影、空间理解 |
技术限制 | 功耗热量限制,图形性能有限 | 功耗热量限制,生态相对不足 | 视场角小,重量,电池续航 |
统一XR架构设计
// XRPlatformManager.cs - 统一平台适配管理器
using UnityEngine;
using UnityEngine.XR;
public enum XRPlatformType
{
Quest,
Pico,
HoloLens,
Other
}
public class XRPlatformManager : MonoBehaviour
{
public static XRPlatformManager Instance { get; private set; }
[SerializeField] private XRPlatformType defaultPlatform = XRPlatformType.Quest;
public XRPlatformType CurrentPlatform { get; private set; }
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
DetectAndInitializePlatform();
}
else
{
Destroy(gameObject);
}
}
private void DetectAndInitializePlatform()
{
string deviceName = SystemInfo.deviceName.ToLower();
string deviceModel = SystemInfo.deviceModel.ToLower();
// 检测Quest
if (deviceName.Contains("quest") || deviceModel.Contains("quest") ||
XRSettings.loadedDeviceName.Contains("oculus"))
{
CurrentPlatform = XRPlatformType.Quest;
}
// 检测Pico
else if (deviceName.Contains("pico") || deviceModel.Contains("pico") ||
XRSettings.loadedDeviceName.Contains("pico"))
{
CurrentPlatform = XRPlatformType.Pico;
}
// 检测HoloLens
else if (deviceName.Contains("hololens") || deviceModel.Contains("hololens") ||
XRSettings.loadedDeviceName.Contains("windowsmr"))
{
CurrentPlatform = XRPlatformType.HoloLens;
}
else
{
Debug.Log("未识别的XR平台,使用默认平台设置");
CurrentPlatform = defaultPlatform;
}
Debug.Log($"检测到XR平台: {CurrentPlatform}");
InitializePlatform();
}
private void InitializePlatform()
{
switch (CurrentPlatform)
{
case XRPlatformType.Quest:
InitializeQuestPlatform();
break;
case XRPlatformType.Pico:
InitializePicoPlatform();
break;
case XRPlatformType.HoloLens:
InitializeHoloLensPlatform();
break;
default:
Debug.LogWarning("未实现的平台初始化");
break;
}
}
private void InitializeQuestPlatform()
{
// Quest特定初始化
Debug.Log("初始化Quest平台");
#if UNITY_ANDROID && !UNITY_EDITOR
// Quest特定API调用
// OVRManager.instance.SetupInsightPassthrough();
#endif
}
private void InitializePicoPlatform()
{
// Pico特定初始化
Debug.Log("初始化Pico平台");
#if UNITY_ANDROID && !UNITY_EDITOR
// Pico特定API调用
#endif
}
private void InitializeHoloLensPlatform()
{
// HoloLens特定初始化
Debug.Log("初始化HoloLens平台");
#if WINDOWS_UWP && !UNITY_EDITOR
// HoloLens特定API调用
#endif
}
}
跨平台项目配置
// 定义预处理器指令
// 在Player Settings -> Other Settings -> Scripting Define Symbols中添加
// QUEST_SDK, PICO_SDK, HOLOLENS_SDK
// CrossPlatformSettings.cs - 跨平台设置器
using UnityEngine;
public class CrossPlatformSettings : MonoBehaviour
{
[Header("通用XR设置")]
[SerializeField] private bool enableVRMode = true;
[SerializeField] private bool enableMixedRealityMode = false;
[Range(60, 120)]
[SerializeField] private int targetFrameRate = 72;
[Header("Quest特定设置")]
[SerializeField] private bool enableQuestHandTracking = true;
[SerializeField] private bool enableQuestPassthrough = false;
[Header("Pico特定设置")]
[SerializeField] private bool enablePicoEyeTracking = false;
[Header("HoloLens特定设置")]
[SerializeField] private bool enableHoloLensSpatialMapping = true;
[SerializeField] private bool enableHoloLensVoiceCommands = true;
private void Awake()
{
ApplyCommonSettings();
ApplyPlatformSpecificSettings();
}
private void ApplyCommonSettings()
{
// 设置目标帧率
Application.targetFrameRate = targetFrameRate;
// 防止休眠
Screen.sleepTimeout = SleepTimeout.NeverSleep;
}
private void ApplyPlatformSpecificSettings()
{
XRPlatformType platform = XRPlatformManager.Instance.CurrentPlatform;
switch(platform)
{
case XRPlatformType.Quest:
ApplyQuestSettings();
break;
case XRPlatformType.Pico:
ApplyPicoSettings();
break;
case XRPlatformType.HoloLens:
ApplyHoloLensSettings();
break;
}
}
private void ApplyQuestSettings()
{
#if QUEST_SDK
Debug.Log("应用Quest特定设置");
// 示例:设置手部追踪
if (enableQuestHandTracking)
{
// OVRPlugin.HandTrackingEnabled = true;
}
// 示例:设置Passthrough
if (enableQuestPassthrough)
{
// OVRManger.instance.isInsightPassthroughEnabled = true;
}
#endif
}
private void ApplyPicoSettings()
{
#if PICO_SDK
Debug.Log("应用Pico特定设置");
// 示例:设置眼动追踪
if (enablePicoEyeTracking)
{
// Pvr_UnitySDKAPI.EyeTracking.StartEyeTracking();
}
#endif
}
private void ApplyHoloLensSettings()
{
#if HOLOLENS_SDK
Debug.Log("应用HoloLens特定设置");
// 示例:设置空间映射
if (enableHoloLensSpatialMapping)
{
// 启用空间映射组件
}
// 示例:启用语音命令
if (enableHoloLensVoiceCommands)
{
// 初始化语音识别
}
#endif
}
}
二、Quest平台开发
性能优化策略
// QuestPerformanceOptimizer.cs - Quest平台性能优化
using UnityEngine;
public class QuestPerformanceOptimizer : MonoBehaviour
{
[Header("自动性能优化")]
[SerializeField] private bool enableDynamicResolution = true;
[SerializeField] private bool enableFixedFoveatedRendering = true;
[SerializeField] private bool enableAdaptiveQuality = true;
[Header("性能等级")]
[SerializeField] private int cpuLevel = 2; // 0-4
[SerializeField] private int gpuLevel = 3; // 0-4
private void Start()
{
ApplyPerformanceSettings();
}
private void ApplyPerformanceSettings()
{
#if QUEST_SDK && !UNITY_EDITOR
try
{
// 设置CPU/GPU性能等级
OVRManager.cpuLevel = cpuLevel;
OVRManager.gpuLevel = gpuLevel;
// 开启固定注视点渲染
if (enableFixedFoveatedRendering)
{
OVRManager.fixedFoveatedRenderingLevel = OVRManager.FixedFoveatedRenderingLevel.High;
OVRManager.useDynamicFixedFoveatedRendering = true;
}
// 启用动态分辨率
if (enableDynamicResolution)
{
OVRManager.eyeTextureResolutionScale = 1.0f;
OVRManager.eyeTextureResolutionScaleUsesLodBias = true;
}
// 自适应品质
if (enableAdaptiveQuality)
{
OVRManager.AdaptivePerformance = true;
}
Debug.Log("Quest性能优化设置已应用");
}
catch (System.Exception e)
{
Debug.LogError($"设置Quest性能参数时出错: {e.Message}");
}
#endif
}
// 监控性能并动态调整
private void Update()
{
#if QUEST_SDK && !UNITY_EDITOR
if (enableAdaptiveQuality)
{
// 获取当前帧率
float fps = 1.0f / Time.smoothDeltaTime;
// 如果帧率下降,降低质量
if (fps < 60)
{
ReduceQuality();
}
// 如果帧率稳定,可以尝试提高质量
else if (fps > 80)
{
IncreaseQuality();
}
}
#endif
}
private void ReduceQuality()
{
#if QUEST_SDK && !UNITY_EDITOR
// 降低渲染分辨率
float currentScale = OVRManager.eyeTextureResolutionScale;
OVRManager.eyeTextureResolutionScale = Mathf.Max(0.7f, currentScale - 0.05f);
// 提高注视点渲染等级
if (OVRManager.fixedFoveatedRenderingLevel < OVRManager.FixedFoveatedRenderingLevel.High)
{
OVRManager.fixedFoveatedRenderingLevel += 1;
}
#endif
}
private void IncreaseQuality()
{
#if QUEST_SDK && !UNITY_EDITOR
// 提高渲染分辨率
float currentScale = OVRManager.eyeTextureResolutionScale;
OVRManager.eyeTextureResolutionScale = Mathf.Min(1.2f, currentScale + 0.02f);
// 降低注视点渲染等级
if (OVRManager.fixedFoveatedRenderingLevel > OVRManager.FixedFoveatedRenderingLevel.Off)
{
OVRManager.fixedFoveatedRenderingLevel -= 1;
}
#endif
}
}
Quest手势追踪与交互
// QuestHandTracking.cs - Quest手部追踪和交互
using UnityEngine;
#if QUEST_SDK
using OVRTouchSample;
#endif
public class QuestHandTracking : MonoBehaviour
{
[Header("手部模型")]
[SerializeField] private GameObject leftHandModel;
[SerializeField] private GameObject rightHandModel;
[SerializeField] private GameObject leftControllerModel;
[SerializeField] private GameObject rightControllerModel;
[Header("交互设置")]
[SerializeField] private float pinchThreshold = 0.7f;
[SerializeField] private LayerMask interactableLayers;
[SerializeField] private float grabDistance = 0.2f;
// 手势状态
private bool isLeftHandTracked = false;
private bool isRightHandTracked = false;
private bool isLeftPinching = false;
private bool isRightPinching = false;
private Transform leftIndexTip;
private Transform rightIndexTip;
// 抓取的对象
private InteractableObject currentLeftInteractable;
private InteractableObject currentRightInteractable;
private void Start()
{
InitializeHandTracking();
}
private void InitializeHandTracking()
{
#if QUEST_SDK && !UNITY_EDITOR
// 初始化手部追踪
OVRManager.instance.handTrackingEnabled = true;
// 获取手指尖Transform
if (leftHandModel != null)
{
leftIndexTip = FindFingerTip(leftHandModel, "index");
}
if (rightHandModel != null)
{
rightIndexTip = FindFingerTip(rightHandModel, "index");
}
// 初始隐藏手模型
SetHandModelsActive(false);
SetControllerModelsActive(true);
#endif
}
private Transform FindFingerTip(GameObject handModel, string fingerName)
{
// 通常手指尖会有一个特定命名的骨骼或Transform
Transform[] allTransforms = handModel.GetComponentsInChildren<Transform>();
foreach (Transform t in allTransforms)
{
if (t.name.ToLower().Contains(fingerName) && t.name.ToLower().Contains("tip"))
{
return t;
}
}
return null;
}
private void Update()
{
UpdateHandTracking();
CheckForInteractions();
}
private void UpdateHandTracking()
{
#if QUEST_SDK && !UNITY_EDITOR
// 检测手部追踪状态
isLeftHandTracked = OVRInput.IsControllerConnected(OVRInput.Controller.LHand);
isRightHandTracked = OVRInput.IsControllerConnected(OVRInput.Controller.RHand);
// 根据手部追踪状态切换手模型和控制器模型
if (isLeftHandTracked || isRightHandTracked)
{
if (leftHandModel != null) leftHandModel.SetActive(isLeftHandTracked);
if (rightHandModel != null) rightHandModel.SetActive(isRightHandTracked);
if (leftControllerModel != null) leftControllerModel.SetActive(!isLeftHandTracked);
if (rightControllerModel != null) rightControllerModel.SetActive(!isRightHandTracked);
}
// 更新手势状态
if (isLeftHandTracked)
{
UpdateHandGestures(OVRHand.Hand.HandLeft, ref isLeftPinching);
}
if (isRightHandTracked)
{
UpdateHandGestures(OVRHand.Hand.HandRight, ref isRightPinching);
}
#endif
}
#if QUEST_SDK
private void UpdateHandGestures(OVRHand.Hand handType, ref bool isPinching)
{
OVRHand hand = handType == OVRHand.Hand.HandLeft ?
OVRInput.GetActiveController(OVRInput.Hand.HandLeft).GetComponent<OVRHand>() :
OVRInput.GetActiveController(OVRInput.Hand.HandRight).GetComponent<OVRHand>();
if (hand != null)
{
// 检测捏合手势
float pinchStrength = hand.GetFingerPinchStrength(OVRHand.HandFinger.Index);
// 更新捏合状态 (添加滞后效应避免抖动)
if (pinchStrength >= pinchThreshold && !isPinching)
{
isPinching = true;
OnPinchStart(handType);
}
else if (pinchStrength < pinchThreshold * 0.8f && isPinching)
{
isPinching = false;
OnPinchEnd(handType);
}
}
}
#endif
private void OnPinchStart(OVRHand.Hand handType)
{
// 处理开始捏合事件
if (handType == OVRHand.Hand.HandLeft)
{
TryGrabObject(leftIndexTip, ref currentLeftInteractable);
}
else
{
TryGrabObject(rightIndexTip, ref currentRightInteractable);
}
}
private void OnPinchEnd(OVRHand.Hand handType)
{
// 处理结束捏合事件
if (handType == OVRHand.Hand.HandLeft && currentLeftInteractable != null)
{
currentLeftInteractable.OnRelease();
currentLeftInteractable = null;
}
else if (handType == OVRHand.Hand.HandRight && currentRightInteractable != null)
{
currentRightInteractable.OnRelease();
currentRightInteractable = null;
}
}
private void TryGrabObject(Transform fingerTip, ref InteractableObject currentInteractable)
{
if (fingerTip == null) return;
// 射线检测可交互物体
RaycastHit hit;
if (Physics.SphereCast(fingerTip.position, 0.02f, fingerTip.forward, out hit, grabDistance, interactableLayers))
{
InteractableObject interactable = hit.collider.GetComponent<InteractableObject>();
if (interactable != null && interactable.CanInteract())
{
currentInteractable = interactable;
currentInteractable.OnGrab(fingerTip);
}
}
}
private void CheckForInteractions()
{
// 更新已抓取物体的位置
if (currentLeftInteractable != null && leftIndexTip != null)
{
currentLeftInteractable.UpdatePosition(leftIndexTip);
}
if (currentRightInteractable != null && rightIndexTip != null)
{
currentRightInteractable.UpdatePosition(rightIndexTip);
}
}
private void SetHandModelsActive(bool active)
{
if (leftHandModel != null) leftHandModel.SetActive(active);
if (rightHandModel != null) rightHandModel.SetActive(active);
}
private void SetControllerModelsActive(bool active)
{
if (leftControllerModel != null) leftControllerModel.SetActive(active);
if (rightControllerModel != null) rightControllerModel.SetActive(active);
}
}
// InteractableObject.cs - 可交互对象基类
public class InteractableObject : MonoBehaviour
{
[SerializeField] private bool isGrabbable = true;
[SerializeField] private bool isSnappable = false;
[SerializeField] private Transform snapPoint;
private bool isGrabbed = false;
private Transform grabber;
private Vector3 grabOffset;
private Rigidbody rb;
private void Awake()
{
rb = GetComponent<Rigidbody>();
}
public bool CanInteract()
{
return isGrabbable && !isGrabbed;
}
public void OnGrab(Transform newGrabber)
{
if (!isGrabbable) return;
isGrabbed = true;
grabber = newGrabber;
// 计算抓取偏移
grabOffset = transform.position - grabber.position;
// 如果有刚体,变更物理行为
if (rb != null)
{
rb.isKinematic = true;
}
// 触发抓取事件
OnGrabEvent();
}
public void UpdatePosition(Transform grabber)
{
if (!isGrabbed) return;
// 移动物体
if (isSnappable && snapPoint != null)
{
// 使用吸附点
transform.position = Vector3.Lerp(transform.position,
grabber.position + grabOffset,
Time.deltaTime * 15f);
}
else
{
// 直接跟随手指
transform.position = grabber.position + grabOffset;
}
}
public void OnRelease()
{
if (!isGrabbed) return;
isGrabbed = false;
// 如果有刚体,恢复物理
if (rb != null)
{
rb.isKinematic = false;
}
// 触发释放事件
OnReleaseEvent();
}
// 可重写的事件方法
protected virtual void OnGrabEvent()
{
// 子类重写以添加特定行为
}
protected virtual void OnReleaseEvent()
{
// 子类重写以添加特定行为
}
}
三、Pico平台开发
Pico控制器和手势系统
// PicoControllerManager.cs - Pico控制器和手势管理
using UnityEngine;
#if PICO_SDK
using Unity.XR.PXR;
#endif
public class PicoControllerManager : MonoBehaviour
{
[Header("控制器设置")]
[SerializeField] private GameObject leftControllerPrefab;
[SerializeField] private GameObject rightControllerPrefab;
[Header("手势设置")]
[SerializeField] private bool enableHandTracking = true;
[SerializeField] private GameObject leftHandPrefab;
[SerializeField] private GameObject rightHandPrefab;
// 控制器引用
private GameObject leftController;
private GameObject rightController;
private GameObject leftHand;
private GameObject rightHand;
// 手势状态
private bool handTrackingActive = false;
private void Start()
{
InitializeControllers();
if (enableHandTracking)
{
InitializeHandTracking();
}
}
private void InitializeControllers()
{
// 实例化控制器模型
if (leftControllerPrefab != null)
{
leftController = Instantiate(leftControllerPrefab);
}
if (rightControllerPrefab != null)
{
rightController = Instantiate(rightControllerPrefab);
}
#if PICO_SDK && !UNITY_EDITOR
// 初始化Pico控制器
PXR_Input.SetControllerVibration(1.0f, 1.0f, 0.1f, PXR_Input.Controller.LeftController);
PXR_Input.SetControllerVibration(1.0f, 1.0f, 0.1f, PXR_Input.Controller.RightController);
Debug.Log("Pico控制器已初始化");
#endif
}
private void InitializeHandTracking()
{
#if PICO_SDK && !UNITY_EDITOR
// 初始化Pico手部追踪
PXR_HandTracking.EnableHandTracking(true);
// 实例化手部模型
if (leftHandPrefab != null)
{
leftHand = Instantiate(leftHandPrefab);
leftHand.SetActive(false);
}
if (rightHandPrefab != null)
{
rightHand = Instantiate(rightHandPrefab);
rightHand.SetActive(false);
}
Debug.Log("Pico手部追踪已初始化");
#endif
}
private void Update()
{
UpdateControllerStates();
if (enableHandTracking)
{
UpdateHandTracking();
}
}
private void UpdateControllerStates()
{
#if PICO_SDK && !UNITY_EDITOR
// 获取控制器状态
Vector3 leftPos = PXR_Input.GetLocalPosition(PXR_Input.Controller.LeftController);
Quaternion leftRot = PXR_Input.GetLocalRotation(PXR_Input.Controller.LeftController);
Vector3 rightPos = PXR_Input.GetLocalPosition(PXR_Input.Controller.RightController);
Quaternion rightRot = PXR_Input.GetLocalRotation(PXR_Input.Controller.RightController);
// 更新控制器位置
if (leftController != null)
{
leftController.transform.localPosition = leftPos;
leftController.transform.localRotation = leftRot;
}
if (rightController != null)
{
rightController.transform.localPosition = rightPos;
rightController.transform.localRotation = rightRot;
}
// 处理按键输入
if (PXR_Input.GetDown(PXR_Input.ButtonMask.A, PXR_Input.Controller.RightController))
{
OnButtonAPressed();
}
if (PXR_Input.GetDown(PXR_Input.ButtonMask.X, PXR_Input.Controller.LeftController))
{
OnButtonXPressed();
}
// 处理扳机键
float leftTrigger = PXR_Input.GetTriggerValue(PXR_Input.Controller.LeftController);
float rightTrigger = PXR_Input.GetTriggerValue(PXR_Input.Controller.RightController);
if (leftTrigger > 0.7f)
{
OnLeftTriggerPressed(leftTrigger);
}
if (rightTrigger > 0.7f)
{
OnRightTriggerPressed(rightTrigger);
}
#endif
}
private void UpdateHandTracking()
{
#if PICO_SDK && !UNITY_EDITOR
// 检查手部追踪是否激活
bool isLeftHandTracked = PXR_HandTracking.GetTrackingState(PXR_HandTracking.HandType.HandLeft);
bool isRightHandTracked = PXR_HandTracking.GetTrackingState(PXR_HandTracking.HandType.HandRight);
handTrackingActive = isLeftHandTracked || isRightHandTracked;
// 根据手部追踪状态切换可见性
if (leftController != null) leftController.SetActive(!isLeftHandTracked);
if (rightController != null) rightController.SetActive(!isRightHandTracked);
if (leftHand != null) leftHand.SetActive(isLeftHandTracked);
if (rightHand != null) rightHand.SetActive(isRightHandTracked);
// 更新手部位置
if (isLeftHandTracked && leftHand != null)
{
UpdateHandModel(PXR_HandTracking.HandType.HandLeft, leftHand);
}
if (isRightHandTracked && rightHand != null)
{
UpdateHandModel(PXR_HandTracking.HandType.HandRight, rightHand);
}
// 检测手势
if (isLeftHandTracked)
{
CheckHandGestures(PXR_HandTracking.HandType.HandLeft);
}
if (isRightHandTracked)
{
CheckHandGestures(PXR_HandTracking.HandType.HandRight);
}
#endif
}
#if PICO_SDK
private void UpdateHandModel(PXR_HandTracking.HandType handType, GameObject handModel)
{
// 更新手部关节位置
for (PXR_HandTracking.HandJoint joint = PXR_HandTracking.HandJoint.JointWrist;
joint <= PXR_HandTracking.HandJoint.JointPinkyTip;
joint++)
{
// 获取关节位置和旋转
Vector3 position;
Quaternion rotation;
float radius;
if (PXR_HandTracking.GetJointPose(joint, handType, out position, out rotation, out radius))
{
// 查找对应的手部骨骼
Transform jointTransform = FindJointTransform(handModel, joint);
if (jointTransform != null)
{
jointTransform.localPosition = position;
jointTransform.localRotation = rotation;
}
}
}
}
private Transform FindJointTransform(GameObject handModel, PXR_HandTracking.HandJoint joint)
{
// 这个函数需要根据您的手部模型骨骼结构来实现
// 下面是一个简单的示例,实际上您需要映射PICO SDK的关节枚举到您模型中的骨骼名称
string jointName = joint.ToString().Replace("Joint", "");
Transform[] allTransforms = handModel.GetComponentsInChildren<Transform>();
foreach (Transform t in allTransforms)
{
if (t.name.Contains(jointName))
{
return t;
}
}
return null;
}
private void CheckHandGestures(PXR_HandTracking.HandType handType)
{
// 检测手指弯曲度判断手势
float thumbBend = PXR_HandTracking.GetJointBendValue(PXR_HandTracking.HandJoint.JointThumbProximal, handType);
float indexBend = PXR_HandTracking.GetJointBendValue(PXR_HandTracking.HandJoint.JointIndexProximal, handType);
float middleBend = PXR_HandTracking.GetJointBendValue(PXR_HandTracking.HandJoint.JointMiddleProximal, handType);
// 检测捏合手势 (拇指和食指)
if (thumbBend > 0.5f && indexBend > 0.7f && middleBend < 0.3f)
{
OnPinchGesture(handType);
}
// 检测握拳手势
if (thumbBend > 0.7f && indexBend > 0.7f && middleBend > 0.7f)
{
OnFistGesture(handType);
}
}
#endif
// 手势事件处理
private void OnPinchGesture(PXR_HandTracking.HandType handType)
{
// 处理捏合手势
Debug.Log($"检测到捏合手势: {handType}");
}
private void OnFistGesture(PXR_HandTracking.HandType handType)
{
// 处理握拳手势
Debug.Log($"检测到握拳手势: {handType}");
}
// 按键事件处理
private void OnButtonAPressed()
{
Debug.Log("按下A键");
}
private void OnButtonXPressed()
{
Debug.Log("按下X键");
}
private void OnLeftTriggerPressed(float value)
{
Debug.Log($"按下左扳机: {value}");
}
private void OnRightTriggerPressed(float value)
{
Debug.Log($"按下右扳机: {value}");
}
// 控制器震动
public void TriggerHapticFeedback(bool isLeft, float intensity, float duration)
{
#if PICO_SDK && !UNITY_EDITOR
PXR_Input.Controller controller = isLeft ?
PXR_Input.Controller.LeftController :
PXR_Input.Controller.RightController;
PXR_Input.SetControllerVibration(intensity, 1.0f, duration, controller);
#endif
}
}
Pico特有功能集成
// PicoFeatureManager.cs - Pico特有功能管理
using UnityEngine;
#if PICO_SDK
using Unity.XR.PXR;
#endif
public class PicoFeatureManager : MonoBehaviour
{
[Header("Pico特性功能")]
[SerializeField] private bool enableEyeTracking = false;
[SerializeField] private bool enableFaceTracking = false;
[SerializeField] private bool enableSpeechRecognition = false;
[Header("眼动追踪")]
[SerializeField] private LayerMask eyeTrackingLayers;
[SerializeField] private float maxEyeRayDistance = 10f;
[SerializeField] private GameObject eyeGazeCursor;
// 眼动追踪状态
private bool eyeTrackingActive = false;
private Vector3 eyeGazeDirection;
private Vector3 eyeGazeOrigin;
private GameObject lastGazedObject;
private void Start()
{
InitializePicoFeatures();
}
private void InitializePicoFeatures()
{
#if PICO_SDK && !UNITY_EDITOR
try
{
// 初始化眼动追踪
if (enableEyeTracking)
{
PXR_EyeTracking.EnableEyeTracking(true);
eyeTrackingActive = true;
if (eyeGazeCursor != null)
{
eyeGazeCursor.SetActive(true);
}
Debug.Log("Pico眼动追踪已启用");
}
// 初始化面部追踪
if (enableFaceTracking)
{
PXR_FaceTracking.EnableFaceTracking(true);
Debug.Log("Pico面部追踪已启用");
}
// 初始化语音识别
if (enableSpeechRecognition)
{
// PICO语音识别初始化...
Debug.Log("Pico语音识别已启用");
}
}
catch (System.Exception e)
{
Debug.LogError($"初始化Pico特性时出错: {e.Message}");
}
#endif
}
private void Update()
{
if (enableEyeTracking)
{
UpdateEyeTracking();
}
if (enableFaceTracking)
{
UpdateFaceTracking();
}
}
private void UpdateEyeTracking()
{
#if PICO_SDK && !UNITY_EDITOR
if (!eyeTrackingActive) return;
// 获取凝视方向和原点
if (PXR_EyeTracking.GetCombinedEyeGazePoint(out eyeGazeDirection) &&
PXR_EyeTracking.GetCombinedEyeGazeOrigin(out eyeGazeOrigin))
{
// 执行射线检测
RaycastHit hit;
if (Physics.Raycast(eyeGazeOrigin, eyeGazeDirection, out hit, maxEyeRayDistance, eyeTrackingLayers))
{
// 更新光标位置
if (eyeGazeCursor != null)
{
eyeGazeCursor.transform.position = hit.point;
eyeGazeCursor.transform.forward = -hit.normal;
}
// 处理凝视事件
GameObject gazedObject = hit.collider.gameObject;
if (gazedObject != lastGazedObject)
{
// 退出上一个物体
if (lastGazedObject != null)
{
IGazeInteractable interactable = lastGazedObject.GetComponent<IGazeInteractable>();
if (interactable != null)
{
interactable.OnGazeExit();
}
}
// 进入新物体
IGazeInteractable newInteractable = gazedObject.GetComponent<IGazeInteractable>();
if (newInteractable != null)
{
newInteractable.OnGazeEnter();
}
lastGazedObject = gazedObject;
}
// 凝视保持事件
if (lastGazedObject != null)
{
IGazeInteractable interactable = lastGazedObject.GetComponent<IGazeInteractable>();
if (interactable != null)
{
interactable.OnGazeStay();
}
}
}
else
{
// 无命中,隐藏光标或移动到远处
if (eyeGazeCursor != null)
{
eyeGazeCursor.transform.position = eyeGazeOrigin + eyeGazeDirection * 5f;
}
// 如果之前有凝视物体,发送退出事件
if (lastGazedObject != null)
{
IGazeInteractable interactable = lastGazedObject.GetComponent<IGazeInteractable>();
if (interactable != null)
{
interactable.OnGazeExit();
}
lastGazedObject = null;
}
}
// 检测瞳孔直径变化
float leftPupilDiameter, rightPupilDiameter;
if (PXR_EyeTracking.GetPupilDiameter(out leftPupilDiameter, out rightPupilDiameter))
{
// 处理瞳孔变化事件,例如亮度自适应
float avgPupilDiameter = (leftPupilDiameter + rightPupilDiameter) / 2f;
OnPupilDiameterChanged(avgPupilDiameter);
}
}
#endif
}
private void UpdateFaceTracking()
{
#if PICO_SDK && !UNITY_EDITOR
// 获取面部追踪数据
PXR_FaceTrackingData faceData = new PXR_FaceTrackingData();
if (PXR_FaceTracking.GetFaceTrackingData(ref faceData))
{
// 处理面部表情数据
ProcessFacialExpressions(faceData);
}
#endif
}
#if PICO_SDK
private void ProcessFacialExpressions(PXR_FaceTrackingData faceData)
{
// 处理眉毛表情
float browRaise = faceData.blendShapeWeights[(int)PXR_FaceTrackingBlendShape.BrowLowererL] +
faceData.blendShapeWeights[(int)PXR_FaceTrackingBlendShape.BrowLowererR];
// 处理嘴部表情
float jawOpen = faceData.blendShapeWeights[(int)PXR_FaceTrackingBlendShape.JawOpen];
float smile = faceData.blendShapeWeights[(int)PXR_FaceTrackingBlendShape.MouthSmileL] +
faceData.blendShapeWeights[(int)PXR_FaceTrackingBlendShape.MouthSmileR];
// 触发表情事件
if (jawOpen > 0.7f)
{
OnMouthOpen(jawOpen);
}
if (smile > 0.6f)
{
OnSmile(smile);
}
if (browRaise > 0.7f)
{
OnBrowRaise(browRaise);
}
}
#endif
// 眼动追踪事件处理
private void OnPupilDiameterChanged(float diameter)
{
// 处理瞳孔直径变化,例如调整应用亮度
float normalizedDiameter = Mathf.Clamp01(diameter / 7f); // 假设最大瞳孔直径为7mm
// 调整场景亮度 - 示例代码
RenderSettings.ambientIntensity = Mathf.Lerp(0.5f, 1.0f, 1f - normalizedDiameter);
}
// 面部表情事件处理
private void OnMouthOpen(float strength)
{
Debug.Log($"检测到嘴部张开: {strength}");
// 触发与嘴部张开相关的交互
}
private void OnSmile(float strength)
{
Debug.Log($"检测到微笑: {strength}");
// 触发与微笑相关的交互
}
private void OnBrowRaise(float strength)
{
Debug.Log($"检测到眉毛抬起: {strength}");
// 触发与惊讶表情相关的交互
}
}
// IGazeInteractable.cs - 眼动交互接口
public interface IGazeInteractable
{
void OnGazeEnter();
void OnGazeStay();
void OnGazeExit();
}
// 示例眼动交互物体
public class GazeInteractableObject : MonoBehaviour, IGazeInteractable
{
[SerializeField] private float gazeActivationTime = 2.0f;
[SerializeField] private Color normalColor = Color.white;
[SerializeField] private Color hoverColor = Color.yellow;
[SerializeField] private Color activatedColor = Color.green;
private Renderer objectRenderer;
private float gazeTimer = 0f;
private bool isGazed = false;
private void Awake()
{
objectRenderer = GetComponent<Renderer>();
objectRenderer.material.color = normalColor;
}
private void Update()
{
if (isGazed)
{
gazeTimer += Time.deltaTime;
// 更新视觉反馈
float t = Mathf.Clamp01(gazeTimer / gazeActivationTime);
objectRenderer.material.color = Color.Lerp(hoverColor, activatedColor, t);
// 激活事件
if (gazeTimer >= gazeActivationTime)
{
OnGazeActivated();
gazeTimer = 0f;
}
}
}
public void OnGazeEnter()
{
isGazed = true;
gazeTimer = 0f;
objectRenderer.material.color = hoverColor;
}
public void OnGazeStay()
{
// 持续凝视处理已在Update中
}
public void OnGazeExit()
{
isGazed = false;
gazeTimer = 0f;
objectRenderer.material.color = normalColor;
}
private void OnGazeActivated()
{
Debug.Log($"通过凝视激活了物体: {gameObject.name}");
// 执行激活逻辑
// 例如:播放动画、触发事件等
transform.localScale *= 1.2f;
// 重置状态
Invoke("ResetScale", 0.5f);
}
private void ResetScale()
{
transform.localScale /= 1.2f;
}
}
四、HoloLens平台开发
空间映射与理解
// HoloLensSpatialMapping.cs - HoloLens空间映射管理
using UnityEngine;
using System.Collections.Generic;
#if HOLOLENS_SDK
using UnityEngine.XR.WSA;
using UnityEngine.XR.WSA.Persistence;
#if !UNITY_2020_1_OR_NEWER
using UnityEngine.XR.WSA.Sharing;
#endif
#endif
public class HoloLensSpatialMapping : MonoBehaviour
{
[Header("空间映射设置")]
[SerializeField] private bool autoStartMapping = true;
[SerializeField] private float mappingExtent = 10.0f;
[SerializeField] private float mappingResolution = 0.05f;
[SerializeField] private Material spatialMappingMaterial;
[SerializeField] private bool visualizeSpatialMesh = true;
[Header("空间锚点")]
[SerializeField] private bool enableSpatialAnchors = true;
// 空间映射组件
private GameObject spatialMappingObject;
// 空间锚点管理
private Dictionary<string, GameObject> spatialAnchors = new Dictionary<string, GameObject>();
#if HOLOLENS_SDK
private SpatialMappingRenderer spatialMappingRenderer;
private SpatialMappingCollider spatialMappingCollider;
private WorldAnchorStore worldAnchorStore;
#endif
private void Start()
{
if (autoStartMapping)
{
InitializeSpatialMapping();
}
if (enableSpatialAnchors)
{
InitializeSpatialAnchors();
}
}
private void InitializeSpatialMapping()
{
#if HOLOLENS_SDK && !UNITY_EDITOR
Debug.Log("初始化HoloLens空间映射");
// 创建空间映射对象
spatialMappingObject = new GameObject("SpatialMapping");
spatialMappingObject.transform.SetParent(transform);
// 添加空间映射组件
spatialMappingRenderer = spatialMappingObject.AddComponent<SpatialMappingRenderer>();
spatialMappingCollider = spatialMappingObject.AddComponent<SpatialMappingCollider>();
// 配置空间映射设置
SpatialMappingManager mappingManager = spatialMappingObject.AddComponent<SpatialMappingManager>();
mappingManager.surfaceObserver.SetVolumeAsAxisAlignedBox(Vector3.zero, new Vector3(mappingExtent, mappingExtent, mappingExtent));
mappingManager.surfaceObserver.SetObservedSurfaceNormals(true);
// 设置渲染参数
spatialMappingRenderer.material = spatialMappingMaterial;
spatialMappingRenderer.enabled = visualizeSpatialMesh;
// 设置碰撞参数
spatialMappingCollider.enableCollisions = true;
spatialMappingCollider.layer = LayerMask.NameToLayer("Spatial");
spatialMappingCollider.material = new PhysicMaterial()
{
bounciness = 0.0f,
dynamicFriction = 0.5f,
staticFriction = 0.5f
};
// 启动映射
mappingManager.StartObserver();
Debug.Log("HoloLens空间映射已启动");
#endif
}
private void InitializeSpatialAnchors()
{
#if HOLOLENS_SDK && !UNITY_EDITOR
Debug.Log("初始化HoloLens空间锚点");
// 加载世界锚点存储
WorldAnchorStore.GetAsync(store =>
{
worldAnchorStore = store;
LoadSavedAnchors();
});
#endif
}
#if HOLOLENS_SDK
private void LoadSavedAnchors()
{
if (worldAnchorStore == null) return;
// 获取所有保存的锚点ID
string[] anchorIds = worldAnchorStore.GetAllIds();
foreach (string anchorId in anchorIds)
{
// 创建锚点容器
GameObject anchorObject = new GameObject($"Anchor_{anchorId}");
anchorObject.transform.SetParent(transform);
// 加载锚点
WorldAnchor anchor = worldAnchorStore.Load(anchorId, anchorObject);
if (anchor != null)
{
spatialAnchors.Add(anchorId, anchorObject);
Debug.Log($"加载空间锚点: {anchorId}");
}
else
{
Debug.LogError($"无法加载锚点: {anchorId}");
Destroy(anchorObject);
}
}
}
#endif
// 创建新的空间锚点
public GameObject CreateSpatialAnchor(Vector3 position, string anchorId = null)
{
#if HOLOLENS_SDK && !UNITY_EDITOR
if (worldAnchorStore == null)
{
Debug.LogError("WorldAnchorStore未初始化");
return null;
}
// 生成唯一ID
if (string.IsNullOrEmpty(anchorId))
{
anchorId = System.Guid.NewGuid().ToString();
}
// 创建锚点物体
GameObject anchorObject = new GameObject($"Anchor_{anchorId}");
anchorObject.transform.position = position;
anchorObject.transform.SetParent(transform);
// 添加世界锚点
WorldAnchor anchor = anchorObject.AddComponent<WorldAnchor>();
// 保存到存储
if (worldAnchorStore.Save(anchorId, anchor))
{
spatialAnchors.Add(anchorId, anchorObject);
Debug.Log($"创建并保存锚点: {anchorId}");
return anchorObject;
}
else
{
Debug.LogError($"无法保存锚点: {anchorId}");
Destroy(anchorObject);
return null;
}
#else
// 编辑器模式下的模拟
GameObject anchorObject = new GameObject($"EditorAnchor_{anchorId ?? System.Guid.NewGuid().ToString()}");
anchorObject.transform.position = position;
anchorObject.transform.SetParent(transform);
return anchorObject;
#endif
}
// 删除空间锚点
public bool DeleteSpatialAnchor(string anchorId)
{
#if HOLOLENS_SDK && !UNITY_EDITOR
if (worldAnchorStore == null || !spatialAnchors.ContainsKey(anchorId))
{
return false;
}
// 删除锚点
GameObject anchorObject = spatialAnchors[anchorId];
bool result = worldAnchorStore.Delete(anchorId);
if (result)
{
spatialAnchors.Remove(anchorId);
Destroy(anchorObject);
Debug.Log($"删除锚点: {anchorId}");
}
else
{
Debug.LogError($"无法删除锚点: {anchorId}");
}
return result;
#else
return true;
#endif
}
// 设置空间映射可见性
public void SetMappingVisibility(bool visible)
{
#if HOLOLENS_SDK && !UNITY_EDITOR
if (spatialMappingRenderer != null)
{
spatialMappingRenderer.enabled = visible;
}
#endif
}
// 设置空间映射物理碰撞
public void SetMappingCollision(bool enabled)
{
#if HOLOLENS_SDK && !UNITY_EDITOR
if (spatialMappingCollider != null)
{
spatialMappingCollider.enableCollisions = enabled;
}
#endif
}
// 获取空间锚点位置
public bool TryGetAnchorPosition(string anchorId, out Vector3 position)
{
position = Vector3.zero;
if (spatialAnchors.ContainsKey(anchorId))
{
position = spatialAnchors[anchorId].transform.position;
return true;
}
return false;
}
// 射线检测空间表面
public bool RaycastSpatialMesh(Ray ray, out RaycastHit hitInfo, float maxDistance = 10f)
{
#if HOLOLENS_SDK && !UNITY_EDITOR
return Physics.Raycast(ray, out hitInfo, maxDistance,
1 << LayerMask.NameToLayer("Spatial"));
#else
// 编辑器模式下模拟平面
Plane groundPlane = new Plane(Vector3.up, Vector3.zero);
float distance;
if (groundPlane.Raycast(ray, out distance) && distance <= maxDistance)
{
hitInfo = new RaycastHit
{
point = ray.GetPoint(distance),
normal = Vector3.up,
distance = distance
};
return true;
}
else
{
hitInfo = new RaycastHit();
return false;
}
#endif
}
}
HoloLens手势和语音交互
// HoloLensInputManager.cs - HoloLens输入管理
using UnityEngine;
using System.Collections.Generic;
#if HOLOLENS_SDK
using UnityEngine.XR.WSA.Input;
using UnityEngine.Windows.Speech;
#endif
public class HoloLensInputManager : MonoBehaviour
{
[Header("手势设置")]
[SerializeField] private bool enableHandGestures = true;
[SerializeField] private float maxRaycastDistance = 5.0f;
[SerializeField] private LayerMask interactionLayers;
[Header("语音设置")]
[SerializeField] private bool enableVoiceCommands = true;
[SerializeField] private string[] voiceCommands;
[SerializeField] private float confidenceThreshold = 0.8f;
[Header("注视设置")]
[SerializeField] private bool enableGazeInteraction = true;
[SerializeField] private GameObject gazeCursor;
// 凝视状态
private GameObject gazeTarget;
private Transform gazeTransform;
private Vector3 gazeDirection;
private Vector3 gazeOrigin;
// 手势识别器
#if HOLOLENS_SDK
private GestureRecognizer gestureRecognizer;
private KeywordRecognizer keywordRecognizer;
#endif
// 委托事件
public delegate void GazeEventHandler(GameObject target);
public delegate void TapEventHandler(GameObject target, Vector3 position);
public delegate void VoiceCommandHandler(string command);
public event GazeEventHandler OnGazeEnter;
public event GazeEventHandler OnGazeExit;
public event TapEventHandler OnTap;
public event VoiceCommandHandler OnVoiceCommand;
private void Awake()
{
// 设置凝视来源
Camera mainCamera = Camera.main;
if (mainCamera != null)
{
gazeTransform = mainCamera.transform;
}
else
{
Debug.LogError("未找到主摄像机,无法初始化凝视功能");
}
}
private void Start()
{
if (enableHandGestures)
{
InitializeGestureRecognizer();
}
if (enableVoiceCommands)
{
InitializeVoiceRecognizer();
}
}
private void InitializeGestureRecognizer()
{
#if HOLOLENS_SDK && !UNITY_EDITOR
Debug.Log("初始化HoloLens手势识别");
gestureRecognizer = new GestureRecognizer();
// 设置识别的手势类型
gestureRecognizer.SetRecognizableGestures(
GestureSettings.Tap |
GestureSettings.Hold |
GestureSettings.ManipulationTranslate
);
// 注册事件处理程序
gestureRecognizer.TappedEvent += HandleTapped;
gestureRecognizer.HoldStartedEvent += HandleHoldStarted;
gestureRecognizer.HoldCompletedEvent += HandleHoldCompleted;
gestureRecognizer.ManipulationStartedEvent += HandleManipulationStarted;
gestureRecognizer.ManipulationUpdatedEvent += HandleManipulationUpdated;
gestureRecognizer.ManipulationCompletedEvent += HandleManipulationCompleted;
// 启动识别器
gestureRecognizer.StartCapturingGestures();
#endif
}
private void InitializeVoiceRecognizer()
{
#if HOLOLENS_SDK && !UNITY_EDITOR
if (voiceCommands == null || voiceCommands.Length == 0)
{
Debug.LogWarning("未指定语音命令");
return;
}
Debug.Log("初始化HoloLens语音识别");
// 创建关键词识别器
keywordRecognizer = new KeywordRecognizer(voiceCommands, (ConfidenceLevel)confidenceThreshold);
// 注册事件
keywordRecognizer.OnPhraseRecognized += HandlePhraseRecognized;
// 启动识别器
keywordRecognizer.Start();
#endif
}
private void Update()
{
if (enableGazeInteraction)
{
UpdateGaze();
}
}
private void UpdateGaze()
{
if (gazeTransform == null) return;
// 更新凝视参数
gazeOrigin = gazeTransform.position;
gazeDirection = gazeTransform.forward;
// 使用射线检测
RaycastHit hitInfo;
GameObject newGazeTarget = null;
if (Physics.Raycast(gazeOrigin, gazeDirection, out hitInfo, maxRaycastDistance, interactionLayers))
{
newGazeTarget = hitInfo.collider.gameObject;
// 更新光标位置
if (gazeCursor != null)
{
gazeCursor.transform.position = hitInfo.point;
gazeCursor.transform.forward = -hitInfo.normal;
gazeCursor.SetActive(true);
}
}
else if (gazeCursor != null)
{
// 没有命中,将光标放在远处
gazeCursor.transform.position = gazeOrigin + gazeDirection * 2.0f;
gazeCursor.transform.forward = gazeDirection;
gazeCursor.SetActive(true);
}
// 如果凝视目标改变,触发事件
if (gazeTarget != newGazeTarget)
{
// 退出旧目标
if (gazeTarget != null)
{
OnGazeExit?.Invoke(gazeTarget);
IHoloLensGazeInteractable interactable = gazeTarget.GetComponent<IHoloLensGazeInteractable>();
if (interactable != null)
{
interactable.OnGazeExit();
}
}
// 进入新目标
if (newGazeTarget != null)
{
OnGazeEnter?.Invoke(newGazeTarget);
IHoloLensGazeInteractable interactable = newGazeTarget.GetComponent<IHoloLensGazeInteractable>();
if (interactable != null)
{
interactable.OnGazeEnter();
}
}
gazeTarget = newGazeTarget;
}
}
#if HOLOLENS_SDK
// 手势事件处理
private void HandleTapped(InteractionSourceKind source, int tapCount, Ray headRay)
{
if (gazeTarget != null)
{
// 触发点击事件
OnTap?.Invoke(gazeTarget, gazeOrigin + gazeDirection * 2.0f);
// 检查组件实现
IHoloLensTapHandler tapHandler = gazeTarget.GetComponent<IHoloLensTapHandler>();
if (tapHandler != null)
{
tapHandler.OnTapped();
}
}
}
private void HandleHoldStarted(InteractionSourceKind source, Ray headRay)
{
if (gazeTarget != null)
{
IHoloLensHoldHandler holdHandler = gazeTarget.GetComponent<IHoloLensHoldHandler>();
if (holdHandler != null)
{
holdHandler.OnHoldStarted();
}
}
}
private void HandleHoldCompleted(InteractionSourceKind source, Ray headRay)
{
if (gazeTarget != null)
{
IHoloLensHoldHandler holdHandler = gazeTarget.GetComponent<IHoloLensHoldHandler>();
if (holdHandler != null)
{
holdHandler.OnHoldCompleted();
}
}
}
private void HandleManipulationStarted(InteractionSourceKind source, Vector3 position, Ray headRay)
{
if (gazeTarget != null)
{
IHoloLensManipulationHandler manipHandler = gazeTarget.GetComponent<IHoloLensManipulationHandler>();
if (manipHandler != null)
{
manipHandler.OnManipulationStarted(position);
}
}
}
private void HandleManipulationUpdated(InteractionSourceKind source, Vector3 position, Ray headRay)
{
if (gazeTarget != null)
{
IHoloLensManipulationHandler manipHandler = gazeTarget.GetComponent<IHoloLensManipulationHandler>();
if (manipHandler != null)
{
manipHandler.OnManipulationUpdated(position);
}
}
}
private void HandleManipulationCompleted(InteractionSourceKind source, Vector3 position, Ray headRay)
{
if (gazeTarget != null)
{
IHoloLensManipulationHandler manipHandler = gazeTarget.GetComponent<IHoloLensManipulationHandler>();
if (manipHandler != null)
{
manipHandler.OnManipulationCompleted(position);
}
}
}
// 语音事件处理
private void HandlePhraseRecognized(PhraseRecognizedEventArgs args)
{
string command = args.text;
float confidence = args.confidence;
Debug.Log($"识别到语音命令: {command} (置信度: {confidence})");
// 触发语音命令事件
OnVoiceCommand?.Invoke(command);
// 查找并触发命令处理程序
if (gazeTarget != null)
{
IHoloLensVoiceHandler voiceHandler = gazeTarget.GetComponent<IHoloLensVoiceHandler>();
if (voiceHandler != null)
{
voiceHandler.OnVoiceCommand(command);
}
}
// 全局命令处理
ProcessGlobalVoiceCommand(command);
}
#endif
// 处理全局语音命令
private void ProcessGlobalVoiceCommand(string command)
{
switch (command.ToLower())
{
case "reset":
// 重置场景
Debug.Log("重置场景");
break;
case "menu":
// 显示菜单
Debug.Log("显示菜单");
break;
// 更多全局命令...
}
}
private void OnDestroy()
{
#if HOLOLENS_SDK
// 清理手势识别器
if (gestureRecognizer != null)
{
gestureRecognizer.TappedEvent -= HandleTapped;
gestureRecognizer.HoldStartedEvent -= HandleHoldStarted;
gestureRecognizer.HoldCompletedEvent -= HandleHoldCompleted;
gestureRecognizer.ManipulationStartedEvent -= HandleManipulationStarted;
gestureRecognizer.ManipulationUpdatedEvent -= HandleManipulationUpdated;
gestureRecognizer.ManipulationCompletedEvent -= HandleManipulationCompleted;
gestureRecognizer.StopCapturingGestures();
gestureRecognizer.Dispose();
}
// 清理语音识别器
if (keywordRecognizer != null)
{
keywordRecognizer.OnPhraseRecognized -= HandlePhraseRecognized;
if (keywordRecognizer.IsRunning)
{
keywordRecognizer.Stop();
}
keywordRecognizer.Dispose();
}
#endif
}
}
// HoloLens交互接口
public interface IHoloLensGazeInteractable
{
void OnGazeEnter();
void OnGazeExit();
}
public interface IHoloLensTapHandler
{
void OnTapped();
}
public interface IHoloLensHoldHandler
{
void OnHoldStarted();
void OnHoldCompleted();
}
public interface IHoloLensManipulationHandler
{
void OnManipulationStarted(Vector3 position);
void OnManipulationUpdated(Vector3 position);
void OnManipulationCompleted(Vector3 position);
}
public interface IHoloLensVoiceHandler
{
void OnVoiceCommand(string command);
}
// HoloLens交互物体示例
public class HoloLensInteractableObject : MonoBehaviour,
IHoloLensGazeInteractable,
IHoloLensTapHandler,
IHoloLensManipulationHandler,
IHoloLensVoiceHandler
{
[SerializeField] private Color normalColor = Color.white;
[SerializeField] private Color hoverColor = Color.yellow;
[SerializeField] private Color selectedColor = Color.green;
[SerializeField] private bool isManipulatable = true;
private Renderer objRenderer;
private bool isSelected = false;
private Vector3 manipulationStartPosition;
private Vector3 objectStartPosition;
private void Awake()
{
objRenderer = GetComponent<Renderer>();
if (objRenderer != null)
{
objRenderer.material.color = normalColor;
}
}
public void OnGazeEnter()
{
if (objRenderer != null && !isSelected)
{
objRenderer.material.color = hoverColor;
}
}
public void OnGazeExit()
{
if (objRenderer != null && !isSelected)
{
objRenderer.material.color = normalColor;
}
}
public void OnTapped()
{
Debug.Log($"点击了物体: {gameObject.name}");
isSelected = !isSelected;
if (objRenderer != null)
{
objRenderer.material.color = isSelected ? selectedColor : normalColor;
}
// 根据选择状态执行操作
if (isSelected)
{
OnSelected();
}
else
{
OnDeselected();
}
}
public void OnHoldStarted()
{
Debug.Log($"开始长按物体: {gameObject.name}");
// 显示上下文菜单或执行特定操作
}
public void OnHoldCompleted()
{
Debug.Log($"完成长按物体: {gameObject.name}");
// 隐藏上下文菜单或完成特定操作
}
public void OnManipulationStarted(Vector3 position)
{
if (!isManipulatable) return;
Debug.Log($"开始操作物体: {gameObject.name}");
manipulationStartPosition = position;
objectStartPosition = transform.position;
}
public void OnManipulationUpdated(Vector3 position)
{
if (!isManipulatable) return;
// 计算移动距离
Vector3 delta = position - manipulationStartPosition;
// 更新物体位置
transform.position = objectStartPosition + delta;
}
public void OnManipulationCompleted(Vector3 position)
{
if (!isManipulatable) return;
Debug.Log($"完成操作物体: {gameObject.name}");
// 可以在这里执行其他完成操作,例如吸附到网格等
}
public void OnVoiceCommand(string command)
{
Debug.Log($"物体 {gameObject.name} 接收到语音命令: {command}");
switch (command.ToLower())
{
case "rotate":
// 旋转物体
transform.Rotate(0, 45, 0);
break;
case "bigger":
// 放大物体
transform.localScale *= 1.2f;
break;
case "smaller":
// 缩小物体
transform.localScale *= 0.8f;
break;
case "reset":
// 重置物体
transform.localScale = Vector3.one;
transform.rotation = Quaternion.identity;
break;
}
}
private void OnSelected()
{
// 特定选择效果
transform.localScale *= 1.1f;
}
private void OnDeselected()
{
// 恢复正常效果
transform.localScale /= 1.1f;
}
}
五、通用XR交互系统
跨平台输入系统
// XRInputSystem.cs - 跨平台XR输入系统
using UnityEngine;
using UnityEngine.XR;
public class XRInputSystem : MonoBehaviour
{
[Header("平台适配")]
[SerializeField] private XRPlatformType targetPlatform;
[SerializeField] private bool autoDetectPlatform = true;
[Header("输入映射")]
[SerializeField] private Transform leftHandTransform;
[SerializeField] private Transform rightHandTransform;
[SerializeField] private Transform headTransform;
[Header("控制器模型")]
[SerializeField] private GameObject questControllerPrefab;
[SerializeField] private GameObject picoControllerPrefab;
[SerializeField] private GameObject hololensHandPrefab;
// 当前激活的控制器模型
private GameObject leftControllerModel;
private GameObject rightControllerModel;
// 输入状态
public Vector2 LeftThumbstick { get; private set; }
public Vector2 RightThumbstick { get; private set; }
public float LeftTrigger { get; private set; }
public float RightTrigger { get; private set; }
public float LeftGrip { get; private set; }
public float RightGrip { get; private set; }
public bool LeftPrimaryButton { get; private set; }
public bool RightPrimaryButton { get; private set; }
public bool LeftSecondaryButton { get; private set; }
public bool RightSecondaryButton { get; private set; }
// 输入事件委托
public delegate void ButtonEventHandler(string buttonName, bool value);
public delegate void AxisEventHandler(string axisName, float value);
public delegate void Vector2EventHandler(string axisName, Vector2 value);
public event ButtonEventHandler OnButtonStateChanged;
public event AxisEventHandler OnAxisChanged;
public event Vector2EventHandler OnVector2Changed;
private void Awake()
{
if (autoDetectPlatform && XRPlatformManager.Instance != null)
{
targetPlatform = XRPlatformManager.Instance.CurrentPlatform;
}
InitializeInputSystem();
}
private void InitializeInputSystem()
{
Debug.Log($"初始化XR输入系统,平台: {targetPlatform}");
// 确保我们有头部Transform
if (headTransform == null)
{
Camera mainCamera = Camera.main;
if (mainCamera != null)
{
headTransform = mainCamera.transform;
}
}
// 根据平台初始化控制器模型
switch (targetPlatform)
{
case XRPlatformType.Quest:
InitializeQuestControllers();
break;
case XRPlatformType.Pico:
InitializePicoControllers();
break;
case XRPlatformType.HoloLens:
InitializeHoloLensControllers();
break;
default:
Debug.LogWarning("未识别的XR平台,使用默认输入设置");
break;
}
}
private void InitializeQuestControllers()
{
#if QUEST_SDK
// 初始化Quest控制器
if (questControllerPrefab != null)
{
if (leftHandTransform == null)
{
GameObject leftHand = new GameObject("LeftHandAnchor");
leftHandTransform = leftHand.transform;
}
if (rightHandTransform == null)
{
GameObject rightHand = new GameObject("RightHandAnchor");
rightHandTransform = rightHand.transform;
}
leftControllerModel = Instantiate(questControllerPrefab, leftHandTransform);
leftControllerModel.name = "LeftQuestController";
rightControllerModel = Instantiate(questControllerPrefab, rightHandTransform);
rightControllerModel.name = "RightQuestController";
// 确保右手模型是右手版本
rightControllerModel.transform.localScale = new Vector3(-1, 1, 1);
}
#endif
}
private void InitializePicoControllers()
{
#if PICO_SDK
// 初始化Pico控制器
if (picoControllerPrefab != null)
{
if (leftHandTransform == null)
{
GameObject leftHand = new GameObject("LeftHandAnchor");
leftHandTransform = leftHand.transform;
}
if (rightHandTransform == null)
{
GameObject rightHand = new GameObject("RightHandAnchor");
rightHandTransform = rightHand.transform;
}
leftControllerModel = Instantiate(picoControllerPrefab, leftHandTransform);
leftControllerModel.name = "LeftPicoController";
rightControllerModel = Instantiate(picoControllerPrefab, rightHandTransform);
rightControllerModel.name = "RightPicoController";
// 确保右手模型是右手版本
rightControllerModel.transform.localScale = new Vector3(-1, 1, 1);
}
#endif
}
private void InitializeHoloLensControllers()
{
#if HOLOLENS_SDK
// 初始化HoloLens手势模型
if (hololensHandPrefab != null)
{
if (leftHandTransform == null)
{
GameObject leftHand = new GameObject("LeftHandAnchor");
leftHandTransform = leftHand.transform;
leftHandTransform.parent = headTransform;
leftHandTransform.localPosition = new Vector3(-0.2f, -0.2f, 0.5f);
}
if (rightHandTransform == null)
{
GameObject rightHand = new GameObject("RightHandAnchor");
rightHandTransform = rightHand.transform;
rightHandTransform.parent = headTransform;
rightHandTransform.localPosition = new Vector3(0.2f, -0.2f, 0.5f);
}
leftControllerModel = Instantiate(hololensHandPrefab, leftHandTransform);
leftControllerModel.name = "LeftHoloLensHand";
rightControllerModel = Instantiate(hololensHandPrefab, rightHandTransform);
rightControllerModel.name = "RightHoloLensHand";
// 设置为右手
rightControllerModel.transform.localScale = new Vector3(-1, 1, 1);
}
#endif
}
private void Update()
{
UpdateControllerStates();
UpdateInputStates();
}
private void UpdateControllerStates()
{
switch (targetPlatform)
{
case XRPlatformType.Quest:
UpdateQuestControllers();
break;
case XRPlatformType.Pico:
UpdatePicoControllers();
break;
case XRPlatformType.HoloLens:
UpdateHoloLensControllers();
break;
default:
// 默认行为可以是使用通用XR输入API
UpdateGenericXRControllers();
break;
}
}
private void UpdateQuestControllers()
{
#if QUEST_SDK && !UNITY_EDITOR
// 更新Quest控制器位置和旋转
if (leftHandTransform != null)
{
leftHandTransform.localPosition = OVRInput.GetLocalControllerPosition(OVRInput.Controller.LTouch);
leftHandTransform.localRotation = OVRInput.GetLocalControllerRotation(OVRInput.Controller.LTouch);
}
if (rightHandTransform != null)
{
rightHandTransform.localPosition = OVRInput.GetLocalControllerPosition(OVRInput.Controller.RTouch);
rightHandTransform.localRotation = OVRInput.GetLocalControllerRotation(OVRInput.Controller.RTouch);
}
// 更新输入状态
LeftThumbstick = OVRInput.Get(OVRInput.Axis2D.PrimaryThumbstick, OVRInput.Controller.LTouch);
RightThumbstick = OVRInput.Get(OVRInput.Axis2D.PrimaryThumbstick, OVRInput.Controller.RTouch);
LeftTrigger = OVRInput.Get(OVRInput.Axis1D.PrimaryIndexTrigger, OVRInput.Controller.LTouch);
RightTrigger = OVRInput.Get(OVRInput.Axis1D.PrimaryIndexTrigger, OVRInput.Controller.RTouch);
LeftGrip = OVRInput.Get(OVRInput.Axis1D.PrimaryHandTrigger, OVRInput.Controller.LTouch);
RightGrip = OVRInput.Get(OVRInput.Axis1D.PrimaryHandTrigger, OVRInput.Controller.RTouch);
bool newLeftPrimaryButton = OVRInput.Get(OVRInput.Button.One, OVRInput.Controller.LTouch);
bool newRightPrimaryButton = OVRInput.Get(OVRInput.Button.One, OVRInput.Controller.RTouch);
bool newLeftSecondaryButton = OVRInput.Get(OVRInput.Button.Two, OVRInput.Controller.LTouch);
bool newRightSecondaryButton = OVRInput.Get(OVRInput.Button.Two, OVRInput.Controller.RTouch);
// 检测按钮状态变化并触发事件
CheckButtonStateChanged("LeftPrimary", newLeftPrimaryButton, ref LeftPrimaryButton);
CheckButtonStateChanged("RightPrimary", newRightPrimaryButton, ref RightPrimaryButton);
CheckButtonStateChanged("LeftSecondary", newLeftSecondaryButton, ref LeftSecondaryButton);
CheckButtonStateChanged("RightSecondary", newRightSecondaryButton, ref RightSecondaryButton);
#endif
}
private void UpdatePicoControllers()
{
#if PICO_SDK && !UNITY_EDITOR
// 更新Pico控制器位置和旋转
if (leftHandTransform != null)
{
leftHandTransform.localPosition = PXR_Input.GetLocalPosition(PXR_Input.Controller.LeftController);
leftHandTransform.localRotation = PXR_Input.GetLocalRotation(PXR_Input.Controller.LeftController);
}
if (rightHandTransform != null)
{
rightHandTransform.localPosition = PXR_Input.GetLocalPosition(PXR_Input.Controller.RightController);
rightHandTransform.localRotation = PXR_Input.GetLocalRotation(PXR_Input.Controller.RightController);
}
// 更新输入状态
LeftThumbstick = PXR_Input.GetAxis2D(PXR_Input.Controller.LeftController);
RightThumbstick = PXR_Input.GetAxis2D(PXR_Input.Controller.RightController);
LeftTrigger = PXR_Input.GetTriggerValue(PXR_Input.Controller.LeftController);
RightTrigger = PXR_Input.GetTriggerValue(PXR_Input.Controller.RightController);
LeftGrip = PXR_Input.GetGripValue(PXR_Input.Controller.LeftController);
RightGrip = PXR_Input.GetGripValue(PXR_Input.Controller.RightController);
bool newLeftPrimaryButton = PXR_Input.GetDown(PXR_Input.ButtonMask.X, PXR_Input.Controller.LeftController);
bool newRightPrimaryButton = PXR_Input.GetDown(PXR_Input.ButtonMask.A, PXR_Input.Controller.RightController);
bool newLeftSecondaryButton = PXR_Input.GetDown(PXR_Input.ButtonMask.Y, PXR_Input.Controller.LeftController);
bool newRightSecondaryButton = PXR_Input.GetDown(PXR_Input.ButtonMask.B, PXR_Input.Controller.RightController);
// 检测按钮状态变化并触发事件
CheckButtonStateChanged("LeftPrimary", newLeftPrimaryButton, ref LeftPrimaryButton);
CheckButtonStateChanged("RightPrimary", newRightPrimaryButton, ref RightPrimaryButton);
CheckButtonStateChanged("LeftSecondary", newLeftSecondaryButton, ref LeftSecondaryButton);
CheckButtonStateChanged("RightSecondary", newRightSecondaryButton, ref RightSecondaryButton);
#endif
}
private void UpdateHoloLensControllers()
{
#if HOLOLENS_SDK && !UNITY_EDITOR
// HoloLens使用手势而非真实控制器
// 这里我们使用非常简化的模拟
// 从手势识别获取手部位置
// 实际应用中使用HandsManager或GestureRecognizer
// 模拟简单的输入映射,例如点击作为主按钮
bool newLeftPrimaryButton = false;
bool newRightPrimaryButton = false;
// 获取手势识别器的数据
InteractionManager.InteractionSourcePressType pressType;
if (InteractionManager.TryGetInteractionSourcePressType(out pressType))
{
if (pressType == InteractionManager.InteractionSourcePressType.Select)
{
InteractionManager.InteractionSourceState sourceState;
if (InteractionManager.TryGetInteractionSourceState(out sourceState))
{
// 根据手的位置确定左右
if (sourceState.sourcePose.position.x < 0)
{
newLeftPrimaryButton = true;
}
else
{
newRightPrimaryButton = true;
}
}
}
}
// 检测按钮状态变化并触发事件
CheckButtonStateChanged("LeftPrimary", newLeftPrimaryButton, ref LeftPrimaryButton);
CheckButtonStateChanged("RightPrimary", newRightPrimaryButton, ref RightPrimaryButton);
#endif
}
private void UpdateGenericXRControllers()
{
InputDevice leftHandDevice = InputDevices.GetDeviceAtXRNode(XRNode.LeftHand);
InputDevice rightHandDevice = InputDevices.GetDeviceAtXRNode(XRNode.RightHand);
// 更新左手控制器
if (leftHandDevice.isValid && leftHandTransform != null)
{
Vector3 position;
Quaternion rotation;
if (leftHandDevice.TryGetFeatureValue(CommonUsages.devicePosition, out position))
{
leftHandTransform.localPosition = position;
}
if (leftHandDevice.TryGetFeatureValue(CommonUsages.deviceRotation, out rotation))
{
leftHandTransform.localRotation = rotation;
}
// 获取输入
Vector2 thumbstick;
if (leftHandDevice.TryGetFeatureValue(CommonUsages.primary2DAxis, out thumbstick))
{
LeftThumbstick = thumbstick;
}
float trigger;
if (leftHandDevice.TryGetFeatureValue(CommonUsages.trigger, out trigger))
{
LeftTrigger = trigger;
}
float grip;
if (leftHandDevice.TryGetFeatureValue(CommonUsages.grip, out grip))
{
LeftGrip = grip;
}
bool primaryButton;
if (leftHandDevice.TryGetFeatureValue(CommonUsages.primaryButton, out primaryButton))
{
CheckButtonStateChanged("LeftPrimary", primaryButton, ref LeftPrimaryButton);
}
bool secondaryButton;
if (leftHandDevice.TryGetFeatureValue(CommonUsages.secondaryButton, out secondaryButton))
{
CheckButtonStateChanged("LeftSecondary", secondaryButton, ref LeftSecondaryButton);
}
}
// 更新右手控制器
if (rightHandDevice.isValid && rightHandTransform != null)
{
Vector3 position;
Quaternion rotation;
if (rightHandDevice.TryGetFeatureValue(CommonUsages.devicePosition, out position))
{
rightHandTransform.localPosition = position;
}
if (rightHandDevice.TryGetFeatureValue(CommonUsages.deviceRotation, out rotation))
{
rightHandTransform.localRotation = rotation;
}
// 获取输入
Vector2 thumbstick;
if (rightHandDevice.TryGetFeatureValue(CommonUsages.primary2DAxis, out thumbstick))
{
RightThumbstick = thumbstick;
}
float trigger;
if (rightHandDevice.TryGetFeatureValue(CommonUsages.trigger, out trigger))
{
RightTrigger = trigger;
}
float grip;
if (rightHandDevice.TryGetFeatureValue(CommonUsages.grip, out grip))
{
RightGrip = grip;
}
bool primaryButton;
if (rightHandDevice.TryGetFeatureValue(CommonUsages.primaryButton, out primaryButton))
{
CheckButtonStateChanged("RightPrimary", primaryButton, ref RightPrimaryButton);
}
bool secondaryButton;
if (rightHandDevice.TryGetFeatureValue(CommonUsages.secondaryButton, out secondaryButton))
{
CheckButtonStateChanged("RightSecondary", secondaryButton, ref RightSecondaryButton);
}
}
}
// 检查按钮状态变化
private void CheckButtonStateChanged(string buttonName, bool newState, ref bool currentState)
{
if (newState != currentState)
{
currentState = newState;
OnButtonStateChanged?.Invoke(buttonName, currentState);
}
}
// 更新所有输入状态并触发事件
private void UpdateInputStates()
{
// 轴向输入检测
static void CheckAxis(string name, float value, float previousValue, AxisEventHandler handler, float threshold = 0.01f)
{
if (Mathf.Abs(value - previousValue) > threshold)
{
handler?.Invoke(name, value);
}
}
// 二维轴向输入检测
static void CheckVector2(string name, Vector2 value, Vector2 previousValue, Vector2EventHandler handler, float threshold = 0.01f)
{
if (Vector2.Distance(value, previousValue) > threshold)
{
handler?.Invoke(name, value);
}
}
// 记录当前值用于比较
static float prevLeftTrigger = LeftTrigger;
static float prevRightTrigger = RightTrigger;
static float prevLeftGrip = LeftGrip;
static float prevRightGrip = RightGrip;
static Vector2 prevLeftThumbstick = LeftThumbstick;
static Vector2 prevRightThumbstick = RightThumbstick;
// 检查轴输入变化
CheckAxis("LeftTrigger", LeftTrigger, prevLeftTrigger, OnAxisChanged);
CheckAxis("RightTrigger", RightTrigger, prevRightTrigger, OnAxisChanged);
CheckAxis("LeftGrip", LeftGrip, prevLeftGrip, OnAxisChanged);
CheckAxis("RightGrip", RightGrip, prevRightGrip, OnAxisChanged);
// 检查二维轴输入变化
CheckVector2("LeftThumbstick", LeftThumbstick, prevLeftThumbstick, OnVector2Changed);
CheckVector2("RightThumbstick", RightThumbstick, prevRightThumbstick, OnVector2Changed);
// 更新之前的值
prevLeftTrigger = LeftTrigger;
prevRightTrigger = RightTrigger;
prevLeftGrip = LeftGrip;
prevRightGrip = RightGrip;
prevLeftThumbstick = LeftThumbstick;
prevRightThumbstick = RightThumbstick;
}
}
跨平台交互系统
// XRInteractionSystem.cs - 跨平台XR交互系统
using UnityEngine;
using UnityEngine.Events;
using System.Collections.Generic;
public class XRInteractionSystem : MonoBehaviour
{
[Header("交互设置")]
[SerializeField] private float interactionDistance = 0.3f;
[SerializeField] private LayerMask interactableLayers;
[SerializeField] private Transform leftInteractor;
[SerializeField] private Transform rightInteractor;
[SerializeField] private bool useRayInteraction = true;
[SerializeField] private float rayLength = 5.0f;
[SerializeField] private GameObject leftRayVisual;
[SerializeField] private GameObject rightRayVisual;
[Header("视觉反馈")]
[SerializeField] private GameObject highlightPrefab;
[SerializeField] private GameObject selectPrefab;
// 交互状态
private XRInteractable leftHoverTarget;
private XRInteractable rightHoverTarget;
private XRInteractable leftSelectedTarget;
private XRInteractable rightSelectedTarget;
// 高亮对象
private GameObject leftHighlight;
private GameObject rightHighlight;
private GameObject leftSelectIndicator;
private GameObject rightSelectIndicator;
// 交互历史队列(用于过滤抖动)
private Queue<XRInteractable> leftHoverHistory = new Queue<XRInteractable>();
private Queue<XRInteractable> rightHoverHistory = new Queue<XRInteractable>();
private const int hoverHistoryLength = 3;
// XR输入系统引用
private XRInputSystem xrInput;
private void Awake()
{
xrInput = FindObjectOfType<XRInputSystem>();
if (xrInput == null)
{
Debug.LogWarning("未找到XRInputSystem,交互系统可能无法正常工作");
}
// 初始化视觉反馈
if (highlightPrefab != null)
{
leftHighlight = Instantiate(highlightPrefab);
rightHighlight = Instantiate(highlightPrefab);
leftHighlight.SetActive(false);
rightHighlight.SetActive(false);
}
if (selectPrefab != null)
{
leftSelectIndicator = Instantiate(selectPrefab);
rightSelectIndicator = Instantiate(selectPrefab);
leftSelectIndicator.SetActive(false);
rightSelectIndicator.SetActive(false);
}
// 初始化射线可视化
if (leftRayVisual != null) leftRayVisual.SetActive(false);
if (rightRayVisual != null) rightRayVisual.SetActive(false);
}
private void Start()
{
// 订阅输入事件
if (xrInput != null)
{
xrInput.OnButtonStateChanged += HandleButtonStateChanged;
xrInput.OnAxisChanged += HandleAxisChanged;
}
}
private void Update()
{
// 更新悬停逻辑
if (leftInteractor != null) UpdateInteractorHover(leftInteractor, true);
if (rightInteractor != null) UpdateInteractorHover(rightInteractor, false);
// 更新射线可视化
UpdateRayVisuals();
// 更新选中物体位置
UpdateSelectedObjects();
}
private void UpdateInteractorHover(Transform interactor, bool isLeft)
{
XRInteractable nearestInteractable = null;
if (useRayInteraction)
{
// 射线交互
nearestInteractable = GetRayInteractable(interactor, isLeft);
}
else
{
// 近距离交互
nearestInteractable = GetNearestInteractable(interactor, isLeft);
}
// 使用历史队列过滤抖动
Queue<XRInteractable> history = isLeft ? leftHoverHistory : rightHoverHistory;
if (nearestInteractable != null)
{
history.Enqueue(nearestInteractable);
while (history.Count > hoverHistoryLength)
{
history.Dequeue();
}
// 检查历史记录中是否有相同的对象
bool isConsistent = true;
XRInteractable firstItem = null;
foreach (XRInteractable item in history)
{
if (firstItem == null)
{
firstItem = item;
}
else if (firstItem != item)
{
isConsistent = false;
break;
}
}
// 如果历史记录中的对象一致,更新悬停目标
if (isConsistent && history.Count == hoverHistoryLength)
{
UpdateHoverTarget(firstItem, isLeft);
}
}
else
{
// 清空历史
history.Clear();
// 更新悬停目标为null
UpdateHoverTarget(null, isLeft);
}
}
private XRInteractable GetNearestInteractable(Transform interactor, bool isLeft)
{
// 查找附近的可交互对象
Collider[] colliders = Physics.OverlapSphere(interactor.position, interactionDistance, interactableLayers);
XRInteractable nearestInteractable = null;
float minDistance = float.MaxValue;
foreach (Collider collider in colliders)
{
XRInteractable interactable = collider.GetComponent<XRInteractable>();
if (interactable != null && interactable.IsInteractable)
{
float distance = Vector3.Distance(interactor.position, interactable.transform.position);
if (distance < minDistance)
{
minDistance = distance;
nearestInteractable = interactable;
}
}
}
return nearestInteractable;
}
private XRInteractable GetRayInteractable(Transform interactor, bool isLeft)
{
RaycastHit hit;
if (Physics.Raycast(interactor.position, interactor.forward, out hit, rayLength, interactableLayers))
{
XRInteractable interactable = hit.collider.GetComponent<XRInteractable>();
if (interactable != null && interactable.IsInteractable)
{
// 更新射线可视化对象
GameObject rayVisual = isLeft ? leftRayVisual : rightRayVisual;
if (rayVisual != null)
{
rayVisual.SetActive(true);
rayVisual.transform.position = interactor.position;
rayVisual.transform.rotation = Quaternion.LookRotation(hit.point - interactor.position);
rayVisual.transform.localScale = new Vector3(0.01f, 0.01f, hit.distance);
}
return interactable;
}
}
return null;
}
private void UpdateRayVisuals()
{
if (!useRayInteraction)
{
// 禁用射线可视化
if (leftRayVisual != null) leftRayVisual.SetActive(false);
if (rightRayVisual != null) rightRayVisual.SetActive(false);
return;
}
// 根据是否有目标更新射线
if (leftHoverTarget == null && leftRayVisual != null)
{
leftRayVisual.SetActive(false);
}
if (rightHoverTarget == null && rightRayVisual != null)
{
rightRayVisual.SetActive(false);
}
}
private void UpdateHoverTarget(XRInteractable newTarget, bool isLeft)
{
XRInteractable currentTarget = isLeft ? leftHoverTarget : rightHoverTarget;
GameObject highlight = isLeft ? leftHighlight : rightHighlight;
// 如果目标没有变化,不需要任何操作
if (newTarget == currentTarget) return;
// 退出旧目标
if (currentTarget != null)
{
currentTarget.OnHoverExit(isLeft);
// 隐藏高亮
if (highlight != null)
{
highlight.SetActive(false);
}
}
// 进入新目标
if (newTarget != null)
{
newTarget.OnHoverEnter(isLeft);
// 显示高亮
if (highlight != null)
{
highlight.SetActive(true);
highlight.transform.position = newTarget.transform.position;
highlight.transform.rotation = newTarget.transform.rotation;
// 尝试适配高亮到目标大小
Renderer targetRenderer = newTarget.GetComponent<Renderer>();
if (targetRenderer != null)
{
Bounds bounds = targetRenderer.bounds;
highlight.transform.localScale = bounds.size * 1.05f;
}
else
{
highlight.transform.localScale = newTarget.transform.localScale * 1.05f;
}
}
}
// 更新当前目标
if (isLeft)
{
leftHoverTarget = newTarget;
}
else
{
rightHoverTarget = newTarget;
}
}
private void HandleButtonStateChanged(string buttonName, bool value)
{
// 根据按钮触发不同的交互
switch (buttonName)
{
case "LeftPrimary":
if (value) SelectObject(true); else ReleaseObject(true);
break;
case "RightPrimary":
if (value) SelectObject(false); else ReleaseObject(false);
break;
case "LeftSecondary":
if (value && leftSelectedTarget != null)
{
leftSelectedTarget.OnSecondaryAction(true);
}
break;
case "RightSecondary":
if (value && rightSelectedTarget != null)
{
rightSelectedTarget.OnSecondaryAction(false);
}
break;
}
}
private void HandleAxisChanged(string axisName, float value)
{
// 将轴输入传递给选中对象
switch (axisName)
{
case "LeftTrigger":
if (leftSelectedTarget != null)
{
leftSelectedTarget.OnTriggerChanged(value, true);
}
break;
case "RightTrigger":
if (rightSelectedTarget != null)
{
rightSelectedTarget.OnTriggerChanged(value, false);
}
break;
case "LeftGrip":
if (leftSelectedTarget != null)
{
leftSelectedTarget.OnGripChanged(value, true);
}
break;
case "RightGrip":
if (rightSelectedTarget != null)
{
rightSelectedTarget.OnGripChanged(value, false);
}
break;
}
}
private void SelectObject(bool isLeft)
{
XRInteractable target = isLeft ? leftHoverTarget : rightHoverTarget;
Transform interactor = isLeft ? leftInteractor : rightInteractor;
GameObject selectVisual = isLeft ? leftSelectIndicator : rightSelectIndicator;
if (target != null && target.IsSelectable && interactor != null)
{
if (isLeft)
{
leftSelectedTarget = target;
}
else
{
rightSelectedTarget = target;
}
// 通知开始选中
target.OnSelectEnter(isLeft, interactor);
// 显示选中指示器
if (selectVisual != null)
{
selectVisual.SetActive(true);
selectVisual.transform.position = target.transform.position;
selectVisual.transform.rotation = target.transform.rotation;
selectVisual.transform.localScale = target.transform.localScale * 1.1f;
}
}
}
private void ReleaseObject(bool isLeft)
{
XRInteractable target = isLeft ? leftSelectedTarget : rightSelectedTarget;
GameObject selectVisual = isLeft ? leftSelectIndicator : rightSelectIndicator;
if (target != null)
{
// 通知结束选中
target.OnSelectExit(isLeft);
// 隐藏选中指示器
if (selectVisual != null)
{
selectVisual.SetActive(false);
}
// 清空选中目标
if (isLeft)
{
leftSelectedTarget = null;
}
else
{
rightSelectedTarget = null;
}
}
}
private void UpdateSelectedObjects()
{
// 更新左手选中物体
if (leftSelectedTarget != null && leftInteractor != null && leftSelectedTarget.IsAttachable)
{
leftSelectedTarget.OnPositionUpdate(leftInteractor, true);
}
// 更新右手选中物体
if (rightSelectedTarget != null && rightInteractor != null && rightSelectedTarget.IsAttachable)
{
rightSelectedTarget.OnPositionUpdate(rightInteractor, false);
}
}
private void OnDestroy()
{
// 取消订阅事件
if (xrInput != null)
{
xrInput.OnButtonStateChanged -= HandleButtonStateChanged;
xrInput.OnAxisChanged -= HandleAxisChanged;
}
}
}
// XRInteractable.cs - 可交互对象基类
public class XRInteractable : MonoBehaviour
{
[Header("交互属性")]
[SerializeField] private bool isInteractable = true;
[SerializeField] private bool isSelectable = true;
[SerializeField] private bool isAttachable = true;
[SerializeField] private bool useLocalSpace = false;
[SerializeField] private Vector3 attachOffset = Vector3.zero;
[SerializeField] private Vector3 attachRotationOffset = Vector3.zero;
[Header("交互事件")]
[SerializeField] private UnityEvent onHoverEnterEvent;
[SerializeField] private UnityEvent onHoverExitEvent;
[SerializeField] private UnityEvent onSelectEnterEvent;
[SerializeField] private UnityEvent onSelectExitEvent;
// 物理属性引用
private Rigidbody objectRigidbody;
private bool wasKinematic = false;
// 交互状态
private bool isHovered = false;
private bool isSelected = false;
private Transform attachTransform;
// 初始状态备份
private Vector3 initialPosition;
private Quaternion initialRotation;
private Vector3 initialScale;
// 属性访问器
public bool IsInteractable => isInteractable;
public bool IsSelectable => isSelectable;
public bool IsAttachable => isAttachable;
private void Awake()
{
objectRigidbody = GetComponent<Rigidbody>();
// 保存初始状态
initialPosition = transform.position;
initialRotation = transform.rotation;
initialScale = transform.localScale;
}
// 悬停开始
public virtual void OnHoverEnter(bool isLeft)
{
isHovered = true;
// 触发Unity事件
onHoverEnterEvent?.Invoke();
// 可以在子类中重写以添加自定义行为
}
// 悬停结束
public virtual void OnHoverExit(bool isLeft)
{
isHovered = false;
// 触发Unity事件
onHoverExitEvent?.Invoke();
// 可以在子类中重写以添加自定义行为
}
// 选中开始
public virtual void OnSelectEnter(bool isLeft, Transform interactor)
{
isSelected = true;
attachTransform = interactor;
// 如果有刚体,改变物理属性
if (objectRigidbody != null && isAttachable)
{
wasKinematic = objectRigidbody.isKinematic;
objectRigidbody.isKinematic = true;
}
// 触发Unity事件
onSelectEnterEvent?.Invoke();
// 可以在子类中重写以添加自定义行为
}
// 选中结束
public virtual void OnSelectExit(bool isLeft)
{
isSelected = false;
// 如果有刚体,恢复物理属性
if (objectRigidbody != null && isAttachable)
{
objectRigidbody.isKinematic = wasKinematic;
// 可以添加一些释放速度
if (!wasKinematic)
{
// 获取控制器速度进行投掷
// 此处需要补充具体实现
}
}
// 触发Unity事件
onSelectExitEvent?.Invoke();
// 可以在子类中重写以添加自定义行为
}
// 位置更新
public virtual void OnPositionUpdate(Transform interactor, bool isLeft)
{
if (!isSelected || !isAttachable || interactor == null) return;
if (useLocalSpace)
{
// 使用局部坐标
transform.position = interactor.TransformPoint(attachOffset);
transform.rotation = interactor.rotation * Quaternion.Euler(attachRotationOffset);
}
else
{
// 使用世界坐标
transform.position = interactor.position + interactor.TransformDirection(attachOffset);
transform.rotation = interactor.rotation * Quaternion.Euler(attachRotationOffset);
}
}
// 扳机输入
public virtual void OnTriggerChanged(float value, bool isLeft)
{
// 默认实现为空,子类可以重写
}
// 握把输入
public virtual void OnGripChanged(float value, bool isLeft)
{
// 默认实现为空,子类可以重写
}
// 辅助按钮操作
public virtual void OnSecondaryAction(bool isLeft)
{
// 默认实现为空,子类可以重写
}
// 重置物体
public virtual void ResetToInitialState()
{
transform.position = initialPosition;
transform.rotation = initialRotation;
transform.localScale = initialScale;
// 重置物理状态
if (objectRigidbody != null)
{
objectRigidbody.velocity = Vector3.zero;
objectRigidbody.angularVelocity = Vector3.zero;
}
}
// 设置交互状态
public void SetInteractable(bool interactable)
{
isInteractable = interactable;
}
// 设置选中状态
public void SetSelectable(bool selectable)
{
isSelectable = selectable;
}
// 设置附加状态
public void SetAttachable(bool attachable)
{
isAttachable = attachable;
}
}
六、多平台渲染与优化策略
跨平台渲染管理器
// XRRenderingManager.cs - 跨平台渲染管理器
using UnityEngine;
using UnityEngine.Rendering;
[DefaultExecutionOrder(-100)] // 确保在其他脚本前执行
public class XRRenderingManager : MonoBehaviour
{
[Header("渲染设置")]
[SerializeField] private bool autoDetectPlatform = true;
[SerializeField] private XRPlatformType targetPlatform;
[Header("Quest设置")]
[SerializeField] private bool enableFFR = true;
[Range(1, 4)]
[SerializeField] private int questQualityLevel = 3;
[Range(0.7f, 1.4f)]
[SerializeField] private float questResolutionScale = 1.0f;
[Header("Pico设置")]
[SerializeField] private bool enableFoveatedRendering = true;
[Range(1, 4)]
[SerializeField] private int picoQualityLevel = 3;
[Range(0.7f, 1.4f)]
[SerializeField] private float picoResolutionScale = 1.0f;
[Header("HoloLens设置")]
[SerializeField] private bool enableHolographicReprojection = true;
[Range(1, 3)]
[SerializeField] private int hololensQualityLevel = 2;
[Range(0.7f, 1.4f)]
[SerializeField] private float hololensResolutionScale = 1.0f;
[Header("动态优化")]
[SerializeField] private bool enableDynamicResolution = true;
[SerializeField] private bool enableAdaptiveQuality = true;
[Range(0.5f, 1.0f)]
[SerializeField] private float minResolutionScale = 0.7f;
[Range(60, 90)]
[SerializeField] private float targetFrameRate = 72f;
// 性能监控
private float[] frameTimes = new float[60];
private int frameIndex = 0;
private float lastResolutionAdjustTime = 0f;
private float resolutionAdjustInterval = 2.0f;
// 当前状态
private float currentResolutionScale;
private int currentQualityLevel;
private void Awake()
{
if (autoDetectPlatform && XRPlatformManager.Instance != null)
{
targetPlatform = XRPlatformManager.Instance.CurrentPlatform;
}
ApplyRenderingSettings();
}
private void ApplyRenderingSettings()
{
Debug.Log($"应用XR渲染设置,平台: {targetPlatform}");
switch (targetPlatform)
{
case XRPlatformType.Quest:
ApplyQuestSettings();
break;
case XRPlatformType.Pico:
ApplyPicoSettings();
break;
case XRPlatformType.HoloLens:
ApplyHololensSettings();
break;
default:
ApplyDefaultSettings();
break;
}
// 设置应用目标帧率
Application.targetFrameRate = (int)targetFrameRate;
}
private void ApplyQuestSettings()
{
#if QUEST_SDK && !UNITY_EDITOR
try
{
Debug.Log("应用Quest渲染设置");
// 设置固定注视点渲染
if (enableFFR)
{
OVRManager.fixedFoveatedRenderingLevel = OVRManager.FixedFoveatedRenderingLevel.High;
OVRManager.useDynamicFixedFoveatedRendering = true;
}
else
{
OVRManager.fixedFoveatedRenderingLevel = OVRManager.FixedFoveatedRenderingLevel.Off;
}
// 设置渲染分辨率
OVRManager.eyeTextureResolutionScale = questResolutionScale;
currentResolutionScale = questResolutionScale;
// 设置性能等级
int cpuLevel = Mathf.Clamp(questQualityLevel - 1, 0, 3);
int gpuLevel = Mathf.Clamp(questQualityLevel - 1, 0, 3);
OVRManager.cpuLevel = cpuLevel;
OVRManager.gpuLevel = gpuLevel;
currentQualityLevel = questQualityLevel;
}
catch (System.Exception e)
{
Debug.LogError($"设置Quest渲染参数时出错: {e.Message}");
}
#endif
}
private void ApplyPicoSettings()
{
#if PICO_SDK && !UNITY_EDITOR
try
{
Debug.Log("应用Pico渲染设置");
// 设置注视点渲染
if (enableFoveatedRendering)
{
Unity.XR.PXR.PXR_Manager.Instance.foveationLevel = Unity.XR.PXR.FoveationLevel.High;
}
else
{
Unity.XR.PXR.PXR_Manager.Instance.foveationLevel = Unity.XR.PXR.FoveationLevel.None;
}
// 设置渲染分辨率
Unity.XR.PXR.PXR_Manager.Instance.eyeTexureResolutionScale = picoResolutionScale;
currentResolutionScale = picoResolutionScale;
// 设置质量等级
QualitySettings.SetQualityLevel(Mathf.Clamp(picoQualityLevel - 1, 0, QualitySettings.names.Length - 1));
currentQualityLevel = picoQualityLevel;
}
catch (System.Exception e)
{
Debug.LogError($"设置Pico渲染参数时出错: {e.Message}");
}
#endif
}
private void ApplyHololensSettings()
{
#if HOLOLENS_SDK && !UNITY_EDITOR
try
{
Debug.Log("应用HoloLens渲染设置");
// 设置全息重投影
if (enableHolographicReprojection)
{
UnityEngine.XR.WSA.HolographicSettings.EnableReprojection(
UnityEngine.XR.WSA.HolographicSettings.ReprojectionMode.PositionAndOrientation);
}
else
{
UnityEngine.XR.WSA.HolographicSettings.EnableReprojection(
UnityEngine.XR.WSA.HolographicSettings.ReprojectionMode.Disabled);
}
// 设置渲染分辨率
UnityEngine.XR.XRSettings.eyeTextureResolutionScale = hololensResolutionScale;
currentResolutionScale = hololensResolutionScale;
// 设置质量等级
QualitySettings.SetQualityLevel(Mathf.Clamp(hololensQualityLevel - 1, 0, QualitySettings.names.Length - 1));
currentQualityLevel = hololensQualityLevel;
}
catch (System.Exception e)
{
Debug.LogError($"设
Debug.LogError($"设置HoloLens渲染参数时出错: {e.Message}");
}
#endif
}
private void ApplyDefaultSettings()
{
Debug.Log("应用通用XR渲染设置");
// 设置渲染分辨率
UnityEngine.XR.XRSettings.eyeTextureResolutionScale = 1.0f;
currentResolutionScale = 1.0f;
// 设置适当的质量等级
int qualityLevel = Mathf.Clamp(2, 0, QualitySettings.names.Length - 1); // 中等质量
QualitySettings.SetQualityLevel(qualityLevel);
currentQualityLevel = qualityLevel + 1;
}
private void Update()
{
if (enableDynamicResolution || enableAdaptiveQuality)
{
MonitorPerformance();
}
}
private void MonitorPerformance()
{
// 记录帧时间
frameTimes[frameIndex] = Time.deltaTime;
frameIndex = (frameIndex + 1) % frameTimes.Length;
// 每隔一段时间调整分辨率
if (Time.time - lastResolutionAdjustTime > resolutionAdjustInterval)
{
lastResolutionAdjustTime = Time.time;
// 计算平均帧时间
float avgFrameTime = 0;
for (int i = 0; i < frameTimes.Length; i++)
{
avgFrameTime += frameTimes[i];
}
avgFrameTime /= frameTimes.Length;
// 计算当前帧率
float currentFPS = 1.0f / avgFrameTime;
// 调整渲染设置
if (enableDynamicResolution)
{
AdjustResolution(currentFPS);
}
if (enableAdaptiveQuality && currentFPS < targetFrameRate * 0.8f)
{
AdjustQualityLevel(currentFPS);
}
}
}
private void AdjustResolution(float currentFPS)
{
// 帧率低于目标的85%时降低分辨率
if (currentFPS < targetFrameRate * 0.85f && currentResolutionScale > minResolutionScale)
{
float newScale = Mathf.Max(currentResolutionScale - 0.05f, minResolutionScale);
SetResolutionScale(newScale);
Debug.Log($"降低渲染分辨率至 {newScale:F2} (当前帧率: {currentFPS:F1})");
}
// 帧率高于目标的95%时尝试提高分辨率
else if (currentFPS > targetFrameRate * 0.95f && currentResolutionScale < 1.2f)
{
// 更缓慢地增加分辨率
float newScale = Mathf.Min(currentResolutionScale + 0.02f, 1.2f);
SetResolutionScale(newScale);
Debug.Log($"提高渲染分辨率至 {newScale:F2} (当前帧率: {currentFPS:F1})");
}
}
private void AdjustQualityLevel(float currentFPS)
{
// 帧率低于目标的70%时降低质量等级
if (currentFPS < targetFrameRate * 0.7f && currentQualityLevel > 1)
{
int newLevel = currentQualityLevel - 1;
SetQualityLevel(newLevel);
Debug.Log($"降低质量等级至 {newLevel} (当前帧率: {currentFPS:F1})");
}
}
private void SetResolutionScale(float scale)
{
currentResolutionScale = scale;
switch (targetPlatform)
{
case XRPlatformType.Quest:
#if QUEST_SDK && !UNITY_EDITOR
OVRManager.eyeTextureResolutionScale = scale;
#endif
break;
case XRPlatformType.Pico:
#if PICO_SDK && !UNITY_EDITOR
Unity.XR.PXR.PXR_Manager.Instance.eyeTexureResolutionScale = scale;
#endif
break;
case XRPlatformType.HoloLens:
#if HOLOLENS_SDK && !UNITY_EDITOR
UnityEngine.XR.XRSettings.eyeTextureResolutionScale = scale;
#endif
break;
default:
UnityEngine.XR.XRSettings.eyeTextureResolutionScale = scale;
break;
}
}
private void SetQualityLevel(int level)
{
currentQualityLevel = level;
switch (targetPlatform)
{
case XRPlatformType.Quest:
#if QUEST_SDK && !UNITY_EDITOR
int cpuLevel = Mathf.Clamp(level - 1, 0, 3);
int gpuLevel = Mathf.Clamp(level - 1, 0, 3);
OVRManager.cpuLevel = cpuLevel;
OVRManager.gpuLevel = gpuLevel;
#endif
break;
default:
// 适当调整Unity质量等级
QualitySettings.SetQualityLevel(Mathf.Clamp(level - 1, 0, QualitySettings.names.Length - 1));
break;
}
// 调用自定义质量调整
OnQualityLevelChanged(level);
}
// 可以在子类中重写以添加额外的质量调整
protected virtual void OnQualityLevelChanged(int level)
{
// 常见的质量参数调整
switch (level)
{
case 1: // 低质量
RenderSettings.shadowDistance = 5f;
QualitySettings.shadowResolution = ShadowResolution.Low;
QualitySettings.shadowCascades = 1;
QualitySettings.antiAliasing = 0;
QualitySettings.realtimeReflectionProbes = false;
break;
case 2: // 中等质量
RenderSettings.shadowDistance = 20f;
QualitySettings.shadowResolution = ShadowResolution.Medium;
QualitySettings.shadowCascades = 2;
QualitySettings.antiAliasing = 2;
QualitySettings.realtimeReflectionProbes = false;
break;
case 3: // 高质量
RenderSettings.shadowDistance = 40f;
QualitySettings.shadowResolution = ShadowResolution.High;
QualitySettings.shadowCascades = 2;
QualitySettings.antiAliasing = 4;
QualitySettings.realtimeReflectionProbes = true;
break;
case 4: // 超高质量
RenderSettings.shadowDistance = 60f;
QualitySettings.shadowResolution = ShadowResolution.VeryHigh;
QualitySettings.shadowCascades = 4;
QualitySettings.antiAliasing = 8;
QualitySettings.realtimeReflectionProbes = true;
break;
}
}
// 为其他脚本提供公共方法
public void BoostPerformanceForScenario()
{
// 临时提高性能的方法,例如在复杂场景转换时调用
float previousScale = currentResolutionScale;
SetResolutionScale(minResolutionScale);
// 在短时间后恢复
StartCoroutine(RestoreResolutionAfterDelay(previousScale, 2.0f));
}
private System.Collections.IEnumerator RestoreResolutionAfterDelay(float targetScale, float delay)
{
yield return new WaitForSeconds(delay);
SetResolutionScale(targetScale);
}
}
高效渲染批次合并与优化工具
// XRBatchingOptimizer.cs - XR批处理优化器
using UnityEngine;
using System.Collections.Generic;
#if UNITY_EDITOR
using UnityEditor;
#endif
public class XRBatchingOptimizer : MonoBehaviour
{
[Header("批处理设置")]
[SerializeField] private bool enableStaticBatching = true;
[SerializeField] private bool enableDynamicBatching = true;
[SerializeField] private bool enableGPUInstancing = true;
[Header("优化设置")]
[SerializeField] private bool combineStaticMeshes = true;
[SerializeField] private bool generateLightmapUVs = true;
[SerializeField] private bool organizeObjectsByMaterial = true;
[Header("运行时监控")]
[SerializeField] private bool monitorBatchCount = true;
[SerializeField] private bool logWarnings = true;
[SerializeField] private int warningBatchThreshold = 100;
// 批次统计
private int drawCallCount = 0;
private int batchCount = 0;
private int savedBatches = 0;
// 缓存用于查找相同材质的对象
private Dictionary<Material, List<Renderer>> materialGroups = new Dictionary<Material, List<Renderer>>();
private void Awake()
{
// 设置批处理参数
if (Application.isPlaying)
{
ApplyBatchingSettings();
if (combineStaticMeshes)
{
CombineStaticMeshesAtRuntime();
}
}
}
private void ApplyBatchingSettings()
{
// 尽管这些是项目设置,但在运行时仍可以通过反射修改某些参数
// 这部分代码实际上不会修改GraphicsSettings中的值,只是展示可能的方法
Debug.Log($"应用XR批处理设置 - 静态批处理: {enableStaticBatching}, 动态批处理: {enableDynamicBatching}, GPU实例化: {enableGPUInstancing}");
// 集中启用GPU实例化
if (enableGPUInstancing)
{
EnableGPUInstancingOnMaterials();
}
}
private void EnableGPUInstancingOnMaterials()
{
Renderer[] renderers = FindObjectsOfType<Renderer>();
int enabledCount = 0;
foreach (Renderer renderer in renderers)
{
// 只处理非粒子系统渲染器
if (!(renderer is ParticleSystemRenderer))
{
foreach (Material material in renderer.sharedMaterials)
{
if (material != null && !material.enableInstancing && material.shader.supportsInstancing)
{
material.enableInstancing = true;
enabledCount++;
}
}
}
}
Debug.Log($"为 {enabledCount} 个材质启用了GPU实例化");
}
private void Start()
{
if (monitorBatchCount)
{
// 注册到相机预渲染事件以收集批处理统计信息
Camera.onPreRender += OnCameraPreRender;
InvokeRepeating("LogBatchStatistics", 5f, 10f);
}
if (organizeObjectsByMaterial)
{
OrganizeObjectsByMaterial();
}
}
private void OnCameraPreRender(Camera cam)
{
// 只监视主摄像机
if (cam.CompareTag("MainCamera"))
{
drawCallCount = UnityStats.drawCalls;
batchCount = UnityStats.batches;
savedBatches = UnityStats.renderInstancedBatches;
if (logWarnings && batchCount > warningBatchThreshold)
{
Debug.LogWarning($"批次数量 ({batchCount}) 超过警告阈值 ({warningBatchThreshold})!");
}
}
}
private void LogBatchStatistics()
{
Debug.Log($"渲染统计 - 绘制调用: {drawCallCount}, 批次: {batchCount}, 实例化批次: {savedBatches}");
}
private void CombineStaticMeshesAtRuntime()
{
// 在运行时合并静态网格以减少绘制调用
// 注意:这可能会增加内存使用量和加载时间
// 按材质分组
Dictionary<Material, List<MeshFilter>> meshGroups = new Dictionary<Material, List<MeshFilter>>();
MeshFilter[] meshFilters = FindObjectsOfType<MeshFilter>();
foreach (MeshFilter meshFilter in meshFilters)
{
// 检查是否是静态对象且有对应的渲染器
if (meshFilter.gameObject.isStatic && meshFilter.sharedMesh != null)
{
MeshRenderer meshRenderer = meshFilter.GetComponent<MeshRenderer>();
if (meshRenderer != null && meshRenderer.enabled && meshRenderer.sharedMaterial != null)
{
Material material = meshRenderer.sharedMaterial;
if (!meshGroups.ContainsKey(material))
{
meshGroups[material] = new List<MeshFilter>();
}
meshGroups[material].Add(meshFilter);
}
}
}
// 为每个材质组合并网格
int combinedGroupCount = 0;
foreach (var group in meshGroups)
{
if (group.Value.Count > 1) // 只有当有多个网格时才需要合并
{
CombineMeshGroup(group.Key, group.Value);
combinedGroupCount++;
}
}
Debug.Log($"运行时合并了 {combinedGroupCount} 组网格");
}
private void CombineMeshGroup(Material material, List<MeshFilter> meshFilters)
{
// 每组最多合并1023个网格(Unity的限制)
const int MAX_MESHES_PER_GROUP = 1023;
for (int groupIndex = 0; groupIndex < meshFilters.Count; groupIndex += MAX_MESHES_PER_GROUP)
{
int meshesInThisGroup = Mathf.Min(MAX_MESHES_PER_GROUP, meshFilters.Count - groupIndex);
// 准备组合数据
CombineInstance[] combineInstances = new CombineInstance[meshesInThisGroup];
for (int i = 0; i < meshesInThisGroup; i++)
{
MeshFilter meshFilter = meshFilters[groupIndex + i];
combineInstances[i] = new CombineInstance();
combineInstances[i].mesh = meshFilter.sharedMesh;
combineInstances[i].transform = meshFilter.transform.localToWorldMatrix;
}
// 创建新的组合网格
GameObject combinedObject = new GameObject($"Combined_Mesh_{material.name}_{groupIndex/MAX_MESHES_PER_GROUP}");
combinedObject.transform.SetParent(transform);
combinedObject.transform.localPosition = Vector3.zero;
combinedObject.transform.localRotation = Quaternion.identity;
combinedObject.transform.localScale = Vector3.one;
MeshFilter newMeshFilter = combinedObject.AddComponent<MeshFilter>();
MeshRenderer newRenderer = combinedObject.AddComponent<MeshRenderer>();
// 创建组合网格
Mesh combinedMesh = new Mesh();
combinedMesh.CombineMeshes(combineInstances, true);
newMeshFilter.sharedMesh = combinedMesh;
newRenderer.sharedMaterial = material;
// 禁用或销毁原始对象
for (int i = 0; i < meshesInThisGroup; i++)
{
meshFilters[groupIndex + i].gameObject.SetActive(false);
}
}
}
private void OrganizeObjectsByMaterial()
{
Renderer[] renderers = FindObjectsOfType<Renderer>();
materialGroups.Clear();
// 分组收集共享相同材质的渲染器
foreach (Renderer renderer in renderers)
{
if (renderer.sharedMaterial != null)
{
Material material = renderer.sharedMaterial;
if (!materialGroups.ContainsKey(material))
{
materialGroups[material] = new List<Renderer>();
}
materialGroups[material].Add(renderer);
}
}
// 输出分析结果
foreach (var group in materialGroups)
{
if (group.Value.Count > 10)
{
Debug.Log($"发现大量共享材质: {group.Key.name} - 使用对象数: {group.Value.Count}");
}
}
// 寻找材质不必要的重复使用
FindDuplicateMaterialUsage();
}
private void FindDuplicateMaterialUsage()
{
Dictionary<string, List<Material>> materialsByName = new Dictionary<string, List<Material>>();
// 按名称分组材质
foreach (var material in materialGroups.Keys)
{
string name = material.name;
// 删除材质名称中的实例标记
name = name.Replace("(Instance)", "").Trim();
if (!materialsByName.ContainsKey(name))
{
materialsByName[name] = new List<Material>();
}
materialsByName[name].Add(material);
}
// 查找同名但不同实例的材质
foreach (var nameGroup in materialsByName)
{
if (nameGroup.Value.Count > 1)
{
Debug.LogWarning($"发现可能的重复材质: {nameGroup.Key} - {nameGroup.Value.Count}个不同实例");
// 输出使用这些材质的对象
foreach (Material material in nameGroup.Value)
{
int objectCount = materialGroups[material].Count;
Debug.Log($" - 材质实例 {material.GetInstanceID()} 被 {objectCount} 个对象使用");
}
}
}
}
private void OnDestroy()
{
// 取消注册事件
if (monitorBatchCount)
{
Camera.onPreRender -= OnCameraPreRender;
CancelInvoke("LogBatchStatistics");
}
}
#if UNITY_EDITOR
// 编辑器工具方法
[MenuItem("XR工具/优化/自动设置静态批处理标记")]
private static void SetStaticBatchingFlags()
{
Renderer[] renderers = FindObjectsOfType<Renderer>();
int count = 0;
foreach (Renderer renderer in renderers)
{
// 如果对象有MeshFilter且没有移动脚本
MeshFilter meshFilter = renderer.GetComponent<MeshFilter>();
Rigidbody rb = renderer.GetComponent<Rigidbody>();
if (meshFilter != null && rb == null &&
!renderer.GetComponent<Animation>() &&
!renderer.GetComponent<MonoBehaviour>())
{
// 设置为静态批处理
StaticEditorFlags flags = GameObjectUtility.GetStaticEditorFlags(renderer.gameObject);
flags |= StaticEditorFlags.BatchingStatic;
GameObjectUtility.SetStaticEditorFlags(renderer.gameObject, flags);
count++;
}
}
Debug.Log($"自动为 {count} 个对象设置了静态批处理标记");
}
[MenuItem("XR工具/优化/合并相同材质")]
private static void MergeSameMaterial()
{
// 查找所有使用相同名称材质的渲染器
Dictionary<string, List<Renderer>> renderersByMaterialName = new Dictionary<string, List<Renderer>>();
Dictionary<string, Material> firstMaterialByName = new Dictionary<string, Material>();
Renderer[] renderers = FindObjectsOfType<Renderer>();
foreach (Renderer renderer in renderers)
{
foreach (Material material in renderer.sharedMaterials)
{
if (material != null)
{
string name = material.name.Replace("(Instance)", "").Trim();
if (!renderersByMaterialName.ContainsKey(name))
{
renderersByMaterialName[name] = new List<Renderer>();
firstMaterialByName[name] = material;
}
renderersByMaterialName[name].Add(renderer);
}
}
}
// 统一每个组的材质
int replacedCount = 0;
foreach (var group in renderersByMaterialName)
{
if (group.Value.Count > 1)
{
Material firstMaterial = firstMaterialByName[group.Key];
foreach (Renderer renderer in group.Value)
{
Material[] materials = renderer.sharedMaterials;
bool materialReplaced = false;
for (int i = 0; i < materials.Length; i++)
{
if (materials[i] != null)
{
string name = materials[i].name.Replace("(Instance)", "").Trim();
if (name == group.Key && materials[i] != firstMaterial)
{
materials[i] = firstMaterial;
materialReplaced = true;
replacedCount++;
}
}
}
if (materialReplaced)
{
renderer.sharedMaterials = materials;
}
}
}
}
Debug.Log($"合并了 {replacedCount} 个材质引用");
}
[MenuItem("XR工具/优化/启用所有材质GPU实例化")]
private static void EnableAllMaterialsGPUInstancing()
{
Material[] materials = Resources.FindObjectsOfTypeAll<Material>();
int count = 0;
foreach (Material material in materials)
{
if (!material.enableInstancing && material.shader.supportsInstancing)
{
material.enableInstancing = true;
count++;
}
}
Debug.Log($"为 {count} 个材质启用了GPU实例化");
}
#endif
}
多平台共享着色器模板
// XRPlatformShader.shader - 多平台优化着色器
Shader "XR/PlatformOptimizedShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color ("Color", Color) = (1,1,1,1)
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
[Toggle(ENABLE_NORMAL_MAP)] _EnableNormalMap ("Enable Normal Map", Float) = 0
[NoScaleOffset] _BumpMap ("Normal Map", 2D) = "bump" {}
[Toggle(ENABLE_EMISSION)] _EnableEmission ("Enable Emission", Float) = 0
[HDR] _EmissionColor ("Emission Color", Color) = (0,0,0,1)
[NoScaleOffset] _EmissionMap ("Emission Map", 2D) = "white" {}
}
// 多个变体以支持不同平台和优化级别
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry" }
LOD 300
// ======== 高品质渲染路径(Quest 2/Pico 4高级) ========
CGPROGRAM
#pragma target 3.5
#pragma surface surf Standard fullforwardshadows
#pragma shader_feature_local ENABLE_NORMAL_MAP
#pragma shader_feature_local ENABLE_EMISSION
#pragma multi_compile_instancing
sampler2D _MainTex;
sampler2D _BumpMap;
sampler2D _EmissionMap;
struct Input
{
float2 uv_MainTex;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
half _Glossiness;
half _Metallic;
fixed4 _Color;
half4 _EmissionColor;
UNITY_INSTANCING_BUFFER_START(Props)
// 如果需要实例化的属性,放在这里
UNITY_INSTANCING_BUFFER_END(Props)
void surf (Input IN, inout SurfaceOutputStandard o)
{
UNITY_SETUP_INSTANCE_ID(IN);
// 基础材质属性
fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
// 法线贴图
#ifdef ENABLE_NORMAL_MAP
o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_MainTex));
#endif
// 自发光
#ifdef ENABLE_EMISSION
o.Emission = tex2D(_EmissionMap, IN.uv_MainTex).rgb * _EmissionColor.rgb;
#endif
}
ENDCG
}
// 中级品质变体
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry" }
LOD 200
CGPROGRAM
#pragma target 3.0
#pragma surface surf StandardSpecular noshadow
#pragma shader_feature_local ENABLE_EMISSION
#pragma multi_compile_instancing
sampler2D _MainTex;
sampler2D _EmissionMap;
struct Input
{
float2 uv_MainTex;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
half _Glossiness;
half _Metallic;
fixed4 _Color;
half4 _EmissionColor;
void surf (Input IN, inout SurfaceOutputStandardSpecular o)
{
UNITY_SETUP_INSTANCE_ID(IN);
// 简化的材质属性
fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
o.Smoothness = _Glossiness;
o.Specular = _Metallic * 0.5; // 简化金属度计算
o.Alpha = c.a;
// 自发光
#ifdef ENABLE_EMISSION
o.Emission = tex2D(_EmissionMap, IN.uv_MainTex).rgb * _EmissionColor.rgb;
#endif
}
ENDCG
}
// 低级品质变体
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry" }
LOD 100
CGPROGRAM
#pragma target 2.5
#pragma surface surf Lambert noambient noshadow
#pragma multi_compile_instancing
sampler2D _MainTex;
struct Input
{
float2 uv_MainTex;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
fixed4 _Color;
void surf (Input IN, inout SurfaceOutput o)
{
UNITY_SETUP_INSTANCE_ID(IN);
// 极简材质属性
fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
o.Alpha = c.a;
}
ENDCG
}
Fallback "Diffuse"
CustomEditor "XRShaderGUI"
}
自定义XR着色器GUI编辑器
#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;
// XRShaderGUI.cs - 自定义XR着色器GUI
public class XRShaderGUI : ShaderGUI
{
// 用于检测平台的属性
private MaterialProperty emissionEnabledProp = null;
private MaterialProperty normalMapEnabledProp = null;
private MaterialProperty mainTexProp = null;
private MaterialProperty colorProp = null;
private MaterialProperty glossinessProp = null;
private MaterialProperty metallicProp = null;
private MaterialProperty bumpMapProp = null;
private MaterialProperty emissionColorProp = null;
private MaterialProperty emissionMapProp = null;
// UI状态
private bool showAdvancedOptions = false;
private bool showPlatformSettings = false;
private bool showPerformanceInfo = false;
// 当前平台推荐设置
private XRPlatformType currentPlatform = XRPlatformType.Quest;
public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties)
{
// 获取目标材质
Material material = materialEditor.target as Material;
// 获取属性
FindProperties(properties);
// 绘制标题
EditorGUILayout.Space();
EditorGUILayout.LabelField("XR平台优化着色器", EditorStyles.boldLabel);
DrawDivider();
// 绘制平台优化区域
DrawPlatformSettingsSection(material);
// 绘制基础属性
EditorGUILayout.Space();
EditorGUILayout.LabelField("基础属性", EditorStyles.boldLabel);
materialEditor.TexturePropertySingleLine(new GUIContent("主纹理"), mainTexProp, colorProp);
materialEditor.TextureScaleOffsetProperty(mainTexProp);
// PBR属性
materialEditor.RangeProperty(glossinessProp, "平滑度");
materialEditor.RangeProperty(metallicProp, "金属度");
// 法线贴图
bool useNormalMap = normalMapEnabledProp.floatValue > 0.5f;
useNormalMap = EditorGUILayout.Toggle(new GUIContent("使用法线贴图"), useNormalMap);
normalMapEnabledProp.floatValue = useNormalMap ? 1.0f : 0.0f;
if (useNormalMap)
{
EditorGUI.indentLevel++;
materialEditor.TexturePropertySingleLine(new GUIContent("法线贴图"), bumpMapProp);
EditorGUI.indentLevel--;
}
// 自发光
bool useEmission = emissionEnabledProp.floatValue > 0.5f;
useEmission = EditorGUILayout.Toggle(new GUIContent("使用自发光"), useEmission);
emissionEnabledProp.floatValue = useEmission ? 1.0f : 0.0f;
if (useEmission)
{
EditorGUI.indentLevel++;
materialEditor.TexturePropertyWithHDRColor(
new GUIContent("自发光贴图"), emissionMapProp, emissionColorProp, false);
EditorGUI.indentLevel--;
}
// 高级属性
EditorGUILayout.Space();
showAdvancedOptions = EditorGUILayout.Foldout(showAdvancedOptions, "高级设置", true);
if (showAdvancedOptions)
{
EditorGUI.indentLevel++;
// 绘制高级材质设置
DrawAdvancedSettings(materialEditor, material);
EditorGUI.indentLevel--;
}
// 性能信息
DrawPerformanceInfo(material);
// 应用更改
materialEditor.RenderQueueField();
materialEditor.EnableInstancingField();
materialEditor.DoubleSidedGIField();
}
private void FindProperties(MaterialProperty[] properties)
{
mainTexProp = FindProperty("_MainTex", properties);
colorProp = FindProperty("_Color", properties);
glossinessProp = FindProperty("_Glossiness", properties);
metallicProp = FindProperty("_Metallic", properties);
normalMapEnabledProp = FindProperty("_EnableNormalMap", properties);
bumpMapProp = FindProperty("_BumpMap", properties);
emissionEnabledProp = FindProperty("_EnableEmission", properties);
emissionColorProp = FindProperty("_EmissionColor", properties);
emissionMapProp = FindProperty("_EmissionMap", properties);
}
private void DrawDivider()
{
EditorGUILayout.Space();
Rect rect = EditorGUILayout.GetControlRect(false, 1);
rect.height = 1;
EditorGUI.DrawRect(rect, new Color(0.5f, 0.5f, 0.5f, 0.5f));
EditorGUILayout.Space();
}
private void DrawPlatformSettingsSection(Material material)
{
showPlatformSettings = EditorGUILayout.Foldout(showPlatformSettings, "平台优化设置", true);
if (showPlatformSettings)
{
EditorGUI.indentLevel++;
// 平台选择
currentPlatform = (XRPlatformType)EditorGUILayout.EnumPopup("目标XR平台", currentPlatform);
// 应用推荐设置按钮
EditorGUILayout.Space();
if (GUILayout.Button("应用平台推荐设置"))
{
ApplyPlatformRecommendedSettings(material);
}
EditorGUI.indentLevel--;
}
}
private void ApplyPlatformRecommendedSettings(Material material)
{
// 根据不同平台应用优化设置
switch (currentPlatform)
{
case XRPlatformType.Quest:
// Quest优化设置
material.SetFloat("_EnableNormalMap", IsHighEndQuest() ? 1.0f : 0.0f);
material.SetFloat("_EnableEmission", IsHighEndQuest() ? 1.0f : 0.0f);
material.SetFloat("_Glossiness", IsHighEndQuest() ? 0.5f : 0.3f);
// 选择适当的LOD级别子着色器
material.shader.maximumLOD = IsHighEndQuest() ? 300 : 200;
EditorUtility.DisplayDialog("Quest优化设置",
"已应用Quest推荐设置。" + (IsHighEndQuest() ?
"已检测为Quest 2/Pro,启用了完整特性。" :
"已应用性能优化设置。"), "确定");
break;
case XRPlatformType.Pico:
// Pico优化设置
material.SetFloat("_EnableNormalMap", 1.0f);
material.SetFloat("_EnableEmission", 1.0f);
material.SetFloat("_Glossiness", 0.5f);
// 选择适当的LOD级别子着色器
material.shader.maximumLOD = 300;
EditorUtility.DisplayDialog("Pico优化设置",
"已应用Pico推荐设置。已启用法线贴图和自发光功能。", "确定");
break;
case XRPlatformType.HoloLens:
// HoloLens优化设置
material.SetFloat("_EnableNormalMap", 0.0f);
material.SetFloat("_EnableEmission", 1.0f);
material.SetFloat("_Glossiness", 0.3f);
// 选择适当的LOD级别子着色器
material.shader.maximumLOD = 200;
EditorUtility.DisplayDialog("HoloLens优化设置",
"已应用HoloLens推荐设置。禁用法线贴图以提高性能,保留自发光以增强AR可见性。", "确定");
break;
}
// 确保GPU实例化已启用
material.enableInstancing = true;
}
private bool IsHighEndQuest()
{
// 在实际产品中,这可以通过查询编辑器设置或项目配置来确定
// 这里简单返回true,假设是Quest 2或更高版本
return true;
}
private void DrawAdvancedSettings(MaterialEditor materialEditor, Material material)
{
// 渲染模式
EditorGUILayout.LabelField("渲染模式", EditorStyles.boldLabel);
// 创建一个选项列表
string[] blendModes = { "不透明", "透明", "镂空", "添加" };
int currentBlendMode = material.GetInt("_Mode");
currentBlendMode = EditorGUILayout.Popup("混合模式", currentBlendMode, blendModes);
// 如果模式发生变化,更新材质
if (material.GetInt("_Mode") != currentBlendMode)
{
material.SetInt("_Mode", currentBlendMode);
UpdateBlendMode(material, currentBlendMode);
}
// 其他高级设置...
bool receiveShadows = material.GetInt("_ReceiveShadows") > 0;
receiveShadows = EditorGUILayout.Toggle("接收阴影", receiveShadows);
material.SetInt("_ReceiveShadows", receiveShadows ? 1 : 0);
}
private void UpdateBlendMode(Material material, int blendMode)
{
switch (blendMode)
{
case 0: // 不透明
material.SetOverrideTag("RenderType", "Opaque");
material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One);
material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.Zero);
material.SetInt("_ZWrite", 1);
material.DisableKeyword("_ALPHATEST_ON");
material.DisableKeyword("_ALPHABLEND_ON");
material.DisableKeyword("_ALPHAPREMULTIPLY_ON");
material.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Geometry;
break;
case 1: // 透明
material.SetOverrideTag("RenderType", "Transparent");
material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
material.SetInt("_ZWrite", 0);
material.DisableKeyword("_ALPHATEST_ON");
material.EnableKeyword("_ALPHABLEND_ON");
material.DisableKeyword("_ALPHAPREMULTIPLY_ON");
material.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent;
break;
case 2: // 镂空
material.SetOverrideTag("RenderType", "TransparentCutout");
material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One);
material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.Zero);
material.SetInt("_ZWrite", 1);
material.EnableKeyword("_ALPHATEST_ON");
material.DisableKeyword("_ALPHABLEND_ON");
material.DisableKeyword("_ALPHAPREMULTIPLY_ON");
material.renderQueue = (int)UnityEngine.Rendering.RenderQueue.AlphaTest;
break;
case 3: // 添加
material.SetOverrideTag("RenderType", "Transparent");
material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.One);
material.SetInt("_ZWrite", 0);
material.DisableKeyword("_ALPHATEST_ON");
material.DisableKeyword("_ALPHABLEND_ON");
material.DisableKeyword("_ALPHAPREMULTIPLY_ON");
material.EnableKeyword("_ADDITIVE_ON");
material.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent;
break;
}
}
private void DrawPerformanceInfo(Material material)
{
EditorGUILayout.Space();
showPerformanceInfo = EditorGUILayout.Foldout(showPerformanceInfo, "性能分析", true);
if (showPerformanceInfo)
{
EditorGUI.indentLevel++;
GUIStyle warningStyle = new GUIStyle(EditorStyles.label);
GUIStyle goodStyle = new GUIStyle(EditorStyles.label);
warningStyle.normal.textColor = Color.yellow;
goodStyle.normal.textColor = Color.green;
// 计算材质复杂度(简化版)
int complexity = 0;
// 检查法线贴图
if (material.GetFloat("_EnableNormalMap") > 0.5f)
{
complexity += 2;
EditorGUILayout.LabelField("法线贴图", "增加2复杂度", warningStyle);
}
else
{
EditorGUILayout.LabelField("法线贴图", "已禁用", goodStyle);
}
// 检查自发光
if (material.GetFloat("_EnableEmission") > 0.5f)
{
complexity += 1;
EditorGUILayout.LabelField("自发光", "增加1复杂度", warningStyle);
}
else
{
EditorGUILayout.LabelField("自发光", "已禁用", goodStyle);
}
// 检查混合模式
int blendMode = material.GetInt("_Mode");
if (blendMode > 0)
{
complexity += 2;
EditorGUILayout.LabelField("透明/混合", "增加2复杂度", warningStyle);
}
else
{
EditorGUILayout.LabelField("混合模式", "不透明 (高效)", goodStyle);
}
// 检查GPU实例化
if (material.enableInstancing)
{
EditorGUILayout.LabelField("GPU实例化", "已启用", goodStyle);
}
else
{
EditorGUILayout.LabelField("GPU实例化", "未启用", warningStyle);
}
// 总结
EditorGUILayout.Space();
GUIStyle complexityStyle = new GUIStyle(EditorStyles.boldLabel);
if (complexity <= 2)
{
complexityStyle.normal.textColor = Color.green;
EditorGUILayout.LabelField($"总体复杂度: {complexity} (高效)", complexityStyle);
}
else if (complexity <= 4)
{
complexityStyle.normal.textColor = Color.yellow;
EditorGUILayout.LabelField($"总体复杂度: {complexity} (中等)", complexityStyle);
}
else
{
complexityStyle.normal.textColor = Color.red;
EditorGUILayout.LabelField($"总体复杂度: {complexity} (较高)", complexityStyle);
}
// 平台特定提示
EditorGUILayout.Space();
EditorGUILayout.LabelField($"目标平台: {currentPlatform}", EditorStyles.boldLabel);
switch (currentPlatform)
{
case XRPlatformType.Quest:
if (complexity > 3)
{
EditorGUILayout.HelpBox("当前材质复杂度对Quest来说较高。考虑禁用一些特性以提高性能。", MessageType.Warning);
}
break;
case XRPlatformType.HoloLens:
if (complexity > 2)
{
EditorGUILayout.HelpBox("HoloLens对材质复杂度非常敏感。建议尽可能简化。", MessageType.Warning);
}
break;
}
EditorGUI.indentLevel--;
}
}
}
#endif
七、XR平台常见问题解决方案
控制器抖动修复工具
// ControllerStabilizer.cs - 控制器防抖动系统
using UnityEngine;
using System.Collections.Generic;
public class ControllerStabilizer : MonoBehaviour
{
[Header("平滑设置")]
[Range(0, 0.5f)]
[SerializeField] private float positionSmoothing = 0.15f;
[Range(0, 0.5f)]
[SerializeField] private float rotationSmoothing = 0.12f;
[SerializeField] private bool useAdaptiveSmoothing = true;
[SerializeField] private float velocityThreshold = 0.5f;
[SerializeField] private float angularVelocityThreshold = 30f;
[Header("抖动检测")]
[SerializeField] private float jitterThreshold = 0.01f;
[SerializeField] private float jitterTimeThreshold = 0.05f;
[SerializeField] private int stabilizerUpdateRate = 90;
// 内部状态变量
private Vector3 targetPosition;
private Quaternion targetRotation;
private Vector3 smoothedPosition;
private Quaternion smoothedRotation;
// 速度计算
private Vector3 lastPosition;
private Quaternion lastRotation;
private Vector3 velocity;
private Vector3 angularVelocity;
// 抖动检测
private Queue<Vector3> positionSamples = new Queue<Vector3>();
private Queue<Quaternion> rotationSamples = new Queue<Quaternion>();
private float timeSinceLastJitter = 0f;
private bool jitterDetected = false;
// 原始转换引用
private Transform originalTransform;
private void Awake()
{
// 创建一个子对象用于平滑移动
GameObject smoothedObject = new GameObject(gameObject.name + "_Smoothed");
smoothedObject.transform.SetParent(transform.parent);
smoothedObject.transform.localPosition = transform.localPosition;
smoothedObject.transform.localRotation = transform.localRotation;
// 将所有子对象移动到平滑对象下
for (int i = transform.childCount - 1; i >= 0; i--)
{
Transform child = transform.GetChild(i);
child.SetParent(smoothedObject.transform, true);
}
// 保存原始Transform引用
originalTransform = transform;
// 初始化位置和旋转
targetPosition = originalTransform.position;
targetRotation = originalTransform.rotation;
smoothedPosition = targetPosition;
smoothedRotation = targetRotation;
lastPosition = targetPosition;
lastRotation = targetRotation;
// 修改当前游戏对象以添加一个空占位符
transform.SetParent(smoothedObject.transform);
}
private void Start()
{
// 修改更新频率
if (stabilizerUpdateRate > 0)
{
Application.targetFrameRate = stabilizerUpdateRate;
}
}
private void Update()
{
// 获取当前位置和旋转(来自控制器/跟踪源)
targetPosition = originalTransform.position;
targetRotation = originalTransform.rotation;
// 计算速度
velocity = (targetPosition - lastPosition) / Time.deltaTime;
// 计算角速度(简化)
Quaternion deltaRotation = targetRotation * Quaternion.Inverse(lastRotation);
float angle;
Vector3 axis;
deltaRotation.ToAngleAxis(out angle, out axis);
if (angle > 180f) angle -= 360f;
angularVelocity = axis * (angle * Mathf.Deg2Rad / Time.deltaTime);
// 更新位置采样队列
positionSamples.Enqueue(targetPosition);
if (positionSamples.Count > 5) // 保持5个样本
positionSamples.Dequeue();
rotationSamples.Enqueue(targetRotation);
if (rotationSamples.Count > 5)
rotationSamples.Dequeue();
// 检测抖动
DetectJitter();
// 应用平滑
ApplySmoothing();
// 更新历史状态
lastPosition = targetPosition;
lastRotation = targetRotation;
}
private void DetectJitter()
{
if (positionSamples.Count < 3)
return;
Vector3[] positions = positionSamples.ToArray();
// 检查连续3个点的方向变化
for (int i = 2; i < positions.Length; i++)
{
Vector3 dir1 = (positions[i-1] - positions[i-2]).normalized;
Vector3 dir2 = (positions[i] - positions[i-1]).normalized;
// 如果方向突然反转,可能是抖动
if (Vector3.Dot(dir1, dir2) < -0.7f &&
Vector3.Distance(positions[i], positions[i-1]) < jitterThreshold)
{
jitterDetected = true;
timeSinceLastJitter = 0f;
return;
}
}
// 更新抖动计时器
timeSinceLastJitter += Time.deltaTime;
if (timeSinceLastJitter > jitterTimeThreshold)
{
jitterDetected = false;
}
}
private void ApplySmoothing()
{
float posSmooth = positionSmoothing;
float rotSmooth = rotationSmoothing;
// 根据速度自适应调整平滑度
if (useAdaptiveSmoothing)
{
float velocityFactor = Mathf.Clamp01(velocity.magnitude / velocityThreshold);
float angularFactor = Mathf.Clamp01(angularVelocity.magnitude / angularVelocityThreshold);
// 运动越快,平滑度越低
posSmooth = Mathf.Lerp(positionSmoothing, 0.01f, velocityFactor);
rotSmooth = Mathf.Lerp(rotationSmoothing, 0.01f, angularFactor);
}
// 如果检测到抖动,增加平滑度
if (jitterDetected)
{
posSmooth = Mathf.Max(posSmooth, 0.3f);
rotSmooth = Mathf.Max(rotSmooth, 0.3f);
}
// 平滑位置
smoothedPosition = Vector3.Lerp(smoothedPosition, targetPosition, 1 - posSmooth);
// 平滑旋转
smoothedRotation = Quaternion.Slerp(smoothedRotation, targetRotation, 1 - rotSmooth);
// 应用平滑的位置和旋转
transform.parent.position = smoothedPosition;
transform.parent.rotation = smoothedRotation;
}
}
空间映射修复与增强
// SpatialMappingEnhancer.cs - 空间映射修复与增强
using UnityEngine;
using System.Collections.Generic;
public class SpatialMappingEnhancer : MonoBehaviour
{
[Header("平台设置")]
[SerializeField] private XRPlatformType targetPlatform;
[Header("网格优化设置")]
[SerializeField] private bool optimizeMeshes = true;
[SerializeField] private bool simplifyMeshes = true;
[Range(0.01f, 0.5f)]
[SerializeField] private float simplificationDistance = 0.05f;
[SerializeField] private bool fillHoles = true;
[Range(0.05f, 0.5f)]
[SerializeField] private float holeDetectionSize = 0.2f;
[Header("平面检测设置")]
[SerializeField] private bool detectPlanes = true;
[Range(0.5f, 10f)]
[SerializeField] private float minPlaneSize = 0.5f;
[SerializeField] private LayerMask spatialMappingLayer;
[SerializeField] private GameObject planeMarkerPrefab;
[Header("可视化")]
[SerializeField] private bool visualizeOptimizedMeshes = true;
[SerializeField] private Material optimizedMeshMaterial;
[SerializeField] private bool visualizePlanes = true;
// 内部状态
private List<MeshFilter> originalMeshFilters = new List<MeshFilter>();
private List<MeshFilter> optimizedMeshFilters = new List<MeshFilter>();
private List<GameObject> detectedPlanes = new List<GameObject>();
private bool hasInitialized = false;
private void Start()
{
// 延迟初始化,等待空间映射完成
Invoke("Initialize", 2.0f);
}
private void Initialize()
{
Debug.Log("初始化空间映射增强器");
// 收集所有空间映射网格
CollectSpatialMeshes();
if (optimizeMeshes)
{
OptimizeSpatialMeshes();
}
if (detectPlanes)
{
DetectPlanesInEnvironment();
}
hasInitialized = true;
}
private void CollectSpatialMeshes()
{
// 查找具有空间映射层的网格
MeshFilter[] meshFilters = FindObjectsOfType<MeshFilter>();
foreach (MeshFilter filter in meshFilters)
{
if (((1 << filter.gameObject.layer) & spatialMappingLayer) != 0)
{
originalMeshFilters.Add(filter);
Debug.Log($"找到空间映射网格: {filter.gameObject.name}");
}
}
Debug.Log($"总共找到 {originalMeshFilters.Count} 个空间映射网格");
}
private void OptimizeSpatialMeshes()
{
foreach (MeshFilter originalFilter in originalMeshFilters)
{
// 创建优化的网格对象
GameObject optimizedObj = new GameObject(originalFilter.gameObject.name + "_Optimized");
optimizedObj.transform.SetParent(transform);
optimizedObj.transform.position = originalFilter.transform.position;
optimizedObj.transform.rotation = originalFilter.transform.rotation;
optimizedObj.transform.localScale = originalFilter.transform.localScale;
// 添加组件
MeshFilter optimizedFilter = optimizedObj.AddComponent<MeshFilter>();
MeshRenderer renderer = optimizedObj.AddComponent<MeshRenderer>();
MeshCollider collider = optimizedObj.AddComponent<MeshCollider>();
// 复制和优化网格
Mesh optimizedMesh = OptimizeMesh(originalFilter.sharedMesh);
// 应用优化的网格
optimizedFilter.sharedMesh = optimizedMesh;
collider.sharedMesh = optimizedMesh;
// 设置材质
if (visualizeOptimizedMeshes && optimizedMeshMaterial != null)
{
renderer.sharedMaterial = optimizedMeshMaterial;
}
else
{
// 尝试从原始对象复制材质
MeshRenderer originalRenderer = originalFilter.GetComponent<MeshRenderer>();
if (originalRenderer != null)
{
renderer.sharedMaterial = originalRenderer.sharedMaterial;
}
}
// 将原始网格禁用
originalFilter.gameObject.SetActive(false);
// 添加到优化列表
optimizedMeshFilters.Add(optimizedFilter);
}
Debug.Log($"已优化 {optimizedMeshFilters.Count} 个空间映射网格");
}
private Mesh OptimizeMesh(Mesh originalMesh)
{
if (originalMesh == null)
return null;
Mesh optimizedMesh = new Mesh();
// 复制原始网格数据
Vector3[] vertices = originalMesh.vertices;
int[] triangles = originalMesh.triangles;
Vector3[] normals = originalMesh.normals;
Vector2[] uvs = originalMesh.uv;
if (simplifyMeshes)
{
// 简化网格
// 注意:实际实现会使用更复杂的算法
Vector3[] simplifiedVertices;
int[] simplifiedTriangles;
Vector3[] simplifiedNormals;
SimplifyMesh(vertices, triangles, normals,
out simplifiedVertices, out simplifiedTriangles, out simplifiedNormals);
vertices = simplifiedVertices;
triangles = simplifiedTriangles;
normals = simplifiedNormals;
// 重新生成UVs
uvs = GenerateSimpleUVs(vertices);
}
if (fillHoles)
{
// 检测和填充孔洞
FillMeshHoles(ref vertices, ref triangles, ref normals, ref uvs);
}
// 分配处理后的数据
optimizedMesh.vertices = vertices;
optimizedMesh.triangles = triangles;
optimizedMesh.normals = normals;
optimizedMesh.uv = uvs;
// 重新计算边界
optimizedMesh.RecalculateBounds();
return optimizedMesh;
}
private void SimplifyMesh(Vector3[] vertices, int[] triangles, Vector3[] normals,
out Vector3[] simplifiedVertices, out int[] simplifiedTriangles, out Vector3[] simplifiedNormals)
{
// 这只是一个简化的演示版本
// 实际的网格简化算法会更加复杂
// 为了演示,将保留边缘顶点,合并距离较近的顶点
Dictionary<Vector3, int> uniqueVertices = new Dictionary<Vector3, int>();
List<Vector3> newVertices = new List<Vector3>();
List<int> newTriangles = new List<int>();
List<Vector3> newNormals = new List<Vector3>();
// 映射表存储旧顶点索引到新顶点索引的映射
int[] vertexMapping = new int[vertices.Length];
// 处理每个顶点
for (int i = 0; i < vertices.Length; i++)
{
Vector3 roundedPos = RoundVector(vertices[i], simplificationDistance);
if (uniqueVertices.ContainsKey(roundedPos))
{
// 如果已存在相似位置的顶点,使用该顶点
vertexMapping[i] = uniqueVertices[roundedPos];
}
else
{
// 否则添加新顶点
int newIndex = newVertices.Count;
uniqueVertices[roundedPos] = newIndex;
vertexMapping[i] = newIndex;
newVertices.Add(vertices[i]);
newNormals.Add(normals[i]);
}
}
// 重建三角形
for (int i = 0; i < triangles.Length; i += 3)
{
int a = vertexMapping[triangles[i]];
int b = vertexMapping[triangles[i + 1]];
int c = vertexMapping[triangles[i + 2]];
// 跳过退化三角形
if (a != b && b != c && a != c)
{
newTriangles.Add(a);
newTriangles.Add(b);
newTriangles.Add(c);
}
}
simplifiedVertices = newVertices.ToArray();
simplifiedTriangles = newTriangles.ToArray();
simplifiedNormals = newNormals.ToArray();
Debug.Log($"简化网格: 从 {vertices.Length} 顶点减少到 {simplifiedVertices.Length} 顶点");
}
private Vector3 RoundVector(Vector3 vec, float precision)
{
// 将向量舍入到指定精度
return new Vector3(
Mathf.Round(vec.x / precision) * precision,
Mathf.Round(vec.y / precision) * precision,
Mathf.Round(vec.z / precision) * precision
);
}
private Vector2[] GenerateSimpleUVs(Vector3[] vertices)
{
// 简单的UV生成
Vector2[] uvs = new Vector2[vertices.Length];
for (int i = 0; i < vertices.Length; i++)
{
// 使用XZ平面投影
uvs[i] = new Vector2(vertices[i].x, vertices[i].z);
}
return uvs;
}
private void FillMeshHoles(ref Vector3[] vertices, ref int[] triangles, ref Vector3[] normals, ref Vector2[] uvs)
{
// 这是一个复杂的过程,这里只提供一个简化版本
// 实际实现需要边界检测算法
// 检测边界边
Dictionary<Vector2Int, int> edgeCount = new Dictionary<Vector2Int, int>();
for (int i = 0; i < triangles.Length; i += 3)
{
int a = triangles[i];
int b = triangles[i + 1];
int c = triangles[i + 2];
CountEdge(edgeCount, a, b);
CountEdge(edgeCount, b, c);
CountEdge(edgeCount, c, a);
}
// 收集只出现一次的边(边界边)
List<Vector2Int> boundaryEdges = new List<Vector2Int>();
foreach (var edge in edgeCount)
{
if (edge.Value == 1)
{
boundaryEdges.Add(edge.Key);
}
}
// 如果边界边数量过多,跳过填充
if (boundaryEdges.Count > 100)
{
Debug.Log("边界太复杂,跳过孔洞填充");
return;
}
// 寻找连续的边界回路
List<List<int>> boundaryLoops = FindBoundaryLoops(boundaryEdges);
// 填充每个孔洞
List<Vector3> newVertices = new List<Vector3>(vertices);
List<int> newTriangles = new List<int>(triangles);
List<Vector3> newNormals = new List<Vector3>(normals);
List<Vector2> newUVs = new List<Vector2>(uvs);
foreach (var loop in boundaryLoops)
{
if (loop.Count >= 3 && IsHoleSizeValid(vertices, loop))
{
FillHole(loop, newVertices, newTriangles, newNormals, newUVs);
}
}
vertices = newVertices.ToArray();
triangles = newTriangles.ToArray();
normals = newNormals.ToArray();
uvs = newUVs.ToArray();
}
private void CountEdge(Dictionary<Vector2Int, int> edgeCount, int v1, int v2)
{
// 确保边的顶点顺序一致
Vector2Int edge = v1 < v2 ? new Vector2Int(v1, v2) : new Vector2Int(v2, v1);
if (edgeCount.ContainsKey(edge))
{
edgeCount[edge]++;
}
else
{
edgeCount[edge] = 1;
}
}
private List<List<int>> FindBoundaryLoops(List<Vector2Int> boundaryEdges)
{
// 寻找连续的边界回路
List<List<int>> loops = new List<List<int>>();
// 建立顶点连接关系
Dictionary<int, List<int>> vertexConnections = new Dictionary<int, List<int>>();
foreach (var edge in boundaryEdges)
{
if (!vertexConnections.ContainsKey(edge.x))
vertexConnections[edge.x] = new List<int>();
if (!vertexConnections.ContainsKey(edge.y))
vertexConnections[edge.y] = new List<int>();
vertexConnections[edge.x].Add(edge.y);
vertexConnections[edge.y].Add(edge.x);
}
// 寻找回路
HashSet<int> processedVertices = new HashSet<int>();
foreach (var vertex in vertexConnections.Keys)
{
if (!processedVertices.Contains(vertex))
{
List<int> loop = new List<int>();
int current = vertex;
while (true)
{
loop.Add(current);
processedVertices.Add(current);
bool foundNext = false;
foreach (int next in vertexConnections[current])
{
if (!processedVertices.Contains(next))
{
current = next;
foundNext = true;
break;
}
}
if (!foundNext || loop.Count > 1000) // 安全检查
break;
}
if (loop.Count >= 3)
{
loops.Add(loop);
}
}
}
return loops;
}
private bool IsHoleSizeValid(Vector3[] vertices, List<int> loop)
{
// 检查孔洞大小是否在合理范围内
if (loop.Count < 3 || loop.Count > 50) // 过大或过小的孔洞跳过
return false;
// 计算孔洞周长
float perimeter = 0;
for (int i = 0; i < loop.Count; i++)
{
int j = (i + 1) % loop.Count;
perimeter += Vector3.Distance(vertices[loop[i]], vertices[loop[j]]);
}
// 如果周长太小,可能是小裂缝而非需要填充的孔洞
if (perimeter < holeDetectionSize)
return false;
return true;
}
private void FillHole(List<int> boundaryLoop, List<Vector3> vertices, List<int> triangles,
List<Vector3> normals, List<Vector2> uvs)
{
// 这里使用简单的三角形扇形填充
// 实际应用可能需要更复杂的算法,如耳切算法
// 计算孔洞中心点
Vector3 center = Vector3.zero;
Vector3 avgNormal = Vector3.zero;
Vector2 avgUV = Vector2.zero;
foreach (int idx in boundaryLoop)
{
center += vertices[idx];
avgNormal += normals[idx];
avgUV += uvs[idx];
}
center /= boundaryLoop.Count;
avgNormal = (avgNormal / boundaryLoop.Count).normalized;
avgUV /= boundaryLoop.Count;
// 添加中心点
int centerIndex = vertices.Count;
vertices.Add(center);
normals.Add(avgNormal);
uvs.Add(avgUV);
// 创建三角形
for (int i = 0; i < boundaryLoop.Count; i++)
{
int next = (i + 1) % boundaryLoop.Count;
triangles.Add(centerIndex);
triangles.Add(boundaryLoop[i]);
triangles.Add(boundaryLoop[next]);
}
}
private void DetectPlanesInEnvironment()
{
// 检测环境中的平面
foreach (MeshFilter filter in originalMeshFilters)
{
Mesh mesh = filter.sharedMesh;
if (mesh == null) continue;
Vector3[] vertices = mesh.vertices;
int[] triangles = mesh.triangles;
if (vertices.Length == 0 || triangles.Length == 0)
continue;
// 平面检测逻辑
List<Vector3[]> potentialPlanes = FindPotentialPlanes(vertices, triangles, filter.transform);
// 创建平面标记
if (planeMarkerPrefab != null)
{
CreatePlaneMarkers(potentialPlanes);
}
}
}
private List<Vector3[]> FindPotentialPlanes(Vector3[] localVertices, int[] triangles, Transform meshTransform)
{
List<Vector3[]> potentialPlanes = new List<Vector3[]>();
HashSet<int> processedTriangles = new HashSet<int>();
// 将顶点从局部坐标转换为世界坐标
Vector3[] worldVertices = new Vector3[localVertices.Length];
for (int i = 0; i < localVertices.Length; i++)
{
worldVertices[i] = meshTransform.TransformPoint(localVertices[i]);
}
// 处理三角形
for (int i = 0; i < triangles.Length; i += 3)
{
if (processedTriangles.Contains(i))
continue;
// 获取三角形
int a = triangles[i];
int b = triangles[i + 1];
int c = triangles[i + 2];
// 计算三角形法线
Vector3 normal = Vector3.Cross(
worldVertices[b] - worldVertices[a],
worldVertices[c] - worldVertices[a]
).normalized;
// 检查法线是否指向上方
if (Mathf.Abs(Vector3.Dot(normal, Vector3.up)) > 0.95f)
{
// 寻找相连的相似朝向三角形
List<Vector3> planeVertices = new List<Vector3>();
HashSet<int> planeTriangles = new HashSet<int>();
// 将初始三角形加入平面
planeVertices.Add(worldVertices[a]);
planeVertices.Add(worldVertices[b]);
planeVertices.Add(worldVertices[c]);
planeTriangles.Add(i);
processedTriangles.Add(i);
// 扩展平面
GrowPlane(triangles, worldVertices, normal, ref planeVertices, ref planeTriangles, ref processedTriangles);
// 检查平面大小
if (IsLargeEnoughPlane(planeVertices))
{
potentialPlanes.Add(planeVertices.ToArray());
}
}
}
return potentialPlanes;
}
private void GrowPlane(int[] triangles, Vector3[] worldVertices, Vector3 normal,
ref List<Vector3> planeVertices, ref HashSet<int> planeTriangles,
ref HashSet<int> processedTriangles)
{
// 这是一个简化的区域增长算法
// 实际实现会更复杂并考虑边界连接性
bool foundNewTriangles = true;
while (foundNewTriangles)
{
foundNewTriangles = false;
for (int i = 0; i < triangles.Length; i += 3)
{
if (processedTriangles.Contains(i))
continue;
int a = triangles[i];
int b = triangles[i + 1];
int c = triangles[i + 2];
// 检查此三角形是否与平面相连
bool connected = false;
foreach (int idx in new int[] { a, b, c })
{
if (planeVertices.Contains(worldVertices[idx]))
{
connected = true;
break;
}
}
if (connected)
{
// 计算三角形法线
Vector3 triNormal = Vector3.Cross(
worldVertices[b] - worldVertices[a],
worldVertices[c] - worldVertices[a]
).normalized;
// 检查法线是否与平面法线相似
if (Vector3.Dot(normal, triNormal) > 0.95f)
{
// 添加到平面
if (!planeVertices.Contains(worldVertices[a]))
planeVertices.Add(worldVertices[a]);
if (!planeVertices.Contains(worldVertices[b]))
planeVertices.Add(worldVertices[b]);
if (!planeVertices.Contains(worldVertices[c]))
planeVertices.Add(worldVertices[c]);
planeTriangles.Add(i);
processedTriangles.Add(i);
foundNewTriangles = true;
}
}
}
}
}
private bool IsLargeEnoughPlane(List<Vector3> planeVertices)
{
// 找出平面的尺寸
Vector3 min = planeVertices[0];
Vector3 max = planeVertices[0];
foreach (Vector3 v in planeVertices)
{
min = Vector3.Min(min, v);
max = Vector3.Max(max, v);
}
// 计算边界框尺寸
Vector3 size = max - min;
// 检查平面尺寸是否足够大
return size.x > minPlaneSize && size.z > minPlaneSize;
}
private void CreatePlaneMarkers(List<Vector3[]> planes)
{
foreach (Vector3[] planeVertices in planes)
{
// 计算平面中心
Vector3 center = Vector3.zero;
foreach (Vector3 v in planeVertices)
{
center += v;
}
center /= planeVertices.Length;
// 实例化标记物
GameObject marker = Instantiate(planeMarkerPrefab, center, Quaternion.identity);
marker.transform.SetParent(transform);
// 添加到列表
detectedPlanes.Add(marker);
}
Debug.Log($"检测到 {detectedPlanes.Count} 个平面");
}
public void UpdateMappingAsync()
{
// 当空间映射更新时调用此方法
if (!hasInitialized)
{
Initialize();
}
else
{
// 清理旧数据
CleanupOldData();
// 重新收集和处理
CollectSpatialMeshes();
if (optimizeMeshes)
{
OptimizeSpatialMeshes();
}
if (detectPlanes)
{
DetectPlanesInEnvironment();
}
}
}
private void CleanupOldData()
{
// 清理优化的网格
foreach (MeshFilter filter in optimizedMeshFilters)
{
if (filter != null && filter.gameObject != null)
{
Destroy(filter.gameObject);
}
}
optimizedMeshFilters.Clear();
// 清理平面标记
foreach (GameObject plane in detectedPlanes)
{
if (plane != null)
{
Destroy(plane);
}
}
detectedPlanes.Clear();
// 清理原始网格引用
originalMeshFilters.Clear();
}
private void OnDestroy()
{
CleanupOldData();
}
}
XR手势识别系统
// XRGestureRecognizer.cs - XR手势识别系统
using UnityEngine;
using System.Collections.Generic;
using UnityEngine.Events;
public enum GestureType
{
Pinch,
Grab,
Point,
ThumbsUp,
Victory,
Wave,
Swipe,
Custom
}
[System.Serializable]
public class GestureDetectedEvent : UnityEvent<GestureType, Vector3> { }
public class XRGestureRecognizer : MonoBehaviour
{
[Header("平台设置")]
[SerializeField] private XRPlatformType targetPlatform;
[Header("手势设置")]
[SerializeField] private bool trackLeftHand = true;
[SerializeField] private bool trackRightHand = true;
[SerializeField] private bool continuousDetection = true;
[SerializeField] private float gestureTimeout = 0.5f;
[SerializeField] private float confidenceThreshold = 0.8f;
[Header("检测灵敏度")]
[Range(0.5f, 1.0f)]
[SerializeField] private float pinchSensitivity = 0.8f;
[Range(0.5f, 1.0f)]
[SerializeField] private float grabSensitivity = 0.7f;
[Range(0.1f, 0.3f)]
[SerializeField] private float swipeDistanceThreshold = 0.2f;
[Range(0.2f, 1.0f)]
[SerializeField] private float swipeTimeThreshold = 0.5f;
[Header("调试")]
[SerializeField] private bool showDebugInfo = false;
[SerializeField] private bool visualizeHandTracking = false;
[SerializeField] private GameObject handVisualizerPrefab;
[Header("事件")]
public GestureDetectedEvent onGestureDetected;
// 手势数据
private struct FingerData
{
public Vector3 position;
public Vector3 direction;
public float flexion; // 0=直, 1=完全弯曲
public FingerData(Vector3 pos, Vector3 dir, float flex)
{
position = pos;
direction = dir;
flexion = flex;
}
}
private struct HandData
{
public Vector3 palmPosition;
public Vector3 palmNormal;
public FingerData thumb;
public FingerData index;
public FingerData middle;
public FingerData ring;
public FingerData pinky;
public Vector3 wristPosition;
// 轨迹跟踪
public List<Vector3> recentPositions;
public List<float> recentTimestamps;
}
// 状态数据
private HandData leftHandData;
private HandData rightHandData;
private Dictionary<GestureType, float> gestureConfidence = new Dictionary<GestureType, float>();
private Dictionary<GestureType, float> gestureLastDetectedTime = new Dictionary<GestureType, float>();
// 可视化对象
private GameObject leftHandVisualizer;
private GameObject rightHandVisualizer;
// 自定义手势
private class CustomGesture
{
public string name;
public List<Vector3> trajectory;
public float similarity;
}
private List<CustomGesture> customGestures = new List<CustomGesture>();
private void Awake()
{
// 初始化手势数据结构
leftHandData.recentPositions = new List<Vector3>();
leftHandData.recentTimestamps = new List<float>();
rightHandData.recentPositions = new List<Vector3>();
rightHandData.recentTimestamps = new List<float>();
// 初始化手势信息
foreach (GestureType gesture in System.Enum.GetValues(typeof(GestureType)))
{
gestureConfidence[gesture] = 0f;
gestureLastDetectedTime[gesture] = -gestureTimeout;
}
// 初始化手势可视化
if (visualizeHandTracking && handVisualizerPrefab != null)
{
if (trackLeftHand)
{
leftHandVisualizer = Instantiate(handVisualizerPrefab);
leftHandVisualizer.name = "LeftHandVisualizer";
leftHandVisualizer.SetActive(false);
}
if (trackRightHand)
{
rightHandVisualizer = Instantiate(handVisualizerPrefab);
rightHandVisualizer.name = "RightHandVisualizer";
rightHandVisualizer.SetActive(false);
}
}
}
private void Update()
{
// 获取手部数据
bool leftHandValid = trackLeftHand && UpdateHandData(true, ref leftHandData);
bool rightHandValid = trackRightHand && UpdateHandData(false, ref rightHandData);
// 更新手势可视化
if (visualizeHandTracking)
{
UpdateHandVisualization(leftHandValid, leftHandData, leftHandVisualizer);
UpdateHandVisualization(rightHandValid, rightHandData, rightHandVisualizer);
}
// 检测手势
if (leftHandValid)
{
DetectGestures(leftHandData, true);
}
if (rightHandValid)
{
DetectGestures(rightHandData, false);
}
}
private bool UpdateHandData(bool isLeft, ref HandData handData)
{
switch (targetPlatform)
{
case XRPlatformType.Quest:
return UpdateQuestHandData(isLeft, ref handData);
case XRPlatformType.Pico:
return UpdatePicoHandData(isLeft, ref handData);
case XRPlatformType.HoloLens:
return UpdateHoloLensHandData(isLeft, ref handData);
default:
return UpdateGenericXRHandData(isLeft, ref handData);
}
}
private bool UpdateQuestHandData(bool isLeft, ref HandData handData)
{
#if QUEST_SDK && !UNITY_EDITOR
// 获取OVR手部数据
OVRHand hand = isLeft ?
OVRInput.GetActiveController(OVRInput.Hand.HandLeft).GetComponent<OVRHand>() :
OVRInput.GetActiveController(OVRInput.Hand.HandRight).GetComponent<OVRHand>();
if (hand == null || !hand.IsTracked)
return false;
// 更新手掌数据
handData.palmPosition = hand.GetRootPose().position;
handData.palmNormal = hand.GetRootPose().rotation * Vector3.down;
handData.wristPosition = hand.GetWristPose().position;
// 更新拇指数据
OVRHand.HandFinger thumbFinger = OVRHand.HandFinger.Thumb;
OVRSkeleton.BoneId thumbTip = OVRSkeleton.BoneId.Hand_ThumbTip;
OVRSkeleton.BoneId thumbBase = OVRSkeleton.BoneId.Hand_Thumb0;
handData.thumb = GetFingerData(hand, thumbFinger, thumbTip, thumbBase);
// 更新食指数据
OVRHand.HandFinger indexFinger = OVRHand.HandFinger.Index;
OVRSkeleton.BoneId indexTip = OVRSkeleton.BoneId.Hand_IndexTip;
OVRSkeleton.BoneId indexBase = OVRSkeleton.BoneId.Hand_Index1;
handData.index = GetFingerData(hand, indexFinger, indexTip, indexBase);
// 更新中指数据
OVRHand.HandFinger middleFinger = OVRHand.HandFinger.Middle;
OVRSkeleton.BoneId middleTip = OVRSkeleton.BoneId.Hand_MiddleTip;
OVRSkeleton.BoneId middleBase = OVRSkeleton.BoneId.Hand_Middle1;
handData.middle = GetFingerData(hand, middleFinger, middleTip, middleBase);
// 更新无名指数据
OVRHand.HandFinger ringFinger = OVRHand.HandFinger.Ring;
OVRSkeleton.BoneId ringTip = OVRSkeleton.BoneId.Hand_RingTip;
OVRSkeleton.BoneId ringBase = OVRSkeleton.BoneId.Hand_Ring1;
handData.ring = GetFingerData(hand, ringFinger, ringTip, ringBase);
// 更新小指数据
OVRHand.HandFinger pinkyFinger = OVRHand.HandFinger.Pinky;
OVRSkeleton.BoneId pinkyTip = OVRSkeleton.BoneId.Hand_PinkyTip;
OVRSkeleton.BoneId pinkyBase = OVRSkeleton.BoneId.Hand_Pinky1;
handData.pinky = GetFingerData(hand, pinkyFinger, pinkyTip, pinkyBase);
// 更新手部轨迹
UpdateHandTrajectory(ref handData);
return true;
#else
return false;
#endif
}
private bool UpdatePicoHandData(bool isLeft, ref HandData handData)
{
#if PICO_SDK && !UNITY_EDITOR
// 获取Pico手部数据
Unity.XR.PXR.HandType hand = isLeft ?
Unity.XR.PXR.HandType.HandLeft :
Unity.XR.PXR.HandType.HandRight;
if (!Unity.XR.PXR.PXR_HandTracking.GetTrackingState(hand))
return false;
// 更新手掌数据
handData.palmPosition = Unity.XR.PXR.PXR_HandTracking.GetJointPositionFromWrist(
Unity.XR.PXR.HandJoint.JointPalm, hand);
Vector3 palmDir = Unity.XR.PXR.PXR_HandTracking.GetJointPositionFromWrist(
Unity.XR.PXR.HandJoint.JointMiddleProximal, hand) -
Unity.XR.PXR.PXR_HandTracking.GetJointPositionFromWrist(
Unity.XR.PXR.HandJoint.JointWrist, hand);
handData.palmNormal = Vector3.Cross(
palmDir,
Unity.XR.PXR.PXR_HandTracking.GetJointPositionFromWrist(
Unity.XR.PXR.HandJoint.JointIndexProximal, hand) -
Unity.XR.PXR.PXR_HandTracking.GetJointPositionFromWrist(
Unity.XR.PXR.HandJoint.JointPinkyProximal, hand)
).normalized;
handData.wristPosition = Unity.XR.PXR.PXR_HandTracking.GetJointPositionFromWrist(
Unity.XR.PXR.HandJoint.JointWrist, hand);
// 更新拇指数据
Vector3 thumbTipPos = Unity.XR.PXR.PXR_HandTracking.GetJointPositionFromWrist(
Unity.XR.PXR.HandJoint.JointThumbTip, hand);
Vector3 thumbBasePos = Unity.XR.PXR.PXR_HandTracking.GetJointPositionFromWrist(
Unity.XR.PXR.HandJoint.JointThumbProximal, hand);
float thumbFlex = Unity.XR.PXR.PXR_HandTracking.GetJointBendValue(
Unity.XR.PXR.HandJoint.JointThumbProximal, hand);
handData.thumb = new FingerData(
thumbTipPos,
(thumbTipPos - thumbBasePos).normalized,
thumbFlex
);
// 更新食指数据
Vector3 indexTipPos = Unity.XR.PXR.PXR_HandTracking.GetJointPositionFromWrist(
Unity.XR.PXR.HandJoint.JointIndexTip, hand);
Vector3 indexBasePos = Unity.XR.PXR.PXR_HandTracking.GetJointPositionFromWrist(
Unity.XR.PXR.HandJoint.JointIndexProximal, hand);
float indexFlex = Unity.XR.PXR.PXR_HandTracking.GetJointBendValue(
Unity.XR.PXR.HandJoint.JointIndexProximal, hand);
handData.index = new FingerData(
indexTipPos,
(indexTipPos - indexBasePos).normalized,
indexFlex
);
// 中指、无名指和小指数据同样更新...
// 更新手部轨迹
UpdateHandTrajectory(ref handData);
return true;
#else
return false;
#endif
}
private bool UpdateHoloLensHandData(bool isLeft, ref HandData handData)
{
#if HOLOLENS_SDK && !UNITY_EDITOR
// HoloLens手部数据更新
// 这里提供简化实现,实际应用需使用MRTK API
// 对于HoloLens,获取关节数据会更复杂
// 这是一个简化示例
var handSource = Microsoft.MixedReality.Toolkit.InputSystem.InputRecognizer.HandSource;
if (handSource == null)
return false;
var hand = isLeft ?
handSource.GetHand(Microsoft.MixedReality.Toolkit.Utilities.Handedness.Left) :
handSource.GetHand(Microsoft.MixedReality.Toolkit.Utilities.Handedness.Right);
if (hand == null || !hand.IsTracked)
return false;
// 获取关节数据
handData.palmPosition = hand.GetJointPose(
Microsoft.MixedReality.Toolkit.Utilities.TrackedHandJoint.Palm).Position;
handData.wristPosition = hand.GetJointPose(
Microsoft.MixedReality.Toolkit.Utilities.TrackedHandJoint.Wrist).Position;
// 计算手掌法线
var palmPose = hand.GetJointPose(
Microsoft.MixedReality.Toolkit.Utilities.TrackedHandJoint.Palm);
handData.palmNormal = palmPose.Rotation * Vector3.up;
// 获取手指数据
handData.thumb = GetFingerDataFromMRTK(hand,
Microsoft.MixedReality.Toolkit.Utilities.TrackedHandJoint.ThumbTip,
Microsoft.MixedReality.Toolkit.Utilities.TrackedHandJoint.ThumbProximal);
handData.index = GetFingerDataFromMRTK(hand,
Microsoft.MixedReality.Toolkit.Utilities.TrackedHandJoint.IndexTip,
Microsoft.MixedReality.Toolkit.Utilities.TrackedHandJoint.IndexProximal);
// 更新手部轨迹
UpdateHandTrajectory(ref handData);
return true;
#else
return false;
#endif
}
private bool UpdateGenericXRHandData(bool isLeft, ref HandData handData)
{
// 通用XR手部数据更新
// 实际应用中,这里可以使用Unity XR Interaction Toolkit
// 或其他通用API获取手部数据
// 这是模拟数据,用于编辑器测试
#if UNITY_EDITOR
// 使用输入模拟生成手部数据
if (isLeft)
{
if (Input.GetKey(KeyCode.LeftShift))
{
Vector3 handPos = Camera.main.transform.position +
Camera.main.transform.forward * 0.3f +
Camera.main.transform.right * -0.2f;
// 模拟左手数据
handData.palmPosition = handPos;
handData.palmNormal = Camera.main.transform.right;
handData.wristPosition = handPos - Camera.main.transform.up * 0.05f;
// 模拟手指弯曲度
bool pinch = Input.GetKey(KeyCode.Z);
bool grab = Input.GetKey(KeyCode.X);
bool point = Input.GetKey(KeyCode.C);
// 模拟拇指
handData.thumb = new FingerData(
handPos + Camera.main.transform.right * 0.03f,
Camera.main.transform.right,
pinch ? 0.8f : 0.2f
);
// 模拟食指
handData.index = new FingerData(
handPos + Camera.main.transform.forward * 0.05f,
Camera.main.transform.forward,
pinch || grab ? 0.9f : (point ? 0.1f : 0.7f)
);
// 模拟中指
handData.middle = new FingerData(
handPos + Camera.main.transform.forward * 0.04f,
Camera.main.transform.forward,
grab || point ? 0.9f : 0.5f
);
// 更新手部轨迹
UpdateHandTrajectory(ref handData);
return true;
}
}
else // 右手
{
if (Input.GetKey(KeyCode.RightShift))
{
Vector3 handPos = Camera.main.transform.position +
Camera.main.transform.forward * 0.3f +
Camera.main.transform.right * 0.2f;
// 模拟右手数据
handData.palmPosition = handPos;
handData.palmNormal = -Camera.main.transform.right;
handData.wristPosition = handPos - Camera.main.transform.up * 0.05f;
// 模拟手指弯曲度
bool pinch = Input.GetKey(KeyCode.M);
bool grab = Input.GetKey(KeyCode.N);
bool point = Input.GetKey(KeyCode.B);
// 模拟手指数据...
// 与左手类似
// 更新手部轨迹
UpdateHandTrajectory(ref handData);
return true;
}
}
#endif
return false;
}
#if QUEST_SDK
private FingerData GetFingerData(OVRHand hand, OVRHand.HandFinger finger,
OVRSkeleton.BoneId tipBone, OVRSkeleton.BoneId baseBone)
{
// 获取手指位置和方向
Vector3 tipPosition = hand.GetSkeleton().Bones[(int)tipBone].Transform.position;
Vector3 basePosition = hand.GetSkeleton().Bones[(int)baseBone].Transform.position;
Vector3 direction = (tipPosition - basePosition).normalized;
// 获取弯曲度
float flexion = hand.GetFingerPinchStrength(finger);
return new FingerData(tipPosition, direction, flexion);
}
#endif
#if HOLOLENS_SDK
private FingerData GetFingerDataFromMRTK(Microsoft.MixedReality.Toolkit.Input.IMixedRealityHand hand,
Microsoft.MixedReality.Toolkit.Utilities.TrackedHandJoint tipJoint,
Microsoft.MixedReality.Toolkit.Utilities.TrackedHandJoint baseJoint)
{
// 获取手指位置和方向
Vector3 tipPosition = hand.GetJointPose(tipJoint).Position;
Vector3 basePosition = hand.GetJointPose(baseJoint).Position;
Vector3 direction = (tipPosition - basePosition).normalized;
// 估算弯曲度
// MRTK中可以使用手部关节之间的角度估算
float flexion = Vector3.Dot(direction, hand.GetJointPose(baseJoint).Rotation * Vector3.forward);
flexion = Mathf.Clamp01(1.0f - flexion);
return new FingerData(tipPosition, direction, flexion);
}
#endif
private void UpdateHandTrajectory(ref HandData handData)
{
// 添加当前位置和时间戳
handData.recentPositions.Add(handData.palmPosition);
handData.recentTimestamps.Add(Time.time);
// 保持队列大小
const int maxTrajectoryLength = 20;
if (handData.recentPositions.Count > maxTrajectoryLength)
{
handData.recentPositions.RemoveAt(0);
handData.recentTimestamps.RemoveAt(0);
}
}
private void UpdateHandVisualization(bool handValid, HandData handData, GameObject visualizer)
{
if (visualizer == null)
return;
visualizer.SetActive(handValid);
if (handValid)
{
// 更新手部可视化物体的位置和旋转
visualizer.transform.position = handData.palmPosition;
// 根据手掌法线和手指方向计算旋转
Vector3 forward = handData.index.direction;
Vector3 up = handData.palmNormal;
Vector3 right = Vector3.Cross(up, forward).normalized;
forward = Vector3.Cross(right, up).normalized;
visualizer.transform.rotation = Quaternion.LookRotation(forward, up);
// 更新手指可视化
// 这部分需要基于具体可视化模型的结构实现
// 通常需要获取各个手指的骨骼Transform并设置其旋转
}
}
private void DetectGestures(HandData handData, bool isLeft)
{
// 对各种手势进行检测
float pinchConfidence = DetectPinchGesture(handData);
float grabConfidence = DetectGrabGesture(handData);
float pointConfidence = DetectPointGesture(handData);
float thumbsUpConfidence = DetectThumbsUpGesture(handData);
float victoryConfidence = DetectVictoryGesture(handData);
// 检测动态手势
float waveConfidence = DetectWaveGesture(handData);
float swipeConfidence = DetectSwipeGesture(handData);
// 自定义手势匹配
float customConfidence = DetectCustomGestures(handData);
// 更新手势置信度
UpdateGestureConfidence(GestureType.Pinch, pinchConfidence, handData.palmPosition, isLeft);
UpdateGestureConfidence(GestureType.Grab, grabConfidence, handData.palmPosition, isLeft);
UpdateGestureConfidence(GestureType.Point, pointConfidence, handData.palmPosition, isLeft);
UpdateGestureConfidence(GestureType.ThumbsUp, thumbsUpConfidence, handData.palmPosition, isLeft);
UpdateGestureConfidence(GestureType.Victory, victoryConfidence, handData.palmPosition, isLeft);
UpdateGestureConfidence(GestureType.Wave, waveConfidence, handData.palmPosition, isLeft);
UpdateGestureConfidence(GestureType.Swipe, swipeConfidence, handData.palmPosition, isLeft);
UpdateGestureConfidence(GestureType.Custom, customConfidence, handData.palmPosition, isLeft);
// 调试信息
if (showDebugInfo)
{
string handName = isLeft ? "左手" : "右手";
Debug.Log($"{handName} 手势置信度: 捏合={pinchConfidence:F2}, 抓取={grabConfidence:F2}, " +
$"指向={pointConfidence:F2}, 点赞={thumbsUpConfidence:F2}, 胜利={victoryConfidence:F2}, " +
$"挥手={waveConfidence:F2}, 滑动={swipeConfidence:F2}");
}
}
private float DetectPinchGesture(HandData handData)
{
// 检测拇指和食指捏合
float thumbIndexDistance = Vector3.Distance(handData.thumb.position, handData.index.position);
// 判断拇指和食指是否足够接近
float maxPinchDistance = 0.03f; // 3厘米
float distanceRatio = Mathf.Clamp01(1.0f - (thumbIndexDistance / maxPinchDistance));
// 结合弯曲度判断
float pinchConfidence = distanceRatio *
Mathf.Clamp01(handData.thumb.flexion) *
Mathf.Clamp01(handData.index.flexion);
// 其他手指应该是正常弯曲的
float otherFingersFactor = (handData.middle.flexion + handData.ring.flexion + handData.pinky.flexion) / 3.0f;
return pinchConfidence * pinchSensitivity * otherFingersFactor;
}
private float DetectGrabGesture(HandData handData)
{
// 检测握拳/抓取动作
// 所有手指都应该弯曲
float allFingersFactor = (handData.thumb.flexion +
handData.index.flexion +
handData.middle.flexion +
handData.ring.flexion +
handData.pinky.flexion) / 5.0f;
// 判断手指是否都朝向手掌
float fingerDirectionFactor = 0;
fingerDirectionFactor += Vector3.Dot(handData.index.direction, handData.palmNormal);
fingerDirectionFactor += Vector3.Dot(handData.middle.direction, handData.palmNormal);
fingerDirectionFactor += Vector3.Dot(handData.ring.direction, handData.palmNormal);
fingerDirectionFactor += Vector3.Dot(handData.pinky.direction, handData.palmNormal);
fingerDirectionFactor = Mathf.Clamp01(fingerDirectionFactor / 4.0f);
return allFingersFactor * fingerDirectionFactor * grabSensitivity;
}
private float DetectPointGesture(HandData handData)
{
// 检测指向手势
// 食指应伸直,其他手指应弯曲
float indexStraightness = 1.0f - handData.index.flexion;
float otherFingersBent = (handData.middle.flexion +
handData.ring.flexion +
handData.pinky.flexion) / 3.0f;
// 拇指可能伸直或弯曲,因此不纳入考虑
return indexStraightness * otherFingersBent;
}
private float DetectThumbsUpGesture(HandData handData)
{
// 检测大拇指朝上手势
// 拇指应伸直并指向上方,其他手指应弯曲
float thumbStraightness = 1.0f - handData.thumb.flexion;
float thumbUpFactor = Vector3.Dot(handData.thumb.direction, Vector3.up);
thumbUpFactor = Mathf.Clamp01(thumbUpFactor);
float otherFingersBent = (handData.index.flexion +
handData.middle.flexion +
handData.ring.flexion +
handData.pinky.flexion) / 4.0f;
return thumbStraightness * thumbUpFactor * otherFingersBent;
}
private float DetectVictoryGesture(HandData handData)
{
// 检测胜利/V字手势
// 食指和中指应伸直,其他手指应弯曲
float indexStraightness = 1.0f - handData.index.flexion;
float middleStraightness = 1.0f - handData.middle.flexion;
float otherFingersBent = (handData.thumb.flexion +
handData.ring.flexion +
handData.pinky.flexion) / 3.0f;
// 检查食指和中指是否分开
float separation = Vector3.Distance(handData.index.position, handData.middle.position);
float expectedSeparation = 0.03f; // 期望的分离距离
float separationFactor = Mathf.Clamp01(separation / expectedSeparation);
return indexStraightness * middleStraightness * otherFingersBent * separationFactor;
}
private float DetectWaveGesture(HandData handData)
{
// 检测挥手手势
if (handData.recentPositions.Count < 5)
return 0;
// 分析手部轨迹的上下运动
float verticalMotion = 0;
Vector3 prevPos = handData.recentPositions[0];
for (int i = 1; i < handData.recentPositions.Count; i++)
{
Vector3 currentPos = handData.recentPositions[i];
float verticalDelta = Mathf.Abs(currentPos.y - prevPos.y);
verticalMotion += verticalDelta;
prevPos = currentPos;
}
// 检查是否有足够的垂直运动
float threshold = 0.1f; // 10厘米的累积垂直运动
float waveConfidence = Mathf.Clamp01(verticalMotion / threshold);
// 判断手指是否伸直
float fingersExtended = (1.0f - handData.index.flexion +
1.0f - handData.middle.flexion +
1.0f - handData.ring.flexion +
1.0f - handData.pinky.flexion) / 4.0f;
return waveConfidence * fingersExtended;
}
private float DetectSwipeGesture(HandData handData)
{
// 检测滑动手势
if (handData.recentPositions.Count < 3 || handData.recentTimestamps.Count < 3)
return 0;
// 计算最近的位移
Vector3 startPos = handData.recentPositions[0];
Vector3 endPos = handData.recentPositions[handData.recentPositions.Count - 1];
float displacement = Vector3.Distance(startPos, endPos);
// 计算时间跨度
float startTime = handData.recentTimestamps[0];
float endTime = handData.recentTimestamps[handData.recentTimestamps.Count - 1];
float duration = endTime - startTime;
// 检查是否有足够的位移和足够短的时间
if (displacement > swipeDistanceThreshold && duration < swipeTimeThreshold)
{
// 计算滑动方向的一致性
Vector3 overallDirection = (endPos - startPos).normalized;
float directionConsistency = 0;
for (int i = 1; i < handData.recentPositions.Count; i++)
{
Vector3 segmentDirection = (handData.recentPositions[i] - handData.recentPositions[i-1]).normalized;
directionConsistency += Vector3.Dot(segmentDirection, overallDirection);
}
directionConsistency /= (handData.recentPositions.Count - 1);
// 计算最终置信度
float swipeConfidence = Mathf.Clamp01(displacement / swipeDistanceThreshold) *
Mathf.Clamp01(directionConsistency) *
Mathf.Clamp01(1.0f - (duration / swipeTimeThreshold));
return swipeConfidence;
}
return 0;
}
private float DetectCustomGestures(HandData handData)
{
if (customGestures.Count == 0)
return 0;
// 提取当前手势轨迹
if (handData.recentPositions.Count < 5)
return 0;
// 对轨迹进行归一化
List<Vector3> normalizedTrajectory = NormalizeTrajectory(handData.recentPositions);
// 与所有自定义手势进行比较
float bestMatchScore = 0;
foreach (CustomGesture gesture in customGestures)
{
float similarity = CalculateTrajectorySimularity(normalizedTrajectory, gesture.trajectory);
gesture.similarity = similarity;
if (similarity > bestMatchScore)
{
bestMatchScore = similarity;
}
}
return bestMatchScore;
}
private List<Vector3> NormalizeTrajectory(List<Vector3> trajectory)
{
// 归一化轨迹,使其与参考坐标系无关
if (trajectory.Count < 2)
return trajectory;
// 找出轨迹的中心
Vector3 center = Vector3.zero;
foreach (Vector3 point in trajectory)
{
center += point;
}
center /= trajectory.Count;
// 找出轨迹的最大范围
float maxDistance = 0;
foreach (Vector3 point in trajectory)
{
float distance = Vector3.Distance(point, center);
if (distance > maxDistance)
{
maxDistance = distance;
}
}
if (maxDistance < 0.001f)
return trajectory;
// 归一化后的轨迹
List<Vector3> normalized = new List<Vector3>();
foreach (Vector3 point in trajectory)
{
normalized.Add((point - center) / maxDistance);
}
return normalized;
}
private float CalculateTrajectorySimularity(List<Vector3> trajectory1, List<Vector3> trajectory2)
{
// 这里使用动态时间规整(DTW)算法比较两个轨迹的相似度
// 简化版本,实际实现应更复杂
// 重新采样轨迹到相同长度
int sampleCount = 10;
List<Vector3> sampledTraj1 = ResampleTrajectory(trajectory1, sampleCount);
List<Vector3> sampledTraj2 = ResampleTrajectory(trajectory2, sampleCount);
// 计算各点之间的距离
float totalDistance = 0;
for (int i = 0; i < sampleCount; i++)
{
totalDistance += Vector3.Distance(sampledTraj1[i], sampledTraj2[i]);
}
// 计算平均距离并转换为相似度
float avgDistance = totalDistance / sampleCount;
float similarity = Mathf.Clamp01(1.0f - (avgDistance / 2.0f));
return similarity;
}
private List<Vector3> ResampleTrajectory(List<Vector3> trajectory, int sampleCount)
{
// 将轨迹重新采样到指定数量的点
List<Vector3> sampled = new List<Vector3>();
if (trajectory.Count <= 1 || sampleCount <= 1)
{
// 处理边界情况
sampled.Add(trajectory[0]);
return sampled;
}
// 计算轨迹总长度
float totalLength = 0;
for (int i = 1; i < trajectory.Count; i++)
{
totalLength += Vector3.Distance(trajectory[i], trajectory[i-1]);
}
// 理想的采样间隔
float idealSegmentLength = totalLength / (sampleCount - 1);
// 第一个采样点始终是轨迹的开始
sampled.Add(trajectory[0]);
float distanceSinceLastSample = 0;
for (int i = 1; i < trajectory.Count; i++)
{
float segmentLength = Vector3.Distance(trajectory[i], trajectory[i-1]);
// 如果当前累积距离加上当前段长度超过了理想间隔
if (distanceSinceLastSample + segmentLength >= idealSegmentLength)
{
// 计算插值点
float remainingDistance = idealSegmentLength - distanceSinceLastSample;
float t = remainingDistance / segmentLength;
Vector3 interpolatedPoint = Vector3.Lerp(trajectory[i-1], trajectory[i], t);
sampled.Add(interpolatedPoint);
// 继续在同一段内寻找更多采样点
distanceSinceLastSample = 0;
i--; // 保持在同一段
// 如果我们已经有了足够的样本点
if (sampled.Count >= sampleCount)
break;
}
else
{
// 累积距离
distanceSinceLastSample += segmentLength;
}
}
// 确保最后一点被采样
if (sampled.Count < sampleCount)
{
sampled.Add(trajectory[trajectory.Count - 1]);
}
return sampled;
}
private void UpdateGestureConfidence(GestureType gesture, float confidence, Vector3 position, bool isLeft)
{
// 更新手势置信度并触发事件
if (confidence >= confidenceThreshold)
{
float lastTime = gestureLastDetectedTime[gesture];
// 如果是连续检测或者经过了足够的时间
if (continuousDetection || Time.time - lastTime >= gestureTimeout)
{
gestureLastDetectedTime[gesture] = Time.time;
gestureConfidence[gesture] = confidence;
// 触发手势事件
string handPrefix = isLeft ? "左手" : "右手";
Debug.Log($"检测到{handPrefix} {gesture} 手势 (置信度: {confidence:F2})");
onGestureDetected?.Invoke(gesture, position);
}
}
else
{
// 渐变降低置信度
gestureConfidence[gesture] = Mathf.Lerp(gestureConfidence[gesture], 0, Time.deltaTime * 5f);
}
}
// 添加自定义手势
public void AddCustomGesture(string name, List<Vector3> trajectory)
{
if (trajectory.Count < 5)
{
Debug.LogWarning("轨迹点太少,无法添加自定义手势");
return;
}
CustomGesture gesture = new CustomGesture()
{
name = name,
trajectory = NormalizeTrajectory(trajectory),
similarity = 0
};
customGestures.Add(gesture);
Debug.Log($"添加了自定义手势: {name}");
}
// 清除录制的轨迹
public void ClearTrajectory(bool isLeft)
{
if (isLeft)
{
leftHandData.recentPositions.Clear();
leftHandData.recentTimestamps.Clear();
}
else
{
rightHandData.recentPositions.Clear();
rightHandData.recentTimestamps.Clear();
}
}
private void OnDestroy()
{
// 清理可视化对象
if (leftHandVisualizer != null)
Destroy(leftHandVisualizer);
if (rightHandVisualizer != null)
Destroy(rightHandVisualizer);
}
}
八、企业级XR应用开发经验
多平台许可和硬件识别系统
// XRLicenseManager.cs - XR许可证管理器
using UnityEngine;
using System;
using System.Collections;
using System.Text;
using System.Security.Cryptography;
using UnityEngine.Networking;
public class XRLicenseManager : MonoBehaviour
{
[Header("许可配置")]
[SerializeField] private string licenseKey;
[SerializeField] private bool validateOnStart = true;
[SerializeField] private bool enforceHardwareValidation = false;
[SerializeField] private string licenseServerURL = "https://yourserver.com/validate";