From af0a999191a423cf4b34201665692d307e018375 Mon Sep 17 00:00:00 2001 From: Rowland <975945824@qq.com> Date: Mon, 15 Sep 2025 10:03:47 +0800 Subject: [PATCH 1/3] =?UTF-8?q?vr=E7=AC=AC=E4=B8=80=E6=AC=A1=E6=8F=90?= =?UTF-8?q?=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 112 ++++ .../samples/04-Material-Blending/main.py | 2 +- RenderPipelineFile/samples/06-Car/main.py | 5 - core/alvr_streamer.py | 509 ---------------- core/vr_input_handler.py | 430 ------------- core/vr_manager.py | 573 ------------------ demo/VR_ALVR_实现指南.md | 374 ------------ demo/VR测试说明.md | 253 -------- main.py | 144 ----- requirements/vr-requirements.txt | 18 - test_metallic_gradient.png | Bin 1304 -> 0 bytes test_metallic_stripes.png | Bin 1308 -> 0 bytes test_roughness_checkerboard.png | Bin 1364 -> 0 bytes test_roughness_circle.png | Bin 19408 -> 0 bytes test_roughness_gradient.png | Bin 1304 -> 0 bytes ui/vr_control_panel.py | 412 ------------- vr_test.py | 468 -------------- 17 files changed, 113 insertions(+), 3187 deletions(-) create mode 100644 CLAUDE.md delete mode 100644 core/alvr_streamer.py delete mode 100644 core/vr_input_handler.py delete mode 100644 core/vr_manager.py delete mode 100644 demo/VR_ALVR_实现指南.md delete mode 100644 demo/VR测试说明.md delete mode 100644 requirements/vr-requirements.txt delete mode 100644 test_metallic_gradient.png delete mode 100644 test_metallic_stripes.png delete mode 100644 test_roughness_checkerboard.png delete mode 100644 test_roughness_circle.png delete mode 100644 test_roughness_gradient.png delete mode 100644 ui/vr_control_panel.py delete mode 100644 vr_test.py diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..1fc3378b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,112 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## 项目概述 + +这是一个基于Panda3D的3D渲染引擎和场景编辑器,集成了PyQt5界面和多种高级功能: + +- 3D场景编辑器(模型导入、材质系统、碰撞检测) +- GUI元素管理(2D/3D GUI组件) +- 项目管理系统(场景保存/加载、项目打包) +- Cesium地图集成 +- 渲染管线增强(RenderPipelineFile) + +## 运行和构建命令 + +### 启动应用程序 +```bash +python Start_Run.py [project_path] +``` +或者直接: +```bash +python main.py +``` + +### 依赖安装 +```bash +# 主要依赖 +pip install -r requirements/requirements.txt + +# Conda环境依赖 +pip install -r requirements/conda-requirements.txt +``` + +### 工具脚本 +```bash +# 安装FBX到GLTF转换工具 +./install_fbx2gltf.sh +``` + +## 核心架构 + +### 主要模块结构 +``` +EG/ +├── main.py # 应用程序入口点 +├── Start_Run.py # 启动脚本(路径配置) +├── core/ # 核心功能模块 +│ ├── world.py # 3D世界核心(继承Panda3DWorld) +│ ├── scene_manager.py # 场景和模型管理 +│ ├── selection.py # 对象选择系统 +│ ├── event_handler.py # 事件处理 +│ └── tool_manager.py # 工具系统 +├── gui/ # GUI元素管理 +│ └── gui_manager.py # 2D/3D GUI组件 +├── ui/ # 用户界面 +│ ├── widgets.py # 自定义Qt组件 +│ ├── property_panel.py # 属性面板 +│ └── interface_manager.py # 界面管理 +├── scene/ # 场景相关 +│ └── scene_manager.py # 场景管理器 +├── project/ # 项目管理 +│ └── project_manager.py # 项目生命周期 +├── RenderPipelineFile/ # 渲染管线扩展 +└── QPanda3D/ # Panda3D Qt集成 +``` + +### 核心设计模式 + +1. **模块化架构**: 每个功能模块独立,通过管理器类协调 +2. **事件驱动**: EventHandler统一处理用户交互和系统事件 +3. **组件系统**: SelectionSystem、ToolManager等可插拔组件 +4. **MVC分离**: UI组件、核心逻辑和数据管理分离 + +### 主要依赖集成 + +- **Panda3D 1.10.15**: 3D渲染引擎 +- **PyQt5**: GUI框架 +- **QPanda3D**: Panda3D的Qt集成 +- **RenderPipeline**: 高级渲染功能 + +## 开发指南 + +### 添加新功能模块 +1. 在对应目录下创建新的Python文件 +2. 继承相应的基类(如Panda3DWorld用于3D功能) +3. 在main.py中集成新模块 +4. 更新界面管理器以添加UI控制 + +### 材质和渲染 +- 材质系统集成在scene/scene_manager.py +- 支持PBR材质和自定义着色器 +- RenderPipelineFile提供高级渲染特性 + + +### GUI开发 +- 使用PyQt5构建主界面 +- 3D GUI元素通过gui/gui_manager.py管理 +- 自定义组件在ui/widgets.py中定义 + +## 文件约定 + +- Python文件使用UTF-8编码 +- 中文注释和文档字符串 +- 模块顶部包含功能描述注释 +- 类和方法使用描述性命名 + +## 注意事项 + +- 项目依赖多个大型库(Panda3D、PyQt5、RenderPipeline) +- Cesium集成需要WebEngine支持 +- 某些功能可能需要特定的系统配置 \ No newline at end of file diff --git a/RenderPipelineFile/samples/04-Material-Blending/main.py b/RenderPipelineFile/samples/04-Material-Blending/main.py index 296373b2..a56cf579 100644 --- a/RenderPipelineFile/samples/04-Material-Blending/main.py +++ b/RenderPipelineFile/samples/04-Material-Blending/main.py @@ -53,7 +53,7 @@ class Application(ShowBase): # ------ End of render pipeline code, thats it! ------ # Set time of day - self.render_pipeline.daytime_mgr.time = "12:43" + self.render_pipeline.daytime_mgr.time = "6:43" # Load the scene model = loader.loadModel("scene/Scene.bam") diff --git a/RenderPipelineFile/samples/06-Car/main.py b/RenderPipelineFile/samples/06-Car/main.py index da6715f8..1720104b 100644 --- a/RenderPipelineFile/samples/06-Car/main.py +++ b/RenderPipelineFile/samples/06-Car/main.py @@ -52,11 +52,6 @@ class MainApp(ShowBase): # Load the scene model = loader.loadModel("scene/scene.bam") # model = loader.loadModel("scene2/Scene.bam") - model_0 = self.loader.loadModel("/home/tiger/下载/Benci/source/s65/s65/s65.fbx") - model_0.reparentTo(self.render) - model_0.setScale(0.01) - model_0.setPos(-8, 42, 0) - model_0.setHpr(0, 90, 0) model.reparent_to(render) self.render_pipeline.prepare_scene(model) diff --git a/core/alvr_streamer.py b/core/alvr_streamer.py deleted file mode 100644 index 2783c5bd..00000000 --- a/core/alvr_streamer.py +++ /dev/null @@ -1,509 +0,0 @@ -""" -ALVR串流处理器 - -负责与ALVR服务器通信和视频流传输 -支持Quest等VR头显的无线串流 -""" - -import socket -import struct -import threading -import json -import time -import subprocess -import psutil -from direct.showbase.DirectObject import DirectObject -from panda3d.core import Texture, PNMImage - - -class ALVRStreamer(DirectObject): - """ALVR串流处理器""" - - def __init__(self, world, vr_manager): - super().__init__() - self.world = world - self.vr_manager = vr_manager - - # ALVR服务器配置 - self.alvr_server_ip = "127.0.0.1" - self.alvr_server_port = 9943 - self.alvr_streaming_port = 9944 - - # 连接状态 - self.connected = False - self.streaming = False - self.server_socket = None - self.streaming_socket = None - - # 流媒体配置 - self.stream_width = 2880 # Quest 2 推荐分辨率 - self.stream_height = 1700 - self.stream_fps = 72 - self.bitrate = 150 # Mbps - self.codec = "h264" - - # 线程管理 - self.connection_thread = None - self.streaming_thread = None - self.running = False - - # 性能统计 - self.frame_count = 0 - self.last_fps_time = time.time() - self.current_fps = 0 - self.latency = 0 - - print("✓ ALVR串流处理器初始化完成") - - def initialize(self): - """初始化ALVR串流""" - try: - # 检查ALVR服务器是否运行 - if not self._check_alvr_server(): - print("ALVR服务器未运行,尝试启动...") - if not self._start_alvr_server(): - print("无法启动ALVR服务器") - return False - - # 连接到ALVR服务器 - if not self._connect_to_server(): - print("无法连接到ALVR服务器") - return False - - # 配置流媒体设置 - self._configure_streaming() - - # 启动串流线程 - self._start_streaming_threads() - - print("✓ ALVR串流初始化成功") - return True - - except Exception as e: - print(f"ALVR初始化错误: {str(e)}") - return False - - def _check_alvr_server(self): - """检查ALVR服务器是否运行""" - try: - # 检查进程 - for proc in psutil.process_iter(['pid', 'name', 'cmdline']): - if 'alvr' in proc.info['name'].lower(): - return True - - # 尝试连接端口 - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(2) - result = sock.connect_ex((self.alvr_server_ip, self.alvr_server_port)) - sock.close() - - return result == 0 - - except Exception as e: - print(f"检查ALVR服务器错误: {str(e)}") - return False - - def _start_alvr_server(self): - """启动ALVR服务器""" - try: - # 尝试启动ALVR服务器 - # 这里需要根据实际的ALVR安装路径调整 - alvr_paths = [ - "/usr/local/bin/alvr_server", - "/usr/bin/alvr_server", - "C:/Program Files/ALVR/alvr_server.exe", - "C:/ALVR/alvr_server.exe" - ] - - for path in alvr_paths: - try: - subprocess.Popen([path], shell=True) - time.sleep(3) # 等待服务器启动 - if self._check_alvr_server(): - return True - except FileNotFoundError: - continue - - return False - - except Exception as e: - print(f"启动ALVR服务器错误: {str(e)}") - return False - - def _connect_to_server(self): - """连接到ALVR服务器""" - try: - # 创建TCP连接 - self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.server_socket.settimeout(5) - self.server_socket.connect((self.alvr_server_ip, self.alvr_server_port)) - - # 发送握手消息 - handshake_data = { - "type": "handshake", - "client_name": "Panda3D_VR_Engine", - "version": "1.0", - "capabilities": { - "video": True, - "audio": True, - "tracking": True, - "haptics": True - } - } - - self._send_message(handshake_data) - - # 接收响应 - response = self._receive_message() - if response and response.get("type") == "handshake_response": - self.connected = True - print("✓ 已连接到ALVR服务器") - return True - - return False - - except Exception as e: - print(f"连接ALVR服务器错误: {str(e)}") - return False - - def _send_message(self, data): - """发送消息到ALVR服务器""" - try: - message = json.dumps(data).encode('utf-8') - length = struct.pack('= 1.0: - self.current_fps = self.frame_count - self.frame_count = 0 - self.last_fps_time = current_time - - def start_streaming(self): - """开始串流""" - if not self.connected: - print("未连接到ALVR服务器") - return False - - start_message = {"type": "start_streaming"} - self._send_message(start_message) - return True - - def stop_streaming(self): - """停止串流""" - if not self.connected: - return - - stop_message = {"type": "stop_streaming"} - self._send_message(stop_message) - self.streaming = False - - def send_haptic_feedback(self, controller_id, duration, intensity): - """发送触觉反馈""" - if not self.connected: - return - - haptic_message = { - "type": "haptic_feedback", - "controller_id": controller_id, - "duration": duration, - "intensity": intensity - } - - self._send_message(haptic_message) - - def get_streaming_status(self): - """获取串流状态""" - return { - "connected": self.connected, - "streaming": self.streaming, - "fps": self.current_fps, - "latency": self.latency, - "resolution": f"{self.stream_width}x{self.stream_height}", - "bitrate": self.bitrate - } - - def set_stream_quality(self, width, height, fps, bitrate): - """设置串流质量""" - self.stream_width = width - self.stream_height = height - self.stream_fps = fps - self.bitrate = bitrate - - # 如果正在串流,重新配置 - if self.streaming: - self._configure_streaming() - - def shutdown(self): - """关闭串流""" - print("关闭ALVR串流...") - - self.running = False - self.streaming = False - - # 发送断开消息 - if self.connected: - disconnect_message = {"type": "disconnect"} - self._send_message(disconnect_message) - - # 关闭连接 - if self.server_socket: - self.server_socket.close() - - if self.streaming_socket: - self.streaming_socket.close() - - # 等待线程结束 - if self.connection_thread: - self.connection_thread.join(timeout=2) - - if self.streaming_thread: - self.streaming_thread.join(timeout=2) - - self.connected = False - print("✓ ALVR串流已关闭") - - def is_connected(self): - """检查是否连接""" - return self.connected - - def is_streaming(self): - """检查是否在串流""" - return self.streaming \ No newline at end of file diff --git a/core/vr_input_handler.py b/core/vr_input_handler.py deleted file mode 100644 index 2438e601..00000000 --- a/core/vr_input_handler.py +++ /dev/null @@ -1,430 +0,0 @@ -""" -VR输入处理器 - -处理VR控制器输入、手势识别和VR交互 -支持多种VR控制器和手势输入 -""" - -from direct.showbase.DirectObject import DirectObject -from panda3d.core import Vec3, Point3, CollisionRay, CollisionNode, CollisionHandlerQueue -from direct.task import Task -import time - - -class VRInputHandler(DirectObject): - """VR输入处理器""" - - def __init__(self, world, vr_manager): - super().__init__() - self.world = world - self.vr_manager = vr_manager - - # 控制器状态 - self.controllers = {} - self.controller_nodes = {} - self.controller_rays = {} - - # 手势识别 - self.gesture_enabled = True - self.gesture_history = [] - self.gesture_threshold = 0.1 - - # 交互系统 - self.interaction_enabled = True - self.selected_object = None - self.grab_offset = Vec3(0, 0, 0) - - # 输入映射 - self.input_mappings = { - 'trigger': self._handle_trigger, - 'grip': self._handle_grip, - 'touchpad': self._handle_touchpad, - 'menu': self._handle_menu, - 'system': self._handle_system - } - - print("✓ VR输入处理器初始化完成") - - def start_input_handling(self): - """启动输入处理""" - if not self.vr_manager.is_vr_enabled(): - print("VR未启用,无法启动输入处理") - return False - - # 启动输入更新任务 - self.world.taskMgr.add(self._update_input, "vr_input_update") - - # 设置控制器可视化 - self._setup_controller_visualization() - - print("✓ VR输入处理已启动") - return True - - def stop_input_handling(self): - """停止输入处理""" - self.world.taskMgr.remove("vr_input_update") - self._cleanup_controller_visualization() - print("✓ VR输入处理已停止") - - def _update_input(self, task): - """更新输入处理""" - if not self.vr_manager.is_vr_enabled(): - return Task.cont - - try: - # 更新所有控制器 - self._update_controllers() - - # 处理手势识别 - if self.gesture_enabled: - self._process_gestures() - - # 处理交互 - if self.interaction_enabled: - self._process_interactions() - - except Exception as e: - print(f"VR输入更新错误: {str(e)}") - - return Task.cont - - def _update_controllers(self): - """更新控制器状态""" - # 获取控制器姿态 - controller_poses = self.vr_manager.controller_poses - - for controller_id, pose in controller_poses.items(): - # 获取控制器输入 - input_data = self.vr_manager.get_controller_input(controller_id) - if not input_data: - continue - - # 更新控制器状态 - if controller_id not in self.controllers: - self.controllers[controller_id] = {} - - prev_state = self.controllers[controller_id].copy() - self.controllers[controller_id] = input_data - - # 更新控制器可视化 - self._update_controller_visualization(controller_id, pose) - - # 处理输入事件 - self._process_controller_input(controller_id, input_data, prev_state) - - def _process_controller_input(self, controller_id, current_state, prev_state): - """处理控制器输入""" - # 检查按钮状态变化 - for input_type, handler in self.input_mappings.items(): - if input_type in current_state: - current_value = current_state[input_type] - prev_value = prev_state.get(input_type, 0) - - # 处理按钮按下/释放 - if isinstance(current_value, (int, float)): - if current_value > 0.5 and prev_value <= 0.5: - handler(controller_id, 'press', current_value) - elif current_value <= 0.5 and prev_value > 0.5: - handler(controller_id, 'release', current_value) - elif current_value > 0.5: - handler(controller_id, 'hold', current_value) - - # 处理触摸板 - elif isinstance(current_value, tuple) and len(current_value) == 2: - if current_value != prev_value: - handler(controller_id, 'move', current_value) - - def _handle_trigger(self, controller_id, action, value): - """处理扳机输入""" - if action == 'press': - print(f"控制器 {controller_id} 扳机按下 (强度: {value:.2f})") - self._try_grab_object(controller_id) - elif action == 'release': - print(f"控制器 {controller_id} 扳机释放") - self._try_release_object(controller_id) - - def _handle_grip(self, controller_id, action, value): - """处理握持输入""" - if action == 'press': - print(f"控制器 {controller_id} 握持按下 (强度: {value:.2f})") - self._toggle_interaction_mode(controller_id) - elif action == 'release': - print(f"控制器 {controller_id} 握持释放") - - def _handle_touchpad(self, controller_id, action, value): - """处理触摸板输入""" - if action == 'move': - x, y = value - print(f"控制器 {controller_id} 触摸板: ({x:.2f}, {y:.2f})") - - # 根据触摸板位置执行不同操作 - if abs(x) > 0.7: # 左右滑动 - self._handle_horizontal_swipe(controller_id, x) - elif abs(y) > 0.7: # 上下滑动 - self._handle_vertical_swipe(controller_id, y) - - def _handle_menu(self, controller_id, action, value): - """处理菜单按钮""" - if action == 'press': - print(f"控制器 {controller_id} 菜单按钮按下") - self._show_vr_menu(controller_id) - - def _handle_system(self, controller_id, action, value): - """处理系统按钮""" - if action == 'press': - print(f"控制器 {controller_id} 系统按钮按下") - # 系统按钮通常由VR系统处理 - - def _handle_horizontal_swipe(self, controller_id, direction): - """处理水平滑动""" - if direction > 0: - print(f"控制器 {controller_id} 右滑") - self._switch_tool(controller_id, 'next') - else: - print(f"控制器 {controller_id} 左滑") - self._switch_tool(controller_id, 'prev') - - def _handle_vertical_swipe(self, controller_id, direction): - """处理垂直滑动""" - if direction > 0: - print(f"控制器 {controller_id} 上滑") - self._zoom_in(controller_id) - else: - print(f"控制器 {controller_id} 下滑") - self._zoom_out(controller_id) - - def _try_grab_object(self, controller_id): - """尝试抓取对象""" - if controller_id not in self.controllers: - return - - # 获取控制器射线 - ray = self._get_controller_ray(controller_id) - if not ray: - return - - # 执行射线检测 - hit_object = self._raycast_from_controller(controller_id) - if hit_object: - self.selected_object = hit_object - controller_pose = self.controllers[controller_id].get('pose') - if controller_pose: - # 计算抓取偏移 - object_pos = hit_object.getPos() - controller_pos = controller_pose.getTranslate() - self.grab_offset = object_pos - controller_pos - - print(f"抓取对象: {hit_object.getName()}") - - # 发送抓取事件 - self.world.event_handler.messenger.send('vr-object-grabbed', [hit_object, controller_id]) - - def _try_release_object(self, controller_id): - """尝试释放对象""" - if self.selected_object: - print(f"释放对象: {self.selected_object.getName()}") - - # 发送释放事件 - self.world.event_handler.messenger.send('vr-object-released', [self.selected_object, controller_id]) - - self.selected_object = None - self.grab_offset = Vec3(0, 0, 0) - - def _raycast_from_controller(self, controller_id): - """从控制器发射射线检测""" - if controller_id not in self.controllers: - return None - - controller_pose = self.controllers[controller_id].get('pose') - if not controller_pose: - return None - - # 获取控制器位置和方向 - controller_pos = controller_pose.getTranslate() - controller_forward = controller_pose.getQuat().getForward() - - # 创建射线 - ray = CollisionRay() - ray.setOrigin(controller_pos) - ray.setDirection(controller_forward) - - # 执行碰撞检测 - traverser = self.world.cTrav if hasattr(self.world, 'cTrav') else None - if not traverser: - return None - - handler = CollisionHandlerQueue() - collision_node = CollisionNode('vr_controller_ray') - collision_node.addSolid(ray) - - ray_np = self.world.render.attachNewNode(collision_node) - traverser.addCollider(ray_np, handler) - - # 遍历碰撞 - traverser.traverse(self.world.render) - - # 清理 - ray_np.removeNode() - - # 返回最近的碰撞对象 - if handler.getNumEntries() > 0: - handler.sortEntries() - entry = handler.getEntry(0) - return entry.getIntoNodePath() - - return None - - def _get_controller_ray(self, controller_id): - """获取控制器射线""" - return self.controller_rays.get(controller_id) - - def _setup_controller_visualization(self): - """设置控制器可视化""" - print("设置控制器可视化...") - - # 为每个控制器创建可视化节点 - for controller_id in self.controllers: - self._create_controller_model(controller_id) - - def _create_controller_model(self, controller_id): - """创建控制器模型""" - # 创建简单的控制器模型(立方体) - from panda3d.core import CardMaker - - cm = CardMaker(f"controller_{controller_id}") - cm.setFrame(-0.05, 0.05, -0.05, 0.05) - - controller_node = self.world.render.attachNewNode(cm.generate()) - controller_node.setColor(0.2, 0.8, 1.0, 0.8) - controller_node.setScale(0.1, 0.2, 0.05) - - self.controller_nodes[controller_id] = controller_node - - # 创建控制器射线可视化 - self._create_controller_ray_visual(controller_id) - - def _create_controller_ray_visual(self, controller_id): - """创建控制器射线可视化""" - from panda3d.core import LineSegs - - # 创建射线线段 - lines = LineSegs() - lines.setColor(1, 0, 0, 0.5) - lines.moveTo(0, 0, 0) - lines.drawTo(0, 2, 0) # 2米长的射线 - - ray_node = self.world.render.attachNewNode(lines.create()) - ray_node.setRenderModeWireframe() - ray_node.hide() # 默认隐藏 - - self.controller_rays[controller_id] = ray_node - - def _update_controller_visualization(self, controller_id, pose): - """更新控制器可视化""" - if controller_id in self.controller_nodes: - node = self.controller_nodes[controller_id] - node.setMat(pose) - - if controller_id in self.controller_rays: - ray_node = self.controller_rays[controller_id] - ray_node.setMat(pose) - - def _cleanup_controller_visualization(self): - """清理控制器可视化""" - for node in self.controller_nodes.values(): - node.removeNode() - - for ray in self.controller_rays.values(): - ray.removeNode() - - self.controller_nodes.clear() - self.controller_rays.clear() - - def _process_gestures(self): - """处理手势识别""" - # 简单的手势识别逻辑 - # 这里可以实现更复杂的手势识别算法 - pass - - def _process_interactions(self): - """处理交互逻辑""" - # 如果有选中的对象,更新其位置 - if self.selected_object: - self._update_grabbed_object() - - def _update_grabbed_object(self): - """更新被抓取对象的位置""" - if not self.selected_object: - return - - # 找到抓取该对象的控制器 - grabbing_controller = None - for controller_id, controller_state in self.controllers.items(): - if controller_state.get('trigger', 0) > 0.5: - grabbing_controller = controller_id - break - - if not grabbing_controller: - return - - # 更新对象位置 - controller_pose = self.controllers[grabbing_controller].get('pose') - if controller_pose: - controller_pos = controller_pose.getTranslate() - new_pos = controller_pos + self.grab_offset - self.selected_object.setPos(new_pos) - - def _toggle_interaction_mode(self, controller_id): - """切换交互模式""" - self.interaction_enabled = not self.interaction_enabled - print(f"交互模式: {'启用' if self.interaction_enabled else '禁用'}") - - def _show_vr_menu(self, controller_id): - """显示VR菜单""" - print(f"显示VR菜单 (控制器 {controller_id})") - # 这里可以实现VR菜单显示逻辑 - pass - - def _switch_tool(self, controller_id, direction): - """切换工具""" - print(f"切换工具: {direction} (控制器 {controller_id})") - # 这里可以实现工具切换逻辑 - pass - - def _zoom_in(self, controller_id): - """放大""" - print(f"放大 (控制器 {controller_id})") - # 实现放大逻辑 - pass - - def _zoom_out(self, controller_id): - """缩小""" - print(f"缩小 (控制器 {controller_id})") - # 实现缩小逻辑 - pass - - def show_controller_rays(self, show=True): - """显示/隐藏控制器射线""" - for ray in self.controller_rays.values(): - if show: - ray.show() - else: - ray.hide() - - def get_controller_state(self, controller_id): - """获取控制器状态""" - return self.controllers.get(controller_id, {}) - - def get_all_controllers(self): - """获取所有控制器""" - return list(self.controllers.keys()) - - def set_gesture_enabled(self, enabled): - """设置手势识别启用状态""" - self.gesture_enabled = enabled - print(f"手势识别: {'启用' if enabled else '禁用'}") - - def set_interaction_enabled(self, enabled): - """设置交互启用状态""" - self.interaction_enabled = enabled - print(f"VR交互: {'启用' if enabled else '禁用'}") \ No newline at end of file diff --git a/core/vr_manager.py b/core/vr_manager.py deleted file mode 100644 index c6830b06..00000000 --- a/core/vr_manager.py +++ /dev/null @@ -1,573 +0,0 @@ -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) - -from panda3d.core import * -from direct.showbase.DirectObject import DirectObject -from direct.task import Task -import sys - -class VRManager(DirectObject): - """VR管理器 - 处理VR系统初始化、追踪和渲染""" - - def __init__(self, world): - super().__init__() - self.world = world - self.vr_enabled = False - self.vr_system = None - self.vr_compositor = None - self.render_width = 1920 - self.render_height = 1080 - self.alvr_enabled = False - - # 模拟模式设置 - self.simulation_mode = False - self.simulation_data = { - 'head_pose': {'position': [0, 0, 1.6], 'rotation': [0, 0, 0, 1]}, - 'controller_poses': { - 0: {'position': [-0.3, 0, 1.2], 'rotation': [0, 0, 0, 1], 'connected': True}, - 1: {'position': [0.3, 0, 1.2], 'rotation': [0, 0, 0, 1], 'connected': True} - }, - 'render_size': (1920, 1080) - } - - # 立体渲染缓冲区 - self.left_eye_buffer = None - self.right_eye_buffer = None - self.left_eye_camera = None - self.right_eye_camera = None - - # 控制器相关 - self.controller_nodes = {} - self.controller_poses = {} - - print("✓ VR管理器初始化完成") - - def initialize_vr(self, force_simulation=False): - """初始化VR系统""" - try: - # 检查是否强制使用模拟模式 - if force_simulation: - print("🔧 强制启用VR模拟模式") - return self._init_simulation_mode() - - # 检查OpenVR支持 - if not self._check_openvr_support(): - print("⚠ OpenVR支持不可用,切换到模拟模式") - return self._init_simulation_mode() - - # 尝试初始化OpenVR - if not self._init_openvr(): - print("⚠ OpenVR初始化失败,切换到模拟模式") - print("提示: 请确保SteamVR正在运行且VR头盔已连接") - return self._init_simulation_mode() - - # 真实VR模式初始化成功 - print("✓ 真实VR模式初始化成功") - return self._init_real_vr_mode() - - except Exception as e: - print(f"VR初始化错误: {str(e)}") - print("⚠ 切换到模拟模式") - return self._init_simulation_mode() - - def _init_simulation_mode(self): - """初始化模拟模式""" - try: - self.simulation_mode = True - - # 使用模拟数据设置渲染尺寸 - self.render_width, self.render_height = self.simulation_data['render_size'] - print(f"🎮 模拟VR渲染尺寸: {self.render_width}x{self.render_height}") - - # 设置模拟立体渲染 - self._setup_stereo_rendering() - - # 启动模拟VR任务 - self._start_simulation_tasks() - - self.vr_enabled = True - print("✓ VR模拟模式初始化完成") - print("ℹ 模拟模式说明:") - print(" - 头盔追踪: 模拟数据") - print(" - 控制器: 模拟两个控制器") - print(" - 渲染: 立体渲染到窗口") - print(" - 交互: 键盘鼠标模拟") - - return True - - except Exception as e: - print(f"模拟模式初始化错误: {str(e)}") - return False - - def _init_real_vr_mode(self): - """初始化真实VR模式""" - try: - self.simulation_mode = False - - # 设置立体渲染 - self._setup_stereo_rendering() - - # 初始化ALVR - if self._init_alvr(): - print("✓ ALVR串流已启用") - self.alvr_enabled = True - - # 启动VR更新任务 - self._start_vr_tasks() - - self.vr_enabled = True - print("✓ 真实VR系统初始化完成") - return True - - except Exception as e: - print(f"真实VR模式初始化错误: {str(e)}") - return False - - def _check_openvr_support(self): - """检查OpenVR支持""" - try: - import openvr - return True - except ImportError: - print("OpenVR库未安装") - return False - - def _init_openvr(self): - """初始化OpenVR""" - try: - import openvr - - # 初始化OpenVR - self.vr_system = openvr.init(openvr.VRApplication_Scene) - if not self.vr_system: - return False - - # 获取合成器 - self.vr_compositor = openvr.VRCompositor() - if not self.vr_compositor: - return False - - # 获取推荐的渲染尺寸 - self.render_width, self.render_height = self.vr_system.getRecommendedRenderTargetSize() - print(f"VR推荐渲染尺寸: {self.render_width}x{self.render_height}") - - return True - - except Exception as e: - print(f"OpenVR初始化错误: {str(e)}") - return False - - def _setup_stereo_rendering(self): - """设置立体渲染""" - try: - # 创建眼部缓冲区 - self._create_eye_buffers() - - # 创建眼部摄像机 - self._create_eye_cameras() - - # 设置渲染目标 - self._setup_render_targets() - - print("✓ 立体渲染设置完成") - - except Exception as e: - print(f"立体渲染设置错误: {str(e)}") - - def _create_eye_buffers(self): - """创建眼部渲染缓冲区""" - try: - # 创建左眼缓冲区 - self.left_eye_buffer = self.world.win.makeTextureBuffer( - "left_eye", self.render_width, self.render_height - ) - self.left_eye_buffer.setSort(-100) - - # 创建右眼缓冲区 - self.right_eye_buffer = self.world.win.makeTextureBuffer( - "right_eye", self.render_width, self.render_height - ) - self.right_eye_buffer.setSort(-99) - - # 获取纹理 - self.left_eye_texture = self.left_eye_buffer.getTexture() - self.right_eye_texture = self.right_eye_buffer.getTexture() - - print("✓ 眼部渲染缓冲区创建完成") - - except Exception as e: - print(f"眼部缓冲区创建错误: {str(e)}") - - def _create_eye_cameras(self): - """创建眼部摄像机""" - try: - # 创建左眼摄像机 - self.left_eye_camera = self.world.makeCamera(self.left_eye_buffer) - self.left_eye_camera.setPos(-0.032, 0, 0) # 瞳距的一半 - - # 创建右眼摄像机 - self.right_eye_camera = self.world.makeCamera(self.right_eye_buffer) - self.right_eye_camera.setPos(0.032, 0, 0) # 瞳距的一半 - - # 设置投影矩阵 - if not self.simulation_mode: - self._update_eye_projection(0, self.left_eye_camera.node().getLens()) - self._update_eye_projection(1, self.right_eye_camera.node().getLens()) - else: - # 模拟模式使用标准透视投影 - lens = PerspectiveLens() - lens.setFov(110) # 模拟VR FOV - lens.setNearFar(0.1, 1000) - self.left_eye_camera.node().setLens(lens) - self.right_eye_camera.node().setLens(lens) - - print("✓ 眼部摄像机创建完成") - - except Exception as e: - print(f"眼部摄像机创建错误: {str(e)}") - - def _update_eye_projection(self, eye, lens): - """更新眼部投影矩阵""" - try: - if self.simulation_mode: - # 模拟模式使用标准投影 - perspective_lens = PerspectiveLens() - perspective_lens.setFov(110) - perspective_lens.setNearFar(0.1, 1000) - lens.copyFrom(perspective_lens) - return - - import openvr - - # 获取投影矩阵 - projection_matrix = self.vr_system.getProjectionMatrix(eye, 0.1, 1000.0) - - # 转换为Panda3D矩阵 - panda_matrix = self._convert_openvr_matrix(projection_matrix) - - # 设置自定义投影矩阵 - lens.setCustomProjectionMatrix(panda_matrix) - - except Exception as e: - print(f"眼部投影更新错误: {str(e)}") - - def _setup_render_targets(self): - """设置渲染目标""" - try: - # 在模拟模式下,可以选择将渲染结果显示到主窗口 - if self.simulation_mode: - # 创建并排显示的立体视图 - self._setup_simulation_display() - - print("✓ 渲染目标设置完成") - - except Exception as e: - print(f"渲染目标设置错误: {str(e)}") - - def _setup_simulation_display(self): - """设置模拟显示""" - try: - # 创建卡片来显示眼部纹理 - cm = CardMaker("stereo_display") - - # 左眼显示区域 - cm.setFrame(-1, 0, -1, 1) - left_card = self.world.render2d.attachNewNode(cm.generate()) - left_card.setTexture(self.left_eye_texture) - - # 右眼显示区域 - cm.setFrame(0, 1, -1, 1) - right_card = self.world.render2d.attachNewNode(cm.generate()) - right_card.setTexture(self.right_eye_texture) - - print("✓ 模拟立体显示设置完成") - - except Exception as e: - print(f"模拟显示设置错误: {str(e)}") - - def _init_alvr(self): - """初始化ALVR(仅在真实VR模式下)""" - if self.simulation_mode: - print("ℹ 模拟模式: ALVR串流已跳过") - return False - - try: - # ALVR初始化逻辑 - # 这里应该连接到ALVR服务器 - print("✓ ALVR初始化完成") - return True - except Exception as e: - print(f"ALVR初始化错误: {str(e)}") - return False - - def _start_vr_tasks(self): - """启动VR更新任务(真实VR模式)""" - if not self.simulation_mode: - taskMgr.add(self._update_vr_tracking, "vr_tracking") - taskMgr.add(self._update_vr_rendering, "vr_rendering") - - def _start_simulation_tasks(self): - """启动模拟VR任务""" - taskMgr.add(self._update_simulation_tracking, "simulation_tracking") - taskMgr.add(self._update_simulation_rendering, "simulation_rendering") - - def _update_simulation_tracking(self, task): - """更新模拟追踪数据""" - try: - # 模拟头部追踪(可以添加一些变化) - import math - time_factor = task.time * 0.5 - - # 模拟轻微的头部摆动 - head_pos = self.simulation_data['head_pose']['position'] - head_pos[1] = math.sin(time_factor) * 0.05 # 前后轻微摆动 - - # 更新主摄像机位置 - if hasattr(self.world, 'camera'): - self.world.camera.setPos(head_pos[0], head_pos[1], head_pos[2]) - - # 更新控制器位置(模拟手部动作) - for controller_id, pose in self.simulation_data['controller_poses'].items(): - if pose['connected']: - # 模拟控制器轻微移动 - pose['position'][1] = math.sin(time_factor + controller_id) * 0.1 - - # 更新控制器节点位置 - if controller_id in self.controller_nodes: - node = self.controller_nodes[controller_id] - node.setPos(pose['position'][0], pose['position'][1], pose['position'][2]) - - return task.cont - - except Exception as e: - print(f"模拟追踪更新错误: {str(e)}") - return task.cont - - def _update_simulation_rendering(self, task): - """更新模拟渲染""" - try: - # 在模拟模式下,渲染已经由Panda3D自动处理 - # 这里可以添加任何特殊的渲染逻辑 - return task.cont - - except Exception as e: - print(f"模拟渲染更新错误: {str(e)}") - return task.cont - - def _update_vr_tracking(self, task): - """更新VR追踪数据(真实VR模式)""" - try: - if not self.vr_system or self.simulation_mode: - return task.cont - - import openvr - - # 获取设备姿态 - poses, game_poses = self.vr_compositor.waitGetPoses(None, None) - - # 更新头显位置 - if poses[openvr.k_unTrackedDeviceIndex_Hmd].bPoseIsValid: - self._update_main_camera_pose() - - # 更新眼部摄像机 - self._update_eye_cameras() - - # 更新控制器姿态 - self._update_controller_poses(poses) - - return task.cont - - except Exception as e: - print(f"VR追踪更新错误: {str(e)}") - return task.cont - - def _update_vr_rendering(self, task): - """更新VR渲染(真实VR模式)""" - try: - if not self.vr_compositor or self.simulation_mode: - return task.cont - - # 提交帧到合成器 - self._submit_frames_to_compositor() - - return task.cont - - except Exception as e: - print(f"VR渲染更新错误: {str(e)}") - return task.cont - - def _convert_openvr_matrix(self, openvr_matrix): - """转换OpenVR矩阵为Panda3D矩阵""" - # 实现矩阵转换逻辑 - mat = Mat4() - # 这里需要实现具体的矩阵转换 - return mat - - def _update_main_camera_pose(self): - """更新主摄像机姿态""" - try: - if self.simulation_mode: - return - - # 从VR系统获取头显姿态并应用到主摄像机 - pass - - except Exception as e: - print(f"主摄像机姿态更新错误: {str(e)}") - - def _update_eye_cameras(self): - """更新眼部摄像机""" - try: - if self.simulation_mode: - return - - # 更新左右眼摄像机位置 - pass - - except Exception as e: - print(f"眼部摄像机更新错误: {str(e)}") - - def _update_controller_poses(self, poses): - """更新控制器姿态""" - try: - if self.simulation_mode: - return - - # 更新控制器位置和姿态 - pass - - except Exception as e: - print(f"控制器姿态更新错误: {str(e)}") - - def _submit_frames_to_compositor(self): - """提交帧到合成器""" - try: - if not self.vr_compositor or self.simulation_mode: - return - - import openvr - - # 提交左眼纹理 - left_eye_texture = openvr.Texture_t() - left_eye_texture.handle = self.left_eye_texture.getTextureId() - left_eye_texture.eType = openvr.TextureType_OpenGL - left_eye_texture.eColorSpace = openvr.ColorSpace_Gamma - self.vr_compositor.submit(openvr.Eye_Left, left_eye_texture) - - # 提交右眼纹理 - right_eye_texture = openvr.Texture_t() - right_eye_texture.handle = self.right_eye_texture.getTextureId() - right_eye_texture.eType = openvr.TextureType_OpenGL - right_eye_texture.eColorSpace = openvr.ColorSpace_Gamma - self.vr_compositor.submit(openvr.Eye_Right, right_eye_texture) - - except Exception as e: - print(f"帧提交错误: {str(e)}") - - def get_controller_input(self, controller_id): - """获取控制器输入""" - try: - if self.simulation_mode: - # 返回模拟的控制器输入 - return { - 'trigger': 0.0, - 'grip': 0.0, - 'touchpad': {'x': 0.0, 'y': 0.0, 'pressed': False}, - 'menu': False, - 'connected': controller_id in self.simulation_data['controller_poses'] - } - - if not self.vr_system: - return None - - import openvr - - # 获取控制器状态 - result, controller_state = self.vr_system.getControllerState(controller_id) - if not result: - return None - - # 解析控制器输入 - return { - 'trigger': controller_state.rAxis[1].x, - 'grip': controller_state.rAxis[2].x, - 'touchpad': { - 'x': controller_state.rAxis[0].x, - 'y': controller_state.rAxis[0].y, - 'pressed': controller_state.rAxis[0].x != 0 or controller_state.rAxis[0].y != 0 - }, - 'menu': controller_state.ulButtonPressed & (1 << openvr.k_EButton_ApplicationMenu) != 0, - 'connected': True - } - - except Exception as e: - print(f"控制器输入获取错误: {str(e)}") - return None - - def shutdown_vr(self): - """关闭VR系统""" - try: - # 停止任务 - if self.simulation_mode: - taskMgr.remove("simulation_tracking") - taskMgr.remove("simulation_rendering") - else: - taskMgr.remove("vr_tracking") - taskMgr.remove("vr_rendering") - - # 关闭OpenVR - if self.vr_system and not self.simulation_mode: - import openvr - openvr.shutdown() - - # 清理资源 - self.left_eye_buffer = None - self.right_eye_buffer = None - self.left_eye_camera = None - self.right_eye_camera = None - - self.vr_enabled = False - self.simulation_mode = False - - print("✓ VR系统已关闭") - - except Exception as e: - print(f"VR关闭错误: {str(e)}") - - def is_vr_enabled(self): - """检查VR是否启用""" - return self.vr_enabled - - def get_vr_info(self): - """获取VR系统信息""" - info = { - 'enabled': self.vr_enabled, - 'simulation_mode': self.simulation_mode, - 'render_size': (self.render_width, self.render_height), - 'alvr_enabled': self.alvr_enabled - } - - if self.simulation_mode: - info['mode'] = 'simulation' - info['controllers'] = len(self.simulation_data['controller_poses']) - else: - info['mode'] = 'real_vr' - info['openvr_connected'] = self.vr_system is not None - - return info - - # 便捷方法 - def enable_simulation_mode(self): - """启用模拟模式(调试用)""" - return self.initialize_vr(force_simulation=True) - - def get_simulation_data(self): - """获取模拟数据""" - return self.simulation_data if self.simulation_mode else None - - def update_simulation_data(self, key, value): - """更新模拟数据""" - if self.simulation_mode and key in self.simulation_data: - self.simulation_data[key] = value - return True - return False \ No newline at end of file diff --git a/demo/VR_ALVR_实现指南.md b/demo/VR_ALVR_实现指南.md deleted file mode 100644 index b788f31e..00000000 --- a/demo/VR_ALVR_实现指南.md +++ /dev/null @@ -1,374 +0,0 @@ -# VR + ALVR 串流实现指南 - -## 🎯 概述 - -本指南详细介绍了如何在Panda3D引擎中实现VR支持,并通过ALVR串流到Quest等VR头显设备。 - -## 🏗️ 系统架构 - -### 核心组件 - -1. **VRManager** (`core/vr_manager.py`) - - 负责VR系统初始化 - - 管理立体渲染(左右眼) - - 处理VR设备跟踪 - - 集成OpenVR接口 - -2. **VRInputHandler** (`core/vr_input_handler.py`) - - 处理VR控制器输入 - - 手势识别系统 - - 交互逻辑处理 - -3. **ALVRStreamer** (`core/alvr_streamer.py`) - - ALVR服务器通信 - - 视频流编码和传输 - - 性能监控 - -4. **VRControlPanel** (`ui/vr_control_panel.py`) - - VR系统GUI控制界面 - - 实时状态监控 - - 参数调整面板 - -## 🔧 安装和配置 - -### 1. 系统依赖 - -```bash -# 安装Python依赖 -pip install -r requirements/vr-requirements.txt - -# 安装OpenVR运行时 -# Windows: 下载并安装SteamVR -# Linux: -sudo apt-get install steam -# 然后在Steam中安装SteamVR -``` - -### 2. ALVR服务器设置 - -```bash -# 下载ALVR服务器 -# 从 https://github.com/alvr-org/ALVR 下载最新版本 - -# Windows -# 1. 解压到 C:\ALVR\ -# 2. 运行 alvr_server.exe - -# Linux -# 1. 解压到 /usr/local/bin/ -# 2. 运行 alvr_server -``` - -### 3. Quest设备配置 - -```bash -# 1. 在Quest上安装ALVR客户端 -# 2. 启用开发者模式 -# 3. 连接到同一WiFi网络 -# 4. 配置防火墙允许ALVR通信(端口9943-9944) -``` - -## 🚀 使用方法 - -### 快速启动 - -```python -# 在主程序中 -world = MyWorld() - -# 一键启用VR模式 -if world.enableVRMode(): - print("VR模式已启用") - - # 检查VR状态 - status = world.getVRStatus() - print(f"VR设备: {status['vr_info']}") - print(f"ALVR连接: {status['alvr_connected']}") - print(f"串流状态: {status['alvr_streaming']}") -``` - -### 详细控制 - -```python -# 分步骤启动VR -world = MyWorld() - -# 1. 初始化VR系统 -if world.initializeVR(): - print("VR系统初始化成功") - - # 2. 启动VR输入处理 - world.startVRInput() - - # 3. 显示控制器射线 - world.showControllerRays(True) - - # 4. 初始化ALVR串流 - if world.initializeALVR(): - print("ALVR初始化成功") - - # 5. 设置串流质量 - world.setALVRStreamQuality( - width=2880, - height=1700, - fps=72, - bitrate=150 - ) - - # 6. 开始串流 - world.startALVRStreaming() -``` - -### GUI控制界面 - -```python -# 创建VR控制面板 -from ui.vr_control_panel import VRControlPanel - -vr_panel = VRControlPanel(world) -vr_panel.show() - -# 面板功能: -# - 一键启用/禁用VR -# - ALVR串流控制 -# - 质量参数调整 -# - 实时性能监控 -# - 控制器状态显示 -``` - -## 🎮 VR交互功能 - -### 控制器输入处理 - -```python -# 获取控制器状态 -controllers = world.getAllControllers() -for controller_id in controllers: - state = world.getControllerState(controller_id) - - # 检查扳机按下 - if state['trigger'] > 0.5: - print(f"控制器 {controller_id} 扳机按下") - - # 检查触摸板输入 - touchpad_x, touchpad_y = state['touchpad'] - if abs(touchpad_x) > 0.5: - print(f"触摸板水平滑动: {touchpad_x}") -``` - -### 对象抓取和操作 - -```python -# VR输入处理器自动处理对象抓取 -# 扳机按下时自动检测和抓取对象 -# 扳机释放时自动释放对象 - -# 监听VR事件 -world.event_handler.accept('vr-object-grabbed', onObjectGrabbed) -world.event_handler.accept('vr-object-released', onObjectReleased) - -def onObjectGrabbed(object_node, controller_id): - print(f"抓取对象: {object_node.getName()}") - -def onObjectReleased(object_node, controller_id): - print(f"释放对象: {object_node.getName()}") -``` - -### 触觉反馈 - -```python -# 发送触觉反馈 -world.sendHapticFeedback( - controller_id=0, - duration=0.5, # 持续时间(秒) - intensity=0.8 # 强度(0-1) -) -``` - -## 📊 性能优化 - -### 渲染优化 - -```python -# 调整VR渲染质量 -vr_manager = world.vr_manager -vr_manager.render_scale = 1.0 # 渲染缩放 (0.5-2.0) - -# 设置多重采样抗锯齿 -fb_props = FrameBufferProperties() -fb_props.setMultisamples(4) # 4x MSAA -``` - -### 网络优化 - -```python -# 优化ALVR串流参数 -world.setALVRStreamQuality( - width=2160, # 降低分辨率提高性能 - height=1200, - fps=60, # 降低帧率减少延迟 - bitrate=100 # 降低比特率适应网络带宽 -) -``` - -### 系统资源监控 - -```python -# 获取性能状态 -streaming_status = world.getALVRStreamingStatus() -print(f"串流FPS: {streaming_status['fps']}") -print(f"延迟: {streaming_status['latency']} ms") -print(f"分辨率: {streaming_status['resolution']}") -``` - -## 🔍 故障排除 - -### 常见问题 - -1. **VR系统初始化失败** - ``` - 错误: OpenVR初始化失败 - 解决: 确保SteamVR已安装并运行 - ``` - -2. **ALVR连接失败** - ``` - 错误: 无法连接到ALVR服务器 - 解决: - - 检查ALVR服务器是否运行 - - 确认防火墙设置 - - 检查网络连接 - ``` - -3. **串流质量问题** - ``` - 问题: 画面卡顿或延迟高 - 解决: - - 降低分辨率和帧率 - - 检查WiFi信号强度 - - 调整比特率设置 - ``` - -### 调试命令 - -```python -# 启用调试模式 -world.vr_manager.debug_mode = True - -# 获取详细状态信息 -vr_info = world.getVRInfo() -print(f"VR设备信息: {vr_info}") - -# 检查网络连接 -if world.isALVRConnected(): - print("ALVR连接正常") -else: - print("ALVR连接失败") -``` - -## 🎯 高级功能 - -### 自定义VR交互 - -```python -# 创建自定义VR交互逻辑 -class CustomVRInteraction: - def __init__(self, world): - self.world = world - - def handleCustomGesture(self, gesture_data): - # 处理自定义手势 - pass - - def createVRMenu(self, controller_id): - # 在VR中创建3D菜单 - pass -``` - -### 多用户VR支持 - -```python -# 支持多个VR用户 -class MultiUserVRManager: - def __init__(self): - self.vr_users = {} - - def addVRUser(self, user_id, vr_manager): - self.vr_users[user_id] = vr_manager - - def syncVRUsers(self): - # 同步多用户VR状态 - pass -``` - -## 📋 配置文件 - -### VR配置 (`config/vr_config.yaml`) - -```yaml -vr: - enabled: true - render_scale: 1.0 - tracking_space: "standing" - -alvr: - server_ip: "127.0.0.1" - server_port: 9943 - streaming_port: 9944 - - video: - width: 2880 - height: 1700 - fps: 72 - bitrate: 150 - codec: "h264" - - audio: - enabled: true - sample_rate: 48000 - channels: 2 -``` - -## 🚀 部署建议 - -### 开发环境 - -```bash -# 创建VR开发环境 -python -m venv vr_env -source vr_env/bin/activate -pip install -r requirements/vr-requirements.txt - -# 启动开发服务器 -python main.py --vr-mode -``` - -### 生产环境 - -```bash -# 优化生产部署 -# 1. 使用专用VR计算机 -# 2. 配置高性能网络 -# 3. 启用GPU加速 -# 4. 监控系统性能 -``` - -## 📚 参考资料 - -- [OpenVR API文档](https://github.com/ValveSoftware/openvr/wiki/API-Documentation) -- [ALVR项目主页](https://github.com/alvr-org/ALVR) -- [Panda3D VR指南](https://docs.panda3d.org/1.10/python/programming/render-to-texture/index) -- [Quest开发者文档](https://developer.oculus.com/documentation/quest/) - -## 🤝 贡献指南 - -欢迎贡献代码和改进建议!请遵循以下步骤: - -1. Fork此仓库 -2. 创建功能分支 -3. 提交代码修改 -4. 创建Pull Request - -## 📄 许可证 - -本项目采用MIT许可证。详见LICENSE文件。 \ No newline at end of file diff --git a/demo/VR测试说明.md b/demo/VR测试说明.md deleted file mode 100644 index 7303f308..00000000 --- a/demo/VR测试说明.md +++ /dev/null @@ -1,253 +0,0 @@ -# VR测试说明 - -## 📋 测试条件说明 - -### 🎮 VR测试的两种模式 - -#### 1. 模拟模式(推荐用于开发和调试) -- **无需VR硬件** -- **无需SteamVR** -- **无需ALVR服务器** -- 自动启用,当真实VR不可用时 -- 模拟头盔和控制器追踪数据 -- 立体渲染显示到窗口 -- 适用于功能测试和开发 - -#### 2. 真实VR模式(需要完整VR设备) -- **需要VR头盔** -- **需要SteamVR运行** -- **需要ALVR服务器(Quest无线)** -- 真实的6DOF追踪 -- 立体渲染到VR头盔 -- 完整的VR交互体验 - -## 🔧 软件要求 - -### 必需依赖 -```bash -# 安装VR相关依赖 -pip install -r requirements/vr-requirements.txt - -# 主要包含: -# - openvr>=1.26.7 -# - psutil>=5.9.0 -# - numpy>=1.21.0 -# - Pillow>=9.0.1 -``` - -### 系统要求 -- Python 3.8+ -- Panda3D -- PyQt5 -- OpenGL支持的显卡 - -## 🎮 硬件要求(完整VR模式) - -### 最低硬件要求 -- **显卡**: GTX 1060 / RX 580 或更好 -- **内存**: 8GB RAM -- **处理器**: Intel i5-4590 / AMD FX 8350 或更好 -- **USB**: 至少1个USB 3.0端口 - -### 支持的VR头盔 -- **Quest 2/3**: 有线(USB-C)或无线(ALVR) -- **Valve Index**: DisplayPort + USB 3.0 -- **HTC Vive**: HDMI + USB 3.0 -- **其他OpenVR兼容设备** - -## 🌐 连接方式 - -### 有线连接 -1. **Quest 2/3**: USB-C数据线连接PC -2. **Valve Index**: DisplayPort + USB 3.0 -3. **HTC Vive**: HDMI + USB 3.0 - -### 无线连接(推荐Quest) -1. **下载ALVR服务器**: [GitHub](https://github.com/alvr-org/ALVR) -2. **在PC上运行ALVR服务器** -3. **在Quest上安装ALVR客户端** -4. **连接到同一Wi-Fi网络(5GHz推荐)** - -## 🔄 启动顺序 - -### 完整VR模式启动顺序 -1. **连接VR头盔**到PC -2. **启动SteamVR** -3. **(可选)启动ALVR服务器**(Quest无线) -4. **运行VR测试脚本** - -### 模拟模式启动顺序 -1. **直接运行VR测试脚本** -2. **系统会自动切换到模拟模式** - -## 📊 测试项目说明 - -### 1. 基本VR功能测试 -- VR系统初始化 -- 控制器检测 -- 追踪数据获取 -- 立体渲染测试 - -### 2. VR模拟模式测试 -- 强制启用模拟模式 -- 模拟数据更新 -- 控制器输入模拟 -- 立体渲染显示 - -### 3. ALVR串流测试 -- ALVR服务器连接 -- 串流质量设置 -- 触觉反馈测试 -- 串流开关控制 - -### 4. VR GUI控制面板 -- 图形界面控制 -- 实时状态监控 -- 参数调整 -- 系统开关 - -### 5. VR交互功能测试 -- 控制器射线显示 -- 手势识别 -- 对象交互 -- 输入处理 - -## 🚀 快速开始 - -### 开发和调试(推荐) -```bash -# 直接运行,会自动使用模拟模式 -python vr_test.py - -# 选择 "2. VR模拟模式测试" -``` - -### 完整VR测试 -```bash -# 确保VR设备已连接并启动SteamVR -# 然后运行 -python vr_test.py - -# 选择 "1. 基本VR功能测试" -``` - -## 💡 故障排除 - -### 常见问题 - -#### 1. OpenVR初始化失败 -**现象**: 显示"OpenVR初始化失败" -**解决方案**: -- 检查SteamVR是否运行 -- 确认VR头盔被系统识别 -- 重启SteamVR和头盔 -- 系统会自动切换到模拟模式 - -#### 2. 控制器未检测到 -**现象**: 控制器显示"未连接" -**解决方案**: -- 确保控制器已配对 -- 检查控制器电量 -- 在SteamVR中重新配对 -- 模拟模式下会显示模拟控制器 - -#### 3. ALVR连接失败 -**现象**: ALVR串流测试失败 -**解决方案**: -- 确保ALVR服务器正在运行 -- 检查Quest上的ALVR客户端 -- 确认PC和Quest在同一网络 -- 使用5GHz Wi-Fi网络 - -#### 4. 性能问题 -**现象**: 帧率低或卡顿 -**解决方案**: -- 降低渲染分辨率 -- 关闭不必要的后台程序 -- 检查显卡驱动更新 -- 使用模拟模式进行开发 - -### 日志分析 -测试过程中的详细日志可以帮助诊断问题: -- `✓` 表示成功 -- `⚠` 表示警告(非致命) -- `✗` 表示失败 -- `🔧` 表示配置或调试信息 - -## 📈 性能优化建议 - -### 开发阶段 -1. **使用模拟模式**进行功能开发 -2. **降低渲染分辨率**提高帧率 -3. **关闭不必要的特效** -4. **使用性能分析工具** - -### 生产部署 -1. **使用真实VR模式** -2. **根据硬件配置调整质量** -3. **启用异步时间扭曲** -4. **优化渲染管线** - -## 🔍 调试技巧 - -### 1. 检查VR系统状态 -```python -# 在代码中添加调试信息 -vr_info = world.getVRInfo() -print(f"VR模式: {vr_info['mode']}") -print(f"模拟模式: {vr_info['simulation_mode']}") -``` - -### 2. 监控性能 -```python -# 获取VR系统状态 -status = world.getVRStatus() -print(f"VR启用: {status['vr_enabled']}") -print(f"控制器数量: {len(status['controllers'])}") -``` - -### 3. 测试特定功能 -```python -# 强制启用模拟模式 -world.vr_manager.enable_simulation_mode() - -# 更新模拟数据 -world.vr_manager.update_simulation_data('head_pose', new_data) -``` - -## 📚 API参考 - -### VR系统控制 -- `world.initializeVR()` - 初始化VR系统 -- `world.shutdownVR()` - 关闭VR系统 -- `world.isVREnabled()` - 检查VR状态 -- `world.getVRInfo()` - 获取VR信息 - -### 模拟模式 -- `world.vr_manager.enable_simulation_mode()` - 启用模拟模式 -- `world.vr_manager.get_simulation_data()` - 获取模拟数据 -- `world.vr_manager.update_simulation_data()` - 更新模拟数据 - -### ALVR串流 -- `world.initializeALVR()` - 初始化ALVR -- `world.startALVRStreaming()` - 开始串流 -- `world.stopALVRStreaming()` - 停止串流 -- `world.setALVRStreamQuality()` - 设置质量 - -### VR输入 -- `world.startVRInput()` - 启动输入处理 -- `world.showControllerRays()` - 显示控制器射线 -- `world.getAllControllers()` - 获取所有控制器 -- `world.getControllerState()` - 获取控制器状态 - -## 🎯 总结 - -现在的VR测试系统具有以下优势: - -1. **自动适应**: 自动在真实VR和模拟模式之间切换 -2. **开发友好**: 无需VR硬件即可开发和测试 -3. **完整功能**: 支持所有VR功能的测试 -4. **详细反馈**: 提供清晰的状态信息和错误提示 -5. **灵活配置**: 可根据需要调整各种参数 - -无论您是否拥有VR设备,都可以使用这个测试系统来验证VR功能的正确性。 \ No newline at end of file diff --git a/main.py b/main.py index 9bca08bc..de7d3872 100644 --- a/main.py +++ b/main.py @@ -19,9 +19,6 @@ from core.selection import SelectionSystem from core.event_handler import EventHandler from core.tool_manager import ToolManager from core.script_system import ScriptManager -from core.vr_manager import VRManager -from core.vr_input_handler import VRInputHandler -from core.alvr_streamer import ALVRStreamer from gui.gui_manager import GUIManager from core.terrain_manager import TerrainManager from scene.scene_manager import SceneManager @@ -83,10 +80,6 @@ class MyWorld(CoreWorld): # 初始化界面管理系统 self.interface_manager = InterfaceManager(self) - # 初始化VR系统 - self.vr_manager = VRManager(self) - self.vr_input_handler = VRInputHandler(self, self.vr_manager) - self.alvr_streamer = ALVRStreamer(self, self.vr_manager) # 启动脚本系统 self.script_manager.start_system() @@ -606,143 +599,6 @@ class MyWorld(CoreWorld): def listAllScripts(self): """列出所有脚本信息""" return self.script_manager.list_all_scripts() - # ==================== VR系统功能代理 ==================== - - # VR系统控制方法 - 代理到vr_manager - def initializeVR(self): - """初始化VR系统""" - return self.vr_manager.initialize_vr() - - def shutdownVR(self): - """关闭VR系统""" - return self.vr_manager.shutdown_vr() - - def isVREnabled(self): - """检查VR是否启用""" - return self.vr_manager.is_vr_enabled() - - def getVRInfo(self): - """获取VR系统信息""" - return self.vr_manager.get_vr_info() - - # VR输入处理方法 - 代理到vr_input_handler - def startVRInput(self): - """启动VR输入处理""" - return self.vr_input_handler.start_input_handling() - - def stopVRInput(self): - """停止VR输入处理""" - return self.vr_input_handler.stop_input_handling() - - def showControllerRays(self, show=True): - """显示/隐藏控制器射线""" - return self.vr_input_handler.show_controller_rays(show) - - def getControllerState(self, controller_id): - """获取控制器状态""" - return self.vr_input_handler.get_controller_state(controller_id) - - def getAllControllers(self): - """获取所有控制器""" - return self.vr_input_handler.get_all_controllers() - - def setVRGestureEnabled(self, enabled): - """设置VR手势识别启用状态""" - return self.vr_input_handler.set_gesture_enabled(enabled) - - def setVRInteractionEnabled(self, enabled): - """设置VR交互启用状态""" - return self.vr_input_handler.set_interaction_enabled(enabled) - - # ALVR串流方法 - 代理到alvr_streamer - def initializeALVR(self): - """初始化ALVR串流""" - return self.alvr_streamer.initialize() - - def startALVRStreaming(self): - """开始ALVR串流""" - return self.alvr_streamer.start_streaming() - - def stopALVRStreaming(self): - """停止ALVR串流""" - return self.alvr_streamer.stop_streaming() - - def getALVRStreamingStatus(self): - """获取ALVR串流状态""" - return self.alvr_streamer.get_streaming_status() - - def setALVRStreamQuality(self, width, height, fps, bitrate): - """设置ALVR串流质量""" - return self.alvr_streamer.set_stream_quality(width, height, fps, bitrate) - - def sendHapticFeedback(self, controller_id, duration, intensity): - """发送触觉反馈""" - return self.alvr_streamer.send_haptic_feedback(controller_id, duration, intensity) - - def shutdownALVR(self): - """关闭ALVR串流""" - return self.alvr_streamer.shutdown() - - def isALVRConnected(self): - """检查ALVR是否连接""" - return self.alvr_streamer.is_connected() - - def isALVRStreaming(self): - """检查ALVR是否在串流""" - return self.alvr_streamer.is_streaming() - - # 便捷方法 - def enableVRMode(self): - """启用VR模式(一键启动)""" - print("启用VR模式...") - - # 1. 初始化VR系统 - if not self.initializeVR(): - print("VR系统初始化失败") - return False - - # 2. 启动VR输入处理 - if not self.startVRInput(): - print("VR输入处理启动失败") - return False - - # 3. 初始化ALVR串流 - if self.initializeALVR(): - print("✓ ALVR串流已启用") - # 自动开始串流 - self.startALVRStreaming() - else: - print("⚠ ALVR串流启用失败,但VR系统仍可用") - - print("✓ VR模式已启用") - return True - - def disableVRMode(self): - """禁用VR模式(一键关闭)""" - print("禁用VR模式...") - - # 1. 停止ALVR串流 - self.stopALVRStreaming() - self.shutdownALVR() - - # 2. 停止VR输入处理 - self.stopVRInput() - - # 3. 关闭VR系统 - self.shutdownVR() - - print("✓ VR模式已禁用") - - def getVRStatus(self): - """获取VR系统总体状态""" - return { - "vr_enabled": self.isVREnabled(), - "vr_info": self.getVRInfo(), - "controllers": self.getAllControllers(), - "alvr_connected": self.isALVRConnected(), - "alvr_streaming": self.isALVRStreaming(), - "streaming_status": self.getALVRStreamingStatus() if self.isALVRConnected() else None - } def loadCesiumTileset(self,tileset_url,position=(0,0,0)): return self.scene_manager.load_cesium_tileset(tileset_url,position) diff --git a/requirements/vr-requirements.txt b/requirements/vr-requirements.txt deleted file mode 100644 index 57cbb876..00000000 --- a/requirements/vr-requirements.txt +++ /dev/null @@ -1,18 +0,0 @@ -# VR功能依赖包 -# 用于支持VR显示和ALVR串流功能 - -# OpenVR Python库 - VR系统核心 -openvr>=1.26.7 - -# 网络和通信 -psutil>=5.9.0 - -# 数学和科学计算 -numpy>=1.21.0 - -# 图像处理 -Pillow>=9.0.1 - -# 现有核心依赖 -Panda3D>=1.10.15 -PyQt5>=5.15.9 \ No newline at end of file diff --git a/test_metallic_gradient.png b/test_metallic_gradient.png deleted file mode 100644 index 11dbcc812cdc6a5d0115a5c14248d152a038fbd1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1304 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&w3=!$sk4H3kNj`<^b2Ar*7pUNU54FyJ}7 zq3n+%zZ-v~;)KK2#@_G$9C&-LC5&MQ(*aS2bqocp4cZJbj1RaN!bgQhLt->djAn&F cUMSpQ-!zHkpQ7K@8K5H1)78&qol`;+0Db^kO#lD@ diff --git a/test_metallic_stripes.png b/test_metallic_stripes.png deleted file mode 100644 index 956c8661f3d20aa537e1744bd66b374588bb187d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1308 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&w3=!$sk4H3kNj$DS^ZAr*7pUfL*lz<`I@ z@bvHZb9xq7RW5kC@wa7n`px72FaO{D=Fc34M=S#B4Hk??I2HUD3Yj{D9Y%#lLt->d fjAn&FUnuAYUgrK^?WMK~RP1@W`njxgN@xNAmdRkl diff --git a/test_roughness_checkerboard.png b/test_roughness_checkerboard.png deleted file mode 100644 index 3ee2bd65e7ccda1e9fb3c3f4e3e4020febf2ee37..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1364 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&w3=!$sk4H3kM&X-^l&kcv5P?{4gMHRND8 zc>L@A&FeXszHBw#+1f0s`B``G)8B^wOa9&eWNYuoP{`CF>@bJn5sQF&g9YOePKALM z+Hv#V{P|C-Kj%NcJ@_04458ff=j-49V1D-g#-McG*>C^;=-i)OZ+)L_kVPywb>&sp z{i{AMZ%}`Ha5`_>_vh(9@BO*`(`-;>3Rup({rU5K>wgVDtp}&`Ht)5!KmGmL{d3zH h2Ty?l3p=Kc-|Vd?l)rZR<*|W^OHWrnmvv4FO#rtdjrafn diff --git a/test_roughness_circle.png b/test_roughness_circle.png deleted file mode 100644 index 5ab3d4d699503e94f433c5c8fe1b13b5aa170a87..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19408 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

