From ded28e0097544ade099de8830c08f27f2ac4e646 Mon Sep 17 00:00:00 2001 From: Rowland <975945824@qq.com> Date: Mon, 15 Sep 2025 16:41:35 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=89=8B=E6=9F=84=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VR_GUIDE.md | 194 ---------- _tmp_material.py | 6 - core/vr_actions.py | 583 ++++++++++++++++++++++++++++++ core/vr_controller.py | 282 +++++++++++++++ core/vr_interaction.py | 432 ++++++++++++++++++++++ core/vr_manager.py | 349 +++++++++++++++++- core/vr_visualization.py | 658 ++++++++++++++++++++++++++++++++++ vr_actions/actions.json | 89 +++++ vr_actions/bindings_vive.json | 106 ++++++ 9 files changed, 2498 insertions(+), 201 deletions(-) delete mode 100644 VR_GUIDE.md delete mode 100644 _tmp_material.py create mode 100644 core/vr_actions.py create mode 100644 core/vr_controller.py create mode 100644 core/vr_interaction.py create mode 100644 core/vr_visualization.py create mode 100644 vr_actions/actions.json create mode 100644 vr_actions/bindings_vive.json diff --git a/VR_GUIDE.md b/VR_GUIDE.md deleted file mode 100644 index 652e828f..00000000 --- a/VR_GUIDE.md +++ /dev/null @@ -1,194 +0,0 @@ -# VR模式使用指南 - -## 概述 - -本项目现已支持VR模式,通过SteamVR将Panda3D引擎的画面传输到VR头显设备。您可以在虚拟现实环境中体验和编辑3D场景。 - -## 系统要求 - -### 硬件要求 -- 支持SteamVR的VR头显设备(如HTC Vive、Oculus Rift、Valve Index等) -- VR头显控制器(可选,用于交互) -- 满足VR运行要求的计算机配置 - -### 软件要求 -- Windows 10/11 或 Linux(支持OpenVR) -- SteamVR运行时 -- Python 3.7+ -- 项目依赖库(见安装步骤) - -## 安装与设置 - -### 1. 安装VR依赖 -```bash -# 安装OpenVR Python库 -pip install openvr==2.2.0 - -# 或者安装所有项目依赖 -pip install -r requirements/requirements.txt -``` - -### 2. 安装SteamVR -1. 安装Steam客户端 -2. 在Steam中搜索并安装"SteamVR" -3. 连接您的VR头显设备 -4. 按照SteamVR设置向导完成房间设置 - -### 3. 验证VR设置 -运行VR测试脚本来验证安装: -```bash -python vr_test.py -``` - -## 使用方法 - -### 启动VR模式 - -1. **启动SteamVR** - - 在Steam中启动SteamVR - - 确保VR头显已连接并处于就绪状态 - -2. **启动应用程序** - ```bash - python main.py - ``` - -3. **进入VR模式** - - 在菜单栏中选择 `VR` → `进入VR模式` - - 系统会自动检测VR设备并初始化VR渲染 - -4. **戴上VR头显** - - 现在您可以在VR环境中查看和操作3D场景 - -### VR菜单选项 - -- **进入VR模式**: 启用VR渲染,将画面输出到VR头显 -- **退出VR模式**: 退出VR模式,回到桌面显示 -- **VR状态**: 查看VR系统状态和设备信息 -- **VR设置**: 配置VR渲染参数和性能选项 - -### VR交互 - -目前支持的VR交互功能: -- **头部追踪**: 自动追踪头显位置和朝向 -- **房间规模移动**: 在设定的VR空间内移动 -- **立体显示**: 为左右眼提供不同视角的立体图像 - -## 配置选项 - -### VR设置对话框 - -在 `VR` → `VR设置` 中可以配置: - -- **渲染质量**: 调整VR渲染的画质等级 -- **抗锯齿**: 设置抗锯齿级别 -- **刷新率**: 配置VR头显刷新率 -- **异步重投影**: 启用/禁用ATW技术 - -### 性能优化建议 - -1. **降低渲染分辨率**: 如果性能不足,可以降低VR渲染质量 -2. **减少抗锯齿**: 关闭或降低抗锯齿级别 -3. **优化场景复杂度**: 减少场景中的多边形数量 -4. **关闭不必要的特效**: 暂时关闭高级渲染特效 - -## 故障排除 - -### 常见问题 - -**Q: 无法进入VR模式,提示"VR系统不可用"** -A: 请检查: -- SteamVR是否正在运行 -- VR头显是否正确连接 -- OpenVR库是否已安装 (`pip install openvr`) - -**Q: VR画面卡顿或延迟** -A: 尝试: -- 降低VR渲染质量 -- 关闭其他占用GPU的程序 -- 检查VR头显连接线是否松动 - -**Q: VR菜单显示为灰色(不可用)** -A: 检查: -- VR管理器是否正确初始化 -- 查看控制台错误信息 -- 运行 `python vr_test.py` 进行诊断 - -**Q: 只能看到一只眼的画面** -A: 这可能是: -- VR渲染缓冲区创建失败 -- OpenVR投影矩阵设置错误 -- 检查图形驱动程序是否最新 - -### 诊断工具 - -使用内置的VR测试工具进行问题诊断: -```bash -python vr_test.py -``` - -该工具会检测: -- 基础环境和依赖 -- VR管理器状态 -- VR设备可用性 -- VR初始化过程 -- UI菜单集成 - -### 日志信息 - -VR相关的日志信息会输出到控制台,包括: -- VR系统初始化状态 -- 渲染缓冲区创建信息 -- 追踪数据更新 -- 错误和警告信息 - -## 开发者信息 - -### VR架构 - -``` -main.py (MyWorld) -├── core/vr_manager.py (VRManager) -│ ├── OpenVR集成 -│ ├── 渲染缓冲区管理 -│ ├── 相机控制 -│ └── 追踪数据处理 -└── ui/main_window.py - ├── VR菜单 - ├── VR设置对话框 - └── VR事件处理 -``` - -### 核心类 - -- **VRManager**: VR功能的核心管理类 -- **MainWindow**: UI集成和事件处理 -- **MyWorld**: VR管理器的宿主类 - -### 扩展开发 - -如需添加新的VR功能,可以: - -1. 在 `VRManager` 中添加新方法 -2. 在 `MainWindow` 中添加对应的UI控件 -3. 更新VR设置对话框 -4. 添加相应的测试代码 - -## 技术支持 - -如果遇到问题,请: - -1. 运行 `python vr_test.py` 获取诊断信息 -2. 检查控制台日志 -3. 确认SteamVR和硬件设置正确 -4. 提交问题时请包含详细的错误信息和系统配置 - -## 版本信息 - -- VR功能版本: 1.0.0 -- 支持的OpenVR版本: 2.2.0+ -- 兼容的SteamVR版本: 最新版本 - ---- - -**注意**: VR功能目前处于初始版本,可能还有一些限制和待完善的功能。我们会持续改进和优化VR体验。 \ No newline at end of file diff --git a/_tmp_material.py b/_tmp_material.py deleted file mode 100644 index 8e4fa492..00000000 --- a/_tmp_material.py +++ /dev/null @@ -1,6 +0,0 @@ -# Autogenerated -name = 'Plastic-R0.0' -roughness = 0.0 -ior = 1.51 -basecolor = (1, 0, 0) -mat_type = 'default' diff --git a/core/vr_actions.py b/core/vr_actions.py new file mode 100644 index 00000000..378476db --- /dev/null +++ b/core/vr_actions.py @@ -0,0 +1,583 @@ +""" +VR动作系统模块 + +基于OpenVR Action系统,提供高级的输入处理和动作映射: +- VR动作清单管理 +- 按钮和轴输入处理 +- 触觉反馈 +- 动作集管理 +""" + +import json +import os +from pathlib import Path +from direct.showbase.DirectObject import DirectObject + +try: + import openvr + OPENVR_AVAILABLE = True +except ImportError: + OPENVR_AVAILABLE = False + + +class VRActionManager(DirectObject): + """VR动作管理器 - 处理OpenVR动作系统""" + + def __init__(self, vr_manager): + """初始化VR动作管理器 + + Args: + vr_manager: VR管理器实例 + """ + super().__init__() + + self.vr_manager = vr_manager + self.vr_input = None + self.action_set_handles = [] + self.action_handles = {} + + # 预定义的标准动作 + self.standard_actions = { + # 姿态动作 + 'pose': '/actions/default/in/Pose', + + # 按钮动作 + 'trigger': '/actions/default/in/Trigger', + 'grip': '/actions/default/in/Grip', + 'menu': '/actions/default/in/Menu', + 'system': '/actions/default/in/System', + 'trackpad_click': '/actions/default/in/TrackpadClick', + 'trackpad_touch': '/actions/default/in/TrackpadTouch', + 'a_button': '/actions/default/in/AButton', + 'b_button': '/actions/default/in/BButton', + + # 轴动作 + 'trackpad': '/actions/default/in/Trackpad', + 'joystick': '/actions/default/in/Joystick', + 'squeeze': '/actions/default/in/Squeeze', + + # 震动输出 + 'haptic': '/actions/default/out/Haptic' + } + + # 动作集 + self.default_action_set = '/actions/default' + + print("✓ VR动作管理器初始化完成") + + def initialize(self): + """初始化VR动作系统""" + if not OPENVR_AVAILABLE or not self.vr_manager.vr_system: + print("⚠️ VR系统不可用,无法初始化动作系统") + return False + + try: + print("🎮 正在初始化VR动作系统...") + + # 获取VR输入接口 + self.vr_input = openvr.VRInput() + if not self.vr_input: + print("❌ 无法获取VR输入接口") + return False + + # 创建动作清单文件 + manifest_path = self._create_action_manifest() + if not manifest_path: + print("❌ 无法创建动作清单") + return False + + # 加载动作清单 + error = self.vr_input.setActionManifestPath(manifest_path) + if error != openvr.VRInputError_None: + print(f"❌ 加载动作清单失败: {error}") + return False + + # 获取动作句柄 + self._load_action_handles() + + # 创建动作集 + self._setup_action_sets() + + print("✅ VR动作系统初始化成功") + return True + + except Exception as e: + print(f"❌ VR动作系统初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_action_manifest(self): + """创建VR动作清单文件""" + try: + # 动作清单配置 + manifest_data = { + "actions": [ + # 姿态动作 + { + "name": "/actions/default/in/Pose", + "type": "pose" + }, + + # 数字动作(按钮) + { + "name": "/actions/default/in/Trigger", + "type": "boolean" + }, + { + "name": "/actions/default/in/Grip", + "type": "boolean" + }, + { + "name": "/actions/default/in/Menu", + "type": "boolean" + }, + { + "name": "/actions/default/in/System", + "type": "boolean" + }, + { + "name": "/actions/default/in/TrackpadClick", + "type": "boolean" + }, + { + "name": "/actions/default/in/TrackpadTouch", + "type": "boolean" + }, + { + "name": "/actions/default/in/AButton", + "type": "boolean" + }, + { + "name": "/actions/default/in/BButton", + "type": "boolean" + }, + + # 模拟动作(轴) + { + "name": "/actions/default/in/Trackpad", + "type": "vector2" + }, + { + "name": "/actions/default/in/Joystick", + "type": "vector2" + }, + { + "name": "/actions/default/in/Squeeze", + "type": "vector1" + }, + + # 震动输出 + { + "name": "/actions/default/out/Haptic", + "type": "vibration" + } + ], + + "action_sets": [ + { + "name": "/actions/default", + "usage": "single" + } + ], + + "default_bindings": [ + { + "controller_type": "vive_controller", + "binding_url": "bindings_vive.json" + }, + { + "controller_type": "oculus_touch", + "binding_url": "bindings_oculus.json" + }, + { + "controller_type": "knuckles", + "binding_url": "bindings_index.json" + } + ], + + "localization": [ + { + "language_tag": "zh_CN", + "/actions/default/in/Trigger": "扳机", + "/actions/default/in/Grip": "握把", + "/actions/default/in/Menu": "菜单", + "/actions/default/in/System": "系统", + "/actions/default/in/TrackpadClick": "触摸板点击", + "/actions/default/in/TrackpadTouch": "触摸板触摸", + "/actions/default/in/Pose": "手部姿态", + "/actions/default/out/Haptic": "震动反馈" + } + ] + } + + # 保存到临时目录 + manifest_dir = Path.cwd() / "vr_actions" + manifest_dir.mkdir(exist_ok=True) + + manifest_path = manifest_dir / "actions.json" + with open(manifest_path, 'w', encoding='utf-8') as f: + json.dump(manifest_data, f, indent=2, ensure_ascii=False) + + # 创建基本的绑定文件 + self._create_default_bindings(manifest_dir) + + print(f"✓ 动作清单已创建: {manifest_path}") + return str(manifest_path) + + except Exception as e: + print(f"❌ 创建动作清单失败: {e}") + return None + + def _create_default_bindings(self, manifest_dir): + """创建默认的控制器绑定文件""" + # Vive控制器绑定 + vive_bindings = { + "controller_type": "vive_controller", + "description": "Vive控制器绑定", + "name": "EG VR Editor - Vive", + "bindings": { + "/actions/default": { + "sources": [ + { + "inputs": { + "click": { + "output": "/actions/default/in/Trigger" + } + }, + "mode": "button", + "path": "/user/hand/left/input/trigger" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Trigger" + } + }, + "mode": "button", + "path": "/user/hand/right/input/trigger" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Grip" + } + }, + "mode": "button", + "path": "/user/hand/left/input/grip" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Grip" + } + }, + "mode": "button", + "path": "/user/hand/right/input/grip" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Menu" + } + }, + "mode": "button", + "path": "/user/hand/left/input/menu" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Trackpad" + }, + "click": { + "output": "/actions/default/in/TrackpadClick" + }, + "touch": { + "output": "/actions/default/in/TrackpadTouch" + } + }, + "mode": "trackpad", + "path": "/user/hand/left/input/trackpad" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Trackpad" + }, + "click": { + "output": "/actions/default/in/TrackpadClick" + }, + "touch": { + "output": "/actions/default/in/TrackpadTouch" + } + }, + "mode": "trackpad", + "path": "/user/hand/right/input/trackpad" + } + ], + "poses": [ + { + "output": "/actions/default/in/Pose", + "path": "/user/hand/left/pose/raw" + }, + { + "output": "/actions/default/in/Pose", + "path": "/user/hand/right/pose/raw" + } + ], + "haptics": [ + { + "output": "/actions/default/out/Haptic", + "path": "/user/hand/left/output/haptic" + }, + { + "output": "/actions/default/out/Haptic", + "path": "/user/hand/right/output/haptic" + } + ] + } + } + } + + bindings_path = manifest_dir / "bindings_vive.json" + with open(bindings_path, 'w', encoding='utf-8') as f: + json.dump(vive_bindings, f, indent=2) + + print(f"✓ Vive控制器绑定已创建: {bindings_path}") + + def _load_action_handles(self): + """加载动作句柄""" + if not self.vr_input: + return + + try: + for action_name, action_path in self.standard_actions.items(): + handle = self.vr_input.getActionHandle(action_path) + self.action_handles[action_name] = handle + print(f"✓ 加载动作: {action_name} -> {handle}") + + except Exception as e: + print(f"⚠️ 加载动作句柄失败: {e}") + + def _setup_action_sets(self): + """设置动作集""" + if not self.vr_input: + return + + try: + # 获取默认动作集句柄 + action_set_handle = self.vr_input.getActionSetHandle(self.default_action_set) + self.action_set_handles = [action_set_handle] + + print(f"✓ 动作集已设置: {self.default_action_set}") + + except Exception as e: + print(f"⚠️ 设置动作集失败: {e}") + + def update_actions(self): + """更新动作状态 - 每帧调用""" + if not self.vr_input or not self.action_set_handles: + return + + try: + # 更新动作状态 + action_sets = (openvr.VRActiveActionSet_t * len(self.action_set_handles))() + for i, action_set_handle in enumerate(self.action_set_handles): + action_sets[i].ulActionSet = action_set_handle + + self.vr_input.updateActionState(action_sets) + + except Exception as e: + # 限制错误输出频率 + if not hasattr(self, '_last_action_error_frame'): + self._last_action_error_frame = 0 + + if hasattr(self.vr_manager, 'frame_count'): + if self.vr_manager.frame_count - self._last_action_error_frame > 300: # 每5秒输出一次 + print(f"⚠️ 更新动作状态失败: {e}") + self._last_action_error_frame = self.vr_manager.frame_count + + def is_digital_action_pressed(self, action_name, device_path=None): + """检查数字动作是否被按下 + + Args: + action_name: 动作名称 + device_path: 设备路径(可选) + + Returns: + tuple: (是否按下, 设备路径) + """ + if not self.vr_input or action_name not in self.action_handles: + return False, None + + try: + action_handle = self.action_handles[action_name] + device_handle = openvr.k_ulInvalidInputValueHandle + + if device_path: + device_handle = self.vr_input.getInputSourceHandle(device_path) + + action_data = self.vr_input.getDigitalActionData(action_handle, device_handle) + + if device_path and action_data.bActive: + origin_info = self.vr_input.getOriginTrackedDeviceInfo(action_data.activeOrigin) + device_path = origin_info.devicePath + + return action_data.bActive and action_data.bState, device_path + + except Exception as e: + return False, None + + def is_digital_action_just_pressed(self, action_name, device_path=None): + """检查数字动作是否刚刚被按下(上升沿)""" + if not self.vr_input or action_name not in self.action_handles: + return False, None + + try: + action_handle = self.action_handles[action_name] + device_handle = openvr.k_ulInvalidInputValueHandle + + if device_path: + device_handle = self.vr_input.getInputSourceHandle(device_path) + + action_data = self.vr_input.getDigitalActionData(action_handle, device_handle) + + if device_path and action_data.bActive: + origin_info = self.vr_input.getOriginTrackedDeviceInfo(action_data.activeOrigin) + device_path = origin_info.devicePath + + return action_data.bActive and action_data.bChanged and action_data.bState, device_path + + except Exception as e: + return False, None + + def is_digital_action_just_released(self, action_name, device_path=None): + """检查数字动作是否刚刚被释放(下降沿)""" + if not self.vr_input or action_name not in self.action_handles: + return False, None + + try: + action_handle = self.action_handles[action_name] + device_handle = openvr.k_ulInvalidInputValueHandle + + if device_path: + device_handle = self.vr_input.getInputSourceHandle(device_path) + + action_data = self.vr_input.getDigitalActionData(action_handle, device_handle) + + if device_path and action_data.bActive: + origin_info = self.vr_input.getOriginTrackedDeviceInfo(action_data.activeOrigin) + device_path = origin_info.devicePath + + return action_data.bActive and action_data.bChanged and not action_data.bState, device_path + + except Exception as e: + return False, None + + def get_analog_action_value(self, action_name, device_path=None): + """获取模拟动作值 + + Args: + action_name: 动作名称 + device_path: 设备路径(可选) + + Returns: + tuple: (值, 设备路径) - 值为Vec2(x,y)对于vector2,float对于vector1 + """ + if not self.vr_input or action_name not in self.action_handles: + return None, None + + try: + action_handle = self.action_handles[action_name] + device_handle = openvr.k_ulInvalidInputValueHandle + + if device_path: + device_handle = self.vr_input.getInputSourceHandle(device_path) + + analog_data = self.vr_input.getAnalogActionData(action_handle, device_handle) + + if device_path and analog_data.bActive: + origin_info = self.vr_input.getOriginTrackedDeviceInfo(analog_data.activeOrigin) + device_path = origin_info.devicePath + + if analog_data.bActive: + # 根据动作类型返回适当的值 + from panda3d.core import Vec2 + if action_name in ['trackpad', 'joystick']: + return Vec2(analog_data.x, analog_data.y), device_path + else: + return analog_data.x, device_path + + return None, device_path + + except Exception as e: + return None, None + + def get_pose_action_data(self, action_name, device_path=None): + """获取姿态动作数据""" + if not self.vr_input or action_name not in self.action_handles: + return None + + try: + action_handle = self.action_handles[action_name] + device_handle = openvr.k_ulInvalidInputValueHandle + + if device_path: + device_handle = self.vr_input.getInputSourceHandle(device_path) + + pose_data = self.vr_input.getPoseActionDataForNextFrame( + action_handle, + openvr.TrackingUniverseStanding, + device_handle + ) + + return pose_data + + except Exception as e: + return None + + def trigger_haptic_pulse(self, action_name, duration=0.001, frequency=1.0, amplitude=1.0, device_path=None): + """触发震动反馈 + + Args: + action_name: 震动动作名称 + duration: 持续时间(秒) + frequency: 频率 + amplitude: 振幅 (0.0-1.0) + device_path: 设备路径(可选) + """ + if not self.vr_input or action_name not in self.action_handles: + return False + + try: + action_handle = self.action_handles[action_name] + device_handle = openvr.k_ulInvalidInputValueHandle + + if device_path: + device_handle = self.vr_input.getInputSourceHandle(device_path) + + # 触发震动 + self.vr_input.triggerHapticVibrationAction( + action_handle, + 0, # 开始时间 + duration, + frequency, + amplitude, + device_handle + ) + + return True + + except Exception as e: + print(f"⚠️ 触发震动反馈失败: {e}") + return False + + def cleanup(self): + """清理资源""" + self.ignoreAll() + + # 清理动作句柄 + self.action_handles.clear() + self.action_set_handles.clear() + + print("🧹 VR动作管理器已清理") \ No newline at end of file diff --git a/core/vr_controller.py b/core/vr_controller.py new file mode 100644 index 00000000..3b352207 --- /dev/null +++ b/core/vr_controller.py @@ -0,0 +1,282 @@ +""" +VR手柄管理模块 + +基于panda3d-openvr参考实现,提供完整的VR手柄追踪和交互功能: +- 手柄位置和姿态追踪 +- 按钮和触摸板输入处理 +- 手柄可视化和射线显示 +- 震动反馈支持 +""" + +from panda3d.core import ( + NodePath, PandaNode, Vec3, Mat4, LVector3, LMatrix4, + GeomNode, LineSegs, CardMaker, Texture, RenderState, + TransparencyAttrib, ColorAttrib, Vec4 +) +from direct.actor.Actor import Actor +from direct.showbase.DirectObject import DirectObject + +try: + import openvr + OPENVR_AVAILABLE = True +except ImportError: + OPENVR_AVAILABLE = False + +# 导入可视化器 +from .vr_visualization import VRControllerVisualizer + + +class VRController(DirectObject): + """VR手柄基类 - 管理单个手柄的追踪和交互""" + + def __init__(self, vr_manager, name, hand_path, device_index=None): + """初始化VR手柄 + + Args: + vr_manager: VR管理器实例 + name: 手柄名称 ('left' 或 'right') + hand_path: OpenVR手部路径 ('/user/hand/left' 或 '/user/hand/right') + device_index: OpenVR设备索引(可选) + """ + super().__init__() + + self.vr_manager = vr_manager + self.name = name + self.hand_path = hand_path + self.device_index = device_index + + # 手柄状态 + self.is_connected = False + self.is_pose_valid = False + self.pose = Mat4.identMat() + self.velocity = Vec3(0, 0, 0) + self.angular_velocity = Vec3(0, 0, 0) + + # 按钮状态 + self.button_states = {} + self.previous_button_states = {} + self.trigger_value = 0.0 + self.grip_value = 0.0 + self.touchpad_pos = Vec3(0, 0, 0) + self.touchpad_touched = False + + # 3D节点和可视化 + self.anchor_node = None + self.visualizer = None + self.ray_length = 10.0 + + # 初始化 + self._create_anchor() + self._create_visualizer() + + print(f"✓ {name}手柄控制器初始化完成") + + def _create_anchor(self): + """创建手柄锚点节点""" + if self.vr_manager.tracking_space: + self.anchor_node = self.vr_manager.tracking_space.attachNewNode(f'{self.name}-controller') + self.anchor_node.hide() # 初始隐藏,直到获得有效姿态 + + def _create_visualizer(self): + """创建手柄可视化器""" + if self.anchor_node and hasattr(self.vr_manager, 'world'): + self.visualizer = VRControllerVisualizer(self, self.vr_manager.world.render) + elif self.anchor_node: + # 如果没有世界对象,使用基础渲染节点 + from panda3d.core import NodePath + render = NodePath('render') + self.visualizer = VRControllerVisualizer(self, render) + + def set_device_index(self, device_index): + """设置OpenVR设备索引""" + self.device_index = device_index + self.is_connected = True + print(f"📱 {self.name}手柄连接 (设备索引: {device_index})") + + def update_pose(self, pose_data): + """更新手柄姿态 + + Args: + pose_data: OpenVR TrackedDevicePose_t数据 + """ + if not pose_data.bPoseIsValid: + self.is_pose_valid = False + if self.anchor_node: + self.anchor_node.hide() + return + + self.is_pose_valid = True + + # 转换OpenVR矩阵到Panda3D + if hasattr(self.vr_manager, 'convert_mat') and hasattr(self.vr_manager, 'coord_mat_inv') and hasattr(self.vr_manager, 'coord_mat'): + modelview = self.vr_manager.convert_mat(pose_data.mDeviceToAbsoluteTracking) + self.pose = self.vr_manager.coord_mat_inv * modelview * self.vr_manager.coord_mat + else: + # 直接使用矩阵数据 + m = pose_data.mDeviceToAbsoluteTracking.m + self.pose = LMatrix4( + m[0][0], m[1][0], m[2][0], m[3][0], + m[0][1], m[1][1], m[2][1], m[3][1], + m[0][2], m[1][2], m[2][2], m[3][2], + m[0][3], m[1][3], m[2][3], m[3][3] + ) + + # 更新锚点变换 + if self.anchor_node: + self.anchor_node.setMat(self.pose) + self.anchor_node.show() + + # 更新可视化 + if self.visualizer: + self.visualizer.update() + + # 更新速度信息 + vel = pose_data.vVelocity + self.velocity = Vec3(vel[0], vel[1], vel[2]) + + ang_vel = pose_data.vAngularVelocity + self.angular_velocity = Vec3(ang_vel[0], ang_vel[1], ang_vel[2]) + + def update_input_state(self, vr_system): + """更新输入状态 + + Args: + vr_system: OpenVR系统实例 + """ + if not self.is_connected or not OPENVR_AVAILABLE or not vr_system: + return + + # 保存上一帧的按钮状态 + self.previous_button_states = self.button_states.copy() + + # 获取控制器状态 + try: + result, state = vr_system.getControllerState(self.device_index) + if result: + # 更新按钮状态 + for i in range(openvr.k_EButton_Max): + button_mask = 1 << i + self.button_states[i] = (state.rButtonPressed & button_mask) != 0 + + # 更新轴状态(扳机、握把、触摸板) + if len(state.rAxis) > 0: + # 扳机轴通常在axis[1].x + if len(state.rAxis) > 1: + self.trigger_value = state.rAxis[1].x + + # 触摸板轴通常在axis[0] + if len(state.rAxis) > 0: + self.touchpad_pos = Vec3(state.rAxis[0].x, state.rAxis[0].y, 0) + + # 触摸板触摸状态 + self.touchpad_touched = (state.rButtonTouched & (1 << openvr.k_EButton_SteamVR_Touchpad)) != 0 + + except Exception as e: + print(f"⚠️ 更新{self.name}手柄输入状态失败: {e}") + + def is_button_pressed(self, button_id): + """检查按钮是否被按下""" + return self.button_states.get(button_id, False) + + def is_button_just_pressed(self, button_id): + """检查按钮是否刚刚被按下(上升沿)""" + current = self.button_states.get(button_id, False) + previous = self.previous_button_states.get(button_id, False) + return current and not previous + + def is_button_just_released(self, button_id): + """检查按钮是否刚刚被释放(下降沿)""" + current = self.button_states.get(button_id, False) + previous = self.previous_button_states.get(button_id, False) + return not current and previous + + def is_trigger_pressed(self, threshold=0.1): + """检查扳机是否被按下""" + return self.trigger_value > threshold + + def is_grip_pressed(self, threshold=0.1): + """检查握把是否被按下""" + return self.grip_value > threshold + + def show_ray(self, show=True): + """显示或隐藏交互射线""" + if self.visualizer: + if show: + self.visualizer.show_ray() + else: + self.visualizer.hide_ray() + + def set_ray_color(self, color): + """设置射线颜色""" + if self.visualizer and len(color) >= 3: + from panda3d.core import Vec4 + color_vec = Vec4(color[0], color[1], color[2], color[3] if len(color) > 3 else 1.0) + self.visualizer.set_ray_color(color_vec) + + def trigger_haptic_feedback(self, duration=0.001, strength=1.0): + """触发震动反馈 + + Args: + duration: 震动持续时间(秒) + strength: 震动强度 (0.0-1.0) + """ + if not self.is_connected or not OPENVR_AVAILABLE: + return + + try: + if hasattr(self.vr_manager, 'vr_system') and self.vr_manager.vr_system: + # OpenVR的震动API + duration_microseconds = int(duration * 1000000) + self.vr_manager.vr_system.triggerHapticPulse( + self.device_index, + 0, # axis ID (通常为0) + int(strength * 3999) # 强度 (0-3999) + ) + except Exception as e: + print(f"⚠️ {self.name}手柄震动反馈失败: {e}") + + def get_world_position(self): + """获取手柄在世界坐标系中的位置""" + if self.anchor_node: + return self.anchor_node.getPos(self.vr_manager.world.render) + return Vec3(0, 0, 0) + + def get_world_rotation(self): + """获取手柄在世界坐标系中的旋转""" + if self.anchor_node: + return self.anchor_node.getHpr(self.vr_manager.world.render) + return Vec3(0, 0, 0) + + def get_forward_direction(self): + """获取手柄指向的方向向量""" + if self.anchor_node: + # Y轴正方向为前方 + return self.anchor_node.getMat().getRow3(1).getXyz().normalized() + return Vec3(0, 1, 0) + + def cleanup(self): + """清理资源""" + self.ignoreAll() + + if self.visualizer: + self.visualizer.cleanup() + + if self.anchor_node: + self.anchor_node.removeNode() + + self.is_connected = False + print(f"🧹 {self.name}手柄控制器已清理") + + +class LeftController(VRController): + """左手控制器""" + + def __init__(self, vr_manager): + super().__init__(vr_manager, 'left', '/user/hand/left') + + +class RightController(VRController): + """右手控制器""" + + def __init__(self, vr_manager): + super().__init__(vr_manager, 'right', '/user/hand/right') \ No newline at end of file diff --git a/core/vr_interaction.py b/core/vr_interaction.py new file mode 100644 index 00000000..e5544ecc --- /dev/null +++ b/core/vr_interaction.py @@ -0,0 +1,432 @@ +""" +VR交互系统模块 + +提供VR手柄与3D场景的交互功能: +- 射线投射和碰撞检测 +- 对象选择和高亮 +- 对象抓取和移动 +- UI交互 +- 距离抓取 +""" + +from panda3d.core import ( + Vec3, Vec4, Mat4, Point3, CollisionRay, CollisionTraverser, + CollisionNode, CollisionHandlerQueue, BitMask32, NodePath, + CollisionSphere, CollisionTube, RenderState, TransparencyAttrib, + ColorAttrib +) +from direct.showbase.DirectObject import DirectObject + + +class VRInteractionManager(DirectObject): + """VR交互管理器 - 处理手柄与场景的交互""" + + def __init__(self, vr_manager): + """初始化VR交互管理器 + + Args: + vr_manager: VR管理器实例 + """ + super().__init__() + + self.vr_manager = vr_manager + self.world = vr_manager.world if hasattr(vr_manager, 'world') else None + + # 碰撞检测系统 + self.collision_traverser = CollisionTraverser() + self.collision_queue = CollisionHandlerQueue() + + # 射线投射节点 + self.left_ray_node = None + self.right_ray_node = None + self.ray_collision_nodes = {} + + # 选择和抓取状态 + self.selected_objects = {} # 控制器 -> 选中对象 + self.grabbed_objects = {} # 控制器 -> 抓取对象 + self.grab_offsets = {} # 控制器 -> 抓取偏移 + + # 交互参数 + self.selection_range = 50.0 # 选择距离 + self.grab_threshold = 0.5 # 抓取扳机阈值 + self.selection_color = Vec4(0.9, 0.9, 0.2, 1.0) # 选择高亮颜色 + self.grab_color = Vec4(0.2, 0.9, 0.2, 1.0) # 抓取高亮颜色 + + # 高亮状态 + self.highlighted_objects = set() + self.original_colors = {} # 存储对象原始颜色 + + print("✓ VR交互管理器初始化完成") + + def initialize(self): + """初始化交互系统""" + try: + print("🔧 正在初始化VR交互系统...") + + # 创建射线投射节点 + self._create_ray_casters() + + # 设置碰撞检测 + self._setup_collision_detection() + + print("✅ VR交互系统初始化成功") + return True + + except Exception as e: + print(f"❌ VR交互系统初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_ray_casters(self): + """创建射线投射节点""" + # 为左手控制器创建射线 + if self.vr_manager.left_controller and self.vr_manager.left_controller.anchor_node: + self.left_ray_node = self._create_controller_ray('left', self.vr_manager.left_controller.anchor_node) + + # 为右手控制器创建射线 + if self.vr_manager.right_controller and self.vr_manager.right_controller.anchor_node: + self.right_ray_node = self._create_controller_ray('right', self.vr_manager.right_controller.anchor_node) + + def _create_controller_ray(self, controller_name, anchor_node): + """为控制器创建射线投射节点""" + # 创建射线碰撞体 + ray = CollisionRay() + ray.setOrigin(0, 0, 0) # 从控制器原点开始 + ray.setDirection(0, 1, 0) # 沿Y轴正方向 + + # 创建碰撞节点 + ray_collision_node = CollisionNode(f'{controller_name}_ray') + ray_collision_node.addSolid(ray) + + # 设置碰撞掩码 + ray_collision_node.setFromCollideMask(BitMask32.bit(0)) # 射线掩码 + ray_collision_node.setIntoCollideMask(BitMask32.allOff()) # 不接受碰撞 + + # 附加到控制器锚点 + ray_node = anchor_node.attachNewNode(ray_collision_node) + self.ray_collision_nodes[controller_name] = ray_collision_node + + # 注册到碰撞遍历器 + self.collision_traverser.addCollider(ray_node, self.collision_queue) + + print(f"✓ {controller_name}手控制器射线投射已创建") + return ray_node + + def _setup_collision_detection(self): + """设置碰撞检测系统""" + if self.world: + # 使用世界的碰撞系统 + if hasattr(self.world, 'render'): + # 为所有可交互对象设置碰撞体 + self._setup_scene_collision_objects() + else: + print("⚠️ 无法访问世界对象,跳过场景碰撞设置") + + def _setup_scene_collision_objects(self): + """为场景对象设置碰撞体""" + if not self.world or not hasattr(self.world, 'render'): + return + + try: + # 遍历场景中的所有节点,为它们添加碰撞体 + for node_path in self.world.render.findAllMatches("**/+GeomNode"): + self._add_collision_to_object(node_path) + + except Exception as e: + print(f"⚠️ 设置场景碰撞对象失败: {e}") + + def _add_collision_to_object(self, node_path): + """为对象添加碰撞体""" + try: + # 获取对象的边界框 + bounds = node_path.getBounds() + if bounds.isEmpty(): + return + + # 计算边界球 + center = bounds.getCenter() + radius = bounds.getRadius() + + # 创建球形碰撞体 + collision_sphere = CollisionSphere(center, radius) + + # 创建碰撞节点 + collision_node = CollisionNode(f'{node_path.getName()}_collision') + collision_node.addSolid(collision_sphere) + + # 设置碰撞掩码 + collision_node.setIntoCollideMask(BitMask32.bit(0)) # 接受射线碰撞 + collision_node.setFromCollideMask(BitMask32.allOff()) # 不发射射线 + + # 附加碰撞节点 + collision_node_path = node_path.attachNewNode(collision_node) + + # 标记为可交互对象 + node_path.setTag('interactable', 'true') + node_path.setTag('original_name', node_path.getName()) + + except Exception as e: + print(f"⚠️ 为对象 {node_path.getName()} 添加碰撞体失败: {e}") + + def update(self): + """更新交互系统 - 每帧调用""" + if not self.vr_manager.are_controllers_connected(): + return + + # 执行碰撞检测 + self._perform_collision_detection() + + # 更新选择状态 + self._update_selections() + + # 更新抓取状态 + self._update_grabbing() + + def _perform_collision_detection(self): + """执行碰撞检测""" + if self.world and hasattr(self.world, 'render'): + self.collision_traverser.traverse(self.world.render) + + def _update_selections(self): + """更新对象选择状态""" + # 清除之前的选择高亮 + self._clear_selection_highlights() + + # 检查每个控制器的选择 + for controller in self.vr_manager.get_connected_controllers(): + if not controller: + continue + + # 获取最近的碰撞对象 + hit_object = self._get_closest_hit_object(controller.name) + + if hit_object: + # 高亮选中的对象 + self._highlight_object(hit_object, self.selection_color) + self.selected_objects[controller.name] = hit_object + + # 显示控制器射线 + controller.show_ray(True) + controller.set_ray_color([0.9, 0.9, 0.2, 0.8]) # 黄色 + else: + # 没有选中对象 + if controller.name in self.selected_objects: + del self.selected_objects[controller.name] + + # 隐藏射线(除非正在抓取) + if controller.name not in self.grabbed_objects: + controller.show_ray(False) + + def _get_closest_hit_object(self, controller_name): + """获取指定控制器射线最近的碰撞对象""" + if controller_name not in self.ray_collision_nodes: + return None + + closest_object = None + closest_distance = float('inf') + + # 检查碰撞队列中的条目 + for i in range(self.collision_queue.getNumEntries()): + entry = self.collision_queue.getEntry(i) + + # 检查是否是该控制器的射线 + from_node = entry.getFromNodePath() + if from_node.node() == self.ray_collision_nodes[controller_name]: + # 获取碰撞的对象 + hit_node_path = entry.getIntoNodePath() + + # 获取实际的几何对象(父节点) + geom_object = hit_node_path.getParent() + if geom_object and geom_object.hasTag('interactable'): + distance = entry.getSurfacePoint(geom_object).length() + + if distance < closest_distance and distance <= self.selection_range: + closest_distance = distance + closest_object = geom_object + + return closest_object + + def _update_grabbing(self): + """更新对象抓取状态""" + for controller in self.vr_manager.get_connected_controllers(): + if not controller: + continue + + controller_name = controller.name + + # 检查是否按下抓取按钮 + if controller.is_trigger_pressed(threshold=self.grab_threshold): + # 如果还没有抓取对象 + if controller_name not in self.grabbed_objects: + # 尝试抓取选中的对象 + if controller_name in self.selected_objects: + selected_obj = self.selected_objects[controller_name] + self._start_grab(controller, selected_obj) + + # 如果正在抓取,更新对象位置 + if controller_name in self.grabbed_objects: + self._update_grabbed_object(controller) + + else: + # 释放抓取 + if controller_name in self.grabbed_objects: + self._release_grab(controller) + + def _start_grab(self, controller, obj): + """开始抓取对象""" + controller_name = controller.name + + try: + # 计算抓取偏移(对象相对于控制器的位置) + controller_pos = controller.get_world_position() + object_pos = obj.getPos(self.world.render if self.world else obj.getParent()) + + offset = object_pos - controller_pos + self.grab_offsets[controller_name] = offset + + # 记录抓取状态 + self.grabbed_objects[controller_name] = obj + + # 改变对象颜色表示抓取状态 + self._highlight_object(obj, self.grab_color) + + # 触发震动反馈 + controller.trigger_haptic_feedback(0.01, 0.8) + + # 显示绿色射线表示抓取 + controller.show_ray(True) + controller.set_ray_color([0.2, 0.9, 0.2, 0.8]) + + print(f"🤏 {controller_name}手开始抓取对象: {obj.getName()}") + + except Exception as e: + print(f"⚠️ 开始抓取失败: {e}") + + def _update_grabbed_object(self, controller): + """更新被抓取对象的位置""" + controller_name = controller.name + + if controller_name not in self.grabbed_objects: + return + + try: + grabbed_obj = self.grabbed_objects[controller_name] + grab_offset = self.grab_offsets.get(controller_name, Vec3(0, 0, 0)) + + # 计算新位置 + controller_pos = controller.get_world_position() + new_pos = controller_pos + grab_offset + + # 更新对象位置 + grabbed_obj.setPos(self.world.render if self.world else grabbed_obj.getParent(), new_pos) + + # 可选:同步旋转 + if hasattr(controller, 'get_world_rotation'): + controller_rot = controller.get_world_rotation() + grabbed_obj.setHpr(self.world.render if self.world else grabbed_obj.getParent(), controller_rot) + + except Exception as e: + print(f"⚠️ 更新抓取对象失败: {e}") + + def _release_grab(self, controller): + """释放抓取的对象""" + controller_name = controller.name + + if controller_name not in self.grabbed_objects: + return + + try: + grabbed_obj = self.grabbed_objects[controller_name] + + # 恢复对象原始颜色 + self._restore_object_color(grabbed_obj) + + # 清理抓取状态 + del self.grabbed_objects[controller_name] + if controller_name in self.grab_offsets: + del self.grab_offsets[controller_name] + + # 触发震动反馈 + controller.trigger_haptic_feedback(0.005, 0.4) + + print(f"🫳 {controller_name}手释放对象: {grabbed_obj.getName()}") + + except Exception as e: + print(f"⚠️ 释放抓取失败: {e}") + + def _highlight_object(self, obj, color): + """高亮显示对象""" + if obj in self.highlighted_objects: + return + + try: + # 保存原始颜色 + if obj not in self.original_colors: + self.original_colors[obj] = obj.getColor() + + # 设置高亮颜色 + obj.setColor(color) + self.highlighted_objects.add(obj) + + except Exception as e: + print(f"⚠️ 高亮对象失败: {e}") + + def _restore_object_color(self, obj): + """恢复对象原始颜色""" + if obj not in self.highlighted_objects: + return + + try: + # 恢复原始颜色 + if obj in self.original_colors: + obj.setColor(self.original_colors[obj]) + del self.original_colors[obj] + + self.highlighted_objects.discard(obj) + + except Exception as e: + print(f"⚠️ 恢复对象颜色失败: {e}") + + def _clear_selection_highlights(self): + """清除所有选择高亮""" + for obj in list(self.highlighted_objects): + # 只清除非抓取状态的对象 + is_grabbed = any(obj == grabbed_obj for grabbed_obj in self.grabbed_objects.values()) + if not is_grabbed: + self._restore_object_color(obj) + + def get_selected_object(self, controller_name): + """获取指定控制器选中的对象""" + return self.selected_objects.get(controller_name) + + def get_grabbed_object(self, controller_name): + """获取指定控制器抓取的对象""" + return self.grabbed_objects.get(controller_name) + + def is_grabbing(self, controller_name): + """检查指定控制器是否正在抓取对象""" + return controller_name in self.grabbed_objects + + def force_release_all(self): + """强制释放所有抓取的对象""" + for controller in self.vr_manager.get_connected_controllers(): + if controller and controller.name in self.grabbed_objects: + self._release_grab(controller) + + def cleanup(self): + """清理资源""" + self.ignoreAll() + + # 释放所有抓取 + self.force_release_all() + + # 清理碰撞系统 + self.collision_traverser.clearColliders() + self.ray_collision_nodes.clear() + + # 清理高亮状态 + for obj in list(self.highlighted_objects): + self._restore_object_color(obj) + + print("🧹 VR交互管理器已清理") \ No newline at end of file diff --git a/core/vr_manager.py b/core/vr_manager.py index 53f19054..dbd9f59d 100644 --- a/core/vr_manager.py +++ b/core/vr_manager.py @@ -27,6 +27,11 @@ except ImportError: OPENVR_AVAILABLE = False print("警告: OpenVR未安装,VR功能将不可用") +# 导入手柄控制器、动作系统和交互系统 +from .vr_controller import LeftController, RightController +from .vr_actions import VRActionManager +from .vr_interaction import VRInteractionManager + class VRManager(DirectObject): """VR管理器类 - 处理所有VR相关功能""" @@ -87,6 +92,18 @@ class VRManager(DirectObject): # VR提交策略 - 基于参考实现 self.submit_together = True # 是否在right_cb中同时提交左右眼 + # VR手柄控制器 + self.left_controller = None + self.right_controller = None + self.controllers = {} # 设备索引到控制器的映射 + self.tracked_device_anchors = {} # 跟踪设备锚点 + + # VR动作系统 + self.action_manager = VRActionManager(self) + + # VR交互系统 + self.interaction_manager = VRInteractionManager(self) + print("✓ VR管理器初始化完成") def convert_mat(self, mat): @@ -163,6 +180,17 @@ class VRManager(DirectObject): print("❌ 设置VR相机失败") return False + # 初始化手柄控制器 + self._initialize_controllers() + + # 初始化动作系统 + if not self.action_manager.initialize(): + print("⚠️ VR动作系统初始化失败,但VR系统将继续运行") + + # 初始化交互系统 + if not self.interaction_manager.initialize(): + print("⚠️ VR交互系统初始化失败,但VR系统将继续运行") + # 启动VR更新任务 self._start_vr_task() @@ -368,6 +396,15 @@ class VRManager(DirectObject): # 2. 更新相机位置(使用刚获取的最新姿态数据) self._update_camera_poses() + # 3. 更新手柄和其他跟踪设备 + self.update_tracked_devices() + + # 4. 更新VR动作状态 + self.action_manager.update_actions() + + # 5. 更新VR交互系统 + self.interaction_manager.update() + # 注意:纹理提交现在通过渲染回调自动处理 # 定期输出性能报告 @@ -929,4 +966,314 @@ class VRManager(DirectObject): print("✓ 主相机已恢复") except Exception as e: - print(f"⚠️ 恢复主相机失败: {e}") \ No newline at end of file + print(f"⚠️ 恢复主相机失败: {e}") + + def _initialize_controllers(self): + """初始化VR手柄控制器""" + try: + print("🎮 正在初始化VR手柄控制器...") + + # 创建左右手柄控制器实例 + self.left_controller = LeftController(self) + self.right_controller = RightController(self) + + # 检测现有连接的控制器 + self._detect_controllers() + + print("✓ VR手柄控制器初始化完成") + + except Exception as e: + print(f"⚠️ VR手柄初始化失败: {e}") + import traceback + traceback.print_exc() + + def _detect_controllers(self): + """检测并连接VR控制器""" + if not self.vr_system: + return + + try: + for device_index in range(openvr.k_unMaxTrackedDeviceCount): + # 检查设备是否已连接 + if not self.vr_system.isTrackedDeviceConnected(device_index): + continue + + # 获取设备类型 + device_class = self.vr_system.getTrackedDeviceClass(device_index) + + if device_class == openvr.TrackedDeviceClass_Controller: + # 获取控制器角色 + role = self.vr_system.getControllerRoleForTrackedDeviceIndex(device_index) + + if role == openvr.TrackedControllerRole_LeftHand and self.left_controller: + self.left_controller.set_device_index(device_index) + self.controllers[device_index] = self.left_controller + # 为设备创建锚点 + self._create_tracked_device_anchor(device_index, 'left_controller') + + elif role == openvr.TrackedControllerRole_RightHand and self.right_controller: + self.right_controller.set_device_index(device_index) + self.controllers[device_index] = self.right_controller + # 为设备创建锚点 + self._create_tracked_device_anchor(device_index, 'right_controller') + + print(f"🎮 检测到 {len(self.controllers)} 个控制器") + + except Exception as e: + print(f"⚠️ 控制器检测失败: {e}") + + def _create_tracked_device_anchor(self, device_index, name): + """为跟踪设备创建锚点节点""" + if not self.tracking_space: + print(f"⚠️ 无法为设备 {device_index} 创建锚点 - tracking_space未初始化") + return + + try: + # 获取设备模型名称 + if self.vr_system: + model_name = self.vr_system.getStringTrackedDeviceProperty( + device_index, + openvr.Prop_RenderModelName_String + ) + anchor_name = f"{device_index}:{model_name}:{name}" + else: + anchor_name = f"{device_index}:{name}" + + # 创建锚点节点 + device_anchor = self.tracking_space.attachNewNode(anchor_name) + self.tracked_device_anchors[device_index] = device_anchor + + print(f"✓ 为设备 {device_index} 创建锚点: {anchor_name}") + + except Exception as e: + print(f"⚠️ 创建设备锚点失败: {e}") + + def update_tracked_devices(self): + """更新所有跟踪设备的姿态 - 基于参考实现""" + if not self.poses or not self.vr_system: + return + + try: + # 更新每个已连接的控制器 + for device_index, controller in self.controllers.items(): + if device_index < len(self.poses): + pose_data = self.poses[device_index] + + # 更新控制器姿态 + controller.update_pose(pose_data) + + # 更新控制器输入状态 + controller.update_input_state(self.vr_system) + + # 更新其他跟踪设备的锚点 + for device_index in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): + if device_index in self.tracked_device_anchors: + pose_data = self.poses[device_index] + if pose_data.bPoseIsValid: + # 转换姿态矩阵 + modelview = self.convert_mat(pose_data.mDeviceToAbsoluteTracking) + final_matrix = self.coord_mat_inv * modelview * self.coord_mat + + # 更新锚点变换 + anchor = self.tracked_device_anchors[device_index] + anchor.setMat(final_matrix) + anchor.show() + else: + # 姿态无效,隐藏锚点 + self.tracked_device_anchors[device_index].hide() + + except Exception as e: + if self.frame_count % 300 == 0: # 每5秒输出一次错误 + print(f"⚠️ 更新跟踪设备失败: {e}") + + def get_controller_by_role(self, role): + """根据角色获取控制器 + + Args: + role: 'left' 或 'right' + + Returns: + VRController实例或None + """ + if role == 'left': + return self.left_controller + elif role == 'right': + return self.right_controller + return None + + def are_controllers_connected(self): + """检查是否有控制器连接""" + return len(self.controllers) > 0 + + def get_connected_controllers(self): + """获取所有连接的控制器列表""" + return list(self.controllers.values()) + + def trigger_controller_haptic(self, role, duration=0.001, strength=1.0): + """触发控制器震动反馈 + + Args: + role: 'left', 'right' 或 'both' + duration: 震动持续时间(秒) + strength: 震动强度 (0.0-1.0) + """ + if role in ['left', 'both'] and self.left_controller: + self.left_controller.trigger_haptic_feedback(duration, strength) + + if role in ['right', 'both'] and self.right_controller: + self.right_controller.trigger_haptic_feedback(duration, strength) + + # VR动作系统便捷方法 + def is_trigger_pressed(self, hand='any'): + """检查扳机是否被按下 + + Args: + hand: 'left', 'right', 'any' + """ + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + pressed, _ = self.action_manager.is_digital_action_pressed('trigger', device_path) + return pressed + + def is_trigger_just_pressed(self, hand='any'): + """检查扳机是否刚刚被按下""" + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + pressed, _ = self.action_manager.is_digital_action_just_pressed('trigger', device_path) + return pressed + + def is_grip_pressed(self, hand='any'): + """检查握把是否被按下""" + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + pressed, _ = self.action_manager.is_digital_action_pressed('grip', device_path) + return pressed + + def is_grip_just_pressed(self, hand='any'): + """检查握把是否刚刚被按下""" + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + pressed, _ = self.action_manager.is_digital_action_just_pressed('grip', device_path) + return pressed + + def is_menu_pressed(self, hand='any'): + """检查菜单按钮是否被按下""" + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + pressed, _ = self.action_manager.is_digital_action_pressed('menu', device_path) + return pressed + + def is_trackpad_touched(self, hand='any'): + """检查触摸板是否被触摸""" + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + touched, _ = self.action_manager.is_digital_action_pressed('trackpad_touch', device_path) + return touched + + def get_trackpad_position(self, hand='any'): + """获取触摸板位置 + + Returns: + Vec2或None: 触摸板位置 (-1到1的范围) + """ + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + value, _ = self.action_manager.get_analog_action_value('trackpad', device_path) + return value + + # VR交互系统便捷方法 + def get_selected_object(self, hand='any'): + """获取指定手选中的对象 + + Args: + hand: 'left', 'right', 'any' + + Returns: + 选中的对象节点或None + """ + if hand == 'any': + # 返回任意手选中的对象 + for controller in self.get_connected_controllers(): + selected = self.interaction_manager.get_selected_object(controller.name) + if selected: + return selected + return None + else: + return self.interaction_manager.get_selected_object(hand) + + def get_grabbed_object(self, hand='any'): + """获取指定手抓取的对象 + + Args: + hand: 'left', 'right', 'any' + + Returns: + 抓取的对象节点或None + """ + if hand == 'any': + # 返回任意手抓取的对象 + for controller in self.get_connected_controllers(): + grabbed = self.interaction_manager.get_grabbed_object(controller.name) + if grabbed: + return grabbed + return None + else: + return self.interaction_manager.get_grabbed_object(hand) + + def is_grabbing_object(self, hand='any'): + """检查是否正在抓取对象 + + Args: + hand: 'left', 'right', 'any' + + Returns: + bool: 是否正在抓取 + """ + if hand == 'any': + # 检查任意手是否正在抓取 + for controller in self.get_connected_controllers(): + if self.interaction_manager.is_grabbing(controller.name): + return True + return False + else: + return self.interaction_manager.is_grabbing(hand) + + def force_release_all_grabs(self): + """强制释放所有抓取的对象""" + self.interaction_manager.force_release_all() + + def add_interactable_object(self, object_node): + """将对象标记为可交互 + + Args: + object_node: 要标记的对象节点 + """ + self.interaction_manager._add_collision_to_object(object_node) \ No newline at end of file diff --git a/core/vr_visualization.py b/core/vr_visualization.py new file mode 100644 index 00000000..368cda81 --- /dev/null +++ b/core/vr_visualization.py @@ -0,0 +1,658 @@ +""" +VR可视化模块 + +提供VR手柄和交互元素的高级可视化功能: +- 手柄3D模型渲染 +- 交互射线显示 +- 按钮状态可视化 +- 触摸板和扳机反馈 +""" + +from panda3d.core import ( + NodePath, GeomNode, LineSegs, CardMaker, Geom, GeomVertexData, + GeomVertexFormat, GeomVertexWriter, GeomTriangles, GeomPoints, + Vec3, Vec4, Mat4, TransparencyAttrib, RenderState, ColorAttrib, + InternalName, loadPrcFileData +) +from panda3d.core import Texture, Material, TextureStage + +# 启用Assimp支持OBJ文件加载 +loadPrcFileData("", "load-file-type p3assimp") + + +class VRControllerVisualizer: + """VR手柄可视化器""" + + def __init__(self, controller, render_node): + """初始化手柄可视化器 + + Args: + controller: VRController实例 + render_node: 渲染节点 + """ + self.controller = controller + self.render = render_node + + # 可视化节点 + self.visual_node = None + self.model_node = None + self.ray_node = None + self.button_indicator_node = None + + # 射线参数 + self.ray_length = 10.0 + self.ray_color = Vec4(0.9, 0.9, 0.2, 0.8) + self.ray_hit_color = Vec4(0.2, 0.9, 0.2, 0.8) + + # 按钮指示器参数 + self.button_colors = { + 'normal': Vec4(0.3, 0.3, 0.8, 1.0), + 'left': Vec4(0.2, 0.6, 0.9, 1.0), + 'right': Vec4(0.9, 0.3, 0.3, 1.0), + 'trigger': Vec4(0.9, 0.6, 0.2, 1.0), + 'grip': Vec4(0.6, 0.9, 0.3, 1.0), + 'menu': Vec4(0.9, 0.2, 0.9, 1.0), + 'trackpad': Vec4(0.2, 0.9, 0.9, 1.0) + } + + self._create_visual_components() + + def _create_visual_components(self): + """创建可视化组件""" + if not self.controller.anchor_node: + return + + # 创建主可视化节点 + self.visual_node = self.controller.anchor_node.attachNewNode(f'{self.controller.name}_visual') + + # 创建手柄模型 + self._create_controller_model() + + # 创建交互射线 + self._create_interaction_ray() + + # 暂时注释按钮指示器功能,避免额外几何体造成悬空零件 + # self._create_button_indicators() + + def _create_controller_model(self): + """创建手柄3D模型""" + if not self.visual_node: + return + + # 创建模型节点 + self.model_node = self.visual_node.attachNewNode(f'{self.controller.name}_model') + + # 尝试加载SteamVR官方模型 + steamvr_model = self._load_steamvr_model() + + if steamvr_model: + # 使用SteamVR官方模型 + steamvr_model.reparentTo(self.model_node) + + # 应用SteamVR配置中的正确旋转值,绕Y轴(俯仰轴)旋转90度 + # body组件的rotate_xyz: [5.037,0.0,0.0],再加上绕Y轴旋转90度 + # 右手正确,左手需要反向 + if self.controller.name == 'left': + # 左手控制器:绕Y轴俯仰+90度(修正反向) + steamvr_model.setHpr(0, 5.037 + 90, 0) + else: + # 右手控制器:绕Y轴俯仰+90度(保持不变) + steamvr_model.setHpr(0, 5.037 + 90, 0) + + # 设置合适的缩放值 + steamvr_model.setScale(1.0) + + # 打印实际应用的旋转值 + if self.controller.name == 'left': + print(f"🔧 {self.controller.name}手柄:缩放: 1.0,旋转: (0, {5.037 + 90}, 0) [Y轴俯仰+90度]") + else: + print(f"🔧 {self.controller.name}手柄:缩放: 1.0,旋转: (0, {5.037 + 90}, 0) [Y轴俯仰+90度]") + + # 修复纯黑色问题:重新设置材质属性 + self._fix_model_material(steamvr_model) + + # 暂时注释身份标记功能,避免额外几何体造成悬空零件 + # self._apply_controller_identity_marker(steamvr_model) + + # 设置手柄始终显示在上层 + self._set_always_on_top(steamvr_model) + + print(f"✅ {self.controller.name}手柄已加载SteamVR官方模型(缩放: 1.0,实体渲染模式)") + else: + # 降级到改进的程序化模型 + self._create_fallback_model() + print(f"⚠️ {self.controller.name}手柄使用程序化模型(未找到SteamVR模型)") + + def _load_steamvr_model(self): + """加载SteamVR官方手柄模型""" + import os + from panda3d.core import Filename, Texture, TextureStage + + # SteamVR模型基础路径 + steamvr_base_paths = [ + "/home/hello/.local/share/Steam/steamapps/common/SteamVR/resources/rendermodels/vr_controller_vive_1_5", + "/home/hello/.steam/steam/steamapps/common/SteamVR/resources/rendermodels/vr_controller_vive_1_5", + "~/.local/share/Steam/steamapps/common/SteamVR/resources/rendermodels/vr_controller_vive_1_5" + ] + + for base_path in steamvr_base_paths: + expanded_base_path = os.path.expanduser(base_path) + if os.path.exists(expanded_base_path): + print(f"🔍 找到SteamVR模型目录: {expanded_base_path}") + + # 不再添加目录到搜索路径,避免自动加载多余组件 + # from panda3d.core import getModelPath + # getModelPath().appendDirectory(expanded_base_path) + + # 尝试加载不同的模型文件,按优先级排序 + model_files = [ + ("body.obj", "手柄主体"), # 最重要的部分 + ("vr_controller_vive_1_5.obj", "完整手柄模型"), # 组合模型 + ] + + for model_file, description in model_files: + model_path = os.path.join(expanded_base_path, model_file) + if os.path.exists(model_path): + try: + print(f"🎮 尝试加载{description}: {model_file}") + + # 加载主模型 + model = loader.loadModel(Filename.fromOsSpecific(model_path)) + if model: + # 先应用纹理,再修复材质(保持纹理效果) + self._apply_steamvr_textures(model, expanded_base_path) + print(f"✅ 成功加载{description}") + return model + else: + print(f"⚠️ 模型文件存在但加载失败: {model_file}") + + except Exception as e: + print(f"❌ 加载{description}失败: {e}") + continue + + # 不再尝试组合加载多个部件,避免悬空零件问题 + print("⚠️ 单个模型文件加载失败,跳过组合加载以避免悬空零件") + break + + print("❌ 未找到任何SteamVR模型目录") + return None + + def _fix_model_material(self, model): + """修复模型材质,解决纯黑色问题同时保持纹理""" + from panda3d.core import Material, MaterialAttrib + + # 检查模型是否有纹理 + has_texture = model.hasTexture() + + # 创建新的材质 + material = Material() + + if has_texture: + # 有纹理时,设置材质以增强纹理效果 + material.setDiffuse((1.0, 1.0, 1.0, 1.0)) # 白色漫反射让纹理完全显示 + material.setAmbient((0.4, 0.4, 0.4, 1.0)) # 适度环境光 + material.setSpecular((0.3, 0.3, 0.3, 1.0)) # 轻度高光 + material.setShininess(16.0) # 中等光泽度 + print(f"🎨 {self.controller.name}手柄:已修复材质(保持纹理效果)") + else: + # 无纹理时,设置合适的基础颜色 + material.setDiffuse((0.7, 0.7, 0.8, 1.0)) # 略偏蓝的灰色 + material.setAmbient((0.3, 0.3, 0.3, 1.0)) # 环境光 + material.setSpecular((0.5, 0.5, 0.5, 1.0)) # 高光 + material.setShininess(32.0) # 光泽度 + print(f"🎨 {self.controller.name}手柄:已修复材质(使用颜色)") + + # 应用材质到模型 + model.setMaterial(material) + + # 确保模型能正确渲染 + model.setTwoSided(False) + + def _apply_controller_identity_marker(self, model): + """为控制器添加身份标记,区分左右手""" + from panda3d.core import RenderModeAttrib + + # 创建一个小的标识几何体 + marker_geom = self._create_box_geometry(0.005, 0.005, 0.02) + marker_node = model.attachNewNode(marker_geom) + + # 根据左右手设置不同位置和颜色(现在都是Y轴+90度俯仰) + if self.controller.name == 'left': + # 左手控制器:左侧标记 + marker_node.setPos(-0.03, 0.05, 0.02) + marker_node.setColor(0.2, 0.4, 1.0, 1.0) # 蓝色 + print(f"🔵 {self.controller.name}手柄已添加蓝色身份标记") + else: + # 右手控制器:右侧标记 + marker_node.setPos(0.03, 0.05, 0.02) + marker_node.setColor(1.0, 0.2, 0.2, 1.0) # 红色 + print(f"🔴 {self.controller.name}手柄已添加红色身份标记") + + # 让标记发光以便更容易看到 + marker_node.setLightOff() # 不受光照影响,保持明亮 + + # 添加轻微的色彩调整(非常微弱,不影响主要纹理) + if self.controller.name == 'left': + model.setColorScale(0.98, 0.98, 1.02, 1.0) # 极轻微的蓝色调 + else: + model.setColorScale(1.02, 0.98, 0.98, 1.0) # 极轻微的红色调 + + + def _apply_steamvr_textures(self, model, base_path): + """为SteamVR模型应用纹理""" + import os + from panda3d.core import Texture, TextureStage + + # SteamVR纹理文件 + texture_files = { + 'diffuse': 'onepointfive_texture.png', + 'specular': 'onepointfive_spec.png' + } + + textures_applied = 0 + + for texture_type, texture_file in texture_files.items(): + texture_path = os.path.join(base_path, texture_file) + if os.path.exists(texture_path): + try: + texture = loader.loadTexture(texture_path) + if texture: + # 确保纹理能正确加载 + texture.setWrapU(Texture.WMClamp) + texture.setWrapV(Texture.WMClamp) + texture.setMinfilter(Texture.FTLinearMipmapLinear) + texture.setMagfilter(Texture.FTLinear) + + if texture_type == 'diffuse': + # 应用主要漫反射纹理 + model.setTexture(texture) + print(f"✅ 应用了主纹理: {texture_file}") + textures_applied += 1 + elif texture_type == 'specular': + # 应用高光纹理 + ts = TextureStage('specular') + ts.setMode(TextureStage.MModulate) + model.setTexture(ts, texture) + print(f"✅ 应用了高光纹理: {texture_file}") + textures_applied += 1 + except Exception as e: + print(f"⚠️ 应用纹理失败 {texture_file}: {e}") + + if textures_applied == 0: + print(f"⚠️ {self.controller.name}手柄未能加载任何纹理,将使用材质颜色") + else: + print(f"🎨 {self.controller.name}手柄成功应用了 {textures_applied} 个纹理") + + def _load_combined_steamvr_model(self, base_path): + """尝试组合加载多个SteamVR模型部件""" + import os + from panda3d.core import NodePath + + # 重要的模型部件 + important_components = [ + "body.obj", + "trigger.obj", + "trackpad.obj", + "l_grip.obj" if self.controller.name == 'left' else "r_grip.obj" + ] + + combined_model = NodePath("combined_controller") + has_components = False + + for component in important_components: + component_path = os.path.join(base_path, component) + if os.path.exists(component_path): + try: + part = loader.loadModel(component_path) + if part: + part.reparentTo(combined_model) + has_components = True + print(f"✅ 加载了部件: {component}") + except Exception as e: + print(f"⚠️ 加载部件失败 {component}: {e}") + + if has_components: + self._apply_steamvr_textures(combined_model, base_path) + return combined_model + + return None + + def _create_fallback_model(self): + """创建改进的程序化手柄模型作为后备方案""" + # 主体(长条形状) + main_body = self._create_box_geometry(0.025, 0.15, 0.04) + main_node = self.model_node.attachNewNode(main_body) + + # 根据左右手设置不同颜色 + if self.controller.name == 'left': + color = self.button_colors['left'] + else: + color = self.button_colors['right'] + + main_node.setColor(color) + + # 启用光照响应 + from panda3d.core import RenderState, MaterialAttrib, Material + material = Material() + material.setShininess(32) + material.setAmbient((0.2, 0.2, 0.2, 1)) + material.setDiffuse(color) + material.setSpecular((0.5, 0.5, 0.5, 1)) + main_node.setMaterial(material) + + # 扳机区域(小突起) + trigger = self._create_box_geometry(0.015, 0.03, 0.02) + trigger_node = self.model_node.attachNewNode(trigger) + trigger_node.setPos(0, -0.08, 0.03) + trigger_node.setColor(self.button_colors['trigger']) + trigger_node.setMaterial(material) + + # 握把区域 + grip = self._create_box_geometry(0.02, 0.06, 0.03) + grip_node = self.model_node.attachNewNode(grip) + grip_node.setPos(0, 0.05, -0.03) + grip_node.setColor(self.button_colors['grip']) + grip_node.setMaterial(material) + + # 触摸板区域(圆盘) + trackpad = self._create_disc_geometry(0.015, 0.005) + trackpad_node = self.model_node.attachNewNode(trackpad) + trackpad_node.setPos(0, -0.02, 0.04) + trackpad_node.setColor(self.button_colors['trackpad']) + trackpad_node.setMaterial(material) + + def _create_box_geometry(self, width, length, height): + """创建立方体几何体""" + # 创建顶点格式 + format = GeomVertexFormat.getV3n3() + vdata = GeomVertexData('box', format, Geom.UHStatic) + + vertex = GeomVertexWriter(vdata, 'vertex') + normal = GeomVertexWriter(vdata, 'normal') + + # 定义立方体的8个顶点 + vertices = [ + Vec3(-width/2, -length/2, -height/2), + Vec3( width/2, -length/2, -height/2), + Vec3( width/2, length/2, -height/2), + Vec3(-width/2, length/2, -height/2), + Vec3(-width/2, -length/2, height/2), + Vec3( width/2, -length/2, height/2), + Vec3( width/2, length/2, height/2), + Vec3(-width/2, length/2, height/2) + ] + + # 立方体的6个面,每个面4个顶点 + faces = [ + # 底面 (z = -height/2) + [0, 1, 2, 3, Vec3(0, 0, -1)], + # 顶面 (z = height/2) + [7, 6, 5, 4, Vec3(0, 0, 1)], + # 前面 (y = -length/2) + [4, 5, 1, 0, Vec3(0, -1, 0)], + # 后面 (y = length/2) + [3, 2, 6, 7, Vec3(0, 1, 0)], + # 左面 (x = -width/2) + [0, 3, 7, 4, Vec3(-1, 0, 0)], + # 右面 (x = width/2) + [5, 6, 2, 1, Vec3(1, 0, 0)] + ] + + # 添加顶点和法线 + for face in faces: + for i in range(4): + vertex.addData3(vertices[face[i]]) + normal.addData3(face[4]) # 法线向量 + + # 创建几何体 + geom = Geom(vdata) + + # 为每个面创建三角形 + for face_idx in range(6): + base_idx = face_idx * 4 + prim = GeomTriangles(Geom.UHStatic) + # 第一个三角形 + prim.addVertices(base_idx, base_idx + 1, base_idx + 2) + # 第二个三角形 + prim.addVertices(base_idx, base_idx + 2, base_idx + 3) + geom.addPrimitive(prim) + + # 创建几何体节点 + geom_node = GeomNode('box') + geom_node.addGeom(geom) + + return geom_node + + def _create_disc_geometry(self, radius, thickness): + """创建圆盘几何体(用于触摸板)""" + format = GeomVertexFormat.getV3n3() + vdata = GeomVertexData('disc', format, Geom.UHStatic) + + vertex = GeomVertexWriter(vdata, 'vertex') + normal = GeomVertexWriter(vdata, 'normal') + + # 创建圆盘顶点 + segments = 16 + import math + + # 中心点 + vertex.addData3(0, 0, thickness/2) + normal.addData3(0, 0, 1) + + # 圆周点 + for i in range(segments): + angle = 2 * math.pi * i / segments + x = radius * math.cos(angle) + y = radius * math.sin(angle) + + vertex.addData3(x, y, thickness/2) + normal.addData3(0, 0, 1) + + # 创建几何体 + geom = Geom(vdata) + prim = GeomTriangles(Geom.UHStatic) + + # 创建扇形三角形 + for i in range(segments): + next_i = (i + 1) % segments + prim.addVertices(0, i + 1, next_i + 1) + + geom.addPrimitive(prim) + + # 创建几何体节点 + geom_node = GeomNode('disc') + geom_node.addGeom(geom) + + return geom_node + + def _create_interaction_ray(self): + """创建交互射线""" + if not self.visual_node: + return + + # 创建射线几何 + line_segs = LineSegs() + line_segs.setThickness(3) + line_segs.setColor(self.ray_color) + + # 射线主体 + line_segs.moveTo(0, 0, 0) + line_segs.drawTo(0, self.ray_length, 0) + + # 射线端点(小球) + end_point = self._create_sphere_geometry(0.02) + + # 创建射线节点 + geom_node = line_segs.create() + self.ray_node = self.visual_node.attachNewNode(geom_node) + + # 添加端点球 + end_node = self.ray_node.attachNewNode(end_point) + end_node.setPos(0, self.ray_length, 0) + end_node.setColor(self.ray_color) + + # 设置透明度 + self.ray_node.setTransparency(TransparencyAttrib.MAlpha) + + # 默认隐藏射线 + self.ray_node.hide() + + print(f"✓ {self.controller.name}手柄交互射线已创建") + + def _create_sphere_geometry(self, radius): + """创建球体几何体""" + # 简单的立方体作为球体替代 + return self._create_box_geometry(radius, radius, radius) + + def _create_button_indicators(self): + """创建按钮状态指示器""" + if not self.visual_node: + return + + # 创建按钮指示器容器 + self.button_indicator_node = self.visual_node.attachNewNode(f'{self.controller.name}_indicators') + + # 扳机指示器 + trigger_indicator = self._create_box_geometry(0.005, 0.01, 0.005) + self.trigger_indicator = self.button_indicator_node.attachNewNode(trigger_indicator) + self.trigger_indicator.setPos(0.02, -0.08, 0.03) + self.trigger_indicator.setColor(0.2, 0.2, 0.2, 1.0) + + # 握把指示器 + grip_indicator = self._create_box_geometry(0.005, 0.02, 0.005) + self.grip_indicator = self.button_indicator_node.attachNewNode(grip_indicator) + self.grip_indicator.setPos(-0.02, 0.05, -0.03) + self.grip_indicator.setColor(0.2, 0.2, 0.2, 1.0) + + # 触摸板指示器 + trackpad_indicator = self._create_disc_geometry(0.003, 0.002) + self.trackpad_indicator = self.button_indicator_node.attachNewNode(trackpad_indicator) + self.trackpad_indicator.setPos(0, -0.02, 0.045) + self.trackpad_indicator.setColor(0.2, 0.2, 0.2, 1.0) + + print(f"✓ {self.controller.name}手柄按钮指示器已创建") + + def update(self): + """更新可视化状态""" + if not self.controller.is_connected: + self.hide() + return + + self.show() + + # 更新按钮指示器状态 + self._update_button_indicators() + + # 更新射线显示状态 + self._update_ray_display() + + def _update_button_indicators(self): + """更新按钮指示器状态""" + if not hasattr(self, 'trigger_indicator'): + return + + # 扳机指示器 + if self.controller.is_trigger_pressed(): + self.trigger_indicator.setColor(self.button_colors['trigger']) + # 根据扳机值调整位置 + trigger_offset = self.controller.trigger_value * 0.01 + self.trigger_indicator.setPos(0.02, -0.08 + trigger_offset, 0.03) + else: + self.trigger_indicator.setColor(0.2, 0.2, 0.2, 1.0) + self.trigger_indicator.setPos(0.02, -0.08, 0.03) + + # 握把指示器 + if self.controller.is_grip_pressed(): + self.grip_indicator.setColor(self.button_colors['grip']) + else: + self.grip_indicator.setColor(0.2, 0.2, 0.2, 1.0) + + # 触摸板指示器 + if self.controller.touchpad_touched: + self.trackpad_indicator.setColor(self.button_colors['trackpad']) + # 根据触摸位置调整指示器位置 + if hasattr(self.controller, 'touchpad_pos'): + offset_x = self.controller.touchpad_pos.x * 0.01 + offset_y = self.controller.touchpad_pos.y * 0.01 + self.trackpad_indicator.setPos(offset_x, -0.02 + offset_y, 0.045) + else: + self.trackpad_indicator.setColor(0.2, 0.2, 0.2, 1.0) + self.trackpad_indicator.setPos(0, -0.02, 0.045) + + def _update_ray_display(self): + """更新射线显示""" + if not self.ray_node: + return + + # 根据交互状态显示/隐藏射线 + # 这里可以添加更复杂的逻辑,比如只在指向对象时显示 + show_ray = (self.controller.is_trigger_pressed(threshold=0.1) or + self.controller.touchpad_touched) + + if show_ray: + self.show_ray() + else: + self.hide_ray() + + def show(self): + """显示手柄可视化""" + if self.visual_node: + self.visual_node.show() + + def hide(self): + """隐藏手柄可视化""" + if self.visual_node: + self.visual_node.hide() + + def show_ray(self): + """显示交互射线""" + if self.ray_node: + self.ray_node.show() + + def hide_ray(self): + """隐藏交互射线""" + if self.ray_node: + self.ray_node.hide() + + def set_ray_color(self, color): + """设置射线颜色""" + if self.ray_node: + self.ray_node.setColor(color) + + def set_ray_length(self, length): + """设置射线长度""" + self.ray_length = length + # 重新创建射线(简单的实现) + if self.ray_node: + self.ray_node.removeNode() + self._create_interaction_ray() + + def _set_always_on_top(self, model_node): + """设置手柄模型始终显示在上层,不被其他物体遮挡""" + if not model_node: + return + + from panda3d.core import RenderState + + # 设置为固定渲染bin,优先级设为较高值(1000) + # fixed bin中的对象按sort值从小到大渲染,越大越后渲染(越在上层) + model_node.setBin("fixed", 1000) + + # 禁用深度测试和深度写入,确保始终可见 + model_node.setDepthTest(False) + model_node.setDepthWrite(False) + + # 递归设置所有子节点的渲染属性 + for child in model_node.findAllMatches("**"): + child.setBin("fixed", 1000) + child.setDepthTest(False) + child.setDepthWrite(False) + + print(f"🔝 {self.controller.name}手柄已设置为始终显示在上层") + + def cleanup(self): + """清理资源""" + if self.visual_node: + self.visual_node.removeNode() + + print(f"🧹 {self.controller.name}手柄可视化已清理") \ No newline at end of file diff --git a/vr_actions/actions.json b/vr_actions/actions.json new file mode 100644 index 00000000..7e55618f --- /dev/null +++ b/vr_actions/actions.json @@ -0,0 +1,89 @@ +{ + "actions": [ + { + "name": "/actions/default/in/Pose", + "type": "pose" + }, + { + "name": "/actions/default/in/Trigger", + "type": "boolean" + }, + { + "name": "/actions/default/in/Grip", + "type": "boolean" + }, + { + "name": "/actions/default/in/Menu", + "type": "boolean" + }, + { + "name": "/actions/default/in/System", + "type": "boolean" + }, + { + "name": "/actions/default/in/TrackpadClick", + "type": "boolean" + }, + { + "name": "/actions/default/in/TrackpadTouch", + "type": "boolean" + }, + { + "name": "/actions/default/in/AButton", + "type": "boolean" + }, + { + "name": "/actions/default/in/BButton", + "type": "boolean" + }, + { + "name": "/actions/default/in/Trackpad", + "type": "vector2" + }, + { + "name": "/actions/default/in/Joystick", + "type": "vector2" + }, + { + "name": "/actions/default/in/Squeeze", + "type": "vector1" + }, + { + "name": "/actions/default/out/Haptic", + "type": "vibration" + } + ], + "action_sets": [ + { + "name": "/actions/default", + "usage": "single" + } + ], + "default_bindings": [ + { + "controller_type": "vive_controller", + "binding_url": "bindings_vive.json" + }, + { + "controller_type": "oculus_touch", + "binding_url": "bindings_oculus.json" + }, + { + "controller_type": "knuckles", + "binding_url": "bindings_index.json" + } + ], + "localization": [ + { + "language_tag": "zh_CN", + "/actions/default/in/Trigger": "扳机", + "/actions/default/in/Grip": "握把", + "/actions/default/in/Menu": "菜单", + "/actions/default/in/System": "系统", + "/actions/default/in/TrackpadClick": "触摸板点击", + "/actions/default/in/TrackpadTouch": "触摸板触摸", + "/actions/default/in/Pose": "手部姿态", + "/actions/default/out/Haptic": "震动反馈" + } + ] +} \ No newline at end of file diff --git a/vr_actions/bindings_vive.json b/vr_actions/bindings_vive.json new file mode 100644 index 00000000..2dd50b51 --- /dev/null +++ b/vr_actions/bindings_vive.json @@ -0,0 +1,106 @@ +{ + "controller_type": "vive_controller", + "description": "Vive\u63a7\u5236\u5668\u7ed1\u5b9a", + "name": "EG VR Editor - Vive", + "bindings": { + "/actions/default": { + "sources": [ + { + "inputs": { + "click": { + "output": "/actions/default/in/Trigger" + } + }, + "mode": "button", + "path": "/user/hand/left/input/trigger" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Trigger" + } + }, + "mode": "button", + "path": "/user/hand/right/input/trigger" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Grip" + } + }, + "mode": "button", + "path": "/user/hand/left/input/grip" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Grip" + } + }, + "mode": "button", + "path": "/user/hand/right/input/grip" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Menu" + } + }, + "mode": "button", + "path": "/user/hand/left/input/menu" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Trackpad" + }, + "click": { + "output": "/actions/default/in/TrackpadClick" + }, + "touch": { + "output": "/actions/default/in/TrackpadTouch" + } + }, + "mode": "trackpad", + "path": "/user/hand/left/input/trackpad" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Trackpad" + }, + "click": { + "output": "/actions/default/in/TrackpadClick" + }, + "touch": { + "output": "/actions/default/in/TrackpadTouch" + } + }, + "mode": "trackpad", + "path": "/user/hand/right/input/trackpad" + } + ], + "poses": [ + { + "output": "/actions/default/in/Pose", + "path": "/user/hand/left/pose/raw" + }, + { + "output": "/actions/default/in/Pose", + "path": "/user/hand/right/pose/raw" + } + ], + "haptics": [ + { + "output": "/actions/default/out/Haptic", + "path": "/user/hand/left/output/haptic" + }, + { + "output": "/actions/default/out/Haptic", + "path": "/user/hand/right/output/haptic" + } + ] + } + } +} \ No newline at end of file