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/QPanda3D/Panda3DWorld.py b/QPanda3D/Panda3DWorld.py index 78193075..d7012fc9 100644 --- a/QPanda3D/Panda3DWorld.py +++ b/QPanda3D/Panda3DWorld.py @@ -70,6 +70,14 @@ class Panda3DWorld(ShowBase): loadPrcFileData("", f"win-size {width} {height}") loadPrcFileData("", "win-fixed-size #f") # 允许窗口调整大小 + # 🚀 VR性能优化配置 + loadPrcFileData("", "prefer-single-buffer true") # 减少缓冲区交换开销 + loadPrcFileData("", "gl-force-flush false") # 避免强制glFlush导致的性能损失 + loadPrcFileData("", "sync-video false") # 禁用默认VSync,让OpenVR控制 + loadPrcFileData("", "support-stencil false") # 禁用不必要的模板缓冲区 + loadPrcFileData("", "clock-mode non-real-time") # 禁用Panda3D帧率控制,让OpenVR控制 + # loadPrcFileData("", "gl-debug true") # 调试时可启用OpenGL调试 + if (is_fullscreen): loadPrcFileData("", "fullscreen #t") 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/_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/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_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..e293d07f --- /dev/null +++ b/core/vr_controller.py @@ -0,0 +1,432 @@ +""" +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 + + # 摇杆状态 - 用于传送和转向交互 + self.joystick_pos = Vec3(0, 0, 0) # 摇杆位置 (x, y, 0) + self.joystick_touched = False # 摇杆是否被触摸 + self.joystick_pressed = False # 摇杆是否被按下 + self.previous_joystick_pos = Vec3(0, 0, 0) # 上一帧摇杆位置 + + # 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() + self.previous_joystick_pos = Vec3(self.joystick_pos) + + # 获取控制器状态 + try: + result, state = vr_system.getControllerState(self.device_index) + if result: + # 更新按钮状态 - 使用正确的OpenVR属性名 + for i in range(openvr.k_EButton_Max): + button_mask = 1 << i + # OpenVR Python绑定中使用ulButtonPressed而不是rButtonPressed + self.button_states[i] = (state.ulButtonPressed & button_mask) != 0 + + # 更新轴状态(扳机、握把、触摸板、摇杆) + # 兼容不同版本的OpenVR Python绑定 + axis_data = None + if hasattr(state, 'rAxis'): + axis_data = state.rAxis # 旧版本使用rAxis + elif hasattr(state, 'vAxis'): + axis_data = state.vAxis # 新版本使用vAxis + + if axis_data is not None and len(axis_data) > 0: + # 调试输出 - 显示所有轴数据(仅当有变化时) + self._debug_axis_data(axis_data) + + # 扳机轴通常在axis[1].x + if len(axis_data) > 1: + self.trigger_value = axis_data[1].x + + # 触摸板/摇杆轴通常在axis[0] + if len(axis_data) > 0: + self.touchpad_pos = Vec3(axis_data[0].x, axis_data[0].y, 0) + # 摇杆和触摸板通常使用同一个轴,但可以区分设备类型 + self.joystick_pos = Vec3(axis_data[0].x, axis_data[0].y, 0) + + # 额外检查其他轴(某些控制器可能将摇杆分配到不同轴) + if len(axis_data) > 2: + # 有些控制器可能在axis[2]有摇杆数据 + axis2_magnitude = abs(axis_data[2].x) + abs(axis_data[2].y) + if axis2_magnitude > 0.1: # 如果有显著输入,使用这个轴作为摇杆 + self.joystick_pos = Vec3(axis_data[2].x, axis_data[2].y, 0) + + # 检查axis[3]和axis[4](Quest控制器可能使用这些轴) + for axis_idx in range(3, min(len(axis_data), 5)): + axis_magnitude = abs(axis_data[axis_idx].x) + abs(axis_data[axis_idx].y) + if axis_magnitude > 0.1: # 如果有显著输入 + # 覆盖之前的摇杆数据,使用最有活动的轴 + self.joystick_pos = Vec3(axis_data[axis_idx].x, axis_data[axis_idx].y, 0) + # 调试输出 + if not hasattr(self, '_last_axis_notify'): + self._last_axis_notify = {} + if self._last_axis_notify.get(axis_idx, 0) == 0: + print(f"🎮 {self.name}手检测到axis[{axis_idx}]活动: ({axis_data[axis_idx].x:.3f}, {axis_data[axis_idx].y:.3f})") + self._last_axis_notify[axis_idx] = 60 # 60帧后再次提醒 + else: + self._last_axis_notify[axis_idx] -= 1 + + # 触摸板和摇杆触摸状态 + self.touchpad_touched = (state.ulButtonTouched & (1 << openvr.k_EButton_SteamVR_Touchpad)) != 0 + + # 摇杆触摸状态(检查多个可能的按钮) + joystick_touch_mask = 0 + if hasattr(openvr, 'k_EButton_Joystick'): + joystick_touch_mask |= (1 << openvr.k_EButton_Joystick) + if hasattr(openvr, 'k_EButton_Thumbstick'): + joystick_touch_mask |= (1 << openvr.k_EButton_Thumbstick) + + self.joystick_touched = (state.ulButtonTouched & joystick_touch_mask) != 0 or self.touchpad_touched + + # 摇杆按下状态 + joystick_press_mask = 0 + if hasattr(openvr, 'k_EButton_Joystick'): + joystick_press_mask |= (1 << openvr.k_EButton_Joystick) + if hasattr(openvr, 'k_EButton_Thumbstick'): + joystick_press_mask |= (1 << openvr.k_EButton_Thumbstick) + + self.joystick_pressed = (state.ulButtonPressed & joystick_press_mask) != 0 + + except Exception as e: + # 减少错误输出频率 + if not hasattr(self, '_last_input_error_frame'): + self._last_input_error_frame = 0 + + # 获取当前帧数(通过VR管理器) + current_frame = getattr(self.vr_manager, 'frame_count', 0) + + # 每5秒最多输出一次错误(300帧@60fps) + if current_frame - self._last_input_error_frame > 300: + print(f"⚠️ 更新{self.name}手柄输入状态失败: {e}") + self._last_input_error_frame = current_frame + + 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: + # 获取相对于世界坐标系的方向,包含tracking_space的旋转 + if hasattr(self.vr_manager, 'world') and self.vr_manager.world: + # 使用世界变换,包含所有父节点的旋转 + world_transform = self.anchor_node.getMat(self.vr_manager.world.render) + forward = Vec3(world_transform.getRow3(1)) # Y轴 = 前方 + else: + # 备选:使用局部变换 + forward = Vec3(self.anchor_node.getMat().getRow3(1)) + + if forward.length() > 0: + return forward.normalized() + return Vec3(0, 1, 0) + + def is_joystick_touched(self): + """检查摇杆是否被触摸""" + return self.joystick_touched + + def is_joystick_pressed(self): + """检查摇杆是否被按下""" + return self.joystick_pressed + + def get_joystick_position(self): + """获取摇杆位置 + + Returns: + Vec3: 摇杆位置 (x, y, 0),范围 [-1, 1] + """ + return Vec3(self.joystick_pos) + + def get_joystick_delta(self): + """获取摇杆位置变化 + + Returns: + Vec3: 摇杆位置变化向量 + """ + return self.joystick_pos - self.previous_joystick_pos + + def is_joystick_moved(self, threshold=0.01): + """检查摇杆是否移动 + + Args: + threshold: 移动阈值 + + Returns: + bool: 是否移动 + """ + delta = self.get_joystick_delta() + return delta.length() > threshold + + def _debug_axis_data(self, axis_data): + """调试输出轴数据""" + try: + # 只在有活动时输出调试信息 + has_activity = False + active_axes = [] + + for i, axis in enumerate(axis_data): + magnitude = abs(axis.x) + abs(axis.y) + if magnitude > 0.01: # 检测到活动 + has_activity = True + active_axes.append(f"axis[{i}]: ({axis.x:.3f}, {axis.y:.3f})") + + if has_activity: + # 初始化调试计数器 + if not hasattr(self, '_debug_axis_counter'): + self._debug_axis_counter = 0 + + self._debug_axis_counter += 1 + + # 每30帧输出一次详细信息 + if self._debug_axis_counter % 30 == 1: + print(f"🔍 {self.name}手轴数据调试:") + print(f" 总轴数: {len(axis_data)}") + print(f" 活跃轴: {', '.join(active_axes)}") + + # 显示所有轴的当前值(不管是否活跃) + all_axes = [] + for i, axis in enumerate(axis_data): + all_axes.append(f"[{i}]:({axis.x:.3f},{axis.y:.3f})") + print(f" 所有轴: {' '.join(all_axes)}") + + except Exception as e: + print(f"⚠️ 轴数据调试失败: {e}") + + 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_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_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_joystick.py b/core/vr_joystick.py new file mode 100644 index 00000000..feaa1cae --- /dev/null +++ b/core/vr_joystick.py @@ -0,0 +1,701 @@ +""" +VR摇杆交互系统模块 + +提供类似SteamVR的摇杆交互功能: +- 摇杆左右转向(旋转视角) +- 摇杆向前传送预览(抛物线轨迹) +- 松开摇杆执行传送 +- 死区处理和平滑控制 +""" + +import math +from panda3d.core import Vec2, Vec3, Vec4 +from direct.showbase.DirectObject import DirectObject +try: + import openvr + OPENVR_AVAILABLE = True +except ImportError: + OPENVR_AVAILABLE = False + + +class VRJoystickManager(DirectObject): + """VR摇杆管理器 - 处理手柄摇杆的转向和传送功能""" + + def __init__(self, vr_manager): + """初始化VR摇杆管理器 + + Args: + vr_manager: VR管理器实例 + """ + super().__init__() + + self.vr_manager = vr_manager + self.teleport_system = None # 传送系统引用,稍后初始化 + + # 摇杆参数配置 + self.deadzone = 0.15 # 摇杆死区 (0-1) + self.turn_threshold = 0.3 # 转向激活阈值 + self.teleport_threshold = 0.5 # 传送激活阈值 + self.turn_sensitivity = 250.0 # 转向灵敏度(度/秒)- 增加速度 + self.smooth_turning = True # 是否平滑转向 + self.snap_turn_angle = 30.0 # 分段转向角度(度) + + # 摇杆状态跟踪 + self.left_joystick_state = JoystickState() + self.right_joystick_state = JoystickState() + + # 转向状态 + self.left_turn_cooldown = 0.0 # 分段转向冷却时间 + self.right_turn_cooldown = 0.0 + self.snap_turn_cooldown = 0.3 # 分段转向间隔(秒) + + # 传送状态 + self.active_teleport_controller = None # 正在传送的控制器 + self.teleport_preview_active = False # 传送预览是否激活 + + # 互斥状态管理 - 防止同时触发多种操作 + self.interaction_mode = 'none' # 当前交互模式: 'none', 'turning', 'teleporting' + self.left_controller_mode = 'none' # 左手控制器状态 + self.right_controller_mode = 'none' # 右手控制器状态 + self.mode_lock_timeout = 0.1 # 模式锁定超时时间(秒) + self.left_mode_timer = 0.0 # 左手模式计时器 + self.right_mode_timer = 0.0 # 右手模式计时器 + + print("✓ VR摇杆管理器初始化完成") + + def initialize(self, teleport_system): + """初始化摇杆系统 + + Args: + teleport_system: VR传送系统实例 + """ + self.teleport_system = teleport_system + print("✅ VR摇杆系统初始化成功") + + def update(self, dt): + """更新摇杆系统 - 每帧调用 + + Args: + dt: 帧间隔时间(秒) + """ + if not self.vr_manager.are_controllers_connected(): + return + + # 调试计数器 + if not hasattr(self, '_debug_frame_count'): + self._debug_frame_count = 0 + print("🎮 VR摇杆系统开始更新") + + self._debug_frame_count += 1 + + # 更新转向冷却时间 + if self.left_turn_cooldown > 0: + self.left_turn_cooldown -= dt + if self.right_turn_cooldown > 0: + self.right_turn_cooldown -= dt + + # 更新互斥状态计时器 + self._update_interaction_modes(dt) + + # 处理左手控制器摇杆 + if self.vr_manager.left_controller: + self._update_controller_joystick( + self.vr_manager.left_controller, + self.left_joystick_state, + 'left', + dt + ) + + # 处理右手控制器摇杆 + if self.vr_manager.right_controller: + self._update_controller_joystick( + self.vr_manager.right_controller, + self.right_joystick_state, + 'right', + dt + ) + + # 每5秒输出一次状态报告 + if self._debug_frame_count % 300 == 1: # 假设60fps + self._print_debug_status() + + def _update_controller_joystick(self, controller, joystick_state, hand, dt): + """更新单个控制器的摇杆状态 + + Args: + controller: VR控制器实例 + joystick_state: 摇杆状态对象 + hand: 'left' 或 'right' + dt: 帧间隔时间 + """ + # 获取摇杆输入 + joystick_input = self._get_joystick_input(controller, hand) + if joystick_input is None: + return + + # 应用死区 + filtered_input = self._apply_deadzone(joystick_input) + + # 更新摇杆状态 + joystick_state.update(filtered_input) + + # 处理转向(左右移动) + self._handle_turning(filtered_input, hand, dt) + + # 处理传送(向前移动) + self._handle_teleport(controller, filtered_input, joystick_state, hand) + + def _update_interaction_modes(self, dt): + """更新互斥交互模式状态""" + # 更新左手模式计时器 + if self.left_mode_timer > 0: + self.left_mode_timer -= dt + if self.left_mode_timer <= 0: + self.left_controller_mode = 'none' + + # 更新右手模式计时器 + if self.right_mode_timer > 0: + self.right_mode_timer -= dt + if self.right_mode_timer <= 0: + self.right_controller_mode = 'none' + + # 更新全局交互模式 + if self.left_controller_mode == 'none' and self.right_controller_mode == 'none': + if self.interaction_mode != 'none': + # 所有操作结束,恢复自由模式 + self.interaction_mode = 'none' + print("🔓 摇杆交互模式解锁,恢复自由操作") + + def _set_controller_mode(self, hand, mode): + """设置控制器交互模式 + + Args: + hand: 'left' 或 'right' + mode: 'turning', 'teleporting', 'none' + """ + if hand == 'left': + if self.left_controller_mode != mode: + old_mode = self.left_controller_mode + self.left_controller_mode = mode + self.left_mode_timer = self.mode_lock_timeout + if mode != 'none': + print(f"🔒 左手控制器: {old_mode} → {mode}模式") + else: + print(f"🔓 左手控制器解锁: {old_mode} → 自由") + else: + if self.right_controller_mode != mode: + old_mode = self.right_controller_mode + self.right_controller_mode = mode + self.right_mode_timer = self.mode_lock_timeout + if mode != 'none': + print(f"🔒 右手控制器: {old_mode} → {mode}模式") + else: + print(f"🔓 右手控制器解锁: {old_mode} → 自由") + + # 更新全局模式 + if mode != 'none' and self.interaction_mode != mode: + self.interaction_mode = mode + + def _can_use_mode(self, hand, requested_mode): + """检查是否可以使用指定的交互模式 + + Args: + hand: 'left' 或 'right' + requested_mode: 'turning' 或 'teleporting' + + Returns: + bool: 是否可以使用该模式 + """ + current_mode = self.left_controller_mode if hand == 'left' else self.right_controller_mode + + # 如果当前控制器已经是该模式,允许继续 + if current_mode == requested_mode: + return True + + # 如果当前控制器是空闲的,且全局模式兼容,允许切换 + if current_mode == 'none': + if self.interaction_mode == 'none' or self.interaction_mode == requested_mode: + return True + + # 其他情况不允许 + return False + + def _get_joystick_input(self, controller, hand): + """获取摇杆输入 + + Args: + controller: VR控制器实例 + hand: 'left' 或 'right' + + Returns: + Vec2: 摇杆位置 (x, y) 或 None + """ + # 直接从控制器读取摇杆输入(绕过动作系统) + joystick_input = Vec2(0, 0) + + # 优先读取joystick_pos + if hasattr(controller, 'joystick_pos') and controller.joystick_pos: + joystick_input = Vec2(controller.joystick_pos.x, controller.joystick_pos.y) + # 检查是否有有效输入 + if joystick_input.length() > 0.01: + # 调试输出 - 仅在有输入时显示 + if not hasattr(self, '_last_debug_time'): + self._last_debug_time = 0 + self._debug_counter = 0 + + self._debug_counter += 1 + # 每30帧输出一次调试信息 + if self._debug_counter % 30 == 1: + print(f"🎮 {hand}手摇杆输入: ({joystick_input.x:.3f}, {joystick_input.y:.3f})") + + return joystick_input + + # 备选方案:读取touchpad_pos(Quest等设备) + if hasattr(controller, 'touchpad_pos') and controller.touchpad_pos: + touchpad_input = Vec2(controller.touchpad_pos.x, controller.touchpad_pos.y) + # 检查是否有有效输入 + if touchpad_input.length() > 0.01: + # 调试输出 - 仅在有输入时显示 + if not hasattr(self, '_last_debug_time'): + self._last_debug_time = 0 + self._debug_counter = 0 + + self._debug_counter += 1 + # 每30帧输出一次调试信息 + if self._debug_counter % 30 == 1: + print(f"🎮 {hand}手触摸板输入: ({touchpad_input.x:.3f}, {touchpad_input.y:.3f})") + + return touchpad_input + + # 可选:尝试从动作系统获取(如果可用) + if (self.vr_manager.action_manager and + hasattr(self.vr_manager.action_manager, 'get_analog_action_value')): + try: + device_path = f'/user/hand/{hand}' + + # 尝试摇杆 + joystick_value, _ = self.vr_manager.action_manager.get_analog_action_value('joystick', device_path) + if joystick_value is not None: + return Vec2(joystick_value.x, joystick_value.y) + + # 尝试触摸板 + trackpad_value, _ = self.vr_manager.action_manager.get_analog_action_value('trackpad', device_path) + if trackpad_value is not None: + return Vec2(trackpad_value.x, trackpad_value.y) + except Exception: + # 静默忽略动作系统错误 + pass + + return Vec2(0, 0) + + def _apply_deadzone(self, input_vec): + """应用摇杆死区 + + Args: + input_vec: 原始摇杆输入 + + Returns: + Vec2: 应用死区后的输入 + """ + magnitude = input_vec.length() + + if magnitude < self.deadzone: + return Vec2(0, 0) + + # 重新映射到 [0, 1] 范围 + normalized_magnitude = (magnitude - self.deadzone) / (1.0 - self.deadzone) + normalized_magnitude = min(normalized_magnitude, 1.0) + + if magnitude > 0: + direction = input_vec / magnitude + return direction * normalized_magnitude + + return Vec2(0, 0) + + def _handle_turning(self, input_vec, hand, dt): + """处理摇杆转向 + + Args: + input_vec: 摇杆输入向量 + hand: 'left' 或 'right' + dt: 帧间隔时间 + """ + # 检查是否超过转向阈值 + if abs(input_vec.x) < self.turn_threshold: + # 没有转向输入,重置该控制器的转向模式 + current_mode = self.left_controller_mode if hand == 'left' else self.right_controller_mode + if current_mode == 'turning': + self._set_controller_mode(hand, 'none') + return + + # 检查是否可以使用转向模式 + if not self._can_use_mode(hand, 'turning'): + # 当前控制器被传送锁定,忽略转向输入 + current_mode = self.left_controller_mode if hand == 'left' else self.right_controller_mode + if current_mode != 'turning': + print(f"⛔ {hand}手转向被阻止 - 当前模式: {current_mode}") + return + + # 激活转向模式 + self._set_controller_mode(hand, 'turning') + + # 检查冷却时间(分段转向) + cooldown = self.left_turn_cooldown if hand == 'left' else self.right_turn_cooldown + + if self.smooth_turning: + # 平滑转向 - 反转方向:摇杆右移(+x)应该向右转(-angle) + turn_amount = -input_vec.x * self.turn_sensitivity * dt + self._apply_rotation(turn_amount) + + # 调试输出转向 + if not hasattr(self, '_turn_debug_counter'): + self._turn_debug_counter = 0 + self._turn_debug_counter += 1 + if self._turn_debug_counter % 60 == 1: # 每秒输出一次 + print(f"🔄 {hand}手转向: 输入={input_vec.x:.3f}, 角度={turn_amount:.1f}°") + else: + # 分段转向 + if cooldown <= 0: + # 反转方向:摇杆右移应该向右转 + turn_amount = -self.snap_turn_angle if input_vec.x > 0 else self.snap_turn_angle + self._apply_rotation(turn_amount) + + print(f"🔄 {hand}手分段转向: 输入={input_vec.x:.3f}, 角度={turn_amount:.1f}°") + + # 设置冷却时间 + if hand == 'left': + self.left_turn_cooldown = self.snap_turn_cooldown + else: + self.right_turn_cooldown = self.snap_turn_cooldown + + def _apply_rotation(self, angle_degrees): + """应用旋转到VR跟踪空间 + + Args: + angle_degrees: 旋转角度(度) + """ + if not self.vr_manager.tracking_space: + return + + # 绕Z轴旋转(垂直轴) + current_h = self.vr_manager.tracking_space.getH() + new_h = current_h + angle_degrees + self.vr_manager.tracking_space.setH(new_h) + + def _handle_teleport(self, controller, input_vec, joystick_state, hand): + """处理摇杆传送 + + Args: + controller: VR控制器实例 + input_vec: 摇杆输入向量 + joystick_state: 摇杆状态对象 + hand: 'left' 或 'right' + """ + # 检查Y轴向前移动是否超过阈值 + forward_input = input_vec.y + + if forward_input > self.teleport_threshold: + # 检查是否可以使用传送模式 + if not self._can_use_mode(hand, 'teleporting'): + # 当前控制器被转向锁定,忽略传送输入 + current_mode = self.left_controller_mode if hand == 'left' else self.right_controller_mode + if current_mode != 'teleporting': + print(f"⛔ {hand}手传送被阻止 - 当前模式: {current_mode}") + return + + # 激活传送模式 + self._set_controller_mode(hand, 'teleporting') + + # 开始或更新传送预览 + if not joystick_state.teleport_active: + joystick_state.teleport_active = True + self.active_teleport_controller = controller + self.teleport_preview_active = True + + # 触发震动反馈 + controller.trigger_haptic_feedback(0.002, 0.3) + + # 计算传送方向 + direction = self._calculate_teleport_direction(controller, input_vec) + + # 更新传送预览 + if self.teleport_system: + if joystick_state.teleport_just_started: + self.teleport_system.start_teleport_preview(controller, direction) + joystick_state.teleport_just_started = False + else: + self.teleport_system.update_teleport_preview(controller, direction) + + else: + # 检查是否需要执行传送 + if joystick_state.teleport_active: + # 松开摇杆,执行传送 + self._execute_teleport(controller) + joystick_state.teleport_active = False + joystick_state.teleport_just_started = True + + # 重置该控制器的传送模式 + self._set_controller_mode(hand, 'none') + else: + # 没有传送输入,检查是否需要重置传送模式 + current_mode = self.left_controller_mode if hand == 'left' else self.right_controller_mode + if current_mode == 'teleporting': + self._set_controller_mode(hand, 'none') + + def _calculate_teleport_direction(self, controller, input_vec): + """计算传送方向向量 - 基于手柄姿态 + + Args: + controller: VR控制器实例 + input_vec: 摇杆输入向量(仅用于激活,不影响方向) + + Returns: + Vec3: 世界坐标中的传送方向 + """ + # 方法1:直接使用手柄指向(推荐) + if controller and hasattr(controller, 'get_forward_direction'): + try: + # 获取手柄的前向方向 + controller_forward = controller.get_forward_direction() + + # 确保方向向量在水平面上 + horizontal_direction = Vec3(controller_forward.x, controller_forward.y, 0) + + if horizontal_direction.length() > 0: + horizontal_direction.normalize() + + # 调试输出(每30帧输出一次) + if not hasattr(self, '_direction_debug_counter'): + self._direction_debug_counter = 0 + + self._direction_debug_counter += 1 + if self._direction_debug_counter % 30 == 1: + # 获取当前tracking_space的旋转角度用于调试 + tracking_rotation = 0 + if self.vr_manager.tracking_space: + tracking_rotation = self.vr_manager.tracking_space.getH() + + print(f"🎯 综合传送方向: 视角旋转({tracking_rotation:.1f}°) + 手柄姿态({controller_forward.x:.2f},{controller_forward.y:.2f},{controller_forward.z:.2f}) → 最终方向({horizontal_direction.x:.2f},{horizontal_direction.y:.2f})") + + return horizontal_direction + + except Exception as e: + print(f"⚠️ 获取手柄方向失败: {e}") + + # 备选方案:使用玩家朝向(保留原逻辑作为备选) + print("🔄 使用备选传送方向计算...") + + # 获取玩家当前朝向的变换矩阵 + player_transform = None + + # 优先使用头显的世界变换 + if self.vr_manager.hmd_anchor and hasattr(self.vr_manager, 'world') and self.vr_manager.world: + player_transform = self.vr_manager.hmd_anchor.getMat(self.vr_manager.world.render) + # 备选:使用tracking_space的世界变换 + elif self.vr_manager.tracking_space and hasattr(self.vr_manager, 'world') and self.vr_manager.world: + player_transform = self.vr_manager.tracking_space.getMat(self.vr_manager.world.render) + + if player_transform: + # 从变换矩阵提取前向向量 + forward = Vec3(player_transform.getRow3(1)) # Y轴 = 前向 + + # 确保向量在水平面上 + forward.z = 0 + if forward.length() > 0: + forward.normalize() + return forward + + # 最终备选方案:默认前向 + print("⚠️ 无法获取任何朝向,使用默认方向") + return Vec3(0, 1, 0) + + def _execute_teleport(self, controller): + """执行传送 + + Args: + controller: VR控制器实例 + """ + if self.teleport_system and self.teleport_preview_active: + success = self.teleport_system.execute_teleport() + + if success: + # 传送成功,触发震动反馈 + controller.trigger_haptic_feedback(0.005, 0.8) + else: + # 传送失败,触发不同的震动反馈 + controller.trigger_haptic_feedback(0.001, 0.2) + + # 停止传送预览 + self.teleport_system.stop_teleport_preview() + self.teleport_preview_active = False + self.active_teleport_controller = None + + def set_turning_mode(self, smooth=True): + """设置转向模式 + + Args: + smooth: True为平滑转向,False为分段转向 + """ + self.smooth_turning = smooth + print(f"✓ 转向模式设置为: {'平滑转向' if smooth else '分段转向'}") + + def set_turn_sensitivity(self, sensitivity): + """设置转向灵敏度 + + Args: + sensitivity: 转向灵敏度(度/秒) + """ + self.turn_sensitivity = max(10.0, min(180.0, sensitivity)) + print(f"✓ 转向灵敏度设置为: {self.turn_sensitivity}度/秒") + + def apply_config(self, config): + """应用配置 + + Args: + config: VRJoystickConfig配置实例 + """ + try: + self.deadzone = config.deadzone + self.turn_threshold = config.turn_threshold + self.teleport_threshold = config.teleport_threshold + self.turn_sensitivity = config.turn_sensitivity + self.smooth_turning = (config.turn_mode.value == 'smooth') + self.snap_turn_angle = config.snap_turn_angle + self.snap_turn_cooldown = config.snap_turn_cooldown + + # 应用传送系统配置 + if self.teleport_system and hasattr(config, 'teleport_range'): + self.teleport_system.teleport_range = config.teleport_range + if hasattr(config, 'teleport_arc_resolution'): + self.teleport_system.arc_resolution = config.teleport_arc_resolution + if hasattr(config, 'teleport_initial_velocity'): + self.teleport_system.initial_velocity = config.teleport_initial_velocity + if hasattr(config, 'min_teleport_distance'): + self.teleport_system.min_teleport_distance = config.min_teleport_distance + + print("✅ 摇杆配置已成功应用") + + except Exception as e: + print(f"⚠️ 应用配置失败: {e}") + + def get_current_config(self): + """获取当前配置 + + Returns: + dict: 当前配置参数 + """ + return { + 'deadzone': self.deadzone, + 'turn_threshold': self.turn_threshold, + 'teleport_threshold': self.teleport_threshold, + 'turn_sensitivity': self.turn_sensitivity, + 'smooth_turning': self.smooth_turning, + 'snap_turn_angle': self.snap_turn_angle, + 'snap_turn_cooldown': self.snap_turn_cooldown + } + + def _print_debug_status(self): + """打印调试状态信息""" + try: + print("🔍 ======= VR摇杆调试状态 =======") + + # 控制器连接状态 + left_connected = self.vr_manager.left_controller is not None + right_connected = self.vr_manager.right_controller is not None + print(f"📱 控制器状态: 左手={left_connected}, 右手={right_connected}") + + # 检查控制器属性 + if left_connected: + left_ctrl = self.vr_manager.left_controller + has_joystick = hasattr(left_ctrl, 'joystick_pos') + has_touchpad = hasattr(left_ctrl, 'touchpad_pos') + print(f"📊 左手控制器: joystick_pos={has_joystick}, touchpad_pos={has_touchpad}") + + if has_joystick and left_ctrl.joystick_pos: + pos = left_ctrl.joystick_pos + print(f" 当前摇杆位置: ({pos.x:.3f}, {pos.y:.3f}, {pos.z:.3f})") + + if has_touchpad and left_ctrl.touchpad_pos: + pos = left_ctrl.touchpad_pos + print(f" 当前触摸板位置: ({pos.x:.3f}, {pos.y:.3f}, {pos.z:.3f})") + + if right_connected: + right_ctrl = self.vr_manager.right_controller + has_joystick = hasattr(right_ctrl, 'joystick_pos') + has_touchpad = hasattr(right_ctrl, 'touchpad_pos') + print(f"📊 右手控制器: joystick_pos={has_joystick}, touchpad_pos={has_touchpad}") + + if has_joystick and right_ctrl.joystick_pos: + pos = right_ctrl.joystick_pos + print(f" 当前摇杆位置: ({pos.x:.3f}, {pos.y:.3f}, {pos.z:.3f})") + + if has_touchpad and right_ctrl.touchpad_pos: + pos = right_ctrl.touchpad_pos + print(f" 当前触摸板位置: ({pos.x:.3f}, {pos.y:.3f}, {pos.z:.3f})") + + # 摇杆配置 + print(f"⚙️ 摇杆配置:") + print(f" 死区: {self.deadzone}") + print(f" 转向阈值: {self.turn_threshold}") + print(f" 传送阈值: {self.teleport_threshold}") + print(f" 转向模式: {'平滑' if self.smooth_turning else '分段'}") + + # 动作系统状态 + action_mgr_available = (self.vr_manager.action_manager is not None) + print(f"🎯 动作系统: {'可用' if action_mgr_available else '不可用'}") + + # 传送系统状态 + teleport_available = (self.teleport_system is not None) + print(f"🚀 传送系统: {'可用' if teleport_available else '不可用'}") + + print("🔍 ==============================") + + except Exception as e: + print(f"⚠️ 调试状态输出失败: {e}") + + def cleanup(self): + """清理摇杆系统资源""" + try: + # 停止任何活跃的传送预览 + if self.teleport_preview_active and self.teleport_system: + self.teleport_system.stop_teleport_preview() + + self.teleport_preview_active = False + self.active_teleport_controller = None + + self.ignoreAll() + print("🧹 VR摇杆系统已清理") + + except Exception as e: + print(f"⚠️ 清理摇杆系统失败: {e}") + + +class JoystickState: + """摇杆状态跟踪类""" + + def __init__(self): + self.current_input = Vec2(0, 0) # 当前摇杆输入 + self.previous_input = Vec2(0, 0) # 上一帧摇杆输入 + self.teleport_active = False # 传送是否激活 + self.teleport_just_started = True # 传送是否刚开始 + + def update(self, new_input): + """更新摇杆状态 + + Args: + new_input: 新的摇杆输入 + """ + self.previous_input = Vec2(self.current_input) + self.current_input = Vec2(new_input) + + def is_input_changed(self, threshold=0.01): + """检查输入是否发生变化 + + Args: + threshold: 变化阈值 + + Returns: + bool: 是否发生变化 + """ + diff = self.current_input - self.previous_input + return diff.length() > threshold \ No newline at end of file diff --git a/core/vr_joystick_config.py b/core/vr_joystick_config.py new file mode 100644 index 00000000..13401e59 --- /dev/null +++ b/core/vr_joystick_config.py @@ -0,0 +1,268 @@ +""" +VR摇杆配置模块 + +提供摇杆交互的配置选项和预设: +- 不同的转向模式和灵敏度设置 +- 传送参数调整 +- 用户体验优化选项 +""" + +from enum import Enum + + +class TurnMode(Enum): + """转向模式枚举""" + SMOOTH = "smooth" # 平滑转向 + SNAP = "snap" # 分段转向 + + +class JoystickProfile(Enum): + """摇杆配置预设""" + COMFORTABLE = "comfortable" # 舒适模式 - 低灵敏度,平滑转向 + STANDARD = "standard" # 标准模式 - 中等灵敏度,分段转向 + GAMING = "gaming" # 游戏模式 - 高灵敏度,快速响应 + ACCESSIBLE = "accessible" # 无障碍模式 - 更大死区,更高阈值 + + +class VRJoystickConfig: + """VR摇杆配置类""" + + def __init__(self): + """初始化默认配置""" + # 基础参数 + self.deadzone = 0.15 # 摇杆死区 (0-1) + self.turn_threshold = 0.3 # 转向激活阈值 + self.teleport_threshold = 0.5 # 传送激活阈值 + + # 转向设置 + self.turn_mode = TurnMode.SNAP # 转向模式 + self.turn_sensitivity = 250.0 # 转向灵敏度(度/秒)- 增加速度 + self.snap_turn_angle = 30.0 # 分段转向角度(度) + self.snap_turn_cooldown = 0.3 # 分段转向间隔(秒) + + # 传送设置 + self.teleport_range = 20.0 # 最大传送距离 + self.teleport_arc_resolution = 50 # 抛物线精度 + self.teleport_initial_velocity = 10.0 # 传送初始速度 + self.min_teleport_distance = 1.0 # 最小传送距离 + + # 反馈设置 + self.haptic_feedback_enabled = True # 是否启用震动反馈 + self.teleport_start_haptic = (0.002, 0.3) # 传送开始震动 (时长, 强度) + self.teleport_success_haptic = (0.005, 0.8) # 传送成功震动 + self.teleport_fail_haptic = (0.001, 0.2) # 传送失败震动 + + # 可视化设置 + self.show_teleport_arc = True # 显示传送抛物线 + self.show_teleport_target = True # 显示传送目标标记 + self.arc_valid_color = (0.2, 0.9, 0.2, 0.8) # 有效抛物线颜色 + self.arc_invalid_color = (0.9, 0.2, 0.2, 0.8) # 无效抛物线颜色 + + def apply_profile(self, profile: JoystickProfile): + """应用预设配置 + + Args: + profile: 配置预设 + """ + if profile == JoystickProfile.COMFORTABLE: + self._apply_comfortable_profile() + elif profile == JoystickProfile.STANDARD: + self._apply_standard_profile() + elif profile == JoystickProfile.GAMING: + self._apply_gaming_profile() + elif profile == JoystickProfile.ACCESSIBLE: + self._apply_accessible_profile() + + def _apply_comfortable_profile(self): + """舒适模式 - 适合长时间使用""" + self.deadzone = 0.2 + self.turn_threshold = 0.4 + self.teleport_threshold = 0.6 + self.turn_mode = TurnMode.SMOOTH + self.turn_sensitivity = 100.0 # 适中的转向速度 + self.snap_turn_cooldown = 0.4 + print("✓ 已应用舒适模式配置") + + def _apply_standard_profile(self): + """标准模式 - 平衡的体验""" + self.deadzone = 0.15 + self.turn_threshold = 0.3 + self.teleport_threshold = 0.5 + self.turn_mode = TurnMode.SNAP + self.turn_sensitivity = 150.0 # 更快的转向速度 + self.snap_turn_angle = 30.0 + self.snap_turn_cooldown = 0.3 + print("✓ 已应用标准模式配置") + + def _apply_gaming_profile(self): + """游戏模式 - 快速响应""" + self.deadzone = 0.1 + self.turn_threshold = 0.2 + self.teleport_threshold = 0.4 + self.turn_mode = TurnMode.SMOOTH + self.turn_sensitivity = 200.0 # 高速转向 + self.snap_turn_cooldown = 0.2 + print("✓ 已应用游戏模式配置") + + def _apply_accessible_profile(self): + """无障碍模式 - 更容易控制""" + self.deadzone = 0.25 + self.turn_threshold = 0.5 + self.teleport_threshold = 0.7 + self.turn_mode = TurnMode.SNAP + self.turn_sensitivity = 80.0 # 较慢但可控的转向 + self.snap_turn_angle = 45.0 + self.snap_turn_cooldown = 0.5 + print("✓ 已应用无障碍模式配置") + + def set_turn_mode(self, mode: TurnMode): + """设置转向模式 + + Args: + mode: 转向模式 + """ + self.turn_mode = mode + print(f"✓ 转向模式设置为: {mode.value}") + + def set_turn_sensitivity(self, sensitivity: float): + """设置转向灵敏度 + + Args: + sensitivity: 转向灵敏度(度/秒) + """ + self.turn_sensitivity = max(10.0, min(180.0, sensitivity)) + print(f"✓ 转向灵敏度设置为: {self.turn_sensitivity}度/秒") + + def set_deadzone(self, deadzone: float): + """设置摇杆死区 + + Args: + deadzone: 死区大小 (0-1) + """ + self.deadzone = max(0.0, min(0.5, deadzone)) + print(f"✓ 摇杆死区设置为: {self.deadzone}") + + def set_teleport_range(self, range_meters: float): + """设置传送范围 + + Args: + range_meters: 传送范围(米) + """ + self.teleport_range = max(5.0, min(50.0, range_meters)) + print(f"✓ 传送范围设置为: {self.teleport_range}米") + + def enable_haptic_feedback(self, enabled: bool): + """启用或禁用震动反馈 + + Args: + enabled: 是否启用 + """ + self.haptic_feedback_enabled = enabled + print(f"✓ 震动反馈: {'启用' if enabled else '禁用'}") + + def get_config_dict(self): + """获取配置字典 + + Returns: + dict: 配置参数字典 + """ + return { + 'deadzone': self.deadzone, + 'turn_threshold': self.turn_threshold, + 'teleport_threshold': self.teleport_threshold, + 'turn_mode': self.turn_mode.value, + 'turn_sensitivity': self.turn_sensitivity, + 'snap_turn_angle': self.snap_turn_angle, + 'snap_turn_cooldown': self.snap_turn_cooldown, + 'teleport_range': self.teleport_range, + 'haptic_feedback_enabled': self.haptic_feedback_enabled + } + + def apply_to_joystick_manager(self, joystick_manager): + """将配置应用到摇杆管理器 + + Args: + joystick_manager: VRJoystickManager实例 + """ + try: + joystick_manager.deadzone = self.deadzone + joystick_manager.turn_threshold = self.turn_threshold + joystick_manager.teleport_threshold = self.teleport_threshold + joystick_manager.turn_sensitivity = self.turn_sensitivity + joystick_manager.smooth_turning = (self.turn_mode == TurnMode.SMOOTH) + joystick_manager.snap_turn_angle = self.snap_turn_angle + joystick_manager.snap_turn_cooldown = self.snap_turn_cooldown + + # 应用传送系统配置 + if hasattr(joystick_manager, 'teleport_system') and joystick_manager.teleport_system: + teleport_sys = joystick_manager.teleport_system + teleport_sys.teleport_range = self.teleport_range + teleport_sys.arc_resolution = self.teleport_arc_resolution + teleport_sys.initial_velocity = self.teleport_initial_velocity + teleport_sys.min_teleport_distance = self.min_teleport_distance + + print("✅ 配置已成功应用到摇杆管理器") + + except Exception as e: + print(f"⚠️ 应用配置失败: {e}") + + +# 预定义的配置实例 +COMFORTABLE_CONFIG = VRJoystickConfig() +COMFORTABLE_CONFIG.apply_profile(JoystickProfile.COMFORTABLE) + +STANDARD_CONFIG = VRJoystickConfig() +STANDARD_CONFIG.apply_profile(JoystickProfile.STANDARD) + +GAMING_CONFIG = VRJoystickConfig() +GAMING_CONFIG.apply_profile(JoystickProfile.GAMING) + +ACCESSIBLE_CONFIG = VRJoystickConfig() +ACCESSIBLE_CONFIG.apply_profile(JoystickProfile.ACCESSIBLE) + + +def create_custom_config(**kwargs): + """创建自定义配置 + + Args: + **kwargs: 配置参数 + + Returns: + VRJoystickConfig: 自定义配置实例 + """ + config = VRJoystickConfig() + + for key, value in kwargs.items(): + if hasattr(config, key): + setattr(config, key, value) + else: + print(f"⚠️ 未知的配置参数: {key}") + + return config + + +def print_usage_guide(): + """打印使用指南""" + print("🎮 ======= VR摇杆交互使用指南 =======") + print() + print("📋 基本操作:") + print(" • 摇杆左右移动 → 转向(旋转视角)") + print(" • 摇杆向前推 → 显示传送抛物线") + print(" • 松开摇杆 → 执行传送到落点") + print() + print("⚙️ 配置选项:") + print(" • 舒适模式 → 低灵敏度,适合长时间使用") + print(" • 标准模式 → 平衡体验,推荐大多数用户") + print(" • 游戏模式 → 高灵敏度,快速响应") + print(" • 无障碍模式 → 更大死区,容易控制") + print() + print("🎯 使用建议:") + print(" • 首次使用建议从标准模式开始") + print(" • 根据个人喜好调整转向模式(平滑/分段)") + print(" • 可以随时调整死区和灵敏度") + print(" • 传送范围可根据场景大小调整") + print() + + +if __name__ == "__main__": + print_usage_guide() \ No newline at end of file diff --git a/core/vr_manager.py b/core/vr_manager.py index c6830b06..ab1bc036 100644 --- a/core/vr_manager.py +++ b/core/vr_manager.py @@ -1,573 +1,4199 @@ -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) +""" +VR管理器模块 + +负责VR功能的初始化、渲染和交互: +- OpenVR/SteamVR集成 +- VR头显跟踪和渲染 +- VR控制器交互 +- VR模式切换 +""" -from panda3d.core import * -from direct.showbase.DirectObject import DirectObject -from direct.task import Task import sys +import gc +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功能将不可用") + +# 导入手柄控制器、动作系统、交互系统、摇杆系统和传送系统 +from .vr_controller import LeftController, RightController +from .vr_actions import VRActionManager +from .vr_interaction import VRInteractionManager +from .vr_joystick import VRJoystickManager +from .vr_teleport import VRTeleportSystem + class VRManager(DirectObject): - """VR管理器 - 处理VR系统初始化、追踪和渲染""" - + """VR管理器类 - 处理所有VR相关功能""" + def __init__(self, world): + """初始化VR管理器 + + Args: + world: 主世界对象引用 + """ super().__init__() + self.world = world - self.vr_enabled = False 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 - 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 = {} + + # VR纹理和ID缓存 - 修复重复准备问题 + self.vr_left_texture = None + self.vr_right_texture = None + self.left_texture_id = None # 缓存左眼纹理的OpenGL ID + self.right_texture_id = None # 缓存右眼纹理的OpenGL ID + self.textures_prepared = False # 标记纹理是否已准备 + + # VR跟踪数据 + self.hmd_pose = Mat4.identMat() self.controller_poses = {} - + self.tracked_device_poses = [] + self.poses = None # OpenVR渲染姿态数组 + self.game_poses = None # OpenVR游戏逻辑姿态数组 + + # 🚀 对象池和缓存系统 - 修复16-19帧周期性GPU峰值 + self._matrix_pool = [] # Mat4对象池 + self._matrix_pool_size = 8 # 池大小,足够处理多个控制器 + self._cached_matrices = {} # 设备ID到矩阵的缓存 + self._controller_poses_cache = {} # 控制器姿态缓存,避免每帧clear() + + # 🚀 OpenVR Texture对象缓存 - 避免每帧创建openvr.Texture_t() + self._left_ovr_texture = None # 左眼纹理对象缓存 + self._right_ovr_texture = None # 右眼纹理对象缓存 + + # Python垃圾回收控制 + self._gc_control_enabled = True # 是否启用GC控制 + self._gc_disabled = False # GC是否被禁用 + self._manual_gc_interval = 900 # 每900帧手动触发一次GC (15秒@60fps) - 减少GC频率 + self._last_manual_gc_frame = 0 + + # 🚀 立即初始化对象池和GC控制(在其他组件之前) + self._initialize_object_pools() + + # VR渲染参数 + self.eye_width = 1080 + self.eye_height = 1200 + self.near_clip = 0.1 + self.far_clip = 1000.0 + + # VR分辨率缩放优化 + self.resolution_scale = 0.75 # 默认0.75倍分辨率,性能和质量平衡 + self.base_eye_width = 1080 # 原始推荐分辨率 + self.base_eye_height = 1200 + self.scaled_eye_width = 1080 # 实际使用的缩放后分辨率 + self.scaled_eye_height = 1200 + + # VR质量预设 + self.quality_presets = { + 'performance': 0.6, # 性能模式 - 约60%分辨率 + 'balanced': 0.75, # 平衡模式 - 约75%分辨率 + 'quality': 1.0 # 质量模式 - 100%分辨率 + } + self.current_quality_preset = 'balanced' # 默认平衡模式 + + # 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 + + # OpenVR 帧ID跟踪(防止重复提交) + self.openvr_frame_id = 0 + self.left_eye_last_render_frame = -1 + self.right_eye_last_render_frame = -1 + + # 高级性能监控(默认关闭,手动开启) + self.performance_monitoring = False # 是否启用性能监控 + self.debug_output_enabled = False # 是否启用调试输出 + self.debug_mode = 'detailed' # 'brief' 或 'detailed' + self.cpu_usage = 0.0 + self.memory_usage = 0.0 + self.gpu_usage = 0.0 + self.gpu_memory_usage = 0.0 + self.frame_times = [] # 存储最近帧时间 + self.max_frame_time_history = 60 # 保存60帧的历史 + self.last_performance_check = 0 + self.performance_check_interval = 0.5 # 每0.5秒更新一次性能数据 + + # 渲染管线详细监控 + self.enable_pipeline_monitoring = True # 是否启用管线监控 + self.performance_mode_enabled = False # 性能优化模式(禁用监控以减少对象创建) + self.performance_mode_trigger_frame = 600 # 第600帧后启用性能模式 + self.wait_poses_time = 0.0 # waitGetPoses耗时 + self.left_render_time = 0.0 # 左眼渲染耗时 + self.right_render_time = 0.0 # 右眼渲染耗时 + self.submit_time = 0.0 # 纹理提交耗时 + self.left_render_count = 0 # 左眼渲染次数计数 + self.right_render_count = 0 # 右眼渲染次数计数 + self.total_frame_time = 0.0 # 总帧时间 + self.vr_sync_wait_time = 0.0 # VR同步等待时间 + + # 时间监控历史(保存最近30帧) + self.wait_poses_times = [] + self.render_times = [] + self.submit_times = [] + self.sync_wait_times = [] + self.pipeline_history_size = 30 + + # GPU渲染时间监控(OpenVR Frame Timing) + self.enable_gpu_timing = False # 是否启用GPU时间监控(默认关闭) + self.gpu_scene_render_ms = 0.0 # GPU场景渲染时间 + self.gpu_pre_submit_ms = 0.0 # 提交前GPU时间 + self.gpu_post_submit_ms = 0.0 # 提交后GPU时间 + self.gpu_total_render_ms = 0.0 # GPU总渲染时间 + self.gpu_compositor_render_ms = 0.0 # GPU合成器渲染时间 + self.gpu_client_frame_interval_ms = 0.0 # 客户端帧间隔 + self.gpu_timing_history = [] # GPU时间历史记录 + self.gpu_timing_history_size = 30 # GPU时间历史记录大小 + self.gpu_timing_failure_count = 0 # GPU时间获取失败次数 + + # VR系统信息 + self.current_eye_resolution = (self.eye_width, self.eye_height) + self.recommended_eye_resolution = (0, 0) + self.vr_display_frequency = 0.0 + self.vr_vsync_enabled = True + self.vsync_to_photons_ms = 0.0 # VSync到光子的延迟 + self.target_frame_time_ms = 0.0 # 目标帧时间 + self.vsync_window_ms = 0.0 # VSync时间窗口 + self.async_reprojection_enabled = False # 异步重投影状态 + self.motion_smoothing_enabled = False # 运动平滑状态 + + # 🧪 VR测试模式 + self.vr_test_mode = False # 是否启用VR测试模式 + self.test_display_mode = 'stereo' # 'left', 'right', 'stereo' + self.test_display_quad = None # 测试显示的四边形 + self.test_right_quad = None # 右眼显示的四边形(立体模式) + self.stereo_display_created = False # 立体显示是否已创建 + self.test_performance_hud = None # 性能HUD + self.test_performance_text = None # 性能文本节点 + self.test_mode_initialized = False # 测试模式是否已初始化 + self.hud_update_counter = 0 # HUD更新计数器 + self.hud_update_interval = 30 # HUD更新间隔(帧数),30帧约0.5秒@60fps + + # 🔧 VR测试模式调试选项 - 逐步启用普通VR功能 + self.test_mode_submit_texture = False # 是否在测试模式提交纹理到OpenVR + self.test_mode_wait_poses = False # 是否在测试模式调用waitGetPoses + + # waitGetPoses调用策略配置 - 使用高性能的update_task策略 + self.use_prediction_time = 0.011 # 11ms的预测时间 - OpenVR标准值,平衡准确性和延迟 + self.poses_updated_in_task = True # 始终在更新任务中获取姿态(Running Start模式) + + # 尝试导入性能监控库 + self._init_performance_monitoring() + + # VR提交策略 - 基于参考实现 + self.submit_together = True # 是否在right_cb中同时提交左右眼 + + + # Running Start标记 - Valve最佳实践 + self._waitgetposes_called_this_frame = False + + # 姿态缓存 - 修复时序不匹配 + self._cached_render_poses = None # 用于渲染的缓存姿态 + self._first_frame = True # 首帧标记 + + # ATW控制选项 - 备选方案 + self.disable_async_reprojection = False # 是否禁用异步重投影 + + # VR手柄控制器 + self.left_controller = None + self.right_controller = None + self.controllers = {} # 设备索引到控制器的映射 + self.tracked_device_anchors = {} # 跟踪设备锚点 + + # VR动作系统 - 可选择禁用(用于API兼容性问题) + self.disable_action_system = True # 禁用动作系统,使用直接控制器输入 + + if not self.disable_action_system: + try: + self.action_manager = VRActionManager(self) + print("✓ VR动作管理器初始化完成") + except Exception as e: + print(f"⚠️ VR动作管理器初始化失败: {e}") + self.action_manager = None + else: + self.action_manager = None + print("🚫 VR动作系统已禁用,使用直接控制器输入") + + # VR交互系统 - 添加异常保护 + try: + self.interaction_manager = VRInteractionManager(self) + print("✓ VR交互管理器初始化完成") + except Exception as e: + print(f"⚠️ VR交互管理器初始化失败: {e}") + self.interaction_manager = None + + # VR传送系统 - 添加异常保护 + try: + self.teleport_system = VRTeleportSystem(self) + print("✓ VR传送系统初始化完成") + except Exception as e: + print(f"⚠️ VR传送系统初始化失败: {e}") + self.teleport_system = None + + # VR摇杆系统 - 添加异常保护 + try: + self.joystick_manager = VRJoystickManager(self) + print("✓ VR摇杆管理器初始化完成") + except Exception as e: + print(f"⚠️ VR摇杆管理器初始化失败: {e}") + self.joystick_manager = None + print("✓ VR管理器初始化完成") - - def initialize_vr(self, force_simulation=False): + + def _initialize_object_pools(self): + """初始化对象池 - 修复16-19帧周期性GPU峰值""" + try: + # 预填充Mat4对象池 + for _ in range(self._matrix_pool_size): + self._matrix_pool.append(Mat4()) + + print(f"✅ Mat4对象池初始化完成 - 池大小: {self._matrix_pool_size}") + + # 🚀 预创建OpenVR Texture对象 - 消除每帧创建openvr.Texture_t()的开销 + try: + import openvr + self._left_ovr_texture = openvr.Texture_t() + self._right_ovr_texture = openvr.Texture_t() + + # 设置固定属性(这些不变) + self._left_ovr_texture.eType = openvr.TextureType_OpenGL + self._left_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma + self._right_ovr_texture.eType = openvr.TextureType_OpenGL + self._right_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma + + print("✅ OpenVR Texture对象缓存已创建 - 避免每帧创建新对象") + except Exception as texture_error: + print(f"⚠️ OpenVR Texture对象创建失败: {texture_error}") + # 不影响整体初始化,但性能可能不是最优 + + # 启用GC控制 + if self._gc_control_enabled: + # 禁用自动垃圾回收,改为手动控制 + gc.disable() + self._gc_disabled = True + print("✅ Python垃圾回收已禁用,改为手动控制") + + except Exception as e: + print(f"⚠️ 对象池初始化失败: {e}") + + def _get_pooled_matrix(self): + """从对象池获取Mat4对象""" + if self._matrix_pool: + return self._matrix_pool.pop() + else: + # 池为空时创建新对象(不应该发生,但作为备用) + return Mat4() + + def _return_pooled_matrix(self, matrix): + """将Mat4对象返回对象池""" + if len(self._matrix_pool) < self._matrix_pool_size: + # 重置矩阵到单位矩阵 + matrix.identMat() + self._matrix_pool.append(matrix) + + def _manual_gc_control(self): + """手动垃圾回收控制 - 避免VR渲染期间的GC峰值""" + if not self._gc_control_enabled or not self._gc_disabled: + return + + # 🚀 智能GC间隔:性能模式下减少GC频率 + current_interval = self._manual_gc_interval + if self.performance_mode_enabled: + current_interval = self._manual_gc_interval * 2 # 性能模式下间隔翻倍 + + # 每N帧手动触发一次垃圾回收 + if self.frame_count - self._last_manual_gc_frame >= current_interval: + # 在非渲染关键时刻触发GC + collected = gc.collect() + self._last_manual_gc_frame = self.frame_count + + # 仅在收集到对象时输出信息 + if collected > 0: + print(f"🗑️ 手动GC: 清理了 {collected} 个对象 (帧#{self.frame_count})") + + def _update_matrix_from_openvr(self, panda_mat, ovr_matrix): + """直接更新现有Mat4对象的数值,避免创建新对象""" + # 复用_convert_openvr_matrix_to_panda中的转换逻辑 + # X轴行:Panda3D的X轴对应OpenVR的X轴 + panda_mat.setCell(0, 0, ovr_matrix[0][0]) # X_x → X_x + panda_mat.setCell(0, 1, ovr_matrix[0][1]) # X_y → X_y + panda_mat.setCell(0, 2, ovr_matrix[0][2]) # X_z → X_z + panda_mat.setCell(0, 3, ovr_matrix[0][3]) # 位移X分量 + + # Y轴行:Panda3D的Y轴对应OpenVR的-Z轴 + panda_mat.setCell(1, 0, -ovr_matrix[2][0]) # -Z_x → Y_x + panda_mat.setCell(1, 1, -ovr_matrix[2][1]) # -Z_y → Y_y + panda_mat.setCell(1, 2, -ovr_matrix[2][2]) # -Z_z → Y_z + panda_mat.setCell(1, 3, -ovr_matrix[2][3]) # 位移Y分量(-Z位移) + + # Z轴行:Panda3D的Z轴对应OpenVR的Y轴 + panda_mat.setCell(2, 0, ovr_matrix[1][0]) # Y_x → Z_x + panda_mat.setCell(2, 1, ovr_matrix[1][1]) # Y_y → Z_y + panda_mat.setCell(2, 2, ovr_matrix[1][2]) # Y_z → Z_z + panda_mat.setCell(2, 3, ovr_matrix[1][3]) # 位移Z分量(Y位移) + + # 齐次坐标 + panda_mat.setCell(3, 0, 0) + panda_mat.setCell(3, 1, 0) + panda_mat.setCell(3, 2, 0) + panda_mat.setCell(3, 3, 1) + + 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系统""" - 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)}") + if not OPENVR_AVAILABLE: + print("❌ OpenVR不可用,无法初始化VR") return False - def _check_openvr_support(self): - """检查OpenVR支持""" - try: - import openvr + if self.vr_initialized: + print("VR系统已经初始化") return True - except ImportError: - print("OpenVR库未安装") - return False - def _init_openvr(self): - """初始化OpenVR""" try: - import openvr - - # 初始化OpenVR + print("🔄 正在初始化VR系统...") + + # 🚀 确保对象池已正确初始化(备用检查) + if not hasattr(self, '_matrix_pool') or len(self._matrix_pool) == 0: + print("⚠️ 对象池未初始化,正在重新初始化...") + self._initialize_object_pools() + + # 初始化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.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)}") + # 获取推荐的渲染目标尺寸 + base_width, base_height = self.vr_system.getRecommendedRenderTargetSize() + self.base_eye_width = base_width + self.base_eye_height = base_height - 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)}") + # 应用分辨率缩放 + self.scaled_eye_width = int(base_width * self.resolution_scale) + self.scaled_eye_height = int(base_height * self.resolution_scale) - 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()) + # 使用缩放后的分辨率作为实际渲染分辨率 + self.eye_width = self.scaled_eye_width + self.eye_height = self.scaled_eye_height + + self.current_eye_resolution = (self.eye_width, self.eye_height) + self.recommended_eye_resolution = (base_width, base_height) + + print(f"✓ VR基础分辨率: {base_width}x{base_height}") + print(f"✓ VR缩放系数: {self.resolution_scale}") + print(f"✓ VR实际分辨率: {self.eye_width}x{self.eye_height}") + print(f"📊 分辨率优化: {(1 - self.resolution_scale**2) * 100:.1f}% 像素减少") + + # 获取VR系统信息 + try: + # 获取显示频率 + self.vr_display_frequency = self.vr_system.getFloatTrackedDeviceProperty( + openvr.k_unTrackedDeviceIndex_Hmd, + openvr.Prop_DisplayFrequency_Float + ) + print(f"✓ VR显示频率: {self.vr_display_frequency} Hz") + + # 获取IPD(瞳距) + ipd = self.vr_system.getFloatTrackedDeviceProperty( + openvr.k_unTrackedDeviceIndex_Hmd, + openvr.Prop_UserIpdMeters_Float + ) + print(f"✓ IPD(瞳距): {ipd * 1000:.1f}mm") + + # 获取设备制造商和型号 + manufacturer = self.vr_system.getStringTrackedDeviceProperty( + openvr.k_unTrackedDeviceIndex_Hmd, + openvr.Prop_ManufacturerName_String + ) + model = self.vr_system.getStringTrackedDeviceProperty( + openvr.k_unTrackedDeviceIndex_Hmd, + openvr.Prop_ModelNumber_String + ) + print(f"✓ VR设备: {manufacturer} {model}") + + # 获取更多系统配置信息 + try: + # 获取刷新率相关信息 + seconds_since_last_vsync = self.vr_system.getFloatTrackedDeviceProperty( + openvr.k_unTrackedDeviceIndex_Hmd, + openvr.Prop_SecondsFromVsyncToPhotons_Float + ) + self.vsync_to_photons_ms = seconds_since_last_vsync * 1000 + print(f"✓ VSync到光子延迟: {self.vsync_to_photons_ms:.2f}ms") + + # 获取显示延迟信息 + display_refresh_rate = self.vr_display_frequency + if display_refresh_rate > 0: + self.target_frame_time_ms = 1000.0 / display_refresh_rate + print(f"✓ 目标帧时间: {self.target_frame_time_ms:.2f}ms") + + # 计算VSync时间窗口 + self.vsync_window_ms = self.target_frame_time_ms * 0.1 # 10%的窗口 + print(f"✓ VSync时间窗口: ±{self.vsync_window_ms:.2f}ms") + + except Exception as vsync_error: + print(f"⚠️ 获取VSync信息失败: {vsync_error}") + + # 检查是否启用了异步重投影 + try: + if hasattr(openvr, 'VRSettings'): + settings = openvr.VRSettings() + if settings: + self.async_reprojection_enabled = settings.getBool("steamvr", "enableAsyncReprojection") + print(f"✓ 异步重投影: {'启用' if self.async_reprojection_enabled else '禁用'}") + + self.motion_smoothing_enabled = settings.getBool("steamvr", "motionSmoothing") + print(f"✓ 运动平滑: {'启用' if self.motion_smoothing_enabled else '禁用'}") + + except Exception as settings_error: + print(f"⚠️ 获取VR设置失败: {settings_error}") + + except Exception as e: + print(f"⚠️ 获取VR系统信息失败: {e}") + + # 创建OpenVR姿态数组 - 分离渲染和游戏姿态 + poses_t = openvr.TrackedDevicePose_t * openvr.k_unMaxTrackedDeviceCount + self.poses = poses_t() # 渲染姿态(预测的) + self.game_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._optimize_vr_rendering() + + # 初始化手柄控制器 + self._initialize_controllers() + + # 初始化动作系统 - 仅在启用时初始化 + if not self.disable_action_system and self.action_manager: + try: + if not self.action_manager.initialize(): + print("⚠️ VR动作系统初始化失败,但VR系统将继续运行") + except Exception as e: + print(f"⚠️ VR动作系统初始化异常: {e}") 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)}") + if self.disable_action_system: + print("🚫 VR动作系统已禁用,跳过初始化") + else: + print("⚠️ VR动作管理器未创建,跳过动作系统初始化") - 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)}") + # 初始化交互系统 - 检查是否存在 + if self.interaction_manager: + try: + if not self.interaction_manager.initialize(): + print("⚠️ VR交互系统初始化失败,但VR系统将继续运行") + except Exception as e: + print(f"⚠️ VR交互系统初始化异常: {e}") + else: + print("⚠️ VR交互管理器未创建,跳过交互系统初始化") - def _setup_render_targets(self): - """设置渲染目标""" - try: - # 在模拟模式下,可以选择将渲染结果显示到主窗口 - if self.simulation_mode: - # 创建并排显示的立体视图 - self._setup_simulation_display() - - print("✓ 渲染目标设置完成") - - except Exception as e: - print(f"渲染目标设置错误: {str(e)}") + # 初始化VR传送系统 + if self.teleport_system: + try: + if not self.teleport_system.initialize(): + print("⚠️ VR传送系统初始化失败,但VR系统将继续运行") + except Exception as e: + print(f"⚠️ VR传送系统初始化异常: {e}") + else: + print("⚠️ VR传送系统未创建,跳过传送系统初始化") - 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)}") + # 初始化VR摇杆系统 + if self.joystick_manager: + try: + # 传入传送系统引用给摇杆管理器 + self.joystick_manager.initialize(self.teleport_system) + print("✓ VR摇杆系统初始化成功") + except Exception as e: + print(f"⚠️ VR摇杆系统初始化异常: {e}") + else: + print("⚠️ VR摇杆管理器未创建,跳过摇杆系统初始化") - def _init_alvr(self): - """初始化ALVR(仅在真实VR模式下)""" - if self.simulation_mode: - print("ℹ 模拟模式: ALVR串流已跳过") - return False - - try: - # ALVR初始化逻辑 - # 这里应该连接到ALVR服务器 - print("✓ ALVR初始化完成") + # 可选:禁用异步重投影(备选方案) + if self.disable_async_reprojection: + self._disable_async_reprojection() + + # 启动VR更新任务 + self._start_vr_task() + + self.vr_initialized = True + print("✅ VR系统初始化成功") return True + except Exception as e: - print(f"ALVR初始化错误: {str(e)}") + print(f"❌ VR初始化失败: {e}") + import traceback + traceback.print_exc() 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): - """更新模拟追踪数据""" + def _create_vr_buffers(self): + """创建VR渲染缓冲区 - 使用分辨率缩放优化""" 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 - + print(f"🔧 创建VR缓冲区:") + print(f" 推荐分辨率: {self.base_eye_width}x{self.base_eye_height}") + print(f" 缩放系数: {self.resolution_scale}") + print(f" 实际分辨率: {self.eye_width}x{self.eye_height}") + print(f" 像素减少: {(1 - self.resolution_scale**2) * 100:.1f}%") + + # 创建左眼纹理和缓冲区 + 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) + + # 🚀 关键优化:立即准备纹理并缓存OpenGL ID,避免后续重复准备 + print("🔧 准备纹理并缓存OpenGL ID...") + if self._prepare_and_cache_textures(): + print("✅ VR渲染缓冲区和纹理缓存创建成功") + return True + else: + print("❌ 纹理准备失败") + return False + except Exception as e: - print(f"模拟追踪更新错误: {str(e)}") - return task.cont + print(f"❌ 创建VR缓冲区失败: {e}") + import traceback + traceback.print_exc() + return False - def _update_simulation_rendering(self, task): - """更新模拟渲染""" + 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 _prepare_and_cache_textures(self): + """准备纹理并缓存OpenGL ID - 解决重复准备问题""" try: - # 在模拟模式下,渲染已经由Panda3D自动处理 - # 这里可以添加任何特殊的渲染逻辑 - return task.cont - + # 获取graphics state guardian和prepared objects + gsg = self.world.win.getGsg() + if not gsg: + print("❌ 无法获取GraphicsStateGuardian") + return False + + prepared_objects = gsg.getPreparedObjects() + if not prepared_objects: + print("❌ 无法获取PreparedObjects") + return False + + # 准备左眼纹理并缓存ID + if self.vr_left_texture: + print(f" 准备左眼纹理: {self.vr_left_texture.getName()}") + texture_context = self.vr_left_texture.prepareNow(0, prepared_objects, gsg) + if texture_context and hasattr(texture_context, 'getNativeId'): + self.left_texture_id = texture_context.getNativeId() + if self.left_texture_id > 0: + print(f" ✅ 左眼纹理ID缓存: {self.left_texture_id}") + else: + print(" ❌ 左眼纹理ID无效") + return False + else: + print(" ❌ 左眼纹理准备失败") + return False + + # 准备右眼纹理并缓存ID + if self.vr_right_texture: + print(f" 准备右眼纹理: {self.vr_right_texture.getName()}") + texture_context = self.vr_right_texture.prepareNow(0, prepared_objects, gsg) + if texture_context and hasattr(texture_context, 'getNativeId'): + self.right_texture_id = texture_context.getNativeId() + if self.right_texture_id > 0: + print(f" ✅ 右眼纹理ID缓存: {self.right_texture_id}") + else: + print(" ❌ 右眼纹理ID无效") + return False + else: + print(" ❌ 右眼纹理准备失败") + return False + + # 标记纹理已准备 + self.textures_prepared = True + print(" ✅ 纹理准备完成,ID已缓存,后续帧将直接使用缓存ID") + return True + except Exception as e: - print(f"模拟渲染更新错误: {str(e)}") + print(f"❌ 纹理准备和缓存失败: {e}") + import traceback + traceback.print_exc() + return False + + 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: + # 🔍 性能诊断:检查缓冲区类型和属性 + self._diagnose_buffer_performance(buffer, name, width, height) + + # 清除默认渲染纹理 + buffer.clearRenderTextures() + # 🚀 使用RTMBindOrCopy模式 - 已经是最优选择 + # RTMBindOrCopy会尝试直接绑定到纹理,只有硬件不支持时才回退到复制 + # 这已经是Panda3D中最高效的渲染到纹理模式 + buffer.addRenderTexture(texture, GraphicsOutput.RTMBindOrCopy, GraphicsOutput.RTPColor) + + else: + print(f"⚠️ VR缓冲区创建失败: {name} ({width}x{height})") + + return buffer + + def _diagnose_buffer_performance(self, buffer, name, width, height): + """诊断VR缓冲区性能特性""" + try: + # 检查缓冲区类型 + buffer_type = type(buffer).__name__ + + # 检查是否为parasite buffer(性能较差的类型) + is_parasite = 'Parasite' in buffer_type + + # 获取缓冲区信息 + is_valid = buffer.isValid() + is_hardware = hasattr(buffer, 'getGsg') and buffer.getGsg() is not None + + # 输出诊断信息(仅第一次创建时) + if not hasattr(self, '_buffer_diagnosis_shown'): + print(f"🔍 VR缓冲区诊断 [{name}]:") + print(f" 类型: {buffer_type}") + print(f" 分辨率: {width}x{height} ({width*height/1000000:.1f}M像素)") + print(f" 有效性: {'✓' if is_valid else '✗'}") + print(f" 硬件支持: {'✓' if is_hardware else '✗'}") + + if is_parasite: + print(f" ⚠️ 检测到Parasite Buffer - 性能可能受限") + print(f" 建议: 检查显卡是否支持真正的离屏渲染") + else: + print(f" ✓ 使用硬件FBO - 性能良好") + + # 显示帧缓冲区属性 + if hasattr(buffer, 'getFbProperties'): + fbp = buffer.getFbProperties() + if fbp: + print(f" 颜色位数: R{fbp.getRedBits()}G{fbp.getGreenBits()}B{fbp.getBlueBits()}A{fbp.getAlphaBits()}") + if hasattr(fbp, 'getMultisamples') and fbp.getMultisamples() > 0: + print(f" MSAA: {fbp.getMultisamples()}x") + + self._buffer_diagnosis_shown = True + + except Exception as e: + print(f"缓冲区诊断失败: {e}") + + 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) + # 恢复DrawCallback以精确控制渲染时机 + left_dr.setDrawCallback(PythonCallbackObject(self.simple_left_cb)) + + right_dr = self.vr_right_eye_buffer.makeDisplayRegion() + right_dr.setCamera(self.vr_right_camera) + right_dr.setActive(True) + # 恢复DrawCallback以精确控制渲染时机 + right_dr.setDrawCallback(PythonCallbackObject(self.simple_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 _optimize_vr_rendering(self): + """优化VR模式下的渲染管线 - 减少不必要的后处理开销""" + try: + print("🔧 正在优化VR渲染管线...") + + # 检查是否有RenderPipeline + if hasattr(self.world, 'render_pipeline') and self.world.render_pipeline: + print(" 检测到RenderPipeline,正在优化...") + + # 对VR缓冲区应用轻量级渲染设置 + if hasattr(self, 'vr_left_eye_buffer') and self.vr_left_eye_buffer: + self._apply_lightweight_rendering(self.vr_left_eye_buffer, "左眼") + + if hasattr(self, 'vr_right_eye_buffer') and self.vr_right_eye_buffer: + self._apply_lightweight_rendering(self.vr_right_eye_buffer, "右眼") + + print("✅ VR渲染管线优化完成") + else: + print(" 未检测到RenderPipeline,使用默认Panda3D渲染") + + # 禁用VR缓冲区的不必要功能 + self._disable_vr_buffer_extras() + + except Exception as e: + print(f"⚠️ VR渲染优化失败: {e}") + import traceback + traceback.print_exc() + + def _apply_lightweight_rendering(self, buffer, eye_name): + """为VR缓冲区应用轻量级渲染设置""" + try: + # 禁用多重采样抗锯齿(MSAA)以提升性能 + if hasattr(buffer, 'setMultisample'): + buffer.setMultisample(0) + print(f" {eye_name}: 禁用MSAA") + + # 设置更简单的清除颜色操作 + buffer.setClearColorActive(True) + buffer.setClearColor((0.1, 0.2, 0.4, 1.0)) # 深蓝色,便于调试 + + # 禁用深度写入到某些不需要的缓冲区 + # (保留主要深度测试,但减少不必要的写入) + + print(f" {eye_name}: 应用轻量级渲染设置") + + except Exception as e: + print(f"⚠️ {eye_name}缓冲区优化失败: {e}") + + def _disable_vr_buffer_extras(self): + """禁用VR缓冲区的额外功能以提升性能""" + try: + # 禁用VR缓冲区的统计收集 + if hasattr(self, 'vr_left_eye_buffer') and self.vr_left_eye_buffer: + if hasattr(self.vr_left_eye_buffer, 'setOneShot'): + self.vr_left_eye_buffer.setOneShot(False) + + if hasattr(self, 'vr_right_eye_buffer') and self.vr_right_eye_buffer: + if hasattr(self.vr_right_eye_buffer, 'setOneShot'): + self.vr_right_eye_buffer.setOneShot(False) + + print(" VR缓冲区额外功能已优化") + + except Exception as e: + print(f"⚠️ VR缓冲区额外功能优化失败: {e}") + + def _start_vr_task(self): + """启动VR更新任务""" + if self.vr_task: + self.world.taskMgr.remove(self.vr_task) + + # 设置高优先级(sort=-1000),确保在渲染之前执行,参考 panda3d-openvr + self.vr_task = self.world.taskMgr.add(self._update_vr, "update_vr", sort=-1000) + print("✓ VR更新任务已启动(高优先级,sort=-1000)") + + def _update_vr(self, task): + """VR更新任务 - 每帧调用(参考 panda3d-openvr 架构)""" + if not self.vr_enabled or not self.vr_system: 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() - + # 📌 第一件事:waitGetPoses 阻塞等待 VSync(参考 panda3d-openvr) + # 这会阻塞整个任务,确保每个 VSync 周期只执行一次 + should_call_waitgetposes = ( + not self.vr_test_mode or # 普通VR模式总是需要 + self.test_mode_wait_poses # 测试模式:仅当启用姿态等待时 + ) + + if should_call_waitgetposes: + self._wait_get_poses_immediate() + # 立即更新手柄,使用最新姿态(避免过时数据) + self.update_tracked_devices() + + # 性能监控 + self.frame_count += 1 + + # 🚀 自动启用性能模式 - 减少计时对象创建 + if not self.performance_mode_enabled and self.frame_count >= self.performance_mode_trigger_frame: + self.performance_mode_enabled = True + if self.frame_count <= self.performance_mode_trigger_frame + 5: # 只输出一次 + print(f"🎯 性能模式已启用 (帧#{self.frame_count}) - 禁用详细监控以提升性能") + + # 记录帧时间 + self._track_frame_time() + + # 计算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 + + # 📌 使用刚获取的姿态更新相机(而不是旧姿态) + self._update_camera_poses() + + # 输出策略信息(仅第一次) + if not hasattr(self, '_new_architecture_logged'): + print("✅ 新架构已启用 - waitGetPoses 在任务开始阻塞,参考 panda3d-openvr") + print(" 这确保每个 VSync 周期只渲染一次,解决 AlreadySubmitted 错误") + self._new_architecture_logged = True + + # 更新VR动作状态 - 仅在启用时更新 + if not self.disable_action_system and self.action_manager: + self.action_manager.update_actions() + + # 更新VR交互系统 + if self.interaction_manager: + self.interaction_manager.update() + + # 更新VR摇杆系统 + if self.joystick_manager: + # 计算帧间隔时间 + import time + if not hasattr(self, '_last_frame_time'): + self._last_frame_time = time.time() + dt = 0.016 # 默认60fps + else: + current_time = time.time() + dt = current_time - self._last_frame_time + self._last_frame_time = current_time + # 限制dt范围,避免异常情况 + dt = max(0.001, min(0.1, dt)) + + self.joystick_manager.update(dt) + + # 更新系统性能指标(减少频率) + if self.frame_count % 30 == 1: # 每30帧更新一次,减少开销 + self._update_performance_metrics() + + # 更新GPU渲染时间统计(减少频率) + if self.enable_gpu_timing and self.frame_count % 60 == 1: + self._get_gpu_frame_timing() + + # 🚀 手动垃圾回收控制 - 避免16-19帧周期性峰值 + self._manual_gc_control() + + # 定期输出性能报告 - 默认10秒间隔 + report_interval = getattr(self, 'performance_report_interval', 600) + if self.frame_count % report_interval == 1: + self._print_performance_report() + + except Exception as e: + print(f"VR更新错误: {e}") + import traceback + traceback.print_exc() + + return task.cont + + def _sync_gpu_if_needed(self): + """可选的GPU同步 - 仅在需要时使用""" + try: + # 同步等待GPU完成(如果需要) + gsg = self.world.win.getGsg() + if gsg: + gsg.getEngine().syncFrame() + except Exception as e: + print(f"❌ GPU同步异常: {e}") + + def simple_left_cb(self, cbdata): + """简化的左眼渲染回调 - 精确控制渲染和提交""" + try: + # 🔍 精确测量渲染时间 + import time + render_start = time.perf_counter() + + # 触发实际渲染 + cbdata.upcall() + + # 计算真实渲染时间 + self.left_render_time = (time.perf_counter() - render_start) * 1000 # 转换为毫秒 + + # 记录渲染次数 + self.left_render_count += 1 + + # 📌 OpenVR 帧边界检查:防止同一 OpenVR 帧内重复渲染 + if self.left_eye_last_render_frame == self.openvr_frame_id: + return # 已在当前 OpenVR 帧渲染过,跳过 + + # 🔧 OpenVR最佳实践:左眼只渲染,不立即提交 + # 基于官方hellovr示例:两眼都渲染完后再批量提交 + self.left_eye_last_render_frame = self.openvr_frame_id + + # 渐进式VR功能测试:单独启用纹理提交时保留原逻辑 + should_submit = self.vr_test_mode and self.test_mode_submit_texture and not self.test_mode_wait_poses + + if should_submit: + # 测试模式:单独启用纹理提交时的兼容模式 + if self.vr_compositor and self.vr_left_texture: + self.submit_texture(openvr.Eye_Left, self.vr_left_texture) + + if self.vr_test_mode: + # 测试模式:始终触发屏幕显示更新 + self._update_test_display() + + except Exception as e: + print(f"左眼渲染回调错误: {e}") + + def simple_right_cb(self, cbdata): + """简化的右眼渲染回调 - 精确控制渲染和提交""" + try: + # 🔍 精确测量渲染时间 + import time + render_start = time.perf_counter() + + # 触发实际渲染 + cbdata.upcall() + + # 计算真实渲染时间 + self.right_render_time = (time.perf_counter() - render_start) * 1000 # 转换为毫秒 + + # 记录渲染次数 + self.right_render_count += 1 + + # 📌 OpenVR 帧边界检查:防止同一 OpenVR 帧内重复渲染 + if self.right_eye_last_render_frame == self.openvr_frame_id: + return # 已在当前 OpenVR 帧渲染过,跳过 + + # 🔧 OpenVR最佳实践:右眼渲染完成后批量提交两眼纹理 + # 基于官方hellovr示例:避免分散提交导致的VSync阻塞 + self.right_eye_last_render_frame = self.openvr_frame_id + + # 🔧 渐进式VR功能测试:根据调试标志决定启用哪些功能 + should_batch_submit = not self.vr_test_mode or self.test_mode_submit_texture + should_wait_poses = not self.vr_test_mode or self.test_mode_wait_poses + + # 特殊处理:测试模式单独启用功能时的兼容模式 + if self.vr_test_mode: + if self.test_mode_submit_texture and not self.test_mode_wait_poses: + # 单独测试纹理提交:使用原来的分散提交方式 + if self.vr_compositor and self.vr_right_texture: + self.submit_texture(openvr.Eye_Right, self.vr_right_texture) + + # 🚀 测试模式也需要PostPresentHandoff避免36FPS + try: + if hasattr(self.vr_compositor, 'postPresentHandoff'): + self.vr_compositor.postPresentHandoff() + elif hasattr(self.vr_compositor, 'PostPresentHandoff'): + self.vr_compositor.PostPresentHandoff() + except Exception as e: + pass # 测试模式静默忽略错误 + should_batch_submit = False + elif self.test_mode_wait_poses and not self.test_mode_submit_texture: + # 单独测试姿态等待:不提交纹理 + should_batch_submit = False + + if should_batch_submit: + # 🚀 OpenVR最佳实践:批量提交两眼纹理 + # 这是解决36FPS问题的关键修复 + self._batch_submit_textures() + + # 🔧 关键修复:移除Submit后立即WaitGetPoses的错误实现 + # 根据OpenVR官方文档:"Calling WaitGetPoses immediately after Submit is conspicuously wrong" + # WaitGetPoses应该在下一帧开始时通过update_vr_task调用,不是Submit后立即调用 + # + # if should_wait_poses: + # # 错误的实现:Submit后立即获取姿态导致36FPS + # self._wait_get_poses_immediate() # ← 这是36FPS的根本原因! + # + # 正确的实现:让update_vr_task在下一帧开始时调用WaitGetPoses + + if self.vr_test_mode: + # 测试模式:始终更新性能HUD + self._update_test_performance_hud() + + except Exception as e: + print(f"右眼渲染回调错误: {e}") + + 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._update_controller_poses(poses) - - return task.cont - - except Exception as e: - print(f"VR追踪更新错误: {str(e)}") - return task.cont + 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 _wait_get_poses_immediate(self): + """立即获取VR姿态 - 修复ATW闪烁的关键方法(双姿态版本)""" + # 开始计时waitGetPoses操作 + timing = self._start_timing('wait_poses') - 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 + if not self.vr_compositor or not self.poses or not self.game_poses: + self._end_timing(timing) + return + + # 关键修复:传递渲染姿态和游戏姿态数组 + # 渲染姿态用于绘制,游戏姿态用于逻辑,避免ATW过度补偿 + result = self.vr_compositor.waitGetPoses(self.poses, self.game_poses) + + # 📌 waitGetPoses 成功后立即递增 OpenVR 帧ID(新帧开始) + self.openvr_frame_id += 1 + + # 结束计时 + wait_time = self._end_timing(timing) + + # 检查姿态数据的有效性 + 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 + + # 🚀 优化控制器姿态更新:使用缓存,避免每帧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 + + # 检查是否已有此设备的缓存矩阵 + if device_id not in self.controller_poses: + # 第一次检测到该控制器,创建新的缓存矩阵 + self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) + else: + # 复用现有矩阵,只更新数值,避免创建新对象 + cached_mat = self.controller_poses[device_id] + self._update_matrix_from_openvr(cached_mat, controller_matrix) + + valid_poses += 1 + else: + # 设备姿态无效,从字典中移除(如果存在) + if device_id in self.controller_poses: + # 将矩阵返回对象池 + self._return_pooled_matrix(self.controller_poses[device_id]) + del self.controller_poses[device_id] + + # 🔧 关键修复:立即更新手柄和跟踪设备 + # Running Start模式下必须在WaitGetPoses后立即更新,避免手柄消失 + self.update_tracked_devices() + + # 调试信息 - 仅在第一次成功时输出 + if not hasattr(self, '_dual_pose_mode_logged') and valid_poses > 0: + print(f"✅ 双姿态立即获取模式启用 - 有效姿态数: {valid_poses}") + print(" 渲染姿态用于绘制,游戏姿态用于逻辑,防止ATW过度补偿") + print(" 手柄和跟踪设备在WaitGetPoses后立即更新") + self._dual_pose_mode_logged = True + + except Exception as e: + # 限制错误输出频率 + if not hasattr(self, '_last_immediate_error_frame'): + self._last_immediate_error_frame = 0 + + if self.frame_count - self._last_immediate_error_frame > 300: # 每5秒最多输出一次错误 + print(f"立即姿态获取失败: {e}") + self._last_immediate_error_frame = self.frame_count + + self.pose_failures += 1 + + def _wait_get_poses_with_prediction(self): + """使用预测时间获取VR姿态 - 优化性能的新策略""" + try: + if not self.vr_compositor or not self.poses or not self.game_poses: + return + + # 使用预测时间获取姿态 + # 预测时间通常为11-16ms,对应下一个VR帧的时间 + result = self.vr_compositor.waitGetPoses(self.poses, self.game_poses) + + # 检查姿态数据的有效性 + 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_task'): + print("⚠️ HMD姿态数据无效(更新任务模式)") + self._hmd_invalid_warning_task = 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 not hasattr(self, '_update_task_mode_logged') and valid_poses > 0: + print(f"✅ 更新任务姿态获取模式启用 - 有效姿态数: {valid_poses}") + print(f" 预测时间: {self.use_prediction_time*1000:.1f}ms") + self._update_task_mode_logged = True + + except Exception as e: + # 限制错误输出频率 + if not hasattr(self, '_last_task_error_frame'): + self._last_task_error_frame = 0 + + if self.frame_count - self._last_task_error_frame > 300: # 每5秒最多输出一次错误 + print(f"更新任务姿态获取失败: {e}") + self._last_task_error_frame = self.frame_count + + # 记录姿态失败次数 + self.pose_failures += 1 + + def _cache_poses_for_next_frame(self): + """缓存当前姿态供下一帧渲染使用 - 修复时序不匹配""" + try: + if not self.poses or len(self.poses) == 0: + return + + # 如果是第一帧,直接使用当前姿态 + if self._first_frame: + self._cached_render_poses = self.poses + self._first_frame = False + print("✓ 首帧姿态缓存已设置") + return + + # 复制当前渲染姿态到缓存 + # 下一帧将使用这些姿态进行渲染 + poses_t = openvr.TrackedDevicePose_t * openvr.k_unMaxTrackedDeviceCount + cached_poses = poses_t() + + # 复制姿态数据 + for i in range(len(self.poses)): + cached_poses[i] = self.poses[i] + + self._cached_render_poses = cached_poses + + except Exception as e: + print(f"⚠️ 姿态缓存失败: {e}") + + + def _reset_waitgetposes_flag(self, task): + """重置WaitGetPoses标记 - 确保下一帧可以调用WaitGetPoses""" + self._waitgetposes_called_this_frame = False + return task.done + + 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 + """ + # 🚀 使用对象池获取预分配的Mat4对象,避免每帧创建新对象 + mat = self._get_pooled_matrix() + + # 修正的坐标转换矩阵 + # 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) + + # 🚀 优化调试信息 - 避免创建Vec3对象,减少GC压力 + 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}帧)") + + # 直接输出数值,避免创建Vec3对象 + ovr_x, ovr_y, ovr_z = ovr_matrix[0][3], ovr_matrix[1][3], ovr_matrix[2][3] + print(f" OpenVR原始位置: ({ovr_x:.3f}, {ovr_y:.3f}, {ovr_z:.3f})") + + # 直接从矩阵读取数值,避免创建Vec3对象 + panda_x = mat.getCell(0, 3) + panda_y = mat.getCell(1, 3) + panda_z = mat.getCell(2, 3) + print(f" Panda3D转换位置: ({panda_x:.3f}, {panda_y:.3f}, {panda_z:.3f})") + + # 手动验证转换:OpenVR (x,y,z) → Panda3D (x,-z,y) + expected_x = ovr_x + expected_y = -ovr_z + expected_z = ovr_y + print(f" 预期转换结果: ({expected_x:.3f}, {expected_y:.3f}, {expected_z:.3f})") + + # 计算误差,避免创建Vec3对象 + diff_x = panda_x - expected_x + diff_y = panda_y - expected_y + diff_z = panda_z - expected_z + diff_magnitude = (diff_x*diff_x + diff_y*diff_y + diff_z*diff_z)**0.5 + + if diff_magnitude < 0.001: + print(f" ✅ 坐标转换正确 (误差: {diff_magnitude:.6f})") + else: + print(f" ⚠️ 坐标转换可能有误 (误差: {diff_magnitude:.6f})") + print(f" 差异: ({diff_x:.6f}, {diff_y:.6f}, {diff_z:.6f})") - def _convert_openvr_matrix(self, openvr_matrix): - """转换OpenVR矩阵为Panda3D矩阵""" - # 实现矩阵转换逻辑 - mat = Mat4() - # 这里需要实现具体的矩阵转换 return mat - def _update_main_camera_pose(self): - """更新主摄像机姿态""" + def update_hmd(self, pose): + """ + 更新HMD锚点 - 基于参考实现 + """ try: - if self.simulation_mode: - return - - # 从VR系统获取头显姿态并应用到主摄像机 - pass - - except Exception as e: - print(f"主摄像机姿态更新错误: {str(e)}") + # 将OpenVR姿态转换为Panda3D矩阵 + modelview = self.convert_mat(pose.mDeviceToAbsoluteTracking) - def _update_eye_cameras(self): - """更新眼部摄像机""" - try: - if self.simulation_mode: - return - - # 更新左右眼摄像机位置 - pass - - except Exception as e: - print(f"眼部摄像机更新错误: {str(e)}") + # 应用坐标系转换并设置HMD锚点 + self.hmd_anchor.setMat(self.coord_mat_inv * modelview * self.coord_mat) - def _update_controller_poses(self, poses): - """更新控制器姿态""" - try: - if self.simulation_mode: - return - - # 更新控制器位置和姿态 - pass - - except Exception as e: - print(f"控制器姿态更新错误: {str(e)}") + # 获取眼睛到头部的变换 + view_left = self.convert_mat(self.vr_system.getEyeToHeadTransform(openvr.Eye_Left)) + view_right = self.convert_mat(self.vr_system.getEyeToHeadTransform(openvr.Eye_Right)) - 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)}") + # 设置眼睛锚点 + 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) - 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)}") + 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 _update_camera_poses_with_cache(self): + """使用缓存姿态更新相机 - 符合OpenVR时序假设""" + try: + # 使用缓存的姿态,符合OpenVR的时序假设 + # OpenVR假设你用上一次WaitGetPoses的姿态渲染当前提交的帧 + + if not self._cached_render_poses: + # 如果没有缓存姿态,回退到当前姿态(首帧情况) + print("⚠️ 没有缓存姿态,使用当前姿态") + return self._update_camera_poses() + + # 从缓存姿态数组中获取HMD姿态 + if len(self._cached_render_poses) > 0: + hmd_pose = self._cached_render_poses[openvr.k_unTrackedDeviceIndex_Hmd] + if hmd_pose.bPoseIsValid: + self.update_hmd(hmd_pose) + + # 调试信息 - 验证缓存姿态使用 + if not hasattr(self, '_cached_pose_logged'): + print("✅ 使用缓存姿态更新相机 - 符合OpenVR时序假设") + self._cached_pose_logged = True + else: + print("⚠️ 缓存的HMD姿态数据无效") + + except Exception as e: + print(f"使用缓存姿态更新相机失败: {e}") + # 回退到正常更新方式 + self._update_camera_poses() + import traceback + traceback.print_exc() + + + 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() + + # VR性能优化:使用Running Start模式(Valve最佳实践) + print("🚀 VR性能优化:Running Start模式已启用") + print(" 优势:在帧开始时获取姿态,提供VSync前3ms的渲染时间") + print(" 注意:Submit后立即调用WaitGetPoses是错误实现") + self.set_prediction_time(11.0) # 11ms预测时间 - OpenVR标准值,平衡准确性和延迟 + + # 🚀 动态调整Qt Timer频率以支持VR + if hasattr(self.world, 'qtWidget') and self.world.qtWidget: + if hasattr(self.world.qtWidget, 'synchronizer'): + # 设置为144Hz,让OpenVR控制实际渲染节奏 + self.world.qtWidget.synchronizer.setInterval(int(1000/144)) + print("✓ Qt Timer调整为144Hz,让OpenVR控制VR渲染节奏") + + print("✅ VR模式已启用") + return True + + def disable_vr(self): + """禁用VR模式""" + self.vr_enabled = False + + # 清理手柄可视化(但保留控制器对象以便重新启用) + if hasattr(self, 'left_controller') and self.left_controller: + if hasattr(self.left_controller, 'visualizer') and self.left_controller.visualizer: + try: + self.left_controller.visualizer.cleanup() + self.left_controller.visualizer = None + print("✓ 左手控制器可视化已清理") + except Exception as e: + print(f"⚠️ 清理左手控制器可视化失败: {e}") + + if hasattr(self, 'right_controller') and self.right_controller: + if hasattr(self.right_controller, 'visualizer') and self.right_controller.visualizer: + try: + self.right_controller.visualizer.cleanup() + self.right_controller.visualizer = None + print("✓ 右手控制器可视化已清理") + except Exception as e: + print(f"⚠️ 清理右手控制器可视化失败: {e}") + + # 隐藏手柄锚点节点 + if hasattr(self, 'left_controller') and self.left_controller and self.left_controller.anchor_node: + self.left_controller.anchor_node.hide() + + if hasattr(self, 'right_controller') and self.right_controller and self.right_controller.anchor_node: + self.right_controller.anchor_node.hide() + + # 恢复主相机 + self._enable_main_cam() + + # 恢复Qt Timer到60FPS + if hasattr(self.world, 'qtWidget') and self.world.qtWidget: + if hasattr(self.world.qtWidget, 'synchronizer'): + self.world.qtWidget.synchronizer.setInterval(int(1000/60)) + print("✓ Qt Timer恢复为60Hz") + + 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 + + # 🚀 恢复Python垃圾回收并清理对象池 + if self._gc_disabled: + # 最后一次手动垃圾回收 + collected = gc.collect() + print(f"🗑️ 最终GC清理: {collected} 个对象") + + # 恢复自动垃圾回收 + gc.enable() + self._gc_disabled = False + print("✅ Python垃圾回收已恢复为自动模式") + + # 清理对象池 + if hasattr(self, '_matrix_pool'): + pool_size = len(self._matrix_pool) + self._matrix_pool.clear() + print(f"🧹 Mat4对象池已清理: {pool_size} 个对象") + + # 清理缓存 + if hasattr(self, '_cached_matrices'): + self._cached_matrices.clear() + if hasattr(self, '_controller_poses_cache'): + self._controller_poses_cache.clear() + + # 清理OpenVR Texture对象缓存 + if hasattr(self, '_left_ovr_texture'): + self._left_ovr_texture = None + if hasattr(self, '_right_ovr_texture'): + self._right_ovr_texture = None + print("🧹 OpenVR Texture对象缓存已清理") + + # 清理渲染缓冲区 + 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 + + # 清理控制器 + if hasattr(self, 'left_controller') and self.left_controller: + try: + self.left_controller.cleanup() + self.left_controller = None + print("✓ 左手控制器已清理") + except Exception as e: + print(f"⚠️ 清理左手控制器失败: {e}") + + if hasattr(self, 'right_controller') and self.right_controller: + try: + self.right_controller.cleanup() + self.right_controller = None + print("✓ 右手控制器已清理") + except Exception as e: + print(f"⚠️ 清理右手控制器失败: {e}") + + # 清理控制器字典 + if hasattr(self, 'controllers'): + self.controllers.clear() + print("✓ 控制器字典已清理") + + # 清理VR子系统 + if hasattr(self, 'joystick_manager') and self.joystick_manager: + try: + self.joystick_manager.cleanup() + self.joystick_manager = None + print("✓ VR摇杆系统已清理") + except Exception as e: + print(f"⚠️ 清理VR摇杆系统失败: {e}") + + if hasattr(self, 'teleport_system') and self.teleport_system: + try: + self.teleport_system.cleanup() + self.teleport_system = None + print("✓ VR传送系统已清理") + except Exception as e: + print(f"⚠️ 清理VR传送系统失败: {e}") + + if hasattr(self, 'interaction_manager') and self.interaction_manager: + try: + self.interaction_manager.cleanup() + self.interaction_manager = None + print("✓ VR交互系统已清理") + except Exception as e: + print(f"⚠️ 清理VR交互系统失败: {e}") + + if hasattr(self, 'action_manager') and self.action_manager: + try: + self.action_manager.cleanup() + self.action_manager = None + print("✓ VR动作系统已清理") + except Exception as e: + print(f"⚠️ 清理VR动作系统失败: {e}") + + # 关闭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性能报告""" + if not self.performance_monitoring or not self.debug_output_enabled: + return + + stats = self.get_performance_stats() + + # 简短模式输出 + if self.debug_mode == 'brief': + self._print_brief_performance_report(stats) + return + + print("📊 ======= VR性能监控报告 =======") + + # 帧率和帧时间信息 + print(f"🎯 渲染性能:") + print(f" VR帧率: {stats['vr_fps']:.1f} FPS") + print(f" 平均帧时间: {stats['frame_time_avg']:.2f} ms") + print(f" 最小帧时间: {stats['frame_time_min']:.2f} ms") + print(f" 最大帧时间: {stats['frame_time_max']:.2f} ms") + print(f" 95%帧时间: {stats['frame_time_95th']:.2f} ms") + + # 系统性能 + print(f"💻 系统性能:") + print(f" CPU使用率: {stats['cpu_usage']:.1f}%") + print(f" 内存使用率: {stats['memory_usage']:.1f}%") + + # GPU性能 + print(f"🎮 GPU性能:") + if self.gputil_available or self.nvidia_ml_available: + print(f" GPU使用率: {stats['gpu_usage']:.1f}%") + print(f" 显存使用率: {stats['gpu_memory_usage']:.1f}%") + else: + print(f" GPU监控: 不可用 (需要安装 GPUtil 或 pynvml)") + print(f" 安装命令: pip install GPUtil nvidia-ml-py") + + # GPU渲染时间(OpenVR Frame Timing) + if self.enable_gpu_timing: + print(f"⚡ GPU渲染时间:") + pipeline_stats = self._get_pipeline_stats() + gpu_stats = pipeline_stats.get('gpu_timing', {}) + gpu_current = pipeline_stats.get('current', {}) + + # 检查是否有可用的GPU时间数据 + has_gpu_data = any( + gpu_current.get(field, 0) > 0 + for field in ['gpu_scene_render', 'gpu_total_render', 'gpu_pre_submit', 'gpu_post_submit', 'gpu_compositor_render'] + ) + + if has_gpu_data: + # 显示GPU时间统计(最近30帧平均) + scene_render = gpu_stats.get('scene_render', {'avg': 0}) + total_render = gpu_stats.get('total_render', {'avg': 0}) + pre_submit = gpu_stats.get('pre_submit', {'avg': 0}) + post_submit = gpu_stats.get('post_submit', {'avg': 0}) + compositor = gpu_stats.get('compositor_render', {'avg': 0}) + + if scene_render['avg'] > 0: + print(f" GPU场景渲染: {scene_render['avg']:.2f}ms (min:{scene_render['min']:.1f}, max:{scene_render['max']:.1f})") + if total_render['avg'] > 0: + print(f" GPU总渲染时间: {total_render['avg']:.2f}ms (min:{total_render['min']:.1f}, max:{total_render['max']:.1f})") + if pre_submit['avg'] > 0: + print(f" GPU提交前时间: {pre_submit['avg']:.2f}ms (min:{pre_submit['min']:.1f}, max:{pre_submit['max']:.1f})") + if post_submit['avg'] > 0: + print(f" GPU提交后时间: {post_submit['avg']:.2f}ms (min:{post_submit['min']:.1f}, max:{post_submit['max']:.1f})") + if compositor['avg'] > 0: + print(f" GPU合成器时间: {compositor['avg']:.2f}ms (min:{compositor['min']:.1f}, max:{compositor['max']:.1f})") + + # 显示当前帧GPU时间 + print(f"🔍 当前帧GPU时间:") + if gpu_current.get('gpu_scene_render', 0) > 0: + print(f" 场景渲染: {gpu_current['gpu_scene_render']:.2f}ms") + if gpu_current.get('gpu_total_render', 0) > 0: + print(f" 总渲染: {gpu_current['gpu_total_render']:.2f}ms") + if gpu_current.get('gpu_compositor_render', 0) > 0: + print(f" 合成器: {gpu_current['gpu_compositor_render']:.2f}ms") + + # GPU时间瓶颈分析 + current_total = gpu_current.get('gpu_total_render', 0) + current_scene = gpu_current.get('gpu_scene_render', 0) + if current_total > 12.0: # 假设72fps目标,留出一些余量 + print(f" ⚠️ GPU总渲染时间过长: {current_total:.1f}ms") + elif current_scene > 8.0: + print(f" ⚠️ GPU场景渲染时间偏高: {current_scene:.1f}ms") + + else: + print(f" GPU渲染时间: 暂无数据") + if self.gpu_timing_failure_count > 0: + print(f" 获取失败次数: {self.gpu_timing_failure_count}") + else: + print(f" 等待OpenVR Frame Timing数据...") + else: + print(f"⚡ GPU渲染时间: 已禁用") + + # VR特定指标 + print(f"🥽 VR指标:") + print(f" 总帧数: {stats['frame_count']}") + print(f" 提交失败: {stats['submit_failures']}") + print(f" 姿态失败: {stats['pose_failures']}") + + # 计算失败率 + if stats['frame_count'] > 0: + submit_fail_rate = (stats['submit_failures'] / stats['frame_count']) * 100 + pose_fail_rate = (stats['pose_failures'] / stats['frame_count']) * 100 + print(f" 提交失败率: {submit_fail_rate:.2f}%") + print(f" 姿态失败率: {pose_fail_rate:.2f}%") + + # 渲染管线监控 + if self.enable_pipeline_monitoring: + pipeline_stats = self._get_pipeline_stats() + print(f"⚙️ 渲染管线分析:") + print(f" 当前分辨率: {pipeline_stats['vr_info']['eye_resolution'][0]}x{pipeline_stats['vr_info']['eye_resolution'][1]}") + print(f" 推荐分辨率: {pipeline_stats['vr_info']['recommended_resolution'][0]}x{pipeline_stats['vr_info']['recommended_resolution'][1]}") + print(f" 显示频率: {pipeline_stats['vr_info']['display_frequency']:.1f} Hz") + + # VSync和时序信息 + if pipeline_stats['vr_info']['target_frame_time_ms'] > 0: + print(f"🎯 VSync时序:") + print(f" 目标帧时间: {pipeline_stats['vr_info']['target_frame_time_ms']:.2f}ms") + print(f" VSync到光子: {pipeline_stats['vr_info']['vsync_to_photons_ms']:.2f}ms") + print(f" VSync窗口: ±{pipeline_stats['vr_info']['vsync_window_ms']:.2f}ms") + print(f" 异步重投影: {'启用' if pipeline_stats['vr_info']['async_reprojection'] else '禁用'}") + print(f" 运动平滑: {'启用' if pipeline_stats['vr_info']['motion_smoothing'] else '禁用'}") + + # 分析帧时间是否在目标范围内 + current_frame_time = stats['frame_time_avg'] + target_frame_time = pipeline_stats['vr_info']['target_frame_time_ms'] + if current_frame_time > 0 and target_frame_time > 0: + frame_time_ratio = current_frame_time / target_frame_time + if frame_time_ratio > 1.1: + print(f" ⚠️ 帧时间超标: {current_frame_time:.1f}ms (目标:{target_frame_time:.1f}ms)") + elif frame_time_ratio < 0.9: + print(f" ✅ 帧时间充裕: {current_frame_time:.1f}ms (目标:{target_frame_time:.1f}ms)") + else: + print(f" ✓ 帧时间正常: {current_frame_time:.1f}ms (目标:{target_frame_time:.1f}ms)") + + # 🔧 性能优化诊断 + print(f"🔧 优化诊断:") + current = pipeline_stats.get('current', {}) if self.enable_pipeline_monitoring else {} + + # 🔍 渲染回调诊断 - 新增 + self._print_render_callback_diagnostics() + + # waitGetPoses时序分析 + wait_poses_time = current.get('wait_poses', 0) + if wait_poses_time > 10: + print(f" ⚠️ waitGetPoses时间过长: {wait_poses_time:.1f}ms") + print(f" 可能原因: 错过VSync窗口,建议调整预测时间") + elif wait_poses_time > 5: + print(f" ⚠️ waitGetPoses时间偏高: {wait_poses_time:.1f}ms") + else: + print(f" ✓ waitGetPoses时间正常: {wait_poses_time:.1f}ms") + + # CPU-GPU并行度分析 - 增强诊断 + gpu_total = current.get('gpu_total_render', 0) + cpu_render_left = getattr(self, 'left_render_time', 0) + cpu_render_right = getattr(self, 'right_render_time', 0) + cpu_render_total = cpu_render_left + cpu_render_right + + print(f" 真实渲染时间对比:") + print(f" CPU左眼: {cpu_render_left:.2f}ms") + print(f" CPU右眼: {cpu_render_right:.2f}ms") + print(f" CPU总计: {cpu_render_total:.2f}ms") + print(f" GPU总计: {gpu_total:.2f}ms") + + if cpu_render_total < 1.0: + print(f" ⚠️ CPU渲染时间异常短: {cpu_render_total:.2f}ms") + print(f" 可能原因: 渲染回调未正确执行或渲染被跳过") + elif gpu_total > 0 and cpu_render_total > 0: + ratio = gpu_total / cpu_render_total + if ratio > 10: + print(f" ⚠️ GPU严重等待CPU: 比例{ratio:.1f}:1") + print(f" 建议: 检查OpenGL命令提交时机和渲染状态") + elif ratio > 3: + print(f" ⚠️ GPU等待CPU: 比例{ratio:.1f}:1") + print(f" 建议: 检查OpenGL命令提交是否及时") + else: + print(f" ✓ CPU-GPU时间匹配: 比例{ratio:.1f}:1") + + # 预测时间诊断 + current_prediction = self.use_prediction_time * 1000 + print(f" 预测时间设置: {current_prediction:.1f}ms") + if current_prediction > 15: + print(f" 建议: 预测时间较高,可能增加waitGetPoses延迟") + elif current_prediction < 8: + print(f" 注意: 预测时间较低,可能影响姿态准确性") + else: + print(f" ✓ 预测时间在合理范围内") + + # 优化状态总结 + optimization_score = 0 + if wait_poses_time < 8: + optimization_score += 1 + if stats['vr_fps'] > 60: + optimization_score += 1 + if stats.get('frame_time_avg', 0) < target_frame_time * 1.1: + optimization_score += 1 + + if optimization_score >= 2: + print(f" ✅ 优化效果良好 ({optimization_score}/3)") + else: + print(f" ⚠️ 仍有优化空间 ({optimization_score}/3)") + + print(f"🕐 各阶段耗时 (最近{self.pipeline_history_size}帧平均):") + print(f" waitGetPoses: {pipeline_stats['wait_poses']['avg']:.2f}ms (min:{pipeline_stats['wait_poses']['min']:.1f}, max:{pipeline_stats['wait_poses']['max']:.1f})") + print(f" 渲染总计: {pipeline_stats['render']['avg']:.2f}ms (min:{pipeline_stats['render']['min']:.1f}, max:{pipeline_stats['render']['max']:.1f})") + print(f" 纹理提交: {pipeline_stats['submit']['avg']:.2f}ms (min:{pipeline_stats['submit']['min']:.1f}, max:{pipeline_stats['submit']['max']:.1f})") + if pipeline_stats['sync_wait']['avg'] > 0: + print(f" 同步等待: {pipeline_stats['sync_wait']['avg']:.2f}ms (min:{pipeline_stats['sync_wait']['min']:.1f}, max:{pipeline_stats['sync_wait']['max']:.1f})") + + print(f"🔍 当前帧详情:") + print(f" 左眼渲染: {pipeline_stats['current']['left_render']:.2f}ms") + print(f" 右眼渲染: {pipeline_stats['current']['right_render']:.2f}ms") + print(f" 姿态获取: {pipeline_stats['current']['wait_poses']:.2f}ms") + + # 显示Running Start模式信息 + print(f"🎯 Running Start模式:") + print(f" 模式: Valve Running Start - 帧开始时获取姿态") + print(f" 优势: VSync前3ms获取姿态,提供充足渲染时间") + print(f" 预测时间: {self.use_prediction_time * 1000:.1f}ms") + + # 分析最大瓶颈 + current = pipeline_stats['current'] + bottleneck_analysis = [] + if current['wait_poses'] > 5.0: + bottleneck_analysis.append(f"姿态获取耗时过长({current['wait_poses']:.1f}ms)") + if current['total_render'] > 8.0: + bottleneck_analysis.append(f"渲染耗时过长({current['total_render']:.1f}ms)") + if current['submit'] > 2.0: + bottleneck_analysis.append(f"纹理提交耗时过长({current['submit']:.1f}ms)") + + if bottleneck_analysis: + print(f"🚨 瓶颈分析:") + for analysis in bottleneck_analysis: + print(f" ⚠️ {analysis}") + + # 性能建议 + self._print_performance_recommendations(stats) + + print("===============================") + + def _print_performance_recommendations(self, stats): + """根据性能数据输出优化建议""" + print(f"💡 性能建议:") + + recommendations = [] + + # FPS相关建议 + if stats['vr_fps'] < 60: + recommendations.append(" ⚠️ VR帧率过低,可能影响体验") + + # 帧时间相关建议 + if stats['frame_time_avg'] > 16.7: # 60fps = 16.7ms + recommendations.append(" ⚠️ 平均帧时间过高,建议降低渲染质量") + + if stats['frame_time_max'] > 50: + recommendations.append(" ⚠️ 检测到严重卡顿,检查CPU/GPU负载") + + # CPU建议 + if stats['cpu_usage'] > 80: + recommendations.append(" 🔴 CPU使用率过高,可能存在CPU瓶颈") + elif stats['cpu_usage'] > 60: + recommendations.append(" 🟡 CPU使用率偏高,注意监控") + + # 内存建议 + if stats['memory_usage'] > 85: + recommendations.append(" 🔴 内存使用率过高,可能影响性能") + + # GPU建议 + if self.gputil_available or self.nvidia_ml_available: + if stats['gpu_usage'] > 95: + recommendations.append(" 🔴 GPU使用率接近满载,存在GPU瓶颈") + if stats['gpu_memory_usage'] > 90: + recommendations.append(" 🔴 显存使用率过高,可能需要降低纹理质量") + + # GPU渲染时间建议 + if self.enable_gpu_timing: + if self.gpu_total_render_ms > 12.0: + recommendations.append(" ⚠️ GPU总渲染时间过长,建议优化场景复杂度") + if self.gpu_scene_render_ms > 8.0: + recommendations.append(" ⚠️ GPU场景渲染时间偏高,考虑降低渲染质量") + if self.gpu_compositor_render_ms > 3.0: + recommendations.append(" ⚠️ GPU合成器时间过长,检查VR设置或叠加层") + if self.gpu_timing_failure_count > 100: + recommendations.append(" ⚠️ GPU时间统计频繁失败,可能需要更新OpenVR") + + # 失败率建议 + submit_fail_rate = (stats['submit_failures'] / max(stats['frame_count'], 1)) * 100 + if submit_fail_rate > 1: + recommendations.append(" ⚠️ VR帧提交失败率较高,检查VR系统状态") + + if not recommendations: + recommendations.append(" ✅ 性能表现良好,无明显问题") + + for rec in recommendations: + print(rec) + + def _print_brief_performance_report(self, stats): + """输出简短的VR性能报告""" + # 创建一行简短摘要 + summary = f"🥽 VR性能: {stats['vr_fps']:.1f}fps" + + if stats['frame_time_avg'] > 0: + summary += f" | 帧时间: {stats['frame_time_avg']:.1f}ms" + + if self.psutil_available: + summary += f" | CPU: {stats['cpu_usage']:.0f}%" + summary += f" | 内存: {stats['memory_usage']:.0f}%" + + # 显示GPU信息,如果库不可用则显示提示 + if self.gputil_available or self.nvidia_ml_available: + summary += f" | GPU: {stats['gpu_usage']:.0f}%" + else: + summary += " | GPU: N/A" + + # 添加失败率指示 + if stats['frame_count'] > 0: + submit_fail_rate = (stats['submit_failures'] / stats['frame_count']) * 100 + if submit_fail_rate > 0.1: + summary += f" | 提交失败: {submit_fail_rate:.1f}%" + + # 添加管线关键信息 + if self.enable_pipeline_monitoring: + pipeline_stats = self._get_pipeline_stats() + current = pipeline_stats['current'] + + # 显示关键瓶颈 + if current['wait_poses'] > 5.0: + summary += f" | 姿态: {current['wait_poses']:.1f}ms⚠️" + elif current['wait_poses'] > 0: + summary += f" | 姿态: {current['wait_poses']:.1f}ms" + + if current['total_render'] > 8.0: + summary += f" | 渲染: {current['total_render']:.1f}ms⚠️" + elif current['total_render'] > 0: + summary += f" | 渲染: {current['total_render']:.1f}ms" + + # 添加GPU渲染时间信息 + if self.enable_gpu_timing: + gpu_total = current.get('gpu_total_render', 0) + gpu_scene = current.get('gpu_scene_render', 0) + if gpu_total > 12.0: + summary += f" | GPU: {gpu_total:.1f}ms⚠️" + elif gpu_total > 0: + summary += f" | GPU: {gpu_total:.1f}ms" + elif gpu_scene > 0: + summary += f" | GPU场景: {gpu_scene:.1f}ms" + + # 显示目标帧时间对比 + vr_info = pipeline_stats['vr_info'] + if vr_info['target_frame_time_ms'] > 0: + target = vr_info['target_frame_time_ms'] + current_avg = stats['frame_time_avg'] + if current_avg > target * 1.1: + summary += f" | 目标:{target:.0f}ms⚠️" + else: + summary += f" | 目标:{target:.0f}ms" + + # 性能状态指示 + if stats['vr_fps'] < 72: + summary += " ⚠️" + elif stats['vr_fps'] > 85: + summary += " ✅" + + print(summary) + + # 注意:原来的left_cb和right_cb函数已被删除 + # 它们的功能已集成到_handle_vr_rendering_and_submit方法中 + + # 注意:_safe_submit_texture方法已删除 + # VR纹理提交现在完全由Panda3D的renderFrame()自动处理 + + def submit_texture(self, eye, texture): + """优化的VR纹理提交 - 使用缓存的纹理ID,避免重复prepareNow""" + try: + if not self.vr_compositor: + print("❌ VR compositor不可用") + self.submit_failures += 1 + return + + # 🚀 关键修复:防止同一帧重复提交 + current_frame = getattr(self, 'frame_count', 0) + if not hasattr(self, '_last_submit_frame'): + self._last_submit_frame = {} + + # 检查当前帧是否已经提交过此眼睛的纹理 + if eye in self._last_submit_frame and self._last_submit_frame[eye] == current_frame: + # 静默跳过,避免spam输出 + return + + # 记录当前帧提交 + self._last_submit_frame[eye] = current_frame + + # 🚀 关键优化:直接使用缓存的纹理ID,避免重复prepareNow + if eye == openvr.Eye_Left: + handle = self.left_texture_id + eye_name = "左眼" + elif eye == openvr.Eye_Right: + handle = self.right_texture_id + eye_name = "右眼" + else: + print(f"❌ 未知的眼睛类型: {eye}") + self.submit_failures += 1 + return + + # 检查缓存的纹理ID是否有效 + if not handle or handle <= 0: + print(f"❌ {eye_name}纹理ID缓存无效: {handle}") + print(" 这可能表示纹理准备失败,需要检查_prepare_and_cache_textures()") + self.submit_failures += 1 + return + + # ❌ 移除gsg.flush()调用 - 基于OpenVR官方实践 + # gsg.flush()等同于OpenGL的glFlush(),导致强制CPU-GPU同步 + # 每帧调用2次(左右眼)是GPU周期性峰值的主要原因 + # 参考:OpenVR社区经验表明同步调用是性能杀手 + # + # gsg = self.world.win.getGsg() + # if gsg and hasattr(gsg, 'flush'): + # try: + # gsg.flush() + # except Exception as flush_error: + # if not hasattr(self, '_gsg_flush_error_logged'): + # print(f"⚠️ GSG刷新失败: {flush_error}") + # self._gsg_flush_error_logged = True + + # 🚀 关键优化:使用缓存的OpenVR Texture对象,避免每帧创建 + if eye == openvr.Eye_Left: + ovr_texture = self._left_ovr_texture + else: + ovr_texture = self._right_ovr_texture + + # 检查缓存对象是否存在(向后兼容) + if ovr_texture is None: + # 备用方案:如果缓存对象不存在,创建新的(性能较差) + ovr_texture = openvr.Texture_t() + ovr_texture.eType = openvr.TextureType_OpenGL + ovr_texture.eColorSpace = openvr.ColorSpace_Gamma + if not hasattr(self, '_texture_fallback_warned'): + print("⚠️ 使用Texture对象备用方案(性能次优)") + self._texture_fallback_warned = True + + # 只更新handle,其他属性已预设置 + ovr_texture.handle = handle + + # 提交到VR系统 + error = self.vr_compositor.submit(eye, ovr_texture) + + # 检查错误 + if error and error != openvr.VRCompositorError_None: + print(f"⚠️ VR{eye_name}纹理提交错误: {error}") + self.submit_failures += 1 + else: + # 只在第一次成功时输出 + if not hasattr(self, '_optimized_submit_success_logged'): + print(f"✅ 优化版VR纹理提交成功! 使用缓存ID,避免重复prepareNow") + print(f" {eye_name}纹理ID: {handle}") + self._optimized_submit_success_logged = True + + # 🔧 智能GPU同步策略 - 减少不必要的flush调用 + # 只有在检测到性能问题或每N帧时才强制flush + if eye == openvr.Eye_Right: + self._smart_gpu_sync() + + except Exception as e: + print(f"❌ VR纹理提交异常: {e}") + import traceback + traceback.print_exc() + self.submit_failures += 1 + + def _smart_gpu_sync(self): + """智能GPU同步策略 - 只在必要时进行同步""" + try: + # 初始化同步控制变量 + if not hasattr(self, '_last_gpu_sync_frame'): + self._last_gpu_sync_frame = 0 + self._gpu_sync_interval = 60 # 每60帧强制同步一次(1秒@60FPS) + self._performance_based_sync = True + + current_frame = getattr(self, 'frame_count', 0) + + # 策略1: 性能自适应同步 + if self._performance_based_sync: + # 如果渲染时间过长,增加同步频率 + total_render_time = getattr(self, 'left_render_time', 0) + getattr(self, 'right_render_time', 0) + + if total_render_time > 15.0: # 如果总渲染时间超过15ms + # 高负载时更频繁同步 + sync_interval = 30 # 每30帧同步 + elif total_render_time > 10.0: + sync_interval = 45 # 中等负载 + else: + sync_interval = self._gpu_sync_interval # 正常负载 + + should_sync = (current_frame - self._last_gpu_sync_frame) >= sync_interval + else: + # 策略2: 固定间隔同步 + should_sync = (current_frame - self._last_gpu_sync_frame) >= self._gpu_sync_interval + + # 执行同步 + if should_sync: + gsg = self.world.win.getGsg() + if gsg and hasattr(gsg, 'flush'): + try: + gsg.flush() + self._last_gpu_sync_frame = current_frame + + # 只在首次或Debug时输出 + if not hasattr(self, '_smart_sync_logged') or self.debug_output_enabled: + if not hasattr(self, '_smart_sync_logged'): + print(f"🔧 智能GPU同步已启用 - 间隔:{sync_interval}帧") + self._smart_sync_logged = True + elif self.debug_output_enabled and current_frame % 600 == 1: + print(f"🔧 智能同步触发 (帧#{current_frame}, 间隔:{sync_interval})") + + except Exception as flush_error: + if not hasattr(self, '_smart_sync_error_logged'): + print(f"⚠️ 智能GPU同步失败: {flush_error}") + self._smart_sync_error_logged = True + + except Exception as e: + print(f"⚠️ 智能GPU同步策略异常: {e}") + + 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}") + + 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) + + def _disable_async_reprojection(self): + """禁用异步重投影 - 备选修复方案""" + try: + # 尝试通过OpenVR设置禁用异步重投影 + if hasattr(openvr, 'VRSettings'): + settings = openvr.VRSettings() + if settings: + # 禁用异步重投影 + error = settings.setBool("steamvr", "enableAsyncReprojection", False) + if error == openvr.VRSettingsError_None: + print("✅ 异步重投影已禁用") + else: + print(f"⚠️ 禁用异步重投影失败: 设置错误 {error}") + else: + print("⚠️ 无法获取VR设置接口") + else: + print("⚠️ OpenVR设置接口不可用") + + except Exception as e: + print(f"⚠️ 禁用异步重投影失败: {e}") + + def enable_async_reprojection_disable(self): + """启用异步重投影禁用选项 - 用户可调用的方法""" + self.disable_async_reprojection = True + print("📝 异步重投影禁用选项已启用,将在下次VR初始化时生效") + + def disable_async_reprojection_disable(self): + """禁用异步重投影禁用选项 - 恢复默认行为""" + self.disable_async_reprojection = False + print("📝 异步重投影禁用选项已关闭,将使用默认ATW行为") + + def _start_timing(self, operation_name): + """开始计时操作 - 性能优化版本""" + if not self.enable_pipeline_monitoring or self.performance_mode_enabled: 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 + import time + # 🚀 性能优化:减少字典创建,只在必要时创建 + return { + 'operation': operation_name, + 'start_time': time.perf_counter() } - - if self.simulation_mode: - info['mode'] = 'simulation' - info['controllers'] = len(self.simulation_data['controller_poses']) + + def _end_timing(self, timing_data): + """结束计时并记录结果 - 性能优化版本""" + if not self.enable_pipeline_monitoring or self.performance_mode_enabled or not timing_data: + return 0.0 + + import time + elapsed = (time.perf_counter() - timing_data['start_time']) * 1000 # 转换为毫秒 + + # 保存到相应的历史记录 + operation = timing_data['operation'] + if operation == 'wait_poses': + self.wait_poses_time = elapsed + self.wait_poses_times.append(elapsed) + if len(self.wait_poses_times) > self.pipeline_history_size: + self.wait_poses_times.pop(0) + elif operation == 'left_render': + self.left_render_time = elapsed + elif operation == 'right_render': + self.right_render_time = elapsed + elif operation == 'submit': + self.submit_time = elapsed + self.submit_times.append(elapsed) + if len(self.submit_times) > self.pipeline_history_size: + self.submit_times.pop(0) + elif operation == 'sync_wait': + self.vr_sync_wait_time = elapsed + self.sync_wait_times.append(elapsed) + if len(self.sync_wait_times) > self.pipeline_history_size: + self.sync_wait_times.pop(0) + + # 计算总渲染时间 + total_render = self.left_render_time + self.right_render_time + self.render_times.append(total_render) + if len(self.render_times) > self.pipeline_history_size: + self.render_times.pop(0) + + return elapsed + + def _get_pipeline_stats(self): + """获取渲染管线统计信息""" + def get_stats(times_list): + if not times_list: + return {'avg': 0.0, 'min': 0.0, 'max': 0.0} + return { + 'avg': sum(times_list) / len(times_list), + 'min': min(times_list), + 'max': max(times_list) + } + + # 计算GPU时间统计 + def get_gpu_field_stats(field_name): + """从GPU时间历史记录中提取特定字段的统计信息""" + values = [] + for gpu_data in self.gpu_timing_history: + if field_name in gpu_data: + values.append(gpu_data[field_name]) + return get_stats(values) + + return { + 'wait_poses': get_stats(self.wait_poses_times), + 'render': get_stats(self.render_times), + 'submit': get_stats(self.submit_times), + 'sync_wait': get_stats(self.sync_wait_times), + 'gpu_timing': { + 'scene_render': get_gpu_field_stats('scene_render'), + 'pre_submit': get_gpu_field_stats('pre_submit'), + 'post_submit': get_gpu_field_stats('post_submit'), + 'total_render': get_gpu_field_stats('total_render'), + 'compositor_render': get_gpu_field_stats('compositor_render'), + 'frame_interval': get_gpu_field_stats('frame_interval') + }, + 'current': { + 'wait_poses': self.wait_poses_time, + 'left_render': self.left_render_time, + 'right_render': self.right_render_time, + 'submit': self.submit_time, + 'sync_wait': self.vr_sync_wait_time, + 'total_render': self.left_render_time + self.right_render_time, + 'gpu_scene_render': self.gpu_scene_render_ms, + 'gpu_pre_submit': self.gpu_pre_submit_ms, + 'gpu_post_submit': self.gpu_post_submit_ms, + 'gpu_total_render': self.gpu_total_render_ms, + 'gpu_compositor_render': self.gpu_compositor_render_ms, + 'gpu_frame_interval': self.gpu_client_frame_interval_ms + }, + 'vr_info': { + 'eye_resolution': self.current_eye_resolution, + 'recommended_resolution': self.recommended_eye_resolution, + 'display_frequency': self.vr_display_frequency, + 'vsync_enabled': self.vr_vsync_enabled, + 'target_frame_time_ms': self.target_frame_time_ms, + 'vsync_to_photons_ms': self.vsync_to_photons_ms, + 'vsync_window_ms': self.vsync_window_ms, + 'async_reprojection': self.async_reprojection_enabled, + 'motion_smoothing': self.motion_smoothing_enabled, + 'gpu_timing_enabled': self.enable_gpu_timing, + 'gpu_timing_failures': self.gpu_timing_failure_count + } + } + + + def set_prediction_time(self, prediction_time_ms): + """设置预测时间(仅用于update_task策略) + + Args: + prediction_time_ms: 预测时间,单位毫秒(通常8-16ms) + """ + prediction_time_s = prediction_time_ms / 1000.0 + if prediction_time_s < 0.005 or prediction_time_s > 0.020: + print(f"⚠️ 预测时间超出推荐范围(5-20ms): {prediction_time_ms}ms") + + old_time = self.use_prediction_time * 1000 + self.use_prediction_time = prediction_time_s + print(f"✓ VR预测时间已设置: {old_time:.1f}ms → {prediction_time_ms:.1f}ms") + + + def test_pipeline_monitoring(self): + """测试管线监控功能 - 用于调试和验证""" + print("🔧 正在测试VR管线监控功能...") + + try: + # 测试基本信息获取 + print("📊 基本信息:") + print(f" 当前分辨率: {self.current_eye_resolution}") + print(f" 显示频率: {self.vr_display_frequency} Hz") + print(f" 目标帧时间: {self.target_frame_time_ms:.2f}ms") + + # Running Start模式信息 + print("🎯 Running Start模式:") + print(f" 预测时间: {self.use_prediction_time * 1000:.1f}ms") + print(f" 模式: Valve Running Start - 帧开始时获取姿态") + + # 测试管线统计 + if self.enable_pipeline_monitoring: + pipeline_stats = self._get_pipeline_stats() + print("⚙️ 管线统计:") + print(f" waitGetPoses历史: {len(self.wait_poses_times)}帧") + print(f" 渲染历史: {len(self.render_times)}帧") + print(f" 提交历史: {len(self.submit_times)}帧") + + # 测试性能报告 + print("📋 生成详细性能报告:") + self._print_performance_report() + + print("✅ 管线监控功能测试完成") + else: + print("⚠️ 管线监控已禁用,无法测试统计功能") + + except Exception as e: + print(f"❌ 测试管线监控功能时发生错误: {e}") + import traceback + traceback.print_exc() + + def _init_performance_monitoring(self): + """初始化性能监控库""" + self.psutil_available = False + self.gputil_available = False + self.nvidia_ml_available = False + + try: + import psutil + self.psutil = psutil + self.psutil_available = True + print("✓ psutil性能监控库已加载") + except ImportError: + print("⚠️ psutil库未安装,CPU和内存监控将不可用") + + try: + import GPUtil + self.gputil = GPUtil + self.gputil_available = True + print("✓ GPUtil GPU监控库已加载") + except ImportError: + print("⚠️ GPUtil库未安装,GPU监控将不可用") + + try: + import pynvml + self.pynvml = pynvml + pynvml.nvmlInit() + self.nvidia_ml_available = True + print("✓ NVIDIA-ML GPU监控库已加载") + except ImportError: + print("⚠️ pynvml库未安装,NVIDIA GPU详细监控将不可用") + except Exception as e: + print(f"⚠️ NVIDIA-ML初始化失败: {e}") + + def _get_gpu_frame_timing(self, frames_ago=0): + """获取GPU渲染时间统计 + + Args: + frames_ago: 获取多少帧之前的数据,0表示当前帧 + + Returns: + dict: GPU时间数据,如果获取失败返回None + """ + if not self.enable_gpu_timing or not self.vr_compositor: + return None + + try: + # 调用OpenVR的getFrameTiming API + result, timing = self.vr_compositor.getFrameTiming(framesAgo=frames_ago) + + if not result: + self.gpu_timing_failure_count += 1 + if self.gpu_timing_failure_count % 300 == 1: # 每5秒输出一次错误 + print("⚠️ OpenVR getFrameTiming调用失败") + return None + + # 提取GPU时间数据(单位:毫秒) + gpu_data = {} + + # 检查timing对象是否有GPU时间相关的属性 + if hasattr(timing, 'm_flSceneRenderGpuMs'): + gpu_data['scene_render'] = timing.m_flSceneRenderGpuMs + self.gpu_scene_render_ms = timing.m_flSceneRenderGpuMs + + if hasattr(timing, 'm_flPreSubmitGpuMs'): + gpu_data['pre_submit'] = timing.m_flPreSubmitGpuMs + self.gpu_pre_submit_ms = timing.m_flPreSubmitGpuMs + + if hasattr(timing, 'm_flPostSubmitGpuMs'): + gpu_data['post_submit'] = timing.m_flPostSubmitGpuMs + self.gpu_post_submit_ms = timing.m_flPostSubmitGpuMs + + if hasattr(timing, 'm_flTotalRenderGpuMs'): + gpu_data['total_render'] = timing.m_flTotalRenderGpuMs + self.gpu_total_render_ms = timing.m_flTotalRenderGpuMs + + if hasattr(timing, 'm_flCompositorRenderGpuMs'): + gpu_data['compositor_render'] = timing.m_flCompositorRenderGpuMs + self.gpu_compositor_render_ms = timing.m_flCompositorRenderGpuMs + + if hasattr(timing, 'm_flClientFrameIntervalMs'): + gpu_data['frame_interval'] = timing.m_flClientFrameIntervalMs + self.gpu_client_frame_interval_ms = timing.m_flClientFrameIntervalMs + + # 将GPU时间数据添加到历史记录 + if gpu_data: + self.gpu_timing_history.append(gpu_data) + if len(self.gpu_timing_history) > self.gpu_timing_history_size: + self.gpu_timing_history.pop(0) + + # 调试信息 - 仅在第一次成功时输出 + if not hasattr(self, '_gpu_timing_success_logged'): + available_fields = list(gpu_data.keys()) + print(f"✅ GPU时间统计已启用 - 可用字段: {available_fields}") + self._gpu_timing_success_logged = True + + return gpu_data + + except AttributeError as e: + # OpenVR Python绑定可能不包含某些字段 + if self.gpu_timing_failure_count == 0: + print(f"⚠️ GPU时间统计部分功能不可用: {e}") + print(" 这可能是由于OpenVR Python绑定版本问题") + self.gpu_timing_failure_count += 1 + return None + + except Exception as e: + self.gpu_timing_failure_count += 1 + if self.gpu_timing_failure_count % 300 == 1: # 每5秒输出一次错误 + print(f"⚠️ 获取GPU时间统计失败: {e}") + return None + + def _update_performance_metrics(self): + """更新系统性能指标""" + if not self.performance_monitoring: + return + + import time + current_time = time.time() + + # 限制更新频率 + if current_time - self.last_performance_check < self.performance_check_interval: + return + + self.last_performance_check = current_time + + try: + # 更新CPU和内存使用率 + if self.psutil_available: + self.cpu_usage = self.psutil.cpu_percent(interval=None) + memory = self.psutil.virtual_memory() + self.memory_usage = memory.percent + + # 更新GPU使用率 + self._update_gpu_metrics() + + except Exception as e: + if self.frame_count % 600 == 0: # 每10秒输出一次错误 + print(f"⚠️ 性能监控更新失败: {e}") + + def _update_gpu_metrics(self): + """更新GPU相关指标""" + try: + # 方法1: 使用GPUtil + if self.gputil_available: + gpus = self.gputil.getGPUs() + if gpus: + gpu = gpus[0] # 使用第一个GPU + self.gpu_usage = gpu.load * 100 + self.gpu_memory_usage = gpu.memoryUtil * 100 + + # 方法2: 使用NVIDIA-ML (更精确) + elif self.nvidia_ml_available: + try: + handle = self.pynvml.nvmlDeviceGetHandleByIndex(0) + + # GPU使用率 + utilization = self.pynvml.nvmlDeviceGetUtilizationRates(handle) + self.gpu_usage = utilization.gpu + + # GPU内存使用率 + memory_info = self.pynvml.nvmlDeviceGetMemoryInfo(handle) + self.gpu_memory_usage = (memory_info.used / memory_info.total) * 100 + + except Exception as e: + # NVIDIA-ML可能无法在某些系统上工作 + pass + + except Exception as e: + # GPU监控失败,但不影响VR功能 + pass + + def _track_frame_time(self): + """记录帧时间 - 性能优化版本""" + import time + current_time = time.time() + + if hasattr(self, '_last_frame_time'): + frame_time = (current_time - self._last_frame_time) * 1000 # 转换为毫秒 + + # 🚀 性能优化:性能模式下跳过列表操作以减少内存分配 + if not self.performance_mode_enabled: + # 添加到帧时间历史 + self.frame_times.append(frame_time) + + # 限制历史长度 + if len(self.frame_times) > self.max_frame_time_history: + self.frame_times.pop(0) + + self._last_frame_time = current_time + + def get_performance_stats(self): + """获取详细的性能统计信息""" + stats = { + 'vr_fps': self.vr_fps, + 'frame_count': self.frame_count, + 'submit_failures': self.submit_failures, + 'pose_failures': self.pose_failures, + 'cpu_usage': self.cpu_usage, + 'memory_usage': self.memory_usage, + 'gpu_usage': self.gpu_usage, + 'gpu_memory_usage': self.gpu_memory_usage, + } + + # 计算帧时间统计 + if self.frame_times: + stats['frame_time_avg'] = sum(self.frame_times) / len(self.frame_times) + stats['frame_time_min'] = min(self.frame_times) + stats['frame_time_max'] = max(self.frame_times) + stats['frame_time_95th'] = sorted(self.frame_times)[int(len(self.frame_times) * 0.95)] else: - info['mode'] = 'real_vr' - info['openvr_connected'] = self.vr_system is not None - - return info + stats['frame_time_avg'] = 0 + stats['frame_time_min'] = 0 + stats['frame_time_max'] = 0 + stats['frame_time_95th'] = 0 - # 便捷方法 - def enable_simulation_mode(self): - """启用模拟模式(调试用)""" - return self.initialize_vr(force_simulation=True) + return stats - def get_simulation_data(self): - """获取模拟数据""" - return self.simulation_data if self.simulation_mode else None + def enable_performance_monitoring(self): + """启用性能监控""" + self.performance_monitoring = True + print("✓ VR性能监控已启用") + + def disable_performance_monitoring(self): + """禁用性能监控""" + self.performance_monitoring = False + print("✓ VR性能监控已禁用") + + def enable_gpu_timing_monitoring(self): + """启用GPU时间监控""" + self.enable_gpu_timing = True + print("✓ VR GPU时间监控已启用") + + def disable_gpu_timing_monitoring(self): + """禁用GPU时间监控""" + self.enable_gpu_timing = False + print("✓ VR GPU时间监控已禁用") + + def set_performance_check_interval(self, interval): + """设置性能检查间隔 + + Args: + interval: 检查间隔(秒),建议0.1-2.0之间 + """ + if 0.1 <= interval <= 5.0: + self.performance_check_interval = interval + print(f"✓ 性能监控间隔设置为 {interval:.1f} 秒") + else: + print("⚠️ 性能监控间隔应在0.1-5.0秒之间") + + def set_frame_time_history_size(self, size): + """设置帧时间历史记录大小 + + Args: + size: 历史记录大小(帧数),建议30-300之间 + """ + if 10 <= size <= 1000: + self.max_frame_time_history = size + # 清理超出的历史记录 + if len(self.frame_times) > size: + self.frame_times = self.frame_times[-size:] + print(f"✓ 帧时间历史记录大小设置为 {size} 帧") + else: + print("⚠️ 帧时间历史记录大小应在10-1000帧之间") + + def set_performance_report_interval(self, frames): + """设置性能报告输出间隔 + + Args: + frames: 帧数间隔,建议300-7200之间(5秒-2分钟@60fps) + """ + if 300 <= frames <= 7200: + # 修改_update_vr中的报告间隔 + print(f"✓ 性能报告间隔设置为每 {frames} 帧(约 {frames/60:.1f} 秒@60fps)") + # 这里可以添加一个实例变量来控制 + self.performance_report_interval = frames + else: + print("⚠️ 性能报告间隔应在300-7200帧之间") + + def get_performance_monitoring_config(self): + """获取当前性能监控配置""" + return { + 'enabled': self.performance_monitoring, + 'check_interval': self.performance_check_interval, + 'frame_history_size': self.max_frame_time_history, + 'report_interval': getattr(self, 'performance_report_interval', 1800), + 'psutil_available': self.psutil_available, + 'gputil_available': self.gputil_available, + 'nvidia_ml_available': self.nvidia_ml_available + } + + def print_performance_monitoring_status(self): + """输出性能监控状态""" + config = self.get_performance_monitoring_config() + + print("🔧 ===== VR性能监控配置 =====") + print(f" 监控状态: {'✅ 启用' if config['enabled'] else '❌ 禁用'}") + print(f" 检查间隔: {config['check_interval']:.1f} 秒") + print(f" 帧时间历史: {config['frame_history_size']} 帧") + print(f" 报告间隔: {config['report_interval']} 帧") + + print(f" 可用库:") + print(f" psutil (CPU/内存): {'✅' if config['psutil_available'] else '❌'}") + print(f" GPUtil (GPU): {'✅' if config['gputil_available'] else '❌'}") + print(f" NVIDIA-ML (GPU): {'✅' if config['nvidia_ml_available'] else '❌'}") + print("=============================") + + def force_performance_report(self): + """强制输出一次性能报告""" + print("🔄 手动触发性能报告...") + self._print_performance_report() + + def reset_performance_counters(self): + """重置性能计数器""" + 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 + self.frame_times.clear() + print("✅ 性能计数器已重置") + + def get_current_performance_summary(self): + """获取当前性能摘要(简短版本)""" + stats = self.get_performance_stats() + + summary = f"VR性能: {stats['vr_fps']:.1f}fps" + + if stats['frame_time_avg'] > 0: + summary += f" | 帧时间: {stats['frame_time_avg']:.1f}ms" + + if self.psutil_available: + summary += f" | CPU: {stats['cpu_usage']:.0f}%" + summary += f" | 内存: {stats['memory_usage']:.0f}%" + + # 显示GPU信息,如果库不可用则显示提示 + if self.gputil_available or self.nvidia_ml_available: + summary += f" | GPU: {stats['gpu_usage']:.0f}%" + else: + summary += " | GPU: N/A" + + return summary + + def _print_render_callback_diagnostics(self): + """输出渲染回调诊断信息 - 包含优化效果分析""" + try: + print(f"🔍 DrawCallback渲染诊断:") + + # 回调次数统计 + left_count = self.left_render_count + right_count = self.right_render_count + print(f" 渲染次数: 左眼={left_count}, 右眼={right_count}") + + if left_count == 0 and right_count == 0: + print(f" ❌ VR缓冲区未被渲染 - 这是严重问题!") + return + + # 渲染次数平衡性检查 + if abs(left_count - right_count) > 5: + print(f" ⚠️ 左右眼渲染次数不平衡: 差异={abs(left_count - right_count)}") + else: + print(f" ✓ 左右眼渲染次数平衡") + + # 🔧 真实渲染时间分析 - 显示优化效果 + left_render_time = getattr(self, 'left_render_time', 0) + right_render_time = getattr(self, 'right_render_time', 0) + total_render_time = left_render_time + right_render_time + + print(f" ⏱️ 精确渲染时间测量:") + print(f" 左眼cbdata.upcall(): {left_render_time:.2f}ms") + print(f" 右眼cbdata.upcall(): {right_render_time:.2f}ms") + print(f" 总计: {total_render_time:.2f}ms") + + # 渲染性能评估 + if total_render_time > 16.0: # 超过60FPS时间 + print(f" 🔴 渲染时间过长: {total_render_time:.1f}ms (目标<13.9ms@72Hz)") + print(f" 建议: 检查RenderPipeline优化是否生效") + elif total_render_time > 10.0: + print(f" 🟡 渲染时间偏高: {total_render_time:.1f}ms (可接受)") + else: + print(f" 🟢 渲染性能良好: {total_render_time:.1f}ms") + + # 🔧 渲染管线优化状态检查 + self._check_rendering_optimizations() + + # OpenGL状态诊断 + self._diagnose_opengl_state() + + except Exception as e: + print(f" 渲染回调诊断失败: {e}") + + def _check_rendering_optimizations(self): + """检查渲染优化状态""" + try: + print(f" 🔧 渲染优化状态:") + + # 检查RenderPipeline优化 + if hasattr(self.world, 'render_pipeline') and self.world.render_pipeline: + print(f" RenderPipeline: 已检测并优化") + else: + print(f" RenderPipeline: 未检测到(使用基础Panda3D)") + + # 检查GPU同步优化 + if hasattr(self, '_smart_sync_logged'): + last_sync_frame = getattr(self, '_last_gpu_sync_frame', 0) + current_frame = getattr(self, 'frame_count', 0) + frames_since_sync = current_frame - last_sync_frame + print(f" 智能GPU同步: 已启用 (距离上次同步: {frames_since_sync}帧)") + else: + print(f" 智能GPU同步: 未初始化") + + # 检查对象池状态 + matrix_pool_status = self.get_object_pool_status() + print(f" 对象池: Mat4={matrix_pool_status['matrix_pool_size']}/{matrix_pool_status['matrix_pool_capacity']}") + + # 检查垃圾回收控制 + gc_status = self.get_debug_status() + if gc_status['gc_disabled']: + print(f" GC控制: 已启用手动模式 (间隔:{gc_status['manual_gc_interval']}帧)") + else: + print(f" GC控制: 自动模式") + + except Exception as e: + print(f" 优化状态检查失败: {e}") + + def _diagnose_opengl_state(self): + """诊断OpenGL渲染状态""" + try: + # 检查VR缓冲区状态 + if hasattr(self, 'vr_left_eye_buffer') and self.vr_left_eye_buffer: + left_gsg = self.vr_left_eye_buffer.getGsg() + left_valid = self.vr_left_eye_buffer.isValid() + print(f" 左眼缓冲区: {'有效' if left_valid else '无效'}") + + if hasattr(self, 'vr_right_eye_buffer') and self.vr_right_eye_buffer: + right_gsg = self.vr_right_eye_buffer.getGsg() + right_valid = self.vr_right_eye_buffer.isValid() + print(f" 右眼缓冲区: {'有效' if right_valid else '无效'}") + + # 检查纹理准备状态 + if hasattr(self, 'textures_prepared'): + print(f" 纹理准备状态: {'已准备' if self.textures_prepared else '未准备'}") + + # 检查纹理ID缓存 + if hasattr(self, 'left_texture_id') and hasattr(self, 'right_texture_id'): + left_id = self.left_texture_id or 0 + right_id = self.right_texture_id or 0 + print(f" 纹理ID缓存: 左眼={left_id}, 右眼={right_id}") + + if left_id == 0 or right_id == 0: + print(f" ⚠️ 检测到无效的纹理ID,这可能导致提交失败") + else: + print(f" ✓ 纹理ID缓存正常") + + # 检查场景渲染状态 + if hasattr(self.world, 'render') and self.world.render: + render_children = len(self.world.render.getChildren()) + print(f" 场景节点数: {render_children}") + if render_children == 0: + print(f" ⚠️ 场景为空,可能导致渲染时间异常短") + + except Exception as e: + print(f" OpenGL状态诊断失败: {e}") + + def enable_debug_output(self): + """启用调试输出""" + self.debug_output_enabled = True + print("✓ VR调试输出已启用") + + def disable_debug_output(self): + """禁用调试输出""" + self.debug_output_enabled = False + print("✓ VR调试输出已禁用") + + def set_debug_mode(self, mode): + """设置调试模式 + + Args: + mode: 'brief' 或 'detailed' + """ + if mode in ['brief', 'detailed']: + self.debug_mode = mode + print(f"✓ VR调试模式设置为: {mode}") + else: + print("⚠️ 调试模式只能是 'brief' 或 'detailed'") + + def toggle_debug_output(self): + """切换调试输出状态""" + self.debug_output_enabled = not self.debug_output_enabled + status = "启用" if self.debug_output_enabled else "禁用" + print(f"✓ VR调试输出已{status}") + return self.debug_output_enabled + + def get_debug_status(self): + """获取调试状态""" + return { + 'debug_enabled': self.debug_output_enabled, + 'debug_mode': self.debug_mode, + 'performance_monitoring': self.performance_monitoring, + 'report_interval_frames': getattr(self, 'performance_report_interval', 600), + 'report_interval_seconds': getattr(self, 'performance_report_interval', 600) / 60, # 假设60fps + 'gc_control_enabled': self._gc_control_enabled, + 'gc_disabled': self._gc_disabled, + 'manual_gc_interval': self._manual_gc_interval, + } + + # ====== Python垃圾回收控制方法 ====== + + def enable_gc_control(self): + """启用垃圾回收控制 - 减少VR渲染期间的GC峰值""" + if not self._gc_control_enabled: + self._gc_control_enabled = True + if not self._gc_disabled: + gc.disable() + self._gc_disabled = True + print("✅ VR垃圾回收控制已启用") + else: + print("ℹ️ VR垃圾回收控制已经启用") + + def disable_gc_control(self): + """禁用垃圾回收控制 - 恢复自动垃圾回收""" + if self._gc_control_enabled: + self._gc_control_enabled = False + if self._gc_disabled: + gc.enable() + self._gc_disabled = False + print("✅ VR垃圾回收控制已禁用,恢复自动垃圾回收") + else: + print("ℹ️ VR垃圾回收控制已经禁用") + + def set_manual_gc_interval(self, frames): + """设置手动垃圾回收间隔 + + Args: + frames: 帧数间隔 (建议100-600) + """ + if 50 <= frames <= 1800: + old_interval = self._manual_gc_interval + self._manual_gc_interval = frames + print(f"✅ 手动GC间隔: {old_interval} → {frames} 帧") + else: + print("⚠️ GC间隔应在50-1800帧之间") + + def force_manual_gc(self): + """强制执行一次垃圾回收""" + collected = gc.collect() + print(f"🗑️ 强制GC: 清理了 {collected} 个对象") + return collected + + def get_object_pool_status(self): + """获取对象池状态""" + return { + 'matrix_pool_size': len(self._matrix_pool) if hasattr(self, '_matrix_pool') else 0, + 'matrix_pool_capacity': getattr(self, '_matrix_pool_size', 0), + 'cached_controllers': len(self.controller_poses), + 'cached_matrices': len(getattr(self, '_cached_matrices', {})), + } + + # ====== VR分辨率缩放和质量预设系统 ====== + + def set_resolution_scale(self, scale): + """设置VR分辨率缩放系数 + + Args: + scale: 缩放系数 (0.3-1.0),0.75表示75%分辨率 + """ + if not (0.3 <= scale <= 1.0): + print(f"⚠️ 分辨率缩放系数应在0.3-1.0之间,当前: {scale}") + return False + + old_scale = self.resolution_scale + self.resolution_scale = scale + + # 如果VR已初始化,重新创建缓冲区 + if self.vr_initialized: + self._apply_resolution_scale() + + print(f"✓ VR分辨率缩放: {old_scale} → {scale}") + pixel_reduction = (1 - scale**2) * 100 + print(f"📊 像素减少: {pixel_reduction:.1f}%") + + return True + + def set_quality_preset(self, preset_name): + """设置VR质量预设 + + Args: + preset_name: 'performance', 'balanced', 'quality' + """ + if preset_name not in self.quality_presets: + print(f"⚠️ 未知的质量预设: {preset_name}") + print(f" 可用预设: {list(self.quality_presets.keys())}") + return False + + old_preset = self.current_quality_preset + self.current_quality_preset = preset_name + scale = self.quality_presets[preset_name] + + print(f"🎯 切换VR质量预设: {old_preset} → {preset_name}") + + return self.set_resolution_scale(scale) + + def cycle_quality_preset(self): + """循环切换质量预设""" + presets = list(self.quality_presets.keys()) + current_index = presets.index(self.current_quality_preset) + next_index = (current_index + 1) % len(presets) + next_preset = presets[next_index] + + return self.set_quality_preset(next_preset) + + def _apply_resolution_scale(self): + """应用分辨率缩放,重新创建VR缓冲区""" + try: + # 计算新的分辨率 + self.scaled_eye_width = int(self.base_eye_width * self.resolution_scale) + self.scaled_eye_height = int(self.base_eye_height * self.resolution_scale) + + # 更新当前分辨率 + self.eye_width = self.scaled_eye_width + self.eye_height = self.scaled_eye_height + self.current_eye_resolution = (self.eye_width, self.eye_height) + + print(f"🔄 重新创建VR缓冲区...") + print(f" 新分辨率: {self.eye_width}x{self.eye_height}") + + # 清理旧的缓冲区 + self._cleanup_vr_buffers() + + # 重新创建缓冲区 + if self._create_vr_buffers(): + # 重新设置相机 + self._setup_vr_cameras() + print("✅ VR缓冲区重新创建成功") + return True + else: + print("❌ VR缓冲区重新创建失败") + return False + + except Exception as e: + print(f"❌ 应用分辨率缩放失败: {e}") + import traceback + traceback.print_exc() + return False + + def _cleanup_vr_buffers(self): + """清理旧的VR缓冲区""" + try: + # 清理左眼缓冲区 + if hasattr(self, 'vr_left_eye_buffer') and 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 hasattr(self, 'vr_right_eye_buffer') and 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 hasattr(self, 'vr_left_camera') and self.vr_left_camera: + self.vr_left_camera.removeNode() + self.vr_left_camera = None + + if hasattr(self, 'vr_right_camera') and self.vr_right_camera: + self.vr_right_camera.removeNode() + self.vr_right_camera = None + + except Exception as e: + print(f"⚠️ 清理VR缓冲区时出错: {e}") + + def get_resolution_info(self): + """获取分辨率相关信息""" + return { + 'base_resolution': (self.base_eye_width, self.base_eye_height), + 'current_resolution': (self.eye_width, self.eye_height), + 'resolution_scale': self.resolution_scale, + 'current_preset': self.current_quality_preset, + 'available_presets': self.quality_presets, + 'pixel_reduction_percent': (1 - self.resolution_scale**2) * 100 + } + + def print_resolution_info(self): + """输出分辨率信息""" + info = self.get_resolution_info() + print("🔧 ===== VR分辨率信息 =====") + print(f" 推荐分辨率: {info['base_resolution'][0]}x{info['base_resolution'][1]}") + print(f" 当前分辨率: {info['current_resolution'][0]}x{info['current_resolution'][1]}") + print(f" 缩放系数: {info['resolution_scale']}") + print(f" 当前预设: {info['current_preset']}") + print(f" 像素减少: {info['pixel_reduction_percent']:.1f}%") + print(" 可用预设:") + for name, scale in info['available_presets'].items(): + marker = "✓" if name == info['current_preset'] else " " + print(f" {marker} {name}: {scale} ({scale*100:.0f}%)") + print("==========================") + + # ====== 性能模式控制方法 ====== + + def enable_performance_mode(self): + """手动启用性能模式 - 立即禁用详细监控以提升性能""" + if not self.performance_mode_enabled: + self.performance_mode_enabled = True + print("🎯 性能模式已手动启用 - 禁用详细监控以提升性能") + print(" 现在将减少每帧对象创建,显著提升VR性能稳定性") + else: + print("ℹ️ 性能模式已经启用") + + def disable_performance_mode(self): + """禁用性能模式 - 重新启用详细监控(用于调试)""" + if self.performance_mode_enabled: + self.performance_mode_enabled = False + print("🔍 性能模式已禁用 - 重新启用详细监控") + print(" 注意:这将增加每帧对象创建,可能影响VR性能") + else: + print("ℹ️ 性能模式已经禁用") + + def set_performance_mode_trigger_frame(self, frame_count): + """设置性能模式自动触发的帧数 + + Args: + frame_count: 触发帧数 (建议300-1200) + """ + if 100 <= frame_count <= 3600: + old_trigger = self.performance_mode_trigger_frame + self.performance_mode_trigger_frame = frame_count + print(f"✅ 性能模式触发帧数: {old_trigger} → {frame_count}") + else: + print("⚠️ 触发帧数应在100-3600之间") + + def get_performance_mode_status(self): + """获取性能模式状态""" + return { + 'performance_mode_enabled': self.performance_mode_enabled, + 'trigger_frame': self.performance_mode_trigger_frame, + 'current_frame': self.frame_count, + 'will_trigger_at_frame': self.performance_mode_trigger_frame if not self.performance_mode_enabled else None, + 'gc_interval_normal': self._manual_gc_interval, + 'gc_interval_performance': self._manual_gc_interval * 2, + } + + # ====== VR测试模式 ====== + + def enable_vr_test_mode(self, display_mode='stereo'): + """启用VR测试模式 - 将VR渲染直接显示在屏幕上 + + Args: + display_mode: 显示模式 + - 'stereo': 左右眼并排显示 + - 'left': 只显示左眼 + - 'right': 只显示右眼 + """ + if not self.is_vr_available(): + print("❌ VR系统不可用,无法启动测试模式") + return False + + try: + print(f"🧪 启动VR测试模式 - 显示模式: {display_mode}") + + # 初始化VR系统(如果还没初始化) + if not self.vr_initialized: + if not self.initialize_vr(): + print("❌ VR初始化失败") + return False + + # 🔧 关键修复:确保测试模式的纹理资源已初始化 + # 这解决了测试模式纹理提交失败导致的36FPS问题 + print("🔧 检查VR测试模式纹理资源...") + if not self._ensure_test_mode_textures(): + print("❌ VR测试模式纹理资源初始化失败") + return False + + # 设置测试模式 + self.vr_test_mode = True + self.test_display_mode = display_mode + + # 启用VR渲染但不提交给OpenVR + if not self.vr_enabled: + # 启用VR渲染流程(但会在回调中跳过提交) + self.vr_enabled = True + self._disable_main_cam() + + # 设置高帧率用于测试 + if hasattr(self.world, 'qtWidget') and self.world.qtWidget: + if hasattr(self.world.qtWidget, 'synchronizer'): + self.world.qtWidget.synchronizer.setInterval(int(1000/144)) + print("✓ 测试模式:Qt Timer设置为144Hz") + + # 初始化测试显示系统 + if not self._initialize_test_display(): + print("❌ 测试显示系统初始化失败") + return False + + # 创建性能HUD + if not self._initialize_test_performance_hud(): + print("❌ 性能HUD初始化失败") + return False + + # 恢复主相机以查看测试内容 + self._enable_main_cam() + + # 重置HUD更新计数器 + self.hud_update_counter = 0 + + print("✅ VR测试模式已启用") + print(" - VR内容将显示在屏幕上") + print(" - 不会向OpenVR提交纹理") + print(" - 可以准确测量纯渲染性能") + print(f" - 当前显示模式: {display_mode}") - 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 + + except Exception as e: + print(f"❌ 启动VR测试模式失败: {e}") + import traceback + traceback.print_exc() + return False + + def _ensure_test_mode_textures(self): + """确保VR测试模式的纹理资源已正确初始化 + + 这解决了测试模式启用纹理提交时的36FPS问题: + - VR测试模式可能跳过了VR渲染缓冲区的初始化 + - submit_texture()需要有效的texture ID和OpenVR Texture对象 + - 如果未初始化会导致提交失败,造成帧率减半 + """ + try: + print(" 检查VR渲染缓冲区...") + + # 检查VR渲染缓冲区是否存在 + buffers_exist = ( + hasattr(self, 'vr_left_eye_buffer') and self.vr_left_eye_buffer and + hasattr(self, 'vr_right_eye_buffer') and self.vr_right_eye_buffer + ) + + if not buffers_exist: + print(" ⚠️ VR渲染缓冲区不存在,正在创建...") + if not self._setup_vr_render_buffers(): + print(" ❌ VR渲染缓冲区创建失败") + return False + print(" ✅ VR渲染缓冲区创建成功") + else: + print(" ✅ VR渲染缓冲区已存在") + + # 检查纹理ID是否已缓存 + print(" 检查纹理ID缓存...") + texture_ids_cached = ( + hasattr(self, 'left_texture_id') and self.left_texture_id and self.left_texture_id > 0 and + hasattr(self, 'right_texture_id') and self.right_texture_id and self.right_texture_id > 0 + ) + + if not texture_ids_cached: + print(" ⚠️ 纹理ID未缓存,正在准备纹理...") + if not self._prepare_and_cache_textures(): + print(" ❌ 纹理准备和缓存失败") + return False + print(f" ✅ 纹理ID缓存成功 - 左眼:{self.left_texture_id}, 右眼:{self.right_texture_id}") + else: + print(f" ✅ 纹理ID已缓存 - 左眼:{self.left_texture_id}, 右眼:{self.right_texture_id}") + + # 检查OpenVR Texture对象是否已创建 + print(" 检查OpenVR Texture对象...") + ovr_textures_exist = ( + hasattr(self, '_left_ovr_texture') and self._left_ovr_texture and + hasattr(self, '_right_ovr_texture') and self._right_ovr_texture + ) + + if not ovr_textures_exist: + print(" ⚠️ OpenVR Texture对象未创建,正在创建...") + self._create_cached_ovr_textures() + print(" ✅ OpenVR Texture对象创建成功") + else: + print(" ✅ OpenVR Texture对象已存在") + + print(" ✅ VR测试模式纹理资源检查完成,可安全启用纹理提交") + return True + + except Exception as e: + print(f" ❌ VR测试模式纹理资源检查失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_cached_ovr_textures(self): + """创建缓存的OpenVR Texture对象 - 避免每帧创建新对象""" + try: + import openvr + self._left_ovr_texture = openvr.Texture_t() + self._right_ovr_texture = openvr.Texture_t() + + # 设置固定属性(这些不变) + self._left_ovr_texture.eType = openvr.TextureType_OpenGL + self._left_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma + self._right_ovr_texture.eType = openvr.TextureType_OpenGL + self._right_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma + + print("✅ OpenVR Texture对象缓存已创建") + except Exception as e: + print(f"⚠️ OpenVR Texture对象创建失败: {e}") + # 不抛出异常,使用备用方案 + + def _batch_submit_textures(self): + """批量提交两眼纹理 - OpenVR最佳实践 + + 基于官方hellovr示例的实现: + - 两眼都渲染完成后,快速连续提交 + - 减少submit阻塞时间,避免错过VSync窗口 + - 这是解决36FPS问题的关键 + """ + try: + if not self.vr_compositor: + return False + + # 检查纹理是否准备好 + if not (self.vr_left_texture and self.vr_right_texture): + return False + + # 🚀 关键:快速连续提交两眼,最小化阻塞时间 + # 这符合OpenVR官方示例的做法 + success_left = False + success_right = False + + # 提交左眼纹理 + try: + self.submit_texture(openvr.Eye_Left, self.vr_left_texture) + success_left = True + except Exception as e: + print(f"❌ 批量提交左眼失败: {e}") + + # 立即提交右眼纹理(不等待) + try: + self.submit_texture(openvr.Eye_Right, self.vr_right_texture) + success_right = True + except Exception as e: + print(f"❌ 批量提交右眼失败: {e}") + + # 🚀 关键修复:调用PostPresentHandoff解除compositor阻塞 + # 这是解决36FPS问题的核心 - 确保compositor不会等待VSync + if success_left and success_right: + try: + # PostPresentHandoff告诉compositor我们已完成帧处理 + # 防止compositor等待下一个VSync周期 + if hasattr(self.vr_compositor, 'postPresentHandoff'): + self.vr_compositor.postPresentHandoff() + elif hasattr(self.vr_compositor, 'PostPresentHandoff'): + self.vr_compositor.PostPresentHandoff() + else: + # 备用方案:如果没有PostPresentHandoff,记录警告 + if not hasattr(self, '_post_present_warning_logged'): + print("⚠️ PostPresentHandoff方法未找到,可能影响时序") + self._post_present_warning_logged = True + except Exception as handoff_error: + if not hasattr(self, '_handoff_error_logged'): + print(f"⚠️ PostPresentHandoff调用失败: {handoff_error}") + self._handoff_error_logged = True + + # 记录批量提交状态(仅首次成功时) + if not hasattr(self, '_batch_submit_success_logged'): + print("✅ OpenVR批量提交模式+PostPresentHandoff已启用") + print(" 这应该解决36FPS → 72FPS的问题") + self._batch_submit_success_logged = True + return True + else: + return False + + except Exception as e: + print(f"❌ 批量提交纹理失败: {e}") + return False + + def disable_vr_test_mode(self): + """禁用VR测试模式""" + try: + print("🧪 禁用VR测试模式...") + + # 清理测试显示 + self._cleanup_test_display() + + # 清理性能HUD + self._cleanup_test_performance_hud() + + # 关闭测试模式 + self.vr_test_mode = False + self.test_mode_initialized = False + + # 重置HUD更新计数器 + self.hud_update_counter = 0 + + # 恢复正常VR模式或禁用VR + if self.vr_enabled: + print(" 选择: [1] 恢复正常VR模式 [2] 完全禁用VR") + # 这里可以让用户选择,现在默认禁用VR + self.disable_vr() + + print("✅ VR测试模式已禁用") + return True + + except Exception as e: + print(f"❌ 禁用VR测试模式失败: {e}") + return False + + def switch_test_display_mode(self, display_mode): + """切换测试显示模式 + + Args: + display_mode: 'stereo', 'left', 'right' + """ + if not self.vr_test_mode: + print("⚠️ 请先启用VR测试模式") + return False + + if display_mode not in ['stereo', 'left', 'right']: + print(f"⚠️ 无效的显示模式: {display_mode}") + return False + + print(f"🧪 切换显示模式: {self.test_display_mode} → {display_mode}") + + # 如果切换模式,需要清理旧的显示并重置标志位 + if self.test_display_mode != display_mode: + self._cleanup_test_display() + + self.test_display_mode = display_mode + + # 更新显示 + self._update_test_display() + return True + + def _initialize_test_display(self): + """初始化测试显示系统""" + try: + print("🔧 正在初始化测试显示系统...") + + # 导入必要的Panda3D组件 + from panda3d.core import CardMaker, PandaNode + + # 创建显示四边形 + cm = CardMaker("vr_test_display") + + if self.test_display_mode == 'stereo': + # 并排显示:左眼在左半屏,右眼在右半屏 + cm.setFrame(-1, 1, -0.5, 0.5) # 全屏 + else: + # 单眼显示:占据整个屏幕 + cm.setFrame(-1, 1, -1, 1) + + # 创建节点 + self.test_display_quad = self.world.render2d.attachNewNode(cm.generate()) + + # 设置初始纹理 + self._update_test_display() + + # 设置渲染状态 + self.test_display_quad.setTransparency(0) # 不透明 + self.test_display_quad.setBin("background", 0) # 背景层 + + self.test_mode_initialized = True + print("✅ 测试显示系统初始化完成") + return True + + except Exception as e: + print(f"❌ 测试显示系统初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def _update_test_display(self): + """更新测试显示内容""" + try: + if not self.test_mode_initialized: + return + + # 根据显示模式设置纹理 + if self.test_display_mode == 'left': + if not self.test_display_quad: + return + if self.vr_left_texture: + self.test_display_quad.setTexture(self.vr_left_texture) + elif self.test_display_mode == 'right': + if not self.test_display_quad: + return + if self.vr_right_texture: + self.test_display_quad.setTexture(self.vr_right_texture) + elif self.test_display_mode == 'stereo': + # 立体显示:只在第一次创建,后续只更新纹理 + if not self.stereo_display_created: + self._create_stereo_display() + else: + # 只更新纹理,不重新创建 + if self.test_display_quad and self.vr_left_texture: + self.test_display_quad.setTexture(self.vr_left_texture) + if self.test_right_quad and self.vr_right_texture: + self.test_right_quad.setTexture(self.vr_right_texture) + + except Exception as e: + print(f"⚠️ 更新测试显示失败: {e}") + + def _create_stereo_display(self): + """创建左右眼并排显示""" + try: + # 如果已经创建,直接返回 + if self.stereo_display_created and self.test_display_quad and self.test_right_quad: + return + + # 移除旧的显示 + if self.test_display_quad: + self.test_display_quad.removeNode() + if self.test_right_quad: + self.test_right_quad.removeNode() + + from panda3d.core import CardMaker + + # 创建左眼显示 + left_cm = CardMaker("vr_test_left") + left_cm.setFrame(-1, 0, -1, 1) # 左半屏 + left_quad = self.world.render2d.attachNewNode(left_cm.generate()) + if self.vr_left_texture: + left_quad.setTexture(self.vr_left_texture) + + # 创建右眼显示 + right_cm = CardMaker("vr_test_right") + right_cm.setFrame(0, 1, -1, 1) # 右半屏 + right_quad = self.world.render2d.attachNewNode(right_cm.generate()) + if self.vr_right_texture: + right_quad.setTexture(self.vr_right_texture) + + # 保存引用 + self.test_display_quad = left_quad # 保存左眼引用 + self.test_right_quad = right_quad # 额外保存右眼引用 + self.stereo_display_created = True # 标记已创建 + + print("✓ 立体显示已创建") + + except Exception as e: + print(f"⚠️ 创建立体显示失败: {e}") + self.stereo_display_created = False + + def _cleanup_test_display(self): + """清理测试显示""" + try: + if self.test_display_quad: + self.test_display_quad.removeNode() + self.test_display_quad = None + + if hasattr(self, 'test_right_quad') and self.test_right_quad: + self.test_right_quad.removeNode() + self.test_right_quad = None + + # 重置立体显示标志位 + self.stereo_display_created = False + + print("✓ 测试显示已清理") + + except Exception as e: + print(f"⚠️ 清理测试显示失败: {e}") + + def _initialize_test_performance_hud(self): + """初始化性能HUD""" + try: + print("🔧 正在初始化性能HUD...") + + from direct.gui.OnscreenText import OnscreenText + from panda3d.core import TextNode + + # 创建性能文本显示 + self.test_performance_text = OnscreenText( + text="初始化中...", + pos=(-0.95, 0.9), # 左上角,调整到屏幕内 + scale=0.05, + fg=(1, 1, 0, 1), # 黄色 + align=TextNode.ALeft, + shadow=(0, 0, 0, 0.5), # 黑色阴影 + parent=self.world.render2d + ) + + print("✅ 性能HUD初始化完成") + return True + + except Exception as e: + print(f"❌ 性能HUD初始化失败: {e}") + return False + + def _update_test_performance_hud(self): + """更新性能HUD显示(限制更新频率避免重影)""" + try: + if not self.test_performance_text or not self.vr_test_mode: + return + + # 增加计数器 + self.hud_update_counter += 1 + + # 只在指定间隔更新文本,避免重影 + if self.hud_update_counter < self.hud_update_interval: + return + + # 重置计数器 + self.hud_update_counter = 0 + + # 收集性能数据 + left_render_time = getattr(self, 'left_render_time', 0) + right_render_time = getattr(self, 'right_render_time', 0) + total_render_time = left_render_time + right_render_time + + # 计算FPS + current_fps = self.vr_fps if hasattr(self, 'vr_fps') else 0 + + # 获取系统性能 + cpu_usage = getattr(self, 'cpu_usage', 0) + memory_usage = getattr(self, 'memory_usage', 0) + gpu_usage = getattr(self, 'gpu_usage', 0) + + # 构建显示文本 + hud_text = f"""VR TEST MODE - {self.test_display_mode.upper()} + +Render Performance: + FPS: {current_fps:.1f} + Left Eye: {left_render_time:.2f}ms + Right Eye: {right_render_time:.2f}ms + Total: {total_render_time:.2f}ms + +System Performance: + CPU: {cpu_usage:.1f}% + Memory: {memory_usage:.1f}% + GPU: {gpu_usage:.1f}% + +Render Count: + Left Eye: {getattr(self, 'left_render_count', 0)} + Right Eye: {getattr(self, 'right_render_count', 0)} + +Target: <13.9ms@72Hz +Status: {'GOOD' if total_render_time < 10 else 'HIGH' if total_render_time < 16 else 'SLOW'} + +Hotkeys: +F1=Left F2=Right F3=SideBySide +ESC=Exit Test Mode""" + + # 更新文本 + self.test_performance_text.setText(hud_text) + + except Exception as e: + print(f"⚠️ 更新性能HUD失败: {e}") + + def _cleanup_test_performance_hud(self): + """清理性能HUD""" + try: + if self.test_performance_text: + self.test_performance_text.destroy() + self.test_performance_text = None + + print("✓ 性能HUD已清理") + + except Exception as e: + print(f"⚠️ 清理性能HUD失败: {e}") + + def get_test_mode_status(self): + """获取测试模式状态""" + if not self.vr_test_mode: + return None + + left_render_time = getattr(self, 'left_render_time', 0) + right_render_time = getattr(self, 'right_render_time', 0) + total_render_time = left_render_time + right_render_time + + return { + 'test_mode_enabled': self.vr_test_mode, + 'display_mode': self.test_display_mode, + 'vr_fps': getattr(self, 'vr_fps', 0), + 'left_render_time': left_render_time, + 'right_render_time': right_render_time, + 'total_render_time': total_render_time, + 'left_render_count': getattr(self, 'left_render_count', 0), + 'right_render_count': getattr(self, 'right_render_count', 0), + 'performance_rating': ('excellent' if total_render_time < 10 else + 'good' if total_render_time < 16 else 'poor') + } + + def set_test_mode_features(self, submit_texture=None, wait_poses=None): + """设置测试模式功能开关 - 用于渐进式调试 + + Args: + submit_texture: 是否在测试模式启用纹理提交(True/False/None=保持当前) + wait_poses: 是否在测试模式启用waitGetPoses(True/False/None=保持当前) + """ + if submit_texture is not None: + self.test_mode_submit_texture = submit_texture + if wait_poses is not None: + self.test_mode_wait_poses = wait_poses + + print(f"🔧 VR测试模式功能设置:") + print(f" 纹理提交: {'启用' if self.test_mode_submit_texture else '禁用'}") + print(f" 姿态等待: {'启用' if self.test_mode_wait_poses else '禁用'}") + + # 如果当前在测试模式,输出预期效果 + if self.vr_test_mode: + print(f" 当前测试模式已启用,设置将立即生效") + else: + print(f" 设置已保存,将在下次启用测试模式时生效") + + def get_test_mode_features(self): + """获取当前测试模式功能设置""" + return { + 'submit_texture': self.test_mode_submit_texture, + 'wait_poses': self.test_mode_wait_poses, + 'test_mode_active': self.vr_test_mode + } + + def run_vr_performance_test(self, duration_seconds=30, display_mode='stereo'): + """运行VR性能测试 - 简单的测试入口方法 + + Args: + duration_seconds: 测试持续时间(秒) + display_mode: 显示模式 ('stereo', 'left', 'right') + + Returns: + dict: 测试结果统计 + """ + print("🧪 ======= VR性能测试开始 =======") + print(f" 测试模式: {display_mode}") + print(f" 测试时长: {duration_seconds}秒") + print(" 按ESC键提前退出测试") + print(" F1=左眼 F2=右眼 F3=并排显示") + print("=================================") + + # 启动测试模式 + if not self.enable_vr_test_mode(display_mode): + print("❌ VR测试模式启动失败") + return None + + # 记录测试开始状态 + import time + test_start_time = time.time() + start_frame_count = getattr(self, 'frame_count', 0) + + print("✅ VR测试模式已启动") + print(" - VR内容直接显示在屏幕上") + print(" - 无OpenVR纹理提交开销") + print(" - 实时性能监控已启用") + print(f" - 当前显示: {display_mode.upper()}模式") + print("\n开始性能测试...") + + # 等待测试完成(用户手动停止或超时) + print(f"\n📊 测试将运行 {duration_seconds} 秒") + print(" 实时性能数据显示在屏幕左上角") + print(" 观察帧率和渲染时间变化") + + # 这里可以添加自动停止逻辑,但现在让用户手动控制 + print(f"\n⏰ 请观察 {duration_seconds} 秒后手动调用 disable_vr_test_mode() 停止测试") + print(" 或者随时调用 get_test_mode_status() 查看当前状态") + + return { + 'status': 'running', + 'start_time': test_start_time, + 'start_frame': start_frame_count, + 'display_mode': display_mode, + 'instructions': { + 'stop_test': 'vr_manager.disable_vr_test_mode()', + 'check_status': 'vr_manager.get_test_mode_status()', + 'switch_mode': 'vr_manager.switch_test_display_mode("left/right/stereo")' + } + } \ No newline at end of file diff --git a/core/vr_teleport.py b/core/vr_teleport.py new file mode 100644 index 00000000..25dd7b53 --- /dev/null +++ b/core/vr_teleport.py @@ -0,0 +1,411 @@ +""" +VR传送系统模块 + +提供VR传送功能: +- 抛物线轨迹计算和可视化 +- 传送点有效性检测 +- 传送执行 +- 可视化反馈(抛物线、落点标记) +""" + +import math +from panda3d.core import ( + Vec3, Vec4, Mat4, Point3, CollisionRay, CollisionTraverser, + CollisionNode, CollisionHandlerQueue, BitMask32, NodePath, + CollisionSphere, CollisionPlane, LineSegs, GeomNode, Material, + RenderState, TransparencyAttrib, ColorAttrib +) +from direct.showbase.DirectObject import DirectObject + + +class VRTeleportSystem(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.teleport_range = 20.0 # 最大传送距离 + self.arc_resolution = 50 # 抛物线精度 + self.gravity = -9.8 # 重力系数 + self.initial_velocity = 10.0 # 初始速度 + self.min_teleport_distance = 1.0 # 最小传送距离 + + # 可视化元素 + self.teleport_arc_node = None # 抛物线节点 + self.teleport_target_node = None # 落点标记节点 + self.teleport_invalid_node = None # 无效位置标记 + + # 传送状态 + self.is_teleport_active = False # 是否正在预览传送 + self.teleport_target_pos = None # 传送目标位置 + self.teleport_valid = False # 传送位置是否有效 + self.active_controller = None # 正在使用传送的控制器 + + # 碰撞检测 + self.teleport_collision_traverser = CollisionTraverser() + self.teleport_collision_queue = CollisionHandlerQueue() + + # 可视化颜色 + self.valid_arc_color = Vec4(0.2, 0.9, 0.2, 0.8) # 绿色抛物线 + self.invalid_arc_color = Vec4(0.9, 0.2, 0.2, 0.8) # 红色抛物线 + self.target_color = Vec4(0.2, 0.7, 1.0, 0.9) # 蓝色落点 + + print("✓ VR传送系统初始化完成") + + def initialize(self): + """初始化传送系统""" + try: + print("🔧 正在初始化VR传送系统...") + + # 创建可视化元素 + self._create_teleport_visuals() + + # 设置地面检测 + self._setup_ground_detection() + + print("✅ VR传送系统初始化成功") + return True + + except Exception as e: + print(f"❌ VR传送系统初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_teleport_visuals(self): + """创建传送可视化元素""" + if not self.world or not hasattr(self.world, 'render'): + print("⚠️ 无法创建传送可视化 - 缺少世界渲染节点") + return + + # 创建抛物线节点 + self.teleport_arc_node = self.world.render.attachNewNode("teleport_arc") + self.teleport_arc_node.hide() # 初始隐藏 + + # 创建落点标记 + self._create_target_marker() + + # 创建无效位置标记 + self._create_invalid_marker() + + print("✓ 传送可视化元素已创建") + + def _create_target_marker(self): + """创建传送目标标记""" + try: + # 创建一个圆形平台作为落点标记 + from panda3d.core import CardMaker + + cm = CardMaker("teleport_target") + cm.setFrame(-1, 1, -1, 1) # 2x2的平面 + + self.teleport_target_node = self.world.render.attachNewNode(cm.generate()) + self.teleport_target_node.setScale(1.0) # 2米直径的圆形 + self.teleport_target_node.setP(-90) # 平放在地面 + self.teleport_target_node.setColor(self.target_color) + self.teleport_target_node.setTransparency(TransparencyAttrib.MAlpha) + self.teleport_target_node.hide() + + # 添加PBR材质以兼容RenderPipeline + material = Material() + material.setBaseColor(self.target_color) + material.setRoughness(0.8) + material.setMetallic(0.0) + material.setRefractiveIndex(1.5) + self.teleport_target_node.setMaterial(material, 1) + + except Exception as e: + print(f"⚠️ 创建传送目标标记失败: {e}") + + def _create_invalid_marker(self): + """创建无效位置标记""" + try: + from panda3d.core import CardMaker + + cm = CardMaker("teleport_invalid") + cm.setFrame(-0.8, 0.8, -0.8, 0.8) # 稍小的红色叉号 + + self.teleport_invalid_node = self.world.render.attachNewNode(cm.generate()) + self.teleport_invalid_node.setScale(1.0) + self.teleport_invalid_node.setP(-90) + self.teleport_invalid_node.setColor(self.invalid_arc_color) + self.teleport_invalid_node.setTransparency(TransparencyAttrib.MAlpha) + self.teleport_invalid_node.hide() + + # 添加PBR材质 + material = Material() + material.setBaseColor(self.invalid_arc_color) + material.setRoughness(0.8) + material.setMetallic(0.0) + material.setRefractiveIndex(1.5) + self.teleport_invalid_node.setMaterial(material, 1) + + except Exception as e: + print(f"⚠️ 创建无效位置标记失败: {e}") + + def _setup_ground_detection(self): + """设置地面检测""" + try: + # 假设地面在Z=0平面,使用正确的CollisionPlane API + from panda3d.core import Plane + ground_plane = CollisionPlane(Plane(Vec3(0, 0, 1), Point3(0, 0, 0))) + + ground_node = CollisionNode("ground_detection") + ground_node.addSolid(ground_plane) + ground_node.setIntoCollideMask(BitMask32.bit(1)) # 地面碰撞掩码 + + # 附加到渲染节点 + if self.world and hasattr(self.world, 'render'): + ground_node_path = self.world.render.attachNewNode(ground_node) + print("✓ 地面检测设置成功") + + except Exception as e: + print(f"⚠️ 设置地面检测失败: {e}") + # 继续运行,不让这个错误阻止传送系统 + + def start_teleport_preview(self, controller, direction): + """开始传送预览 + + Args: + controller: VR控制器实例 + direction: 传送方向向量(摇杆输入转换的世界方向) + """ + if not controller or not controller.anchor_node: + return False + + self.is_teleport_active = True + self.active_controller = controller + + # 计算抛物线轨迹 + start_pos = controller.get_world_position() + self._calculate_teleport_trajectory(start_pos, direction) + + # 显示可视化 + self._show_teleport_visuals() + + return True + + def _calculate_teleport_trajectory(self, start_pos, direction): + """计算传送抛物线轨迹 + + Args: + start_pos: 起始位置 + direction: 方向向量 + """ + try: + # 确保方向向量已标准化 + direction = direction.normalized() + + # 计算抛物线点 + trajectory_points = [] + hit_ground = False + self.teleport_valid = False + + # 初始速度向量 + velocity = direction * self.initial_velocity + velocity.z += 2.0 # 给一些向上的初始速度 + + dt = 0.05 # 时间步长 + current_pos = Vec3(start_pos) + current_velocity = Vec3(velocity) + + for i in range(self.arc_resolution): + trajectory_points.append(Point3(current_pos)) + + # 更新位置和速度 + current_pos += current_velocity * dt + current_velocity.z += self.gravity * dt # 应用重力 + + # 检查是否碰撞地面 + if current_pos.z <= 0.1: # 假设地面在z=0 + # 精确计算落地点 + ground_pos = self._calculate_ground_intersection( + trajectory_points[-2] if len(trajectory_points) > 1 else start_pos, + current_pos + ) + trajectory_points.append(ground_pos) + + # 检查传送有效性 + distance = (ground_pos - start_pos).length() + if (distance >= self.min_teleport_distance and + distance <= self.teleport_range): + self.teleport_target_pos = ground_pos + self.teleport_valid = True + else: + self.teleport_target_pos = ground_pos + self.teleport_valid = False + + hit_ground = True + break + + # 超出范围 + if (current_pos - start_pos).length() > self.teleport_range: + break + + # 如果没有碰撞地面,传送无效 + if not hit_ground: + if trajectory_points: + self.teleport_target_pos = trajectory_points[-1] + self.teleport_valid = False + + # 创建抛物线几何体 + self._create_arc_geometry(trajectory_points) + + except Exception as e: + print(f"⚠️ 计算传送轨迹失败: {e}") + self.teleport_valid = False + + def _calculate_ground_intersection(self, p1, p2): + """计算与地面的精确交点""" + if p1.z == p2.z: + return Point3(p2) + + # 线性插值找到z=0的点 + t = -p1.z / (p2.z - p1.z) + t = max(0, min(1, t)) # 限制在0-1范围内 + + intersection = p1 + (p2 - p1) * t + intersection.z = 0.1 # 稍微高于地面 + + return Point3(intersection) + + def _create_arc_geometry(self, points): + """创建抛物线几何体""" + if not points or len(points) < 2: + return + + try: + # 创建线段 + line_segs = LineSegs() + line_segs.setThickness(3) + + # 根据有效性设置颜色 + color = self.valid_arc_color if self.teleport_valid else self.invalid_arc_color + line_segs.setColor(color) + + # 添加线段点 + line_segs.moveTo(points[0]) + for point in points[1:]: + line_segs.drawTo(point) + + # 清除旧的几何体 + self.teleport_arc_node.removeNode() + self.teleport_arc_node = self.world.render.attachNewNode("teleport_arc") + + # 创建新的几何体 + geom_node = line_segs.create() + arc_node_path = self.teleport_arc_node.attachNewNode(geom_node) + + # 设置材质 + material = Material() + material.setBaseColor(color) + material.setRoughness(0.1) + material.setMetallic(0.0) + material.setRefractiveIndex(1.5) + arc_node_path.setMaterial(material, 1) + + # 设置透明度 + arc_node_path.setTransparency(TransparencyAttrib.MAlpha) + + except Exception as e: + print(f"⚠️ 创建抛物线几何体失败: {e}") + + def _show_teleport_visuals(self): + """显示传送可视化""" + if self.teleport_arc_node: + self.teleport_arc_node.show() + + if self.teleport_target_pos: + if self.teleport_valid and self.teleport_target_node: + # 显示有效的传送目标 + self.teleport_target_node.setPos(self.teleport_target_pos) + self.teleport_target_node.show() + self.teleport_invalid_node.hide() + elif self.teleport_invalid_node: + # 显示无效的传送位置 + self.teleport_invalid_node.setPos(self.teleport_target_pos) + self.teleport_invalid_node.show() + self.teleport_target_node.hide() + + def stop_teleport_preview(self): + """停止传送预览""" + self.is_teleport_active = False + self.active_controller = None + + # 隐藏可视化 + if self.teleport_arc_node: + self.teleport_arc_node.hide() + if self.teleport_target_node: + self.teleport_target_node.hide() + if self.teleport_invalid_node: + self.teleport_invalid_node.hide() + + def execute_teleport(self): + """执行传送""" + if not self.teleport_valid or not self.teleport_target_pos: + print("⚠️ 传送位置无效,无法执行传送") + return False + + try: + # 计算传送偏移 + if self.vr_manager.tracking_space: + current_pos = self.vr_manager.tracking_space.getPos() + target_offset = self.teleport_target_pos - self.active_controller.get_world_position() + new_pos = current_pos + target_offset + + # 执行传送 + self.vr_manager.tracking_space.setPos(new_pos) + + print(f"✅ 传送成功: {current_pos} → {new_pos}") + + # 停止预览 + self.stop_teleport_preview() + + return True + else: + print("⚠️ 无法获取VR跟踪空间,传送失败") + return False + + except Exception as e: + print(f"❌ 执行传送失败: {e}") + return False + + def update_teleport_preview(self, controller, direction): + """更新传送预览(摇杆移动时调用)""" + if self.is_teleport_active and controller == self.active_controller: + start_pos = controller.get_world_position() + self._calculate_teleport_trajectory(start_pos, direction) + self._show_teleport_visuals() + + def cleanup(self): + """清理传送系统资源""" + try: + self.stop_teleport_preview() + + if self.teleport_arc_node: + self.teleport_arc_node.removeNode() + self.teleport_arc_node = None + + if self.teleport_target_node: + self.teleport_target_node.removeNode() + self.teleport_target_node = None + + if self.teleport_invalid_node: + self.teleport_invalid_node.removeNode() + self.teleport_invalid_node = None + + self.ignoreAll() + + print("🧹 VR传送系统已清理") + + except Exception as e: + print(f"⚠️ 清理传送系统失败: {e}") \ No newline at end of file diff --git a/core/vr_visualization.py b/core/vr_visualization.py new file mode 100644 index 00000000..c331a299 --- /dev/null +++ b/core/vr_visualization.py @@ -0,0 +1,729 @@ +""" +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): + """修复模型材质,使用RenderPipeline兼容的新Material API""" + from panda3d.core import Material, Vec4 + + # 检查模型是否有纹理 + has_texture = model.hasTexture() + + # 创建新的材质,使用RenderPipeline兼容的API + material = Material() + + if has_texture: + # 有纹理时,设置白色基础颜色让纹理完全显示 + material.setBaseColor(Vec4(1.0, 1.0, 1.0, 1.0)) # 白色让纹理完全显示 + material.setRoughness(0.4) # 中等粗糙度 + material.setMetallic(0.3) # 轻度金属感 + material.setRefractiveIndex(1.5) # 标准折射率 + print(f"🎨 {self.controller.name}手柄:已设置材质(纹理+PBR)") + else: + # 无纹理时,设置手柄颜色 + material.setBaseColor(Vec4(0.7, 0.7, 0.8, 1.0)) # 略偏蓝的灰色 + material.setRoughness(0.5) # 中等粗糙度 + material.setMetallic(0.2) # 轻度金属感 + material.setRefractiveIndex(1.5) # 标准折射率 + print(f"🎨 {self.controller.name}手柄:已设置材质(颜色+PBR)") + + # 应用材质到模型,使用优先级1确保覆盖默认材质 + model.setMaterial(material, 1) + + # 确保模型能正确渲染(双面渲染是NodePath的方法) + model.setTwoSided(False) + + print(f"🔧 {self.controller.name}手柄:已使用RenderPipeline兼容的Material API") + + 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) + + # 使用RenderPipeline兼容的Material API设置射线材质 + from panda3d.core import Material, Vec4 + ray_material = Material() + ray_material.setBaseColor(Vec4(self.ray_color.x, self.ray_color.y, self.ray_color.z, self.ray_color.w)) + ray_material.setRoughness(0.1) # 光滑射线 + ray_material.setMetallic(0.0) # 非金属 + ray_material.setRefractiveIndex(1.5) # 标准折射率 + + # 为射线应用材质 + self.ray_node.setMaterial(ray_material, 1) + self.ray_node.setTwoSided(False) + + # 为端点球设置相同的材质 + end_material = Material() + end_material.setBaseColor(Vec4(self.ray_color.x, self.ray_color.y, self.ray_color.z, self.ray_color.w)) + end_material.setRoughness(0.2) # 略粗糙 + end_material.setMetallic(0.0) # 非金属 + end_material.setRefractiveIndex(1.5) # 标准折射率 + + # 为端点球应用材质 + end_node.setMaterial(end_material, 1) + end_node.setTwoSided(False) + + # 默认隐藏射线 + self.ray_node.hide() + + print(f"✓ {self.controller.name}手柄交互射线已创建(含PBR支持)") + + 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) + + # 为所有按钮指示器设置RenderPipeline兼容的材质 + from panda3d.core import Material, Vec4 + indicator_material = Material() + indicator_material.setBaseColor(Vec4(0.2, 0.2, 0.2, 1.0)) # 深灰色 + indicator_material.setRoughness(0.8) # 比较粗糙的表面 + indicator_material.setMetallic(0.1) # 轻微金属感 + indicator_material.setRefractiveIndex(1.5) # 标准折射率 + + # 为所有指示器应用材质 + for indicator in [self.trigger_indicator, self.grip_indicator, self.trackpad_indicator]: + indicator.setMaterial(indicator_material, 1) + indicator.setTwoSided(False) + + 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) + + # 更新射线材质的baseColor + from panda3d.core import Material, Vec4 + if isinstance(color, (list, tuple)) and len(color) >= 3: + alpha = color[3] if len(color) > 3 else 0.8 + ray_color = Vec4(color[0], color[1], color[2], alpha) + elif isinstance(color, Vec4): + ray_color = color + else: + ray_color = Vec4(0.9, 0.9, 0.2, 0.8) # 默认黄色 + + # 更新射线材质 + ray_material = Material() + ray_material.setBaseColor(ray_color) + ray_material.setRoughness(0.1) + ray_material.setMetallic(0.0) + ray_material.setRefractiveIndex(1.5) + self.ray_node.setMaterial(ray_material, 1) + + 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 hasattr(self, 'ray_node') and self.ray_node: + self.ray_node.removeNode() + self.ray_node = None + print(f"🧹 {self.controller.name}手柄射线已清理") + + # 清理模型节点 + if hasattr(self, 'model_node') and self.model_node: + self.model_node.removeNode() + self.model_node = None + print(f"🧹 {self.controller.name}手柄模型已清理") + + # 清理主可视化节点 + if self.visual_node: + self.visual_node.removeNode() + self.visual_node = None + print(f"🧹 {self.controller.name}手柄主节点已清理") + + print(f"✅ {self.controller.name}手柄可视化已彻底清理") \ 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 8376d4a5..d03ddd78 100644 --- a/main.py +++ b/main.py @@ -21,11 +21,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 core.patrol_system import PatrolSystem -from core.Command_System import CommandManager from gui.gui_manager import GUIManager from core.terrain_manager import TerrainManager from scene.scene_manager import SceneManager @@ -96,10 +91,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() @@ -119,6 +110,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 = True # 是否显示碰撞体 @@ -621,143 +621,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/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/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/run_vr_test.sh b/run_vr_test.sh new file mode 100755 index 00000000..e6202e9a --- /dev/null +++ b/run_vr_test.sh @@ -0,0 +1,123 @@ +#!/bin/bash + +# VR性能测试启动脚本 +# 使用方法: ./run_vr_test.sh + +echo "🚀 VR性能测试启动脚本" +echo "======================" + +# 检查是否在正确的目录 +if [ ! -f "vr_performance_test.py" ]; then + echo "❌ 错误:找不到vr_performance_test.py文件" + echo "请在EG项目根目录运行此脚本" + exit 1 +fi + +# 检查Python是否可用 +if ! command -v python3 &> /dev/null; then + if ! command -v python &> /dev/null; then + echo "❌ 错误:找不到Python解释器" + exit 1 + else + PYTHON_CMD="python" + fi +else + PYTHON_CMD="python3" +fi + +echo "✓ 使用Python解释器: $PYTHON_CMD" + +# 检查核心模块是否存在 +if [ ! -d "core" ]; then + echo "❌ 错误:找不到core目录" + echo "请确保在EG项目根目录中运行" + exit 1 +fi + +if [ ! -f "core/vr_manager.py" ]; then + echo "❌ 错误:找不到core/vr_manager.py文件" + echo "VR管理器模块不存在" + exit 1 +fi + +echo "✓ 核心模块检查通过" + +# 检查VR相关依赖 +echo "🔍 检查VR依赖..." + +$PYTHON_CMD -c "import openvr" 2>/dev/null +if [ $? -eq 0 ]; then + echo "✓ OpenVR Python绑定可用" +else + echo "⚠️ 警告:OpenVR Python绑定不可用" + echo " 如果需要VR功能,请安装: pip install openvr" +fi + +$PYTHON_CMD -c "from panda3d.core import Vec3" 2>/dev/null +if [ $? -eq 0 ]; then + echo "✓ Panda3D可用" +else + echo "❌ 错误:Panda3D不可用" + echo "请安装Panda3D: pip install panda3d" + exit 1 +fi + +# 检查可选的性能监控依赖 +echo "🔍 检查性能监控依赖..." + +$PYTHON_CMD -c "import psutil" 2>/dev/null +if [ $? -eq 0 ]; then + echo "✓ psutil可用(CPU/内存监控)" +else + echo "⚠️ 建议安装psutil以获得完整性能监控: pip install psutil" +fi + +$PYTHON_CMD -c "import GPUtil" 2>/dev/null +if [ $? -eq 0 ]; then + echo "✓ GPUtil可用(GPU监控)" +else + echo "⚠️ 建议安装GPUtil以获得GPU监控: pip install GPUtil" +fi + +echo "" +echo "🎮 启动前检查清单:" +echo "□ VR头显已连接并开机" +echo "□ SteamVR正在运行" +echo "□ VR头显已完成初始设置" +echo "□ VR跟踪系统正常工作" +echo "" + +# 询问用户是否继续 +echo "是否继续启动VR性能测试?" +echo "按Enter继续,或按Ctrl+C取消..." +read -r + +echo "" +echo "🚀 正在启动VR性能测试..." +echo "控制说明:" +echo " ESC - 退出测试" +echo " 1-9 - 设置场景复杂度" +echo " R - 重置性能计数器" +echo " P - 手动输出性能报告" +echo " D - 切换调试模式" +echo "VR分辨率控制:" +echo " Q - 切换质量预设 (性能/平衡/质量)" +echo " [ - 降低分辨率缩放" +echo " ] - 提高分辨率缩放" +echo " I - 显示分辨率信息" +echo "" + +# 启动测试 +$PYTHON_CMD vr_performance_test.py + +# 检查退出状态 +if [ $? -eq 0 ]; then + echo "" + echo "✅ VR性能测试正常结束" +else + echo "" + echo "❌ VR性能测试异常退出(错误代码:$?)" +fi + +echo "📋 检查当前目录中的CSV文件获取详细性能数据" +echo "📖 查看VR_PERFORMANCE_TEST_README.md获取详细使用说明" \ No newline at end of file diff --git a/test_metallic_gradient.png b/test_metallic_gradient.png deleted file mode 100644 index 11dbcc81..00000000 Binary files a/test_metallic_gradient.png and /dev/null differ diff --git a/test_metallic_stripes.png b/test_metallic_stripes.png deleted file mode 100644 index 956c8661..00000000 Binary files a/test_metallic_stripes.png and /dev/null differ diff --git a/test_roughness_checkerboard.png b/test_roughness_checkerboard.png deleted file mode 100644 index 3ee2bd65..00000000 Binary files a/test_roughness_checkerboard.png and /dev/null differ diff --git a/test_roughness_circle.png b/test_roughness_circle.png deleted file mode 100644 index 5ab3d4d6..00000000 Binary files a/test_roughness_circle.png and /dev/null differ diff --git a/test_roughness_gradient.png b/test_roughness_gradient.png deleted file mode 100644 index 11dbcc81..00000000 Binary files a/test_roughness_gradient.png and /dev/null differ diff --git a/ui/main_window.py b/ui/main_window.py index a05d0ae9..a13fb789 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -767,6 +767,105 @@ 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设置') + self.vrMenu.addSeparator() + + # VR调试子菜单 + self.vrDebugMenu = self.vrMenu.addMenu('VR调试') + self.vrDebugToggleAction = self.vrDebugMenu.addAction('启用调试输出') + self.vrDebugToggleAction.setCheckable(True) + self.vrDebugToggleAction.setChecked(False) # 默认关闭(节省资源) + + self.vrShowPerformanceAction = self.vrDebugMenu.addAction('立即显示性能报告') + + self.vrDebugMenu.addSeparator() + + # 调试模式切换 + self.vrDebugModeMenu = self.vrDebugMenu.addMenu('输出模式') + self.vrDebugBriefAction = self.vrDebugModeMenu.addAction('简短模式') + self.vrDebugDetailedAction = self.vrDebugModeMenu.addAction('详细模式') + self.vrDebugBriefAction.setCheckable(True) + self.vrDebugDetailedAction.setCheckable(True) + self.vrDebugDetailedAction.setChecked(True) # 默认详细模式 + + # 创建调试模式动作组(单选) + from PyQt5.QtWidgets import QActionGroup + self.vrDebugModeGroup = QActionGroup(self) + self.vrDebugModeGroup.addAction(self.vrDebugBriefAction) + self.vrDebugModeGroup.addAction(self.vrDebugDetailedAction) + + self.vrDebugMenu.addSeparator() + + # 性能监控选项 + self.vrPerformanceMonitorAction = self.vrDebugMenu.addAction('启用性能监控') + self.vrPerformanceMonitorAction.setCheckable(True) + self.vrPerformanceMonitorAction.setChecked(False) # 默认关闭(节省资源) + + self.vrGpuTimingAction = self.vrDebugMenu.addAction('启用GPU时间监控') + self.vrGpuTimingAction.setCheckable(True) + self.vrGpuTimingAction.setChecked(False) # 默认关闭(节省资源) + + # 管线监控选项 + self.vrPipelineMonitorAction = self.vrDebugMenu.addAction('启用管线监控') + self.vrPipelineMonitorAction.setCheckable(True) + self.vrPipelineMonitorAction.setChecked(False) # 默认关闭(节省资源) + + self.vrDebugMenu.addSeparator() + + # 姿态策略选项 + self.vrPoseStrategyMenu = self.vrDebugMenu.addMenu('姿态策略') + self.vrPoseRenderCallbackAction = self.vrPoseStrategyMenu.addAction('渲染回调策略') + self.vrPoseUpdateTaskAction = self.vrPoseStrategyMenu.addAction('更新任务策略') + self.vrPoseRenderCallbackAction.setCheckable(True) + self.vrPoseUpdateTaskAction.setCheckable(True) + self.vrPoseRenderCallbackAction.setChecked(True) # 默认策略 + + # 创建姿态策略动作组(单选) + self.vrPoseStrategyGroup = QActionGroup(self) + self.vrPoseStrategyGroup.addAction(self.vrPoseRenderCallbackAction) + self.vrPoseStrategyGroup.addAction(self.vrPoseUpdateTaskAction) + + self.vrDebugMenu.addSeparator() + + # 测试功能 + self.vrTestPipelineAction = self.vrDebugMenu.addAction('测试管线监控') + + # VR测试模式 + self.vrTestModeAction = self.vrDebugMenu.addAction('VR测试模式') + self.vrTestModeAction.setCheckable(True) + self.vrTestModeAction.setChecked(False) # 默认关闭 + + # VR测试模式调试子菜单 + self.vrTestDebugMenu = self.vrDebugMenu.addMenu('测试模式调试') + + # 渐进式功能开关 + self.vrTestSubmitTextureAction = self.vrTestDebugMenu.addAction('启用纹理提交') + self.vrTestSubmitTextureAction.setCheckable(True) + self.vrTestSubmitTextureAction.setChecked(False) # 默认关闭 + + self.vrTestWaitPosesAction = self.vrTestDebugMenu.addAction('启用姿态等待') + self.vrTestWaitPosesAction.setCheckable(True) + self.vrTestWaitPosesAction.setChecked(False) # 默认关闭 + + self.vrTestDebugMenu.addSeparator() + + # 快捷测试预设 + self.vrTestStep1Action = self.vrTestDebugMenu.addAction('步骤1: 只启用纹理提交') + self.vrTestStep2Action = self.vrTestDebugMenu.addAction('步骤2: 只启用姿态等待') + self.vrTestStep3Action = self.vrTestDebugMenu.addAction('步骤3: 同时启用两者') + self.vrTestResetAction = self.vrTestDebugMenu.addAction('重置: 禁用所有功能') + + self.vrDebugSettingsAction = self.vrDebugMenu.addAction('调试设置...') + + # 初始状态下禁用退出VR选项 + self.exitVRAction.setEnabled(False) + # 帮助菜单 self.helpMenu = menubar.addMenu('帮助') self.aboutAction = self.helpMenu.addAction('关于') @@ -1857,6 +1956,35 @@ 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) + + # 连接VR调试菜单事件 + self.vrDebugToggleAction.triggered.connect(self.onToggleVRDebug) + self.vrShowPerformanceAction.triggered.connect(self.onShowVRPerformance) + self.vrDebugBriefAction.triggered.connect(lambda: self.onSetVRDebugMode('brief')) + self.vrDebugDetailedAction.triggered.connect(lambda: self.onSetVRDebugMode('detailed')) + self.vrPerformanceMonitorAction.triggered.connect(self.onToggleVRPerformanceMonitor) + self.vrGpuTimingAction.triggered.connect(self.onToggleVRGpuTiming) + self.vrPipelineMonitorAction.triggered.connect(self.onToggleVRPipelineMonitor) + self.vrPoseRenderCallbackAction.triggered.connect(lambda: self.onSetVRPoseStrategy('render_callback')) + self.vrPoseUpdateTaskAction.triggered.connect(lambda: self.onSetVRPoseStrategy('update_task')) + self.vrTestPipelineAction.triggered.connect(self.onTestVRPipeline) + self.vrTestModeAction.triggered.connect(self.onToggleVRTestMode) + + # 连接VR测试模式调试菜单事件 + self.vrTestSubmitTextureAction.triggered.connect(self.onToggleVRTestSubmitTexture) + self.vrTestWaitPosesAction.triggered.connect(self.onToggleVRTestWaitPoses) + self.vrTestStep1Action.triggered.connect(lambda: self.onSetVRTestStep(1)) + self.vrTestStep2Action.triggered.connect(lambda: self.onSetVRTestStep(2)) + self.vrTestStep3Action.triggered.connect(lambda: self.onSetVRTestStep(3)) + self.vrTestResetAction.triggered.connect(lambda: self.onSetVRTestStep(0)) + + self.vrDebugSettingsAction.triggered.connect(self.onShowVRDebugSettings) + def onCopy(self): """复制操作""" try: @@ -3114,13 +3242,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资源...") @@ -3308,6 +3444,545 @@ 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)}") + + # ==================== VR调试事件处理 ==================== + + def onToggleVRDebug(self): + """切换VR调试输出""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + enabled = self.world.vr_manager.toggle_debug_output() + self.vrDebugToggleAction.setChecked(enabled) + + status = "启用" if enabled else "禁用" + QMessageBox.information(self, "VR调试", f"VR调试输出已{status}") + else: + QMessageBox.warning(self, "错误", "VR管理器不可用!") + except Exception as e: + QMessageBox.critical(self, "错误", f"切换VR调试时发生错误:\n{str(e)}") + + def onShowVRPerformance(self): + """立即显示VR性能报告""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + if self.world.vr_manager.vr_enabled: + self.world.vr_manager.force_performance_report() + QMessageBox.information(self, "VR性能", "性能报告已输出到控制台") + else: + QMessageBox.warning(self, "提示", "请先启用VR模式") + else: + QMessageBox.warning(self, "错误", "VR管理器不可用!") + except Exception as e: + QMessageBox.critical(self, "错误", f"显示VR性能报告时发生错误:\n{str(e)}") + + def onSetVRDebugMode(self, mode): + """设置VR调试模式""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + self.world.vr_manager.set_debug_mode(mode) + + # 更新菜单状态 + if mode == 'brief': + self.vrDebugBriefAction.setChecked(True) + self.vrDebugDetailedAction.setChecked(False) + else: + self.vrDebugBriefAction.setChecked(False) + self.vrDebugDetailedAction.setChecked(True) + + mode_name = "简短" if mode == 'brief' else "详细" + QMessageBox.information(self, "VR调试", f"调试模式已设置为:{mode_name}") + else: + QMessageBox.warning(self, "错误", "VR管理器不可用!") + except Exception as e: + QMessageBox.critical(self, "错误", f"设置VR调试模式时发生错误:\n{str(e)}") + + def onToggleVRPerformanceMonitor(self): + """切换VR性能监控""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + enabled = self.vrPerformanceMonitorAction.isChecked() + if enabled: + self.world.vr_manager.enable_performance_monitoring() + else: + self.world.vr_manager.disable_performance_monitoring() + + status = "启用" if enabled else "禁用" + QMessageBox.information(self, "VR性能监控", f"VR性能监控已{status}") + else: + QMessageBox.warning(self, "错误", "VR管理器不可用!") + self.vrPerformanceMonitorAction.setChecked(False) + except Exception as e: + QMessageBox.critical(self, "错误", f"切换VR性能监控时发生错误:\n{str(e)}") + + def onToggleVRGpuTiming(self): + """切换VR GPU时间监控""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + enabled = self.vrGpuTimingAction.isChecked() + if enabled: + self.world.vr_manager.enable_gpu_timing_monitoring() + else: + self.world.vr_manager.disable_gpu_timing_monitoring() + + status = "启用" if enabled else "禁用" + QMessageBox.information(self, "VR GPU监控", f"VR GPU时间监控已{status}") + else: + QMessageBox.warning(self, "错误", "VR管理器不可用!") + self.vrGpuTimingAction.setChecked(False) + except Exception as e: + QMessageBox.critical(self, "错误", f"切换VR GPU时间监控时发生错误:\n{str(e)}") + + def onToggleVRPipelineMonitor(self): + """切换VR管线监控""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + enabled = self.vrPipelineMonitorAction.isChecked() + self.world.vr_manager.enable_pipeline_monitoring = enabled + status = "启用" if enabled else "禁用" + print(f"✓ VR管线监控已{status}") + else: + QMessageBox.warning(self, "错误", "VR管理器不可用!") + self.vrPipelineMonitorAction.setChecked(False) + except Exception as e: + QMessageBox.critical(self, "错误", f"切换VR管线监控时发生错误:\n{str(e)}") + + def onSetVRPoseStrategy(self, strategy): + """设置VR姿态策略""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + success = self.world.vr_manager.set_pose_strategy(strategy) + if success: + strategy_names = { + 'render_callback': '渲染回调策略', + 'update_task': '更新任务策略' + } + QMessageBox.information(self, "VR姿态策略", + f"姿态策略已切换为:{strategy_names.get(strategy, strategy)}") + else: + QMessageBox.warning(self, "错误", f"无效的姿态策略:{strategy}") + else: + QMessageBox.warning(self, "错误", "VR管理器不可用!") + except Exception as e: + QMessageBox.critical(self, "错误", f"设置VR姿态策略时发生错误:\n{str(e)}") + + def onTestVRPipeline(self): + """测试VR管线监控功能""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + self.world.vr_manager.test_pipeline_monitoring() + QMessageBox.information(self, "VR管线测试", "管线监控测试已完成,请查看控制台输出。") + else: + QMessageBox.warning(self, "错误", "VR管理器不可用!") + except Exception as e: + QMessageBox.critical(self, "错误", f"测试VR管线监控时发生错误:\n{str(e)}") + + def onShowVRDebugSettings(self): + """显示VR调试设置对话框""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + dialog = self.createVRDebugSettingsDialog() + dialog.exec_() + else: + QMessageBox.warning(self, "错误", "VR管理器不可用!") + except Exception as e: + QMessageBox.critical(self, "错误", f"打开VR调试设置时发生错误:\n{str(e)}") + + def createVRDebugSettingsDialog(self): + """创建VR调试设置对话框""" + from PyQt5.QtWidgets import QCheckBox, QSlider + from PyQt5.QtCore import Qt + + dialog = QDialog(self) + dialog.setWindowTitle("VR调试设置") + dialog.setModal(True) + dialog.resize(450, 400) + + layout = QVBoxLayout(dialog) + + # 获取当前设置 + vr_manager = self.world.vr_manager + debug_status = vr_manager.get_debug_status() + perf_config = vr_manager.get_performance_monitoring_config() + + # 调试状态显示 + status_group = QGroupBox("调试状态") + status_layout = QVBoxLayout() + + debug_enabled_label = QLabel(f"调试输出: {'启用' if debug_status['debug_enabled'] else '禁用'}") + debug_enabled_label.setStyleSheet(f"color: {'green' if debug_status['debug_enabled'] else 'red'};") + status_layout.addWidget(debug_enabled_label) + + debug_mode_label = QLabel(f"调试模式: {debug_status['debug_mode']}") + status_layout.addWidget(debug_mode_label) + + performance_label = QLabel(f"性能监控: {'启用' if debug_status['performance_monitoring'] else '禁用'}") + performance_label.setStyleSheet(f"color: {'green' if debug_status['performance_monitoring'] else 'red'};") + status_layout.addWidget(performance_label) + + status_group.setLayout(status_layout) + layout.addWidget(status_group) + + # 报告设置 + report_group = QGroupBox("报告设置") + report_layout = QFormLayout() + + # 报告间隔滑块 (5-120秒) + interval_slider = QSlider(Qt.Horizontal) + interval_slider.setMinimum(5) + interval_slider.setMaximum(120) + interval_slider.setValue(int(debug_status['report_interval_seconds'])) + interval_slider.setTickPosition(QSlider.TicksBelow) + interval_slider.setTickInterval(15) + + interval_label = QLabel(f"{int(debug_status['report_interval_seconds'])}秒") + interval_slider.valueChanged.connect(lambda v: interval_label.setText(f"{v}秒")) + + interval_layout = QHBoxLayout() + interval_layout.addWidget(interval_slider) + interval_layout.addWidget(interval_label) + report_layout.addRow("报告间隔:", interval_layout) + + # 性能检查间隔 + check_interval_combo = QComboBox() + check_interval_combo.addItems(["0.1秒", "0.5秒", "1.0秒", "2.0秒"]) + current_check_interval = perf_config['check_interval'] + if current_check_interval == 0.1: + check_interval_combo.setCurrentIndex(0) + elif current_check_interval == 0.5: + check_interval_combo.setCurrentIndex(1) + elif current_check_interval == 1.0: + check_interval_combo.setCurrentIndex(2) + else: + check_interval_combo.setCurrentIndex(3) + report_layout.addRow("性能检查间隔:", check_interval_combo) + + # 帧历史大小 + frame_history_spin = QSpinBox() + frame_history_spin.setMinimum(10) + frame_history_spin.setMaximum(1000) + frame_history_spin.setValue(perf_config['frame_history_size']) + frame_history_spin.setSuffix(" 帧") + report_layout.addRow("帧时间历史:", frame_history_spin) + + report_group.setLayout(report_layout) + layout.addWidget(report_group) + + # 监控项目 + monitor_group = QGroupBox("监控项目") + monitor_layout = QVBoxLayout() + + cpu_check = QCheckBox("CPU使用率") + cpu_check.setChecked(perf_config['psutil_available']) + cpu_check.setEnabled(perf_config['psutil_available']) + monitor_layout.addWidget(cpu_check) + + memory_check = QCheckBox("内存使用率") + memory_check.setChecked(perf_config['psutil_available']) + memory_check.setEnabled(perf_config['psutil_available']) + monitor_layout.addWidget(memory_check) + + gpu_check = QCheckBox("GPU使用率") + gpu_check.setChecked(perf_config['gputil_available'] or perf_config['nvidia_ml_available']) + gpu_check.setEnabled(perf_config['gputil_available'] or perf_config['nvidia_ml_available']) + monitor_layout.addWidget(gpu_check) + + frame_time_check = QCheckBox("帧时间统计") + frame_time_check.setChecked(True) + monitor_layout.addWidget(frame_time_check) + + monitor_group.setLayout(monitor_layout) + layout.addWidget(monitor_group) + + # 按钮 + button_layout = QHBoxLayout() + + apply_button = QPushButton("应用") + reset_button = QPushButton("重置计数器") + ok_button = QPushButton("确定") + cancel_button = QPushButton("取消") + + button_layout.addWidget(apply_button) + button_layout.addWidget(reset_button) + button_layout.addStretch() + button_layout.addWidget(ok_button) + button_layout.addWidget(cancel_button) + + layout.addLayout(button_layout) + + # 连接信号 + def apply_settings(): + try: + # 应用报告间隔 + new_interval_seconds = interval_slider.value() + new_interval_frames = int(new_interval_seconds * 60) # 假设60fps + vr_manager.set_performance_report_interval(new_interval_frames) + + # 应用性能检查间隔 + check_intervals = [0.1, 0.5, 1.0, 2.0] + new_check_interval = check_intervals[check_interval_combo.currentIndex()] + vr_manager.set_performance_check_interval(new_check_interval) + + # 应用帧历史大小 + vr_manager.set_frame_time_history_size(frame_history_spin.value()) + + QMessageBox.information(dialog, "成功", "VR调试设置已应用!") + except Exception as e: + QMessageBox.critical(dialog, "错误", f"应用设置时发生错误:\n{str(e)}") + + def reset_counters(): + try: + vr_manager.reset_performance_counters() + QMessageBox.information(dialog, "成功", "性能计数器已重置!") + except Exception as e: + QMessageBox.critical(dialog, "错误", f"重置计数器时发生错误:\n{str(e)}") + + apply_button.clicked.connect(apply_settings) + reset_button.clicked.connect(reset_counters) + ok_button.clicked.connect(lambda: (apply_settings(), dialog.accept())) + cancel_button.clicked.connect(dialog.reject) + + return dialog + + # ==================== VR测试模式事件处理 ==================== + + def onToggleVRTestMode(self): + """切换VR测试模式""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + if self.vrTestModeAction.isChecked(): + # 启用VR测试模式 + success = self.world.vr_manager.enable_vr_test_mode(display_mode='stereo') + if success: + QMessageBox.information(self, "VR测试模式", + "VR测试模式已启用!\n\n现在VR渲染内容将直接显示在PC屏幕上,无需VR头显。\n\n特点:\n- 显示VR左右眼视图\n- 实时性能监控HUD\n- 复用完整VR渲染管线\n- 可测量纯渲染性能") + print("✅ VR测试模式已启用") + + # 可选:自动开启性能测试 + self.world.vr_manager.run_vr_performance_test(duration_seconds=10) + else: + self.vrTestModeAction.setChecked(False) + QMessageBox.warning(self, "错误", "启用VR测试模式失败!") + else: + # 禁用VR测试模式 + self.world.vr_manager.disable_vr_test_mode() + QMessageBox.information(self, "VR测试模式", "VR测试模式已禁用") + print("✅ VR测试模式已禁用") + else: + self.vrTestModeAction.setChecked(False) + QMessageBox.warning(self, "错误", "VR管理器不可用!") + except Exception as e: + self.vrTestModeAction.setChecked(False) + QMessageBox.critical(self, "错误", f"切换VR测试模式时发生错误:\n{str(e)}") + + def onToggleVRTestSubmitTexture(self): + """切换VR测试模式纹理提交功能""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + enabled = self.vrTestSubmitTextureAction.isChecked() + self.world.vr_manager.set_test_mode_features(submit_texture=enabled) + except Exception as e: + QMessageBox.critical(self, "错误", f"设置纹理提交功能时发生错误:\n{str(e)}") + + def onToggleVRTestWaitPoses(self): + """切换VR测试模式姿态等待功能""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + enabled = self.vrTestWaitPosesAction.isChecked() + self.world.vr_manager.set_test_mode_features(wait_poses=enabled) + except Exception as e: + QMessageBox.critical(self, "错误", f"设置姿态等待功能时发生错误:\n{str(e)}") + + def onSetVRTestStep(self, step): + """设置VR测试步骤""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + if step == 0: # 重置 + self.world.vr_manager.set_test_mode_features(submit_texture=False, wait_poses=False) + self.vrTestSubmitTextureAction.setChecked(False) + self.vrTestWaitPosesAction.setChecked(False) + QMessageBox.information(self, "VR测试", "已重置为基线状态:两个功能都禁用") + elif step == 1: # 只启用纹理提交 + self.world.vr_manager.set_test_mode_features(submit_texture=True, wait_poses=False) + self.vrTestSubmitTextureAction.setChecked(True) + self.vrTestWaitPosesAction.setChecked(False) + QMessageBox.information(self, "VR测试", "步骤1:只启用纹理提交\n观察FPS变化来判断submit_texture是否影响性能") + elif step == 2: # 只启用姿态等待 + self.world.vr_manager.set_test_mode_features(submit_texture=False, wait_poses=True) + self.vrTestSubmitTextureAction.setChecked(False) + self.vrTestWaitPosesAction.setChecked(True) + QMessageBox.information(self, "VR测试", "步骤2:只启用姿态等待\n观察FPS变化来判断waitGetPoses是否影响性能") + elif step == 3: # 同时启用两者 + self.world.vr_manager.set_test_mode_features(submit_texture=True, wait_poses=True) + self.vrTestSubmitTextureAction.setChecked(True) + self.vrTestWaitPosesAction.setChecked(True) + QMessageBox.information(self, "VR测试", "步骤3:同时启用两者\n这应该完全复现普通VR模式的36FPS问题") + except Exception as e: + QMessageBox.critical(self, "错误", f"设置VR测试步骤时发生错误:\n{str(e)}") + def setup_main_window(world,path = None): """设置主窗口的便利函数""" app = QApplication.instance() 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_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_index.json b/vr_actions/bindings_index.json new file mode 100644 index 00000000..f7198f13 --- /dev/null +++ b/vr_actions/bindings_index.json @@ -0,0 +1,133 @@ +{ + "controller_type": "knuckles", + "description": "Valve Index\u63a7\u5236\u5668\u7ed1\u5b9a", + "name": "EG VR Editor - Index", + "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": { + "value": { + "output": "/actions/default/in/Squeeze" + } + }, + "mode": "trigger", + "path": "/user/hand/left/input/grip" + }, + { + "inputs": { + "value": { + "output": "/actions/default/in/Squeeze" + } + }, + "mode": "trigger", + "path": "/user/hand/right/input/grip" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/AButton" + } + }, + "mode": "button", + "path": "/user/hand/right/input/a" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/BButton" + } + }, + "mode": "button", + "path": "/user/hand/right/input/b" + }, + { + "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" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Joystick" + } + }, + "mode": "joystick", + "path": "/user/hand/left/input/thumbstick" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Joystick" + } + }, + "mode": "joystick", + "path": "/user/hand/right/input/thumbstick" + } + ], + "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 diff --git a/vr_actions/bindings_oculus.json b/vr_actions/bindings_oculus.json new file mode 100644 index 00000000..71a697b7 --- /dev/null +++ b/vr_actions/bindings_oculus.json @@ -0,0 +1,118 @@ +{ + "controller_type": "oculus_touch", + "description": "Oculus Touch\u63a7\u5236\u5668\u7ed1\u5b9a", + "name": "EG VR Editor - Oculus Touch", + "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": { + "value": { + "output": "/actions/default/in/Squeeze" + } + }, + "mode": "trigger", + "path": "/user/hand/left/input/grip" + }, + { + "inputs": { + "value": { + "output": "/actions/default/in/Squeeze" + } + }, + "mode": "trigger", + "path": "/user/hand/right/input/grip" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/AButton" + } + }, + "mode": "button", + "path": "/user/hand/right/input/a" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/BButton" + } + }, + "mode": "button", + "path": "/user/hand/right/input/b" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Joystick" + }, + "click": { + "output": "/actions/default/in/TrackpadClick" + } + }, + "mode": "joystick", + "path": "/user/hand/left/input/thumbstick" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Joystick" + }, + "click": { + "output": "/actions/default/in/TrackpadClick" + } + }, + "mode": "joystick", + "path": "/user/hand/right/input/thumbstick" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Menu" + } + }, + "mode": "button", + "path": "/user/hand/left/input/menu" + } + ], + "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 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 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