WavE?*4tH2w{K0;j`Z5%%(K11=>=P9mB2 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 xlh!MsD^`}djAn&F cUMSpQ-!zHkpQ7K@8K5H1)78&qol`;+0Db^kO#lD@ diff --git a/ui/vr_control_panel.py b/ui/vr_control_panel.py deleted file mode 100644 index fbb75c1b..00000000 --- a/ui/vr_control_panel.py +++ /dev/null @@ -1,412 +0,0 @@ -""" -VR控制面板 - -提供VR系统的GUI控制界面 -包括VR启用/禁用、ALVR串流控制、性能监控等功能 -""" - -from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, - QPushButton, QLabel, QProgressBar, QSlider, - QSpinBox, QCheckBox, QComboBox, QTextEdit, - QGridLayout, QFrame, QSizePolicy) -from PyQt5.QtCore import Qt, QTimer, pyqtSignal -from PyQt5.QtGui import QFont, QColor, QPalette - - -class VRControlPanel(QWidget): - """VR控制面板""" - - # 信号定义 - vr_enabled = pyqtSignal(bool) - alvr_streaming_changed = pyqtSignal(bool) - - def __init__(self, world, parent=None): - super().__init__(parent) - self.world = world - self.setupUI() - self.connectSignals() - - # 设置更新定时器 - self.update_timer = QTimer() - self.update_timer.timeout.connect(self.updateStatus) - self.update_timer.start(1000) # 每秒更新一次 - - def setupUI(self): - """设置界面""" - self.setWindowTitle("VR控制面板") - self.setMinimumSize(400, 600) - - # 主布局 - main_layout = QVBoxLayout(self) - - # VR系统控制组 - vr_control_group = self.createVRControlGroup() - main_layout.addWidget(vr_control_group) - - # ALVR串流控制组 - alvr_control_group = self.createALVRControlGroup() - main_layout.addWidget(alvr_control_group) - - # 性能监控组 - performance_group = self.createPerformanceGroup() - main_layout.addWidget(performance_group) - - # 控制器状态组 - controller_group = self.createControllerGroup() - main_layout.addWidget(controller_group) - - # 日志输出组 - log_group = self.createLogGroup() - main_layout.addWidget(log_group) - - # 添加弹性空间 - main_layout.addStretch() - - def createVRControlGroup(self): - """创建VR系统控制组""" - group = QGroupBox("VR系统控制") - layout = QVBoxLayout(group) - - # 状态指示器 - self.vr_status_label = QLabel("VR状态: 未启用") - self.vr_status_label.setStyleSheet("color: red; font-weight: bold;") - layout.addWidget(self.vr_status_label) - - # 启用/禁用VR按钮 - button_layout = QHBoxLayout() - self.enable_vr_button = QPushButton("启用VR") - self.enable_vr_button.clicked.connect(self.enableVR) - button_layout.addWidget(self.enable_vr_button) - - self.disable_vr_button = QPushButton("禁用VR") - self.disable_vr_button.clicked.connect(self.disableVR) - self.disable_vr_button.setEnabled(False) - button_layout.addWidget(self.disable_vr_button) - - layout.addLayout(button_layout) - - # VR设备信息 - self.vr_info_label = QLabel("VR设备: 未连接") - layout.addWidget(self.vr_info_label) - - # VR选项 - options_layout = QGridLayout() - - # 控制器射线显示 - self.show_rays_checkbox = QCheckBox("显示控制器射线") - self.show_rays_checkbox.stateChanged.connect(self.toggleControllerRays) - options_layout.addWidget(self.show_rays_checkbox, 0, 0) - - # 手势识别 - self.gesture_checkbox = QCheckBox("启用手势识别") - self.gesture_checkbox.stateChanged.connect(self.toggleGestureRecognition) - options_layout.addWidget(self.gesture_checkbox, 0, 1) - - # VR交互 - self.interaction_checkbox = QCheckBox("启用VR交互") - self.interaction_checkbox.setChecked(True) - self.interaction_checkbox.stateChanged.connect(self.toggleVRInteraction) - options_layout.addWidget(self.interaction_checkbox, 1, 0) - - layout.addLayout(options_layout) - - return group - - def createALVRControlGroup(self): - """创建ALVR串流控制组""" - group = QGroupBox("ALVR串流控制") - layout = QVBoxLayout(group) - - # 连接状态 - self.alvr_status_label = QLabel("ALVR状态: 未连接") - self.alvr_status_label.setStyleSheet("color: red; font-weight: bold;") - layout.addWidget(self.alvr_status_label) - - # 串流控制按钮 - button_layout = QHBoxLayout() - self.start_streaming_button = QPushButton("开始串流") - self.start_streaming_button.clicked.connect(self.startStreaming) - self.start_streaming_button.setEnabled(False) - button_layout.addWidget(self.start_streaming_button) - - self.stop_streaming_button = QPushButton("停止串流") - self.stop_streaming_button.clicked.connect(self.stopStreaming) - self.stop_streaming_button.setEnabled(False) - button_layout.addWidget(self.stop_streaming_button) - - layout.addLayout(button_layout) - - # 串流质量设置 - quality_layout = QGridLayout() - - # 分辨率 - quality_layout.addWidget(QLabel("分辨率宽度:"), 0, 0) - self.width_spinbox = QSpinBox() - self.width_spinbox.setRange(1920, 4096) - self.width_spinbox.setValue(2880) - self.width_spinbox.setSuffix(" px") - quality_layout.addWidget(self.width_spinbox, 0, 1) - - quality_layout.addWidget(QLabel("分辨率高度:"), 1, 0) - self.height_spinbox = QSpinBox() - self.height_spinbox.setRange(1080, 2160) - self.height_spinbox.setValue(1700) - self.height_spinbox.setSuffix(" px") - quality_layout.addWidget(self.height_spinbox, 1, 1) - - # 帧率 - quality_layout.addWidget(QLabel("帧率:"), 2, 0) - self.fps_spinbox = QSpinBox() - self.fps_spinbox.setRange(60, 120) - self.fps_spinbox.setValue(72) - self.fps_spinbox.setSuffix(" fps") - quality_layout.addWidget(self.fps_spinbox, 2, 1) - - # 比特率 - quality_layout.addWidget(QLabel("比特率:"), 3, 0) - self.bitrate_spinbox = QSpinBox() - self.bitrate_spinbox.setRange(50, 500) - self.bitrate_spinbox.setValue(150) - self.bitrate_spinbox.setSuffix(" Mbps") - quality_layout.addWidget(self.bitrate_spinbox, 3, 1) - - layout.addLayout(quality_layout) - - # 应用设置按钮 - self.apply_quality_button = QPushButton("应用质量设置") - self.apply_quality_button.clicked.connect(self.applyQualitySettings) - layout.addWidget(self.apply_quality_button) - - return group - - def createPerformanceGroup(self): - """创建性能监控组""" - group = QGroupBox("性能监控") - layout = QGridLayout(group) - - # FPS显示 - layout.addWidget(QLabel("渲染FPS:"), 0, 0) - self.fps_label = QLabel("0") - self.fps_label.setStyleSheet("font-weight: bold; color: blue;") - layout.addWidget(self.fps_label, 0, 1) - - # 串流FPS - layout.addWidget(QLabel("串流FPS:"), 1, 0) - self.stream_fps_label = QLabel("0") - self.stream_fps_label.setStyleSheet("font-weight: bold; color: green;") - layout.addWidget(self.stream_fps_label, 1, 1) - - # 延迟 - layout.addWidget(QLabel("延迟:"), 2, 0) - self.latency_label = QLabel("0 ms") - self.latency_label.setStyleSheet("font-weight: bold; color: orange;") - layout.addWidget(self.latency_label, 2, 1) - - # 性能进度条 - layout.addWidget(QLabel("CPU使用率:"), 3, 0) - self.cpu_progress = QProgressBar() - self.cpu_progress.setRange(0, 100) - layout.addWidget(self.cpu_progress, 3, 1) - - layout.addWidget(QLabel("GPU使用率:"), 4, 0) - self.gpu_progress = QProgressBar() - self.gpu_progress.setRange(0, 100) - layout.addWidget(self.gpu_progress, 4, 1) - - return group - - def createControllerGroup(self): - """创建控制器状态组""" - group = QGroupBox("控制器状态") - layout = QVBoxLayout(group) - - # 控制器列表 - self.controller_list = QTextEdit() - self.controller_list.setMaximumHeight(100) - self.controller_list.setReadOnly(True) - layout.addWidget(self.controller_list) - - # 触觉反馈测试 - haptic_layout = QHBoxLayout() - haptic_layout.addWidget(QLabel("触觉反馈测试:")) - - self.haptic_button = QPushButton("发送震动") - self.haptic_button.clicked.connect(self.sendHapticFeedback) - haptic_layout.addWidget(self.haptic_button) - - layout.addLayout(haptic_layout) - - return group - - def createLogGroup(self): - """创建日志输出组""" - group = QGroupBox("系统日志") - layout = QVBoxLayout(group) - - self.log_text = QTextEdit() - self.log_text.setMaximumHeight(150) - self.log_text.setReadOnly(True) - layout.addWidget(self.log_text) - - # 清空日志按钮 - clear_button = QPushButton("清空日志") - clear_button.clicked.connect(self.clearLog) - layout.addWidget(clear_button) - - return group - - def connectSignals(self): - """连接信号""" - # 连接值变化信号 - self.width_spinbox.valueChanged.connect(self.onQualityChanged) - self.height_spinbox.valueChanged.connect(self.onQualityChanged) - self.fps_spinbox.valueChanged.connect(self.onQualityChanged) - self.bitrate_spinbox.valueChanged.connect(self.onQualityChanged) - - def enableVR(self): - """启用VR""" - self.addLog("正在启用VR模式...") - if self.world.enableVRMode(): - self.addLog("✓ VR模式已启用") - self.vr_enabled.emit(True) - else: - self.addLog("✗ VR模式启用失败") - - def disableVR(self): - """禁用VR""" - self.addLog("正在禁用VR模式...") - self.world.disableVRMode() - self.addLog("✓ VR模式已禁用") - self.vr_enabled.emit(False) - - def startStreaming(self): - """开始串流""" - self.addLog("正在开始ALVR串流...") - if self.world.startALVRStreaming(): - self.addLog("✓ ALVR串流已开始") - self.alvr_streaming_changed.emit(True) - else: - self.addLog("✗ ALVR串流开始失败") - - def stopStreaming(self): - """停止串流""" - self.addLog("正在停止ALVR串流...") - self.world.stopALVRStreaming() - self.addLog("✓ ALVR串流已停止") - self.alvr_streaming_changed.emit(False) - - def toggleControllerRays(self, checked): - """切换控制器射线显示""" - self.world.showControllerRays(checked) - self.addLog(f"控制器射线: {'显示' if checked else '隐藏'}") - - def toggleGestureRecognition(self, checked): - """切换手势识别""" - self.world.setVRGestureEnabled(checked) - self.addLog(f"手势识别: {'启用' if checked else '禁用'}") - - def toggleVRInteraction(self, checked): - """切换VR交互""" - self.world.setVRInteractionEnabled(checked) - self.addLog(f"VR交互: {'启用' if checked else '禁用'}") - - def applyQualitySettings(self): - """应用质量设置""" - width = self.width_spinbox.value() - height = self.height_spinbox.value() - fps = self.fps_spinbox.value() - bitrate = self.bitrate_spinbox.value() - - self.world.setALVRStreamQuality(width, height, fps, bitrate) - self.addLog(f"串流质量已设置: {width}x{height} @ {fps}fps, {bitrate}Mbps") - - def sendHapticFeedback(self): - """发送触觉反馈""" - controllers = self.world.getAllControllers() - if controllers: - controller_id = controllers[0] # 使用第一个控制器 - self.world.sendHapticFeedback(controller_id, 0.5, 0.8) - self.addLog(f"已发送触觉反馈到控制器 {controller_id}") - else: - self.addLog("没有可用的控制器") - - def onQualityChanged(self): - """质量设置变化""" - # 可以在这里实现实时质量调整 - pass - - def updateStatus(self): - """更新状态显示""" - # 更新VR状态 - vr_status = self.world.getVRStatus() - - # VR系统状态 - if vr_status['vr_enabled']: - self.vr_status_label.setText("VR状态: 已启用") - self.vr_status_label.setStyleSheet("color: green; font-weight: bold;") - self.enable_vr_button.setEnabled(False) - self.disable_vr_button.setEnabled(True) - - # 更新VR设备信息 - vr_info = vr_status['vr_info'] - if vr_info: - device_info = f"VR设备: {vr_info.get('hmd_manufacturer', 'Unknown')} {vr_info.get('hmd_model', 'Unknown')}" - self.vr_info_label.setText(device_info) - else: - self.vr_status_label.setText("VR状态: 未启用") - self.vr_status_label.setStyleSheet("color: red; font-weight: bold;") - self.enable_vr_button.setEnabled(True) - self.disable_vr_button.setEnabled(False) - self.vr_info_label.setText("VR设备: 未连接") - - # ALVR状态 - if vr_status['alvr_connected']: - self.alvr_status_label.setText("ALVR状态: 已连接") - self.alvr_status_label.setStyleSheet("color: green; font-weight: bold;") - self.start_streaming_button.setEnabled(not vr_status['alvr_streaming']) - self.stop_streaming_button.setEnabled(vr_status['alvr_streaming']) - else: - self.alvr_status_label.setText("ALVR状态: 未连接") - self.alvr_status_label.setStyleSheet("color: red; font-weight: bold;") - self.start_streaming_button.setEnabled(False) - self.stop_streaming_button.setEnabled(False) - - # 性能统计 - streaming_status = vr_status['streaming_status'] - if streaming_status: - self.stream_fps_label.setText(str(streaming_status.get('fps', 0))) - self.latency_label.setText(f"{streaming_status.get('latency', 0)} ms") - - # 控制器状态 - controllers = vr_status['controllers'] - if controllers: - controller_text = f"已连接 {len(controllers)} 个控制器:\n" - for i, controller_id in enumerate(controllers): - controller_text += f"控制器 {i+1}: ID {controller_id}\n" - self.controller_list.setText(controller_text) - else: - self.controller_list.setText("没有连接的控制器") - - def addLog(self, message): - """添加日志""" - import datetime - timestamp = datetime.datetime.now().strftime("%H:%M:%S") - log_message = f"[{timestamp}] {message}" - self.log_text.append(log_message) - print(log_message) # 同时输出到控制台 - - def clearLog(self): - """清空日志""" - self.log_text.clear() - - def closeEvent(self, event): - """关闭事件""" - # 停止更新定时器 - if hasattr(self, 'update_timer'): - self.update_timer.stop() - - # 如果VR启用,先禁用 - if self.world.isVREnabled(): - self.disableVR() - - event.accept() \ No newline at end of file diff --git a/vr_test.py b/vr_test.py deleted file mode 100644 index b35a6ee3..00000000 --- a/vr_test.py +++ /dev/null @@ -1,468 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -VR功能测试脚本 -测试VR系统的各种功能,包括模拟模式和真实VR模式 -""" - -import sys -import os -from main import MyWorld - -def test_basic_vr_functionality(): - """测试基本VR功能""" - print("=== VR基本功能测试 ===") - - # 创建世界实例 - world = MyWorld() - - # 检查VR管理器是否正确初始化 - print("✓ VR管理器已创建") - - # 检查OpenVR库是否可用 - try: - import openvr - print("✓ OpenVR库已安装") - except ImportError: - print("⚠ OpenVR库未安装,将使用模拟模式") - - # 测试VR初始化(会自动回退到模拟模式) - print("\n正在测试VR初始化...") - vr_success = world.initializeVR() - - if vr_success: - print("✓ VR系统初始化成功") - - # 获取VR信息 - vr_info = world.getVRInfo() - print(f"\n📊 VR系统信息:") - print(f" - 模式: {vr_info['mode']}") - print(f" - 启用状态: {vr_info['enabled']}") - print(f" - 模拟模式: {vr_info['simulation_mode']}") - print(f" - 渲染尺寸: {vr_info['render_size']}") - print(f" - 控制器数量: {vr_info.get('controllers', 0)}") - - # 测试控制器输入 - print("\n🎮 测试控制器输入:") - for i in range(2): - controller_input = world.vr_manager.get_controller_input(i) - if controller_input and controller_input.get('connected'): - print(f" 控制器 {i}: 已连接") - print(f" - 扳机: {controller_input['trigger']:.2f}") - print(f" - 握把: {controller_input['grip']:.2f}") - print(f" - 触摸板: ({controller_input['touchpad']['x']:.2f}, {controller_input['touchpad']['y']:.2f})") - else: - print(f" 控制器 {i}: 未连接") - - # 测试VR状态 - print("\n📊 VR系统状态:") - status = world.getVRStatus() - print(f" - VR启用: {status['vr_enabled']}") - print(f" - 控制器总数: {len(status['controllers'])}") - - else: - print("✗ VR系统初始化失败") - return False - - # 关闭VR系统 - world.shutdownVR() - print("\n✓ VR系统已关闭") - - return True - -def test_simulation_mode(): - """测试VR模拟模式""" - print("=== VR模拟模式测试 ===") - - # 创建世界实例 - world = MyWorld() - - # 强制启用模拟模式 - print("强制启用VR模拟模式...") - vr_success = world.vr_manager.enable_simulation_mode() - - if vr_success: - print("✓ VR模拟模式启用成功") - - # 获取模拟数据 - sim_data = world.vr_manager.get_simulation_data() - if sim_data: - print(f"\n🎮 模拟数据:") - print(f" - 头部位置: {sim_data['head_pose']['position']}") - print(f" - 控制器 0 位置: {sim_data['controller_poses'][0]['position']}") - print(f" - 控制器 1 位置: {sim_data['controller_poses'][1]['position']}") - print(f" - 渲染尺寸: {sim_data['render_size']}") - - # 测试更新模拟数据 - print(f"\n🔧 测试模拟数据更新:") - new_head_pos = [0.1, 0.1, 1.7] - success = world.vr_manager.update_simulation_data('head_pose', { - 'position': new_head_pos, - 'rotation': [0, 0, 0, 1] - }) - if success: - print(f" ✓ 头部位置更新为: {new_head_pos}") - - # 测试控制器输入(模拟) - print(f"\n🎮 模拟控制器输入测试:") - for i in range(2): - controller_input = world.vr_manager.get_controller_input(i) - if controller_input: - print(f" 控制器 {i}:") - print(f" - 连接状态: {controller_input['connected']}") - print(f" - 扳机: {controller_input['trigger']}") - print(f" - 握把: {controller_input['grip']}") - print(f" - 菜单键: {controller_input['menu']}") - - else: - print("✗ VR模拟模式启用失败") - return False - - # 关闭VR系统 - world.vr_manager.shutdown_vr() - print("\n✓ VR模拟模式已关闭") - - return True - -def test_alvr_streaming(): - """测试ALVR串流功能""" - print("=== ALVR串流测试 ===") - - # 创建世界实例 - world = MyWorld() - - # 初始化VR系统 - print("初始化VR系统...") - if not world.initializeVR(): - print("✗ VR系统初始化失败") - return False - - # 测试ALVR初始化 - print("\n正在测试ALVR初始化...") - alvr_success = world.initializeALVR() - - if alvr_success: - print("✓ ALVR初始化成功") - - # 测试串流状态 - print("\n📊 ALVR串流状态:") - status = world.getALVRStreamingStatus() - print(f" - 连接状态: {world.isALVRConnected()}") - print(f" - 串流状态: {world.isALVRStreaming()}") - - # 测试串流质量设置 - print("\n🎥 测试串流质量设置:") - quality_success = world.setALVRStreamQuality(1920, 1080, 60, 100) - if quality_success: - print(" ✓ 串流质量设置成功: 1920x1080@60fps, 100Mbps") - else: - print(" ⚠ 串流质量设置失败") - - # 测试触觉反馈 - print("\n🔔 测试触觉反馈:") - for controller_id in range(2): - feedback_success = world.sendHapticFeedback(controller_id, 0.1, 0.5) - if feedback_success: - print(f" ✓ 控制器 {controller_id} 触觉反馈发送成功") - else: - print(f" ⚠ 控制器 {controller_id} 触觉反馈发送失败") - - # 测试串流控制 - print("\n🎮 测试串流控制:") - start_success = world.startALVRStreaming() - if start_success: - print(" ✓ 串流开始成功") - else: - print(" ⚠ 串流开始失败") - - # 等待一下然后停止 - import time - time.sleep(1) - - stop_success = world.stopALVRStreaming() - if stop_success: - print(" ✓ 串流停止成功") - else: - print(" ⚠ 串流停止失败") - - else: - print("⚠ ALVR初始化失败(这在没有ALVR服务器时是正常的)") - print(" 提示: 要使用ALVR功能,请:") - print(" 1. 下载并安装ALVR服务器") - print(" 2. 启动ALVR服务器") - print(" 3. 在Quest头盔上安装并启动ALVR客户端") - - # 关闭系统 - world.shutdownALVR() - world.shutdownVR() - print("\n✓ ALVR和VR系统已关闭") - - return True - -def test_vr_gui_control_panel(): - """测试VR GUI控制面板""" - print("=== VR GUI控制面板测试 ===") - - try: - from ui.vr_control_panel import VRControlPanel - from PyQt5.QtWidgets import QApplication - - # 创建Qt应用程序 - app = QApplication.instance() - if app is None: - app = QApplication(sys.argv) - - # 创建世界实例 - world = MyWorld() - - # 创建VR控制面板 - print("创建VR控制面板...") - control_panel = VRControlPanel(world) - - # 测试面板功能 - print("✓ VR控制面板创建成功") - print(" - 面板标题:", control_panel.windowTitle()) - print(" - 面板大小:", control_panel.size().width(), "x", control_panel.size().height()) - - # 显示面板(非阻塞) - control_panel.show() - print("✓ VR控制面板已显示") - - print("\n💡 GUI控制面板功能:") - print(" - VR系统开关控制") - print(" - ALVR串流控制") - print(" - 实时状态监控") - print(" - 质量设置调整") - print(" - 控制器状态显示") - print(" - 性能监控") - - # 短暂显示然后关闭 - import time - time.sleep(2) - control_panel.close() - - print("\n✓ VR控制面板测试完成") - - except Exception as e: - print(f"✗ VR控制面板测试失败: {str(e)}") - return False - - return True - -def test_vr_interaction(): - """测试VR交互功能""" - print("=== VR交互功能测试 ===") - - # 创建世界实例 - world = MyWorld() - - # 初始化VR系统 - print("初始化VR系统...") - if not world.initializeVR(): - print("✗ VR系统初始化失败") - return False - - # 测试VR输入处理 - print("\n🎮 测试VR输入处理:") - input_success = world.startVRInput() - if input_success: - print(" ✓ VR输入处理启动成功") - - # 测试控制器射线显示 - print("\n🌟 测试控制器射线:") - ray_success = world.showControllerRays(True) - if ray_success: - print(" ✓ 控制器射线显示启用") - else: - print(" ⚠ 控制器射线显示失败") - - # 测试手势识别 - print("\n✋ 测试手势识别:") - gesture_success = world.setVRGestureEnabled(True) - if gesture_success: - print(" ✓ VR手势识别启用") - else: - print(" ⚠ VR手势识别启用失败") - - # 测试VR交互 - print("\n🤏 测试VR交互:") - interaction_success = world.setVRInteractionEnabled(True) - if interaction_success: - print(" ✓ VR交互功能启用") - else: - print(" ⚠ VR交互功能启用失败") - - # 获取控制器状态 - print("\n🎮 控制器状态:") - controllers = world.getAllControllers() - for i, controller in enumerate(controllers): - if controller: - print(f" 控制器 {i}: 可用") - state = world.getControllerState(i) - if state: - print(f" - 位置: {state.get('position', 'N/A')}") - print(f" - 旋转: {state.get('rotation', 'N/A')}") - print(f" - 按钮: {state.get('buttons', 'N/A')}") - else: - print(f" 控制器 {i}: 不可用") - - # 停止VR输入 - world.stopVRInput() - print("\n✓ VR输入处理已停止") - - else: - print(" ⚠ VR输入处理启动失败") - - # 关闭VR系统 - world.shutdownVR() - print("\n✓ VR系统已关闭") - - return True - -def run_all_tests(): - """运行所有测试""" - print("=== 运行所有VR测试 ===") - - tests = [ - ("基本VR功能", test_basic_vr_functionality), - ("VR模拟模式", test_simulation_mode), - ("ALVR串流", test_alvr_streaming), - ("VR GUI控制面板", test_vr_gui_control_panel), - ("VR交互功能", test_vr_interaction) - ] - - results = [] - for test_name, test_func in tests: - print(f"\n{'='*50}") - print(f"正在运行: {test_name}") - print('='*50) - - try: - success = test_func() - results.append((test_name, success)) - if success: - print(f"✓ {test_name} 测试通过") - else: - print(f"✗ {test_name} 测试失败") - except Exception as e: - print(f"✗ {test_name} 测试出错: {str(e)}") - results.append((test_name, False)) - - # 打印总结 - print(f"\n{'='*50}") - print("测试总结") - print('='*50) - - passed = sum(1 for _, success in results if success) - total = len(results) - - for test_name, success in results: - status = "✓ 通过" if success else "✗ 失败" - print(f"{test_name}: {status}") - - print(f"\n总计: {passed}/{total} 个测试通过") - - if passed == total: - print("🎉 所有测试都通过了!") - else: - print("⚠ 部分测试失败,请检查上述输出") - - return passed == total - -def print_vr_requirements(): - """打印VR系统要求""" - print("📋 VR系统要求说明") - print("="*50) - print("🔧 软件要求:") - print(" - Python 3.8+") - print(" - Panda3D") - print(" - PyQt5") - print(" - OpenVR库 (pip install openvr)") - print(" - 其他依赖见 requirements/vr-requirements.txt") - - print("\n🎮 硬件要求(完整VR模式):") - print(" - 支持VR的显卡 (GTX 1060/RX 580 或更好)") - print(" - VR头盔 (Quest 2/3, Valve Index, HTC Vive等)") - print(" - 足够的USB端口或无线连接") - - print("\n🌐 连接方式:") - print(" 有线连接:") - print(" - Valve Index: DisplayPort + USB 3.0") - print(" - HTC Vive: HDMI + USB 3.0") - print(" - Quest 2/3: USB-C (Quest Link)") - print(" 无线连接:") - print(" - Quest 2/3: 通过ALVR串流") - print(" - 需要5GHz Wi-Fi网络") - print(" - 需要ALVR服务器运行") - - print("\n🔄 启动顺序(完整VR模式):") - print(" 1. 确保VR头盔已连接并识别") - print(" 2. 启动SteamVR") - print(" 3. (可选) 启动ALVR服务器(Quest无线)") - print(" 4. 运行VR测试脚本") - - print("\n🎮 模拟模式说明:") - print(" - 无需VR硬件") - print(" - 模拟头盔和控制器追踪") - print(" - 立体渲染到窗口") - print(" - 适用于开发和调试") - - print("\n💡 故障排除:") - print(" - 如果OpenVR初始化失败,系统会自动切换到模拟模式") - print(" - 检查SteamVR是否正在运行") - print(" - 确认VR头盔被系统识别") - print(" - 检查USB/DisplayPort连接") - print(" - 重启SteamVR和头盔") - -def main(): - """主函数""" - print("🎮 VR功能测试脚本") - print("="*50) - - # 显示要求说明 - print_vr_requirements() - - print("\n请选择测试类型:") - print("1. 基本VR功能测试") - print("2. VR模拟模式测试") - print("3. ALVR串流测试") - print("4. VR GUI控制面板") - print("5. VR交互功能测试") - print("6. 运行所有测试") - print("7. 查看VR系统要求") - - try: - choice = input("请输入选择 (1-7): ") - - if choice == "1": - success = test_basic_vr_functionality() - elif choice == "2": - success = test_simulation_mode() - elif choice == "3": - success = test_alvr_streaming() - elif choice == "4": - success = test_vr_gui_control_panel() - elif choice == "5": - success = test_vr_interaction() - elif choice == "6": - success = run_all_tests() - elif choice == "7": - print_vr_requirements() - return - else: - print("无效的选择") - return - - print("\n" + "="*50) - if success: - print("✓ 测试成功") - else: - print("✗ 测试失败") - - except KeyboardInterrupt: - print("\n用户中断测试") - except Exception as e: - print(f"测试过程中发生错误: {str(e)}") - -if __name__ == "__main__": - main() \ No newline at end of file From 6c9c4339f29c326682c6dc41d2a9b324255ec3c7 Mon Sep 17 00:00:00 2001 From: Rowland <975945824@qq.com> Date: Mon, 15 Sep 2025 14:38:18 +0800 Subject: [PATCH 2/3] =?UTF-8?q?vr=E7=94=BB=E9=9D=A2=E6=B7=BB=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VR_GUIDE.md | 194 +++++++ core/vr_manager.py | 932 ++++++++++++++++++++++++++++++++++ main.py | 9 + requirements/requirements.txt | 1 + ui/main_window.py | 192 ++++++- 5 files changed, 1327 insertions(+), 1 deletion(-) create mode 100644 VR_GUIDE.md create mode 100644 core/vr_manager.py diff --git a/VR_GUIDE.md b/VR_GUIDE.md new file mode 100644 index 00000000..652e828f --- /dev/null +++ b/VR_GUIDE.md @@ -0,0 +1,194 @@ +# 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/core/vr_manager.py b/core/vr_manager.py new file mode 100644 index 00000000..53f19054 --- /dev/null +++ b/core/vr_manager.py @@ -0,0 +1,932 @@ +""" +VR管理器模块 + +负责VR功能的初始化、渲染和交互: +- OpenVR/SteamVR集成 +- VR头显跟踪和渲染 +- VR控制器交互 +- VR模式切换 +""" + +import sys +import numpy as np +from panda3d.core import ( + WindowProperties, GraphicsPipe, FrameBufferProperties, + GraphicsOutput, Texture, Camera, PerspectiveLens, MatrixLens, + Mat4, Vec3, TransformState, RenderState, CardMaker, + BitMask32, PandaNode, NodePath, LMatrix4, LVector3, LVector4, + CS_yup_right, CS_default, PythonCallbackObject +) +from direct.task import Task +from direct.showbase.DirectObject import DirectObject + +try: + import openvr + OPENVR_AVAILABLE = True +except ImportError: + OPENVR_AVAILABLE = False + print("警告: OpenVR未安装,VR功能将不可用") + + +class VRManager(DirectObject): + """VR管理器类 - 处理所有VR相关功能""" + + def __init__(self, world): + """初始化VR管理器 + + Args: + world: 主世界对象引用 + """ + super().__init__() + + self.world = world + self.vr_system = None + self.vr_enabled = False + self.vr_initialized = False + + # VR渲染相关 + self.vr_left_eye_buffer = None + self.vr_right_eye_buffer = None + self.vr_left_camera = None + self.vr_right_camera = None + self.vr_compositor = None + + # VR跟踪数据 + self.hmd_pose = Mat4.identMat() + self.controller_poses = {} + self.tracked_device_poses = [] + self.poses = None # OpenVR姿态数组 + + # VR渲染参数 + self.eye_width = 1080 + self.eye_height = 1200 + self.near_clip = 0.1 + self.far_clip = 1000.0 + + # VR任务 + self.vr_task = None + + # VR锚点层级系统 + self.tracking_space = None + self.hmd_anchor = None + self.left_eye_anchor = None + self.right_eye_anchor = None + + # 坐标系转换矩阵 - 使用Panda3D内置方法 + self.coord_mat = LMatrix4.convert_mat(CS_yup_right, CS_default) + self.coord_mat_inv = LMatrix4.convert_mat(CS_default, CS_yup_right) + + # 性能监控 + self.frame_count = 0 + self.last_fps_check = 0 + self.last_fps_time = 0 + self.vr_fps = 0 + self.submit_failures = 0 + self.pose_failures = 0 + + # VR提交策略 - 基于参考实现 + self.submit_together = True # 是否在right_cb中同时提交左右眼 + + print("✓ VR管理器初始化完成") + + def convert_mat(self, mat): + """ + 将OpenVR矩阵转换为Panda3D矩阵 - 基于参考实现 + """ + if len(mat.m) == 4: + result = LMatrix4( + mat.m[0][0], mat.m[1][0], mat.m[2][0], mat.m[3][0], + mat.m[0][1], mat.m[1][1], mat.m[2][1], mat.m[3][1], + mat.m[0][2], mat.m[1][2], mat.m[2][2], mat.m[3][2], + mat.m[0][3], mat.m[1][3], mat.m[2][3], mat.m[3][3]) + elif len(mat.m) == 3: + result = LMatrix4( + mat.m[0][0], mat.m[1][0], mat.m[2][0], 0.0, + mat.m[0][1], mat.m[1][1], mat.m[2][1], 0.0, + mat.m[0][2], mat.m[1][2], mat.m[2][2], 0.0, + mat.m[0][3], mat.m[1][3], mat.m[2][3], 1.0) + return result + + def is_vr_available(self): + """检查VR系统是否可用""" + if not OPENVR_AVAILABLE: + return False + + try: + # 检查SteamVR是否运行 + return openvr.isRuntimeInstalled() and openvr.isHmdPresent() + except Exception as e: + print(f"VR检查失败: {e}") + return False + + def initialize_vr(self): + """初始化VR系统""" + if not OPENVR_AVAILABLE: + print("❌ OpenVR不可用,无法初始化VR") + return False + + if self.vr_initialized: + print("VR系统已经初始化") + return True + + try: + print("🔄 正在初始化VR系统...") + + # 初始化OpenVR - 使用Scene应用类型确保正确的焦点管理 + self.vr_system = openvr.init(openvr.VRApplication_Scene) + if not self.vr_system: + print("❌ 无法初始化OpenVR系统") + return False + + # 获取compositor + self.vr_compositor = openvr.VRCompositor() + if not self.vr_compositor: + print("❌ 无法获取VR Compositor") + return False + + # 获取推荐的渲染目标尺寸 + self.eye_width, self.eye_height = self.vr_system.getRecommendedRenderTargetSize() + print(f"✓ VR渲染目标尺寸: {self.eye_width}x{self.eye_height}") + + # 创建OpenVR姿态数组 + poses_t = openvr.TrackedDevicePose_t * openvr.k_unMaxTrackedDeviceCount + self.poses = poses_t() + print("✓ VR姿态数组已创建") + + # 创建VR渲染缓冲区 + if not self._create_vr_buffers(): + print("❌ 创建VR渲染缓冲区失败") + return False + + # 设置VR相机 + if not self._setup_vr_cameras(): + print("❌ 设置VR相机失败") + return False + + # 启动VR更新任务 + self._start_vr_task() + + self.vr_initialized = True + print("✅ VR系统初始化成功") + return True + + except Exception as e: + print(f"❌ VR初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_vr_buffers(self): + """创建VR渲染缓冲区 - 基于参考实现""" + try: + # 创建左眼纹理和缓冲区 + self.vr_left_texture = self._create_vr_texture("VR Left Eye Texture") + self.vr_left_eye_buffer = self._create_vr_buffer( + "VR Left Eye", + self.vr_left_texture, + self.eye_width, + self.eye_height + ) + + if not self.vr_left_eye_buffer: + print("❌ 创建左眼缓冲区失败") + return False + + # 设置左眼缓冲区属性 + self.vr_left_eye_buffer.setSort(-100) + self.vr_left_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) # 深蓝色背景便于调试 + self.vr_left_eye_buffer.setActive(True) + + # 创建右眼纹理和缓冲区 + self.vr_right_texture = self._create_vr_texture("VR Right Eye Texture") + self.vr_right_eye_buffer = self._create_vr_buffer( + "VR Right Eye", + self.vr_right_texture, + self.eye_width, + self.eye_height + ) + + if not self.vr_right_eye_buffer: + print("❌ 创建右眼缓冲区失败") + return False + + # 设置右眼缓冲区属性 + self.vr_right_eye_buffer.setSort(-99) + self.vr_right_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) # 深蓝色背景便于调试 + self.vr_right_eye_buffer.setActive(True) + + print("✓ VR渲染缓冲区创建成功") + return True + + except Exception as e: + print(f"❌ 创建VR缓冲区失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_vr_texture(self, name): + """创建VR纹理对象 - 基于参考实现""" + texture = Texture(name) + texture.setWrapU(Texture.WMClamp) + texture.setWrapV(Texture.WMClamp) + texture.setMinfilter(Texture.FTLinear) + texture.setMagfilter(Texture.FTLinear) + return texture + + def _create_vr_buffer(self, name, texture, width, height): + """创建VR渲染缓冲区 - 基于参考实现""" + # 设置帧缓冲属性 + fbprops = FrameBufferProperties() + fbprops.setRgbaBits(1, 1, 1, 1) + # 可以在这里添加多重采样抗锯齿 + # fbprops.setMultisamples(4) + + # 创建缓冲区 + buffer = self.world.win.makeTextureBuffer(name, width, height, to_ram=False, fbp=fbprops) + + if buffer: + # 清除默认渲染纹理 + buffer.clearRenderTextures() + # 添加我们的纹理 + buffer.addRenderTexture(texture, GraphicsOutput.RTMBindOrCopy, GraphicsOutput.RTPColor) + + return buffer + + def _setup_vr_cameras(self): + """设置VR相机 - 使用锚点层级系统""" + try: + # 创建VR追踪空间锚点层级 + self.tracking_space = self.world.render.attachNewNode('tracking-space') + self.hmd_anchor = self.tracking_space.attachNewNode('hmd-anchor') + self.left_eye_anchor = self.hmd_anchor.attachNewNode('left-eye') + self.right_eye_anchor = self.hmd_anchor.attachNewNode('right-eye') + + # 获取投影矩阵 + projection_left = self.coord_mat_inv * self.convert_mat( + self.vr_system.getProjectionMatrix(openvr.Eye_Left, self.near_clip, self.far_clip)) + projection_right = self.coord_mat_inv * self.convert_mat( + self.vr_system.getProjectionMatrix(openvr.Eye_Right, self.near_clip, self.far_clip)) + + # 创建左眼相机节点 + left_cam_node = Camera('left-cam') + left_lens = MatrixLens() + left_lens.setUserMat(projection_left) + left_cam_node.setLens(left_lens) + + # 创建右眼相机节点 + right_cam_node = Camera('right-cam') + right_lens = MatrixLens() + right_lens.setUserMat(projection_right) + right_cam_node.setLens(right_lens) + + # 附加相机到眼睛锚点 + self.vr_left_camera = self.left_eye_anchor.attachNewNode(left_cam_node) + self.vr_right_camera = self.right_eye_anchor.attachNewNode(right_cam_node) + + # 设置显示区域并添加渲染回调 + left_dr = self.vr_left_eye_buffer.makeDisplayRegion() + left_dr.setCamera(self.vr_left_camera) + left_dr.setActive(True) + left_dr.setDrawCallback(PythonCallbackObject(self.left_cb)) + + right_dr = self.vr_right_eye_buffer.makeDisplayRegion() + right_dr.setCamera(self.vr_right_camera) + right_dr.setActive(True) + right_dr.setDrawCallback(PythonCallbackObject(self.right_cb)) + + print("✓ VR相机锚点层级系统设置完成") + return True + + except Exception as e: + print(f"❌ 设置VR相机失败: {e}") + import traceback + traceback.print_exc() + return False + + def _get_eye_offset(self, eye): + """获取眼睛相对于头显的偏移""" + try: + if not self.vr_system: + # 使用标准IPD(瞳距)估算值 + ipd = 0.064 # 64mm,平均IPD + if eye == openvr.Eye_Left: + return Vec3(-ipd/2, 0, 0) + else: + return Vec3(ipd/2, 0, 0) + + # 从OpenVR获取眼睛到头显的变换矩阵 + eye_transform = self.vr_system.getEyeToHeadTransform(eye) + + # 提取位移信息 + x = eye_transform[0][3] + y = eye_transform[1][3] + z = eye_transform[2][3] + + return Vec3(x, y, z) + + except Exception as e: + print(f"❌ 获取眼睛偏移失败: {e}") + # 返回默认值 + ipd = 0.064 + if eye == openvr.Eye_Left: + return Vec3(-ipd/2, 0, 0) + else: + return Vec3(ipd/2, 0, 0) + + def _start_vr_task(self): + """启动VR更新任务""" + if self.vr_task: + self.world.taskMgr.remove(self.vr_task) + + self.vr_task = self.world.taskMgr.add(self._update_vr, "update_vr") + print("✓ VR更新任务已启动") + + def _update_vr(self, task): + """VR更新任务 - 每帧调用""" + if not self.vr_enabled or not self.vr_system: + return task.cont + + try: + # 性能监控 + self.frame_count += 1 + + # 计算VR FPS + import time + current_time = time.time() + if self.last_fps_time == 0: + self.last_fps_time = current_time + elif current_time - self.last_fps_time >= 1.0: # 每秒更新一次FPS + self.vr_fps = (self.frame_count - self.last_fps_check) / (current_time - self.last_fps_time) + self.last_fps_check = self.frame_count + self.last_fps_time = current_time + + # 优化的VR更新顺序: + # 1. 立即调用 waitGetPoses 获取最新的姿态数据 + # 这确保我们使用最新数据而不是上一帧的数据 + self._wait_get_poses() + + # 2. 更新相机位置(使用刚获取的最新姿态数据) + self._update_camera_poses() + + # 注意:纹理提交现在通过渲染回调自动处理 + + # 定期输出性能报告 + if self.frame_count % 1800 == 1: # 每30秒@60fps输出一次性能报告 + self._print_performance_report() + + except Exception as e: + print(f"VR更新错误: {e}") + import traceback + traceback.print_exc() + + return task.cont + + def _wait_get_poses(self): + """调用VRCompositor的waitGetPoses来获取焦点和姿态数据""" + try: + if not self.vr_compositor or not self.poses: + return + + # 调用waitGetPoses获取焦点和姿态数据 + # 这个调用可能会阻塞直到下一个VR同步点 + result = self.vr_compositor.waitGetPoses(self.poses, None) + + # 检查姿态数据的有效性 + valid_poses = 0 + + # 更新HMD姿态(设备0通常是头显) + if len(self.poses) > 0 and self.poses[0].bPoseIsValid: + valid_poses += 1 + else: + # 如果HMD姿态无效,不要频繁输出错误信息 + if not hasattr(self, '_hmd_invalid_warning_shown'): + print("⚠️ HMD姿态数据无效") + self._hmd_invalid_warning_shown = True + + # 更新控制器姿态 + self.controller_poses.clear() + for device_id in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): + if self.poses[device_id].bPoseIsValid: + device_class = self.vr_system.getTrackedDeviceClass(device_id) + if device_class == openvr.TrackedDeviceClass_Controller: + controller_matrix = self.poses[device_id].mDeviceToAbsoluteTracking + self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) + valid_poses += 1 + + # 性能监控 - 偶尔输出姿态状态 + if self.frame_count % 600 == 1: # 每10秒输出一次@60fps + print(f"📊 VR姿态状态 - 有效姿态数: {valid_poses}, 总帧数: {self.frame_count}") + + except Exception as e: + # 限制错误输出频率 + if not hasattr(self, '_last_error_frame'): + self._last_error_frame = 0 + + if self.frame_count - self._last_error_frame > 300: # 每5秒最多输出一次错误 + print(f"waitGetPoses失败: {e}") + self._last_error_frame = self.frame_count + + # 记录姿态失败次数 + self.pose_failures += 1 + + def _update_tracking_data(self): + """更新VR追踪数据""" + try: + # 获取设备姿态 + poses = self.vr_system.getDeviceToAbsoluteTrackingPose( + openvr.TrackingUniverseStanding, 0.0, openvr.k_unMaxTrackedDeviceCount + ) + + # 更新HMD姿态(设备0通常是头显) + if poses[0].bPoseIsValid: + hmd_matrix = poses[0].mDeviceToAbsoluteTracking + self.hmd_pose = self._convert_openvr_matrix_to_panda(hmd_matrix) + + # 更新控制器姿态 + for device_id in range(1, openvr.k_unMaxTrackedDeviceCount): + if poses[device_id].bPoseIsValid: + device_class = self.vr_system.getTrackedDeviceClass(device_id) + if device_class == openvr.TrackedDeviceClass_Controller: + controller_matrix = poses[device_id].mDeviceToAbsoluteTracking + self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) + + except Exception as e: + print(f"更新追踪数据失败: {e}") + + def _convert_openvr_matrix_to_panda(self, ovr_matrix): + """将OpenVR矩阵转换为Panda3D矩阵 + + 坐标系转换: + OpenVR: X右, Y上, -Z前(右手坐标系) + Panda3D: X右, Y前, Z上(右手坐标系) + + 转换规则: + OpenVR X → Panda3D X + OpenVR Y → Panda3D Z + OpenVR -Z → Panda3D Y + """ + mat = Mat4() + + # 修正的坐标转换矩阵 + # OpenVR: X右, Y上, -Z前 → Panda3D: X右, Y前, Z上 + # 转换规则: (ovr_x, ovr_y, ovr_z) → (panda_x, panda_y, panda_z) + # (ovr_x, ovr_y, ovr_z) → (ovr_x, -ovr_z, ovr_y) + + # X轴行:Panda3D的X轴对应OpenVR的X轴 + mat.setCell(0, 0, ovr_matrix[0][0]) # X_x → X_x + mat.setCell(0, 1, ovr_matrix[0][1]) # X_y → X_y + mat.setCell(0, 2, ovr_matrix[0][2]) # X_z → X_z + mat.setCell(0, 3, ovr_matrix[0][3]) # 位移X分量 + + # Y轴行:Panda3D的Y轴对应OpenVR的-Z轴 + mat.setCell(1, 0, -ovr_matrix[2][0]) # -Z_x → Y_x + mat.setCell(1, 1, -ovr_matrix[2][1]) # -Z_y → Y_y + mat.setCell(1, 2, -ovr_matrix[2][2]) # -Z_z → Y_z + mat.setCell(1, 3, -ovr_matrix[2][3]) # 位移Y分量(-Z位移) + + # Z轴行:Panda3D的Z轴对应OpenVR的Y轴 + mat.setCell(2, 0, ovr_matrix[1][0]) # Y_x → Z_x + mat.setCell(2, 1, ovr_matrix[1][1]) # Y_y → Z_y + mat.setCell(2, 2, ovr_matrix[1][2]) # Y_z → Z_z + mat.setCell(2, 3, ovr_matrix[1][3]) # 位移Z分量(Y位移) + + # 齐次坐标 + mat.setCell(3, 0, 0) + mat.setCell(3, 1, 0) + mat.setCell(3, 2, 0) + mat.setCell(3, 3, 1) + + # 调试信息 - 验证坐标系转换 + if not hasattr(self, '_coord_debug_counter'): + self._coord_debug_counter = 0 + self._coord_debug_counter += 1 + + if self._coord_debug_counter % 600 == 1: # 每10秒输出一次@60fps + print(f"🔄 坐标系转换调试 (第{self._coord_debug_counter}帧)") + + # 输出原始OpenVR矩阵信息 + ovr_pos = Vec3(ovr_matrix[0][3], ovr_matrix[1][3], ovr_matrix[2][3]) + print(f" OpenVR原始位置: {ovr_pos}") + + # 输出转换后的Panda3D矩阵信息 + # 正确的方法:从矩阵中读取位移(第4列,前3行) + panda_pos = Vec3(mat.getCell(0, 3), mat.getCell(1, 3), mat.getCell(2, 3)) + print(f" Panda3D转换位置: {panda_pos}") + + # 检查矩阵设置是否正确 + # 手动验证位置转换:OpenVR (x,y,z) → Panda3D (x,-z,y) + manual_converted_pos = Vec3(ovr_pos.x, -ovr_pos.z, ovr_pos.y) + print(f" 手动转换结果: {manual_converted_pos}") + + # 检查我们的矩阵是否正确设置了位置 + print(f" 矩阵位置元素: [{mat.getCell(0,3)}, {mat.getCell(1,3)}, {mat.getCell(2,3)}]") + + # 验证转换是否正确 + expected_panda_pos = manual_converted_pos + print(f" 预期Panda3D位置: {expected_panda_pos}") + + # 检查转换是否正确 + diff = panda_pos - expected_panda_pos + diff_magnitude = diff.length() + if diff_magnitude < 0.001: + print(f" ✅ 坐标转换正确 (误差: {diff_magnitude:.6f})") + else: + print(f" ⚠️ 坐标转换可能有误 (误差: {diff_magnitude:.6f})") + print(f" 差异向量: {diff}") + print(f" 实际矩阵第4行: [{mat.getCell(3,0)}, {mat.getCell(3,1)}, {mat.getCell(3,2)}, {mat.getCell(3,3)}]") + + return mat + + def update_hmd(self, pose): + """ + 更新HMD锚点 - 基于参考实现 + """ + try: + # 将OpenVR姿态转换为Panda3D矩阵 + modelview = self.convert_mat(pose.mDeviceToAbsoluteTracking) + + # 应用坐标系转换并设置HMD锚点 + self.hmd_anchor.setMat(self.coord_mat_inv * modelview * self.coord_mat) + + # 获取眼睛到头部的变换 + view_left = self.convert_mat(self.vr_system.getEyeToHeadTransform(openvr.Eye_Left)) + view_right = self.convert_mat(self.vr_system.getEyeToHeadTransform(openvr.Eye_Right)) + + # 设置眼睛锚点 + self.left_eye_anchor.setMat(self.coord_mat_inv * view_left * self.coord_mat) + self.right_eye_anchor.setMat(self.coord_mat_inv * view_right * self.coord_mat) + + except Exception as e: + print(f"更新HMD姿态失败: {e}") + + def _update_camera_poses(self): + """更新相机姿态 - 使用锚点系统简化处理""" + try: + # 使用锚点系统后,相机位置自动跟随锚点 + # 只需要获取HMD姿态并更新锚点即可 + + # 从poses数组中获取HMD姿态 + if hasattr(self, 'poses') and len(self.poses) > 0: + hmd_pose = self.poses[openvr.k_unTrackedDeviceIndex_Hmd] + if hmd_pose.bPoseIsValid: + self.update_hmd(hmd_pose) + else: + print("⚠️ HMD姿态数据无效") + + except Exception as e: + print(f"更新相机姿态失败: {e}") + import traceback + traceback.print_exc() + + def _submit_frames_to_vr(self): + """将渲染帧提交给VR系统""" + try: + if not self.vr_compositor: + return + + # 使用我们创建的纹理对象 + left_texture = self.vr_left_texture + right_texture = self.vr_right_texture + + if left_texture and right_texture: + # 确保纹理已准备并获取OpenGL纹理ID + gsg = self.world.win.getGsg() + prepared_objects = gsg.getPreparedObjects() + + # 准备纹理 - 使用更简单的方法 + try: + # 方法1: 尝试prepareNow + if hasattr(left_texture, 'prepareNow'): + left_texture.prepareNow(0, prepared_objects, gsg) + if hasattr(right_texture, 'prepareNow'): + right_texture.prepareNow(0, prepared_objects, gsg) + except Exception as prep_error: + print(f"纹理准备失败: {prep_error}") + # 继续尝试,可能纹理已经准备好了 + + # 获取OpenGL纹理ID + left_texture_id = self._get_texture_opengl_id(left_texture, prepared_objects, gsg) + right_texture_id = self._get_texture_opengl_id(right_texture, prepared_objects, gsg) + + # 检查是否成功获取了纹理ID + if left_texture_id is not None and left_texture_id > 0 and right_texture_id is not None and right_texture_id > 0: + try: + # 创建OpenVR纹理结构 + left_eye_texture = openvr.Texture_t() + left_eye_texture.handle = int(left_texture_id) + left_eye_texture.eType = openvr.TextureType_OpenGL + left_eye_texture.eColorSpace = openvr.ColorSpace_Gamma + + right_eye_texture = openvr.Texture_t() + right_eye_texture.handle = int(right_texture_id) + right_eye_texture.eType = openvr.TextureType_OpenGL + right_eye_texture.eColorSpace = openvr.ColorSpace_Gamma + + # 提交到VR系统 + error_left = self.vr_compositor.submit(openvr.Eye_Left, left_eye_texture) + error_right = self.vr_compositor.submit(openvr.Eye_Right, right_eye_texture) + + # 检查提交结果 - 在Python OpenVR中,None表示成功 + left_success = (error_left is None or error_left == openvr.VRCompositorError_None) + right_success = (error_right is None or error_right == openvr.VRCompositorError_None) + + if not left_success: + print(f"⚠️ 左眼纹理提交错误: {error_left}") + self.submit_failures += 1 + if not right_success: + print(f"⚠️ 右眼纹理提交错误: {error_right}") + self.submit_failures += 1 + + # 如果两个都成功了,输出成功信息(仅第一次) + if not hasattr(self, '_first_submit_success'): + if left_success and right_success: + print("✅ VR纹理提交成功!缓冲区应该显示场景内容。") + print(f" 左眼纹理ID: {left_texture_id}, 右眼纹理ID: {right_texture_id}") + self._first_submit_success = True + + except Exception as submit_error: + print(f"❌ VR纹理提交过程失败: {submit_error}") + if "DoNotHaveFocus" in str(submit_error): + print("🔍 这通常意味着另一个VR应用程序正在使用VR系统") + self.submit_failures += 1 + else: + # 只在第一次失败时输出警告,避免太多日志 + if not hasattr(self, '_texture_id_warning_shown'): + print("⚠️ 无法获取有效的纹理OpenGL ID,跳过VR帧提交") + print(" 这可能是因为纹理尚未正确准备到GPU") + self._texture_id_warning_shown = True + + except Exception as e: + print(f"提交VR帧失败: {e}") + import traceback + traceback.print_exc() + + def _get_texture_opengl_id(self, texture, prepared_objects, gsg): + """获取纹理的OpenGL ID""" + try: + # 方法1: 使用prepareNow获取正确的纹理上下文 + texture_context = texture.prepareNow(0, prepared_objects, gsg) + if texture_context and hasattr(texture_context, 'getNativeId'): + native_id = texture_context.getNativeId() + if native_id > 0: + print(f"✓ 成功获取纹理 {texture.getName()} 的OpenGL ID: {native_id}") + return native_id + + # 方法2: 备选方案 - 通过prepared_objects获取texture context + if hasattr(prepared_objects, 'getTextureContext'): + texture_context = prepared_objects.getTextureContext(texture) + if texture_context and hasattr(texture_context, 'getNativeId'): + native_id = texture_context.getNativeId() + if native_id > 0: + print(f"✓ 备选方法获取纹理 {texture.getName()} 的OpenGL ID: {native_id}") + return native_id + + # 方法3: 尝试texture对象本身的方法 + if hasattr(texture, 'getNativeId'): + native_id = texture.getNativeId() + if native_id > 0: + print(f"✓ 直接获取纹理 {texture.getName()} 的OpenGL ID: {native_id}") + return native_id + + # 如果所有方法都失败 + print(f"❌ 无法获取纹理 {texture.getName()} 的OpenGL ID") + return None + + except Exception as e: + print(f"获取纹理OpenGL ID失败: {e}") + import traceback + traceback.print_exc() + return None + + def enable_vr(self): + """启用VR模式""" + if not self.is_vr_available(): + print("❌ VR系统不可用") + return False + + if not self.vr_initialized: + if not self.initialize_vr(): + return False + + self.vr_enabled = True + + # 禁用主相机避免干扰VR渲染 + self._disable_main_cam() + + print("✅ VR模式已启用") + return True + + def disable_vr(self): + """禁用VR模式""" + self.vr_enabled = False + + # 恢复主相机 + self._enable_main_cam() + + print("✅ VR模式已禁用") + + def cleanup(self): + """清理VR资源""" + try: + print("🔄 正在清理VR资源...") + + # 停止VR任务 + if self.vr_task: + self.world.taskMgr.remove(self.vr_task) + self.vr_task = None + + # 清理渲染缓冲区 + if self.vr_left_eye_buffer: + self.vr_left_eye_buffer.removeAllDisplayRegions() + self.world.graphicsEngine.removeWindow(self.vr_left_eye_buffer) + self.vr_left_eye_buffer = None + + if self.vr_right_eye_buffer: + self.vr_right_eye_buffer.removeAllDisplayRegions() + self.world.graphicsEngine.removeWindow(self.vr_right_eye_buffer) + self.vr_right_eye_buffer = None + + # 清理相机 + if self.vr_left_camera: + self.vr_left_camera.removeNode() + self.vr_left_camera = None + + if self.vr_right_camera: + self.vr_right_camera.removeNode() + self.vr_right_camera = None + + # 关闭OpenVR + if self.vr_system and OPENVR_AVAILABLE: + try: + openvr.shutdown() + except: + pass + self.vr_system = None + + self.vr_enabled = False + self.vr_initialized = False + + print("✅ VR资源清理完成") + + except Exception as e: + print(f"⚠️ VR清理过程中出错: {e}") + + def get_vr_status(self): + """获取VR状态信息""" + return { + 'available': self.is_vr_available(), + 'initialized': self.vr_initialized, + 'enabled': self.vr_enabled, + 'eye_resolution': (self.eye_width, self.eye_height), + 'device_count': len(self.controller_poses) + (1 if self.vr_enabled else 0), + 'vr_fps': self.vr_fps, + 'frame_count': self.frame_count, + 'submit_failures': self.submit_failures, + 'pose_failures': self.pose_failures + } + + def _print_performance_report(self): + """输出VR性能报告""" + print("📊 === VR性能报告 ===") + print(f" VR帧率: {self.vr_fps:.1f} FPS") + print(f" 总帧数: {self.frame_count}") + print(f" 提交失败: {self.submit_failures}") + print(f" 姿态失败: {self.pose_failures}") + + # 计算失败率 + if self.frame_count > 0: + submit_fail_rate = (self.submit_failures / self.frame_count) * 100 + pose_fail_rate = (self.pose_failures / self.frame_count) * 100 + print(f" 提交失败率: {submit_fail_rate:.2f}%") + print(f" 姿态失败率: {pose_fail_rate:.2f}%") + + print("========================") + + def left_cb(self, cbdata): + """左眼渲染回调 - 基于参考实现""" + # 执行实际的渲染工作 + cbdata.upcall() + # 根据提交策略决定是否立即提交 + if not self.submit_together: + # 分别提交模式:左眼渲染完成后立即提交 + self.submit_texture(openvr.Eye_Left, self.vr_left_texture) + + def right_cb(self, cbdata): + """右眼渲染回调 - 基于参考实现""" + # 执行实际的渲染工作 + cbdata.upcall() + # 根据提交策略决定提交方式 + if self.submit_together: + # 同时提交模式:右眼渲染完成后同时提交左右眼 + self.submit_texture(openvr.Eye_Left, self.vr_left_texture) + self.submit_texture(openvr.Eye_Right, self.vr_right_texture) + else: + # 分别提交模式:只提交右眼 + self.submit_texture(openvr.Eye_Right, self.vr_right_texture) + + def submit_texture(self, eye, texture): + """提交纹理到VR - 基于参考实现,增强调试信息""" + try: + if not self.vr_compositor: + print("❌ VR compositor不可用") + self.submit_failures += 1 + return + + # 获取graphics state guardian和prepared objects + gsg = self.world.win.getGsg() + if not gsg: + print("❌ 无法获取GraphicsStateGuardian") + self.submit_failures += 1 + return + + prepared_objects = gsg.getPreparedObjects() + if not prepared_objects: + print("❌ 无法获取PreparedObjects") + self.submit_failures += 1 + return + + # 准备纹理并获取更详细的错误信息 + if not texture: + print("❌ 纹理对象为空") + self.submit_failures += 1 + return + + print(f"🔍 准备纹理: {texture.getName()}, 大小: {texture.getXSize()}x{texture.getYSize()}") + + texture_context = texture.prepareNow(0, prepared_objects, gsg) + if not texture_context: + print("❌ prepareNow返回空的texture_context") + self.submit_failures += 1 + return + + handle = texture_context.getNativeId() + print(f"🔍 获取OpenGL纹理句柄: {handle}") + + if handle != 0: + ovr_texture = openvr.Texture_t() + ovr_texture.handle = handle + ovr_texture.eType = openvr.TextureType_OpenGL + ovr_texture.eColorSpace = openvr.ColorSpace_Gamma + + eye_name = "左眼" if eye == openvr.Eye_Left else "右眼" + print(f"🔍 提交{eye_name}纹理到VR, 句柄: {handle}") + + # 提交到VR系统 + error = self.vr_compositor.submit(eye, ovr_texture) + + print(f"🔍 VR提交结果: {error}") + + # 检查错误 + if error and error != openvr.VRCompositorError_None: + print(f"⚠️ VR纹理提交错误代码: {error}") + self.submit_failures += 1 + else: + # 只在第一次成功时输出 + if not hasattr(self, '_submit_success_logged'): + print(f"✅ VR纹理提交成功! {eye_name}") + self._submit_success_logged = True + else: + print(f"❌ 无法获取纹理OpenGL句柄: handle = {handle}") + print(f" 纹理状态: 已准备={texture.isPrepared(prepared_objects)}") + print(f" 纹理格式: {texture.getFormat()}") + self.submit_failures += 1 + + except Exception as e: + print(f"❌ VR纹理提交异常: {e}") + import traceback + traceback.print_exc() + self.submit_failures += 1 + + def _disable_main_cam(self): + """禁用主相机 - 基于参考实现""" + try: + # 保存原始相机状态 + if not hasattr(self, '_original_camera_parent'): + self._original_camera_parent = self.world.camera.getParent() + + # 创建空节点并将主相机重新附加到它 + self._empty_world = NodePath("empty_world") + self.world.camera.reparentTo(self._empty_world) + + print("✓ 主相机已禁用") + except Exception as e: + print(f"⚠️ 禁用主相机失败: {e}") + + def _enable_main_cam(self): + """恢复主相机 - 基于参考实现""" + try: + # 恢复原始相机状态 + if hasattr(self, '_original_camera_parent') and self._original_camera_parent: + self.world.camera.reparentTo(self._original_camera_parent) + else: + # 如果没有保存的父节点,重新附加到render + self.world.camera.reparentTo(self.world.render) + + # 清理空世界节点 + if hasattr(self, '_empty_world'): + self._empty_world.removeNode() + delattr(self, '_empty_world') + + print("✓ 主相机已恢复") + except Exception as e: + print(f"⚠️ 恢复主相机失败: {e}") \ No newline at end of file diff --git a/main.py b/main.py index de7d3872..abcf1f99 100644 --- a/main.py +++ b/main.py @@ -97,6 +97,15 @@ class MyWorld(CoreWorld): from core.collision_manager import CollisionManager self.collision_manager = CollisionManager(self) + # 初始化VR管理器 + try: + from core.vr_manager import VRManager + self.vr_manager = VRManager(self) + print("✓ VR管理器初始化完成") + except Exception as e: + print(f"⚠ VR管理器初始化失败: {e}") + self.vr_manager = None + # 调试选项 self.debug_collision = False # 是否显示碰撞体 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 8ac360a5..a4b7d40c 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -98,3 +98,4 @@ webencodings==0.5.1 xdg==5 xkit==0.0.0 zipp==1.0.0 +openvr==2.2.0 diff --git a/ui/main_window.py b/ui/main_window.py index a1cb283e..33e3f247 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -438,6 +438,17 @@ class MainWindow(QMainWindow): self.refreshAssetsAction = self.assetsMenu.addAction('刷新资源') self.refreshAssetsAction.triggered.connect(self.refreshAssetsView) + # VR菜单 + self.vrMenu = menubar.addMenu('VR') + self.enterVRAction = self.vrMenu.addAction('进入VR模式') + self.exitVRAction = self.vrMenu.addAction('退出VR模式') + self.vrMenu.addSeparator() + self.vrStatusAction = self.vrMenu.addAction('VR状态') + self.vrSettingsAction = self.vrMenu.addAction('VR设置') + + # 初始状态下禁用退出VR选项 + self.exitVRAction.setEnabled(False) + # 帮助菜单 self.helpMenu = menubar.addMenu('帮助') self.aboutAction = self.helpMenu.addAction('关于') @@ -898,6 +909,12 @@ class MainWindow(QMainWindow): # self.toggleHotReloadAction.triggered.connect(self.onToggleHotReload) # self.openScriptsManagerAction.triggered.connect(self.onOpenScriptsManager) + # 连接VR菜单事件 + self.enterVRAction.triggered.connect(self.onEnterVR) + self.exitVRAction.triggered.connect(self.onExitVR) + self.vrStatusAction.triggered.connect(self.onShowVRStatus) + self.vrSettingsAction.triggered.connect(self.onShowVRSettings) + def onCreateCesiumView(self): if hasattr(self.world,'gui_manager') and self.world.gui_manager: @@ -1845,13 +1862,21 @@ class MainWindow(QMainWindow): # 清理工具管理器中的进程 if hasattr(self.world, 'tool_manager') and self.world.tool_manager: print("🧹 清理工具管理器进程...") - self.world.tool_manager.cleanup_processes() + if hasattr(self.world.tool_manager, 'cleanup_processes'): + self.world.tool_manager.cleanup_processes() + else: + print("✓ 工具管理器无需清理进程") # 停止更新定时器 if hasattr(self, 'updateTimer') and self.updateTimer: self.updateTimer.stop() print("⏹️ 更新定时器已停止") + # 清理VR资源 + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + print("🧹 清理VR资源...") + self.world.vr_manager.cleanup() + # 清理Panda3D资源 if hasattr(self, 'pandaWidget') and self.pandaWidget: print("🧹 清理Panda3D资源...") @@ -2000,6 +2025,171 @@ class MainWindow(QMainWindow): else: QMessageBox.warning(self, "错误", "高度图地形创建失败!") + # ==================== VR事件处理 ==================== + + def onEnterVR(self): + """进入VR模式""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + success = self.world.vr_manager.enable_vr() + if success: + # 更新菜单状态 + self.enterVRAction.setEnabled(False) + self.exitVRAction.setEnabled(True) + QMessageBox.information(self, "成功", "VR模式已启用!\n请确保您的VR头显已正确连接。") + else: + QMessageBox.warning(self, "错误", "无法启用VR模式!\n请检查:\n1. SteamVR是否正在运行\n2. VR头显是否已连接\n3. OpenVR库是否已正确安装") + else: + QMessageBox.warning(self, "错误", "VR管理器不可用!") + except Exception as e: + QMessageBox.critical(self, "错误", f"启用VR模式时发生错误:\n{str(e)}") + + def onExitVR(self): + """退出VR模式""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + self.world.vr_manager.disable_vr() + # 更新菜单状态 + self.enterVRAction.setEnabled(True) + self.exitVRAction.setEnabled(False) + QMessageBox.information(self, "成功", "已退出VR模式") + else: + QMessageBox.warning(self, "错误", "VR管理器不可用!") + except Exception as e: + QMessageBox.critical(self, "错误", f"退出VR模式时发生错误:\n{str(e)}") + + def onShowVRStatus(self): + """显示VR状态""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + status = self.world.vr_manager.get_vr_status() + + status_text = f"""VR系统状态: + +可用性: {'✅ 可用' if status['available'] else '❌ 不可用'} +初始化: {'✅ 已初始化' if status['initialized'] else '❌ 未初始化'} +启用状态: {'✅ 已启用' if status['enabled'] else '❌ 未启用'} +渲染分辨率: {status['eye_resolution'][0]}x{status['eye_resolution'][1]} +追踪设备数: {status['device_count']} + +提示: +- 如果VR不可用,请确保已安装SteamVR并连接VR头显 +- 如果OpenVR库未安装,请运行:pip install openvr +""" + + QMessageBox.information(self, "VR状态", status_text) + else: + QMessageBox.warning(self, "错误", "VR管理器不可用!") + except Exception as e: + QMessageBox.critical(self, "错误", f"获取VR状态时发生错误:\n{str(e)}") + + def onShowVRSettings(self): + """显示VR设置对话框""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + dialog = self.createVRSettingsDialog() + dialog.exec_() + else: + QMessageBox.warning(self, "错误", "VR管理器不可用!") + except Exception as e: + QMessageBox.critical(self, "错误", f"打开VR设置时发生错误:\n{str(e)}") + + def createVRSettingsDialog(self): + """创建VR设置对话框""" + dialog = QDialog(self) + dialog.setWindowTitle("VR设置") + dialog.setModal(True) + dialog.resize(400, 300) + + layout = QVBoxLayout(dialog) + + # VR状态显示 + status_group = QGroupBox("VR状态") + status_layout = QVBoxLayout() + + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + status = self.world.vr_manager.get_vr_status() + + available_label = QLabel(f"VR可用性: {'是' if status['available'] else '否'}") + available_label.setStyleSheet(f"color: {'green' if status['available'] else 'red'};") + status_layout.addWidget(available_label) + + enabled_label = QLabel(f"VR状态: {'已启用' if status['enabled'] else '未启用'}") + enabled_label.setStyleSheet(f"color: {'green' if status['enabled'] else 'gray'};") + status_layout.addWidget(enabled_label) + + resolution_label = QLabel(f"渲染分辨率: {status['eye_resolution'][0]}x{status['eye_resolution'][1]}") + status_layout.addWidget(resolution_label) + + status_group.setLayout(status_layout) + layout.addWidget(status_group) + + # 渲染设置 + render_group = QGroupBox("渲染设置") + render_layout = QFormLayout() + + # 渲染质量 + quality_combo = QComboBox() + quality_combo.addItems(["低", "中", "高", "超高"]) + quality_combo.setCurrentText("高") + render_layout.addRow("渲染质量:", quality_combo) + + # 抗锯齿 + aa_combo = QComboBox() + aa_combo.addItems(["无", "2x", "4x", "8x"]) + aa_combo.setCurrentText("4x") + render_layout.addRow("抗锯齿:", aa_combo) + + render_group.setLayout(render_layout) + layout.addWidget(render_group) + + # 性能设置 + perf_group = QGroupBox("性能设置") + perf_layout = QFormLayout() + + # 刷新率 + refresh_combo = QComboBox() + refresh_combo.addItems(["72Hz", "90Hz", "120Hz", "144Hz"]) + refresh_combo.setCurrentText("90Hz") + perf_layout.addRow("刷新率:", refresh_combo) + + # 异步重投影 + async_check = QCheckBox("启用异步重投影") + async_check.setChecked(True) + perf_layout.addRow("", async_check) + + perf_group.setLayout(perf_layout) + layout.addWidget(perf_group) + + # 按钮 + button_layout = QHBoxLayout() + + apply_button = QPushButton("应用") + ok_button = QPushButton("确定") + cancel_button = QPushButton("取消") + + button_layout.addWidget(apply_button) + button_layout.addStretch() + button_layout.addWidget(ok_button) + button_layout.addWidget(cancel_button) + + layout.addLayout(button_layout) + + # 连接信号 + apply_button.clicked.connect(lambda: self.applyVRSettings(dialog)) + ok_button.clicked.connect(dialog.accept) + cancel_button.clicked.connect(dialog.reject) + + return dialog + + def applyVRSettings(self, dialog): + """应用VR设置""" + try: + # 这里可以实现设置的保存和应用逻辑 + QMessageBox.information(dialog, "成功", "VR设置已应用!") + except Exception as e: + QMessageBox.critical(dialog, "错误", f"应用VR设置时发生错误:\n{str(e)}") + def setup_main_window(world,path = None): """设置主窗口的便利函数""" app = QApplication.instance() 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 3/3] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=89=8B=E6=9F=84?= =?UTF-8?q?=E7=AE=A1=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