From 11cbefc9058d7cd097ed8466fc2264361f81258c Mon Sep 17 00:00:00 2001 From: Rowland <975945824@qq.com> Date: Tue, 16 Sep 2025 10:43:07 +0800 Subject: [PATCH 01/11] =?UTF-8?q?vr=E7=94=BB=E9=9D=A2=E6=8A=96=E5=8A=A8?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/vr_manager.py | 133 ++++++++++++++++++++++++++++++++-------- 参考文件/panda3d-openvr | 1 + 2 files changed, 107 insertions(+), 27 deletions(-) create mode 160000 参考文件/panda3d-openvr diff --git a/core/vr_manager.py b/core/vr_manager.py index dbd9f59d..f16cb340 100644 --- a/core/vr_manager.py +++ b/core/vr_manager.py @@ -92,6 +92,9 @@ class VRManager(DirectObject): # VR提交策略 - 基于参考实现 self.submit_together = True # 是否在right_cb中同时提交左右眼 + # 帧同步标记 - 修复ATW闪烁 + self._poses_updated_this_frame = False + # VR手柄控制器 self.left_controller = None self.right_controller = None @@ -389,20 +392,16 @@ class VRManager(DirectObject): self.last_fps_time = current_time # 优化的VR更新顺序: - # 1. 立即调用 waitGetPoses 获取最新的姿态数据 - # 这确保我们使用最新数据而不是上一帧的数据 - self._wait_get_poses() + # 注意:WaitGetPoses现在在渲染回调中调用,避免ATW双重预测 + # 这里仅更新非关键的跟踪设备和交互系统 - # 2. 更新相机位置(使用刚获取的最新姿态数据) - self._update_camera_poses() - - # 3. 更新手柄和其他跟踪设备 + # 1. 更新手柄和其他跟踪设备 self.update_tracked_devices() - # 4. 更新VR动作状态 + # 2. 更新VR动作状态 self.action_manager.update_actions() - # 5. 更新VR交互系统 + # 3. 更新VR交互系统 self.interaction_manager.update() # 注意:纹理提交现在通过渲染回调自动处理 @@ -466,6 +465,59 @@ class VRManager(DirectObject): # 记录姿态失败次数 self.pose_failures += 1 + def _wait_get_poses_immediate(self): + """立即获取VR姿态 - 修复ATW闪烁的关键方法""" + try: + if not self.vr_compositor or not self.poses: + return + + # 使用0预测时间来避免双重预测,立即获取当前姿态 + # 这是修复ATW闪烁的关键:在渲染前立即获取,避免时序错误 + result = self.vr_compositor.waitGetPoses(self.poses, None) + + # 检查姿态数据的有效性 + valid_poses = 0 + + # 更新HMD姿态(设备0通常是头显) + if len(self.poses) > 0 and self.poses[0].bPoseIsValid: + valid_poses += 1 + else: + # 如果HMD姿态无效,不要频繁输出错误信息 + if not hasattr(self, '_hmd_invalid_warning_shown'): + print("⚠️ HMD姿态数据无效(立即模式)") + self._hmd_invalid_warning_shown = True + + # 更新控制器姿态 + self.controller_poses.clear() + for device_id in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): + if self.poses[device_id].bPoseIsValid: + device_class = self.vr_system.getTrackedDeviceClass(device_id) + if device_class == openvr.TrackedDeviceClass_Controller: + controller_matrix = self.poses[device_id].mDeviceToAbsoluteTracking + self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) + valid_poses += 1 + + # 调试信息 - 仅在第一次成功时输出 + if not hasattr(self, '_immediate_mode_logged') and valid_poses > 0: + print(f"✅ 立即姿态获取模式启用 - 有效姿态数: {valid_poses}") + self._immediate_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 _reset_frame_flag(self, task): + """重置帧标记 - 确保下一帧可以更新姿态""" + self._poses_updated_this_frame = False + return task.done + def _update_tracking_data(self): """更新VR追踪数据""" try: @@ -840,26 +892,53 @@ class VRManager(DirectObject): print("========================") def left_cb(self, cbdata): - """左眼渲染回调 - 基于参考实现""" - # 执行实际的渲染工作 - cbdata.upcall() - # 根据提交策略决定是否立即提交 - if not self.submit_together: - # 分别提交模式:左眼渲染完成后立即提交 - self.submit_texture(openvr.Eye_Left, self.vr_left_texture) + """左眼渲染回调 - 修复ATW闪烁问题""" + try: + # 在渲染开始前立即获取最新姿态,避免ATW双重预测 + if not hasattr(self, '_poses_updated_this_frame') or not self._poses_updated_this_frame: + self._wait_get_poses_immediate() + self._update_camera_poses() + self._poses_updated_this_frame = True + # 在帧结束时重置标记 + self.world.taskMgr.doMethodLater(0.001, self._reset_frame_flag, "reset_frame_flag") + + # 执行实际的渲染工作 + cbdata.upcall() + + # 根据提交策略决定是否立即提交 + if not self.submit_together: + # 分别提交模式:左眼渲染完成后立即提交 + self.submit_texture(openvr.Eye_Left, self.vr_left_texture) + + except Exception as e: + print(f"左眼渲染回调错误: {e}") def right_cb(self, cbdata): - """右眼渲染回调 - 基于参考实现""" - # 执行实际的渲染工作 - cbdata.upcall() - # 根据提交策略决定提交方式 - if self.submit_together: - # 同时提交模式:右眼渲染完成后同时提交左右眼 - self.submit_texture(openvr.Eye_Left, self.vr_left_texture) - self.submit_texture(openvr.Eye_Right, self.vr_right_texture) - else: - # 分别提交模式:只提交右眼 - self.submit_texture(openvr.Eye_Right, self.vr_right_texture) + """右眼渲染回调 - 修复ATW闪烁问题""" + try: + # 在渲染开始前立即获取最新姿态,避免ATW双重预测 + # 由于左右眼都会调用回调,确保每帧只调用一次WaitGetPoses + if not hasattr(self, '_poses_updated_this_frame') or not self._poses_updated_this_frame: + self._wait_get_poses_immediate() + self._update_camera_poses() + self._poses_updated_this_frame = True + # 在帧结束时重置标记 + self.world.taskMgr.doMethodLater(0.001, self._reset_frame_flag, "reset_frame_flag") + + # 执行实际的渲染工作 + cbdata.upcall() + + # 根据提交策略决定提交方式 + if self.submit_together: + # 同时提交模式:右眼渲染完成后同时提交左右眼 + self.submit_texture(openvr.Eye_Left, self.vr_left_texture) + self.submit_texture(openvr.Eye_Right, self.vr_right_texture) + else: + # 分别提交模式:只提交右眼 + self.submit_texture(openvr.Eye_Right, self.vr_right_texture) + + except Exception as e: + print(f"右眼渲染回调错误: {e}") def submit_texture(self, eye, texture): """提交纹理到VR - 基于参考实现,增强调试信息""" diff --git a/参考文件/panda3d-openvr b/参考文件/panda3d-openvr new file mode 160000 index 00000000..3f956789 --- /dev/null +++ b/参考文件/panda3d-openvr @@ -0,0 +1 @@ +Subproject commit 3f9567897552df6c10078bc124795101cf478f91 -- 2.45.2 From 86b38bcfb333d823592cc0f1da0db5dc74a3718e Mon Sep 17 00:00:00 2001 From: Rowland <975945824@qq.com> Date: Tue, 16 Sep 2025 14:11:46 +0800 Subject: [PATCH 02/11] =?UTF-8?q?vr=E7=94=BB=E9=9D=A2=E6=8A=96=E5=8A=A8?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/vr_manager.py | 139 +++++++++++++++++++++++++++++++++++----- 参考文件/panda3d-openvr | 1 - 2 files changed, 124 insertions(+), 16 deletions(-) delete mode 160000 参考文件/panda3d-openvr diff --git a/core/vr_manager.py b/core/vr_manager.py index f16cb340..7a28e5f8 100644 --- a/core/vr_manager.py +++ b/core/vr_manager.py @@ -60,7 +60,8 @@ class VRManager(DirectObject): self.hmd_pose = Mat4.identMat() self.controller_poses = {} self.tracked_device_poses = [] - self.poses = None # OpenVR姿态数组 + self.poses = None # OpenVR渲染姿态数组 + self.game_poses = None # OpenVR游戏逻辑姿态数组 # VR渲染参数 self.eye_width = 1080 @@ -95,6 +96,13 @@ class VRManager(DirectObject): # 帧同步标记 - 修复ATW闪烁 self._poses_updated_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 @@ -168,10 +176,11 @@ class VRManager(DirectObject): self.eye_width, self.eye_height = self.vr_system.getRecommendedRenderTargetSize() print(f"✓ VR渲染目标尺寸: {self.eye_width}x{self.eye_height}") - # 创建OpenVR姿态数组 + # 创建OpenVR姿态数组 - 分离渲染和游戏姿态 poses_t = openvr.TrackedDevicePose_t * openvr.k_unMaxTrackedDeviceCount - self.poses = poses_t() - print("✓ VR姿态数组已创建") + self.poses = poses_t() # 渲染姿态(预测的) + self.game_poses = poses_t() # 游戏逻辑姿态(当前的) + print("✓ VR渲染和游戏姿态数组已创建") # 创建VR渲染缓冲区 if not self._create_vr_buffers(): @@ -194,6 +203,10 @@ class VRManager(DirectObject): if not self.interaction_manager.initialize(): print("⚠️ VR交互系统初始化失败,但VR系统将继续运行") + # 可选:禁用异步重投影(备选方案) + if self.disable_async_reprojection: + self._disable_async_reprojection() + # 启动VR更新任务 self._start_vr_task() @@ -466,14 +479,14 @@ class VRManager(DirectObject): self.pose_failures += 1 def _wait_get_poses_immediate(self): - """立即获取VR姿态 - 修复ATW闪烁的关键方法""" + """立即获取VR姿态 - 修复ATW闪烁的关键方法(双姿态版本)""" try: - if not self.vr_compositor or not self.poses: + if not self.vr_compositor or not self.poses or not self.game_poses: return - # 使用0预测时间来避免双重预测,立即获取当前姿态 - # 这是修复ATW闪烁的关键:在渲染前立即获取,避免时序错误 - result = self.vr_compositor.waitGetPoses(self.poses, None) + # 关键修复:传递渲染姿态和游戏姿态数组 + # 渲染姿态用于绘制,游戏姿态用于逻辑,避免ATW过度补偿 + result = self.vr_compositor.waitGetPoses(self.poses, self.game_poses) # 检查姿态数据的有效性 valid_poses = 0 @@ -498,9 +511,10 @@ class VRManager(DirectObject): valid_poses += 1 # 调试信息 - 仅在第一次成功时输出 - if not hasattr(self, '_immediate_mode_logged') and valid_poses > 0: - print(f"✅ 立即姿态获取模式启用 - 有效姿态数: {valid_poses}") - self._immediate_mode_logged = True + if not hasattr(self, '_dual_pose_mode_logged') and valid_poses > 0: + print(f"✅ 双姿态立即获取模式启用 - 有效姿态数: {valid_poses}") + print(" 渲染姿态用于绘制,游戏姿态用于逻辑,防止ATW过度补偿") + self._dual_pose_mode_logged = True except Exception as e: # 限制错误输出频率 @@ -513,6 +527,33 @@ class VRManager(DirectObject): 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_frame_flag(self, task): """重置帧标记 - 确保下一帧可以更新姿态""" self._poses_updated_this_frame = False @@ -667,6 +708,37 @@ class VRManager(DirectObject): 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 _submit_frames_to_vr(self): """将渲染帧提交给VR系统""" try: @@ -897,7 +969,10 @@ class VRManager(DirectObject): # 在渲染开始前立即获取最新姿态,避免ATW双重预测 if not hasattr(self, '_poses_updated_this_frame') or not self._poses_updated_this_frame: self._wait_get_poses_immediate() - self._update_camera_poses() + # 缓存当前姿态供下一帧使用,符合OpenVR时序假设 + self._cache_poses_for_next_frame() + # 使用缓存的姿态更新相机(上一帧的姿态) + self._update_camera_poses_with_cache() self._poses_updated_this_frame = True # 在帧结束时重置标记 self.world.taskMgr.doMethodLater(0.001, self._reset_frame_flag, "reset_frame_flag") @@ -920,7 +995,10 @@ class VRManager(DirectObject): # 由于左右眼都会调用回调,确保每帧只调用一次WaitGetPoses if not hasattr(self, '_poses_updated_this_frame') or not self._poses_updated_this_frame: self._wait_get_poses_immediate() - self._update_camera_poses() + # 缓存当前姿态供下一帧使用,符合OpenVR时序假设 + self._cache_poses_for_next_frame() + # 使用缓存的姿态更新相机(上一帧的姿态) + self._update_camera_poses_with_cache() self._poses_updated_this_frame = True # 在帧结束时重置标记 self.world.taskMgr.doMethodLater(0.001, self._reset_frame_flag, "reset_frame_flag") @@ -1355,4 +1433,35 @@ class VRManager(DirectObject): Args: object_node: 要标记的对象节点 """ - self.interaction_manager._add_collision_to_object(object_node) \ No newline at end of file + 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行为") \ No newline at end of file diff --git a/参考文件/panda3d-openvr b/参考文件/panda3d-openvr deleted file mode 160000 index 3f956789..00000000 --- a/参考文件/panda3d-openvr +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3f9567897552df6c10078bc124795101cf478f91 -- 2.45.2 From ef639ab0b162da7ab664eb88df42c452456fecc9 Mon Sep 17 00:00:00 2001 From: Rowland <975945824@qq.com> Date: Tue, 16 Sep 2025 16:30:36 +0800 Subject: [PATCH 03/11] =?UTF-8?q?vr=E6=B7=BB=E5=8A=A0=E8=B0=83=E8=AF=95?= =?UTF-8?q?=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/vr_controller.py | 32 +- core/vr_manager.py | 980 ++++++++++++++++++++++++++++++++++++++++-- ui/main_window.py | 327 ++++++++++++++ 3 files changed, 1286 insertions(+), 53 deletions(-) diff --git a/core/vr_controller.py b/core/vr_controller.py index 3b352207..f9e5fd64 100644 --- a/core/vr_controller.py +++ b/core/vr_controller.py @@ -153,26 +153,38 @@ class VRController(DirectObject): try: result, state = vr_system.getControllerState(self.device_index) if result: - # 更新按钮状态 + # 更新按钮状态 - 使用正确的OpenVR属性名 for i in range(openvr.k_EButton_Max): button_mask = 1 << i - self.button_states[i] = (state.rButtonPressed & button_mask) != 0 + # OpenVR Python绑定中使用ulButtonPressed而不是rButtonPressed + self.button_states[i] = (state.ulButtonPressed & button_mask) != 0 # 更新轴状态(扳机、握把、触摸板) - if len(state.rAxis) > 0: + # OpenVR Python绑定中使用vAxis而不是rAxis + if hasattr(state, 'vAxis') and len(state.vAxis) > 0: # 扳机轴通常在axis[1].x - if len(state.rAxis) > 1: - self.trigger_value = state.rAxis[1].x + if len(state.vAxis) > 1: + self.trigger_value = state.vAxis[1].x # 触摸板轴通常在axis[0] - if len(state.rAxis) > 0: - self.touchpad_pos = Vec3(state.rAxis[0].x, state.rAxis[0].y, 0) + if len(state.vAxis) > 0: + self.touchpad_pos = Vec3(state.vAxis[0].x, state.vAxis[0].y, 0) - # 触摸板触摸状态 - self.touchpad_touched = (state.rButtonTouched & (1 << openvr.k_EButton_SteamVR_Touchpad)) != 0 + # 触摸板触摸状态 - 使用正确的OpenVR属性名 + self.touchpad_touched = (state.ulButtonTouched & (1 << openvr.k_EButton_SteamVR_Touchpad)) != 0 except Exception as e: - print(f"⚠️ 更新{self.name}手柄输入状态失败: {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): """检查按钮是否被按下""" diff --git a/core/vr_manager.py b/core/vr_manager.py index 7a28e5f8..37c289ab 100644 --- a/core/vr_manager.py +++ b/core/vr_manager.py @@ -90,6 +90,54 @@ class VRManager(DirectObject): self.submit_failures = 0 self.pose_failures = 0 + # 高级性能监控 + self.performance_monitoring = True # 是否启用性能监控 + self.debug_output_enabled = True # 是否启用调试输出 + 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.wait_poses_time = 0.0 # waitGetPoses耗时 + self.left_render_time = 0.0 # 左眼渲染耗时 + self.right_render_time = 0.0 # 右眼渲染耗时 + self.submit_time = 0.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 + + # 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 # 运动平滑状态 + + # waitGetPoses调用策略配置 + self.pose_strategy = 'render_callback' # 'render_callback' 或 'update_task' + self.use_prediction_time = 0.011 # 11ms的预测时间(用于update_task策略) + self.poses_updated_in_task = False # 标记是否在更新任务中已获取姿态 + + # 尝试导入性能监控库 + self._init_performance_monitoring() + # VR提交策略 - 基于参考实现 self.submit_together = True # 是否在right_cb中同时提交左右眼 @@ -174,8 +222,77 @@ class VRManager(DirectObject): # 获取推荐的渲染目标尺寸 self.eye_width, self.eye_height = self.vr_system.getRecommendedRenderTargetSize() + self.current_eye_resolution = (self.eye_width, self.eye_height) + self.recommended_eye_resolution = (self.eye_width, self.eye_height) print(f"✓ VR渲染目标尺寸: {self.eye_width}x{self.eye_height}") + # 获取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() # 渲染姿态(预测的) @@ -394,6 +511,9 @@ class VRManager(DirectObject): # 性能监控 self.frame_count += 1 + # 记录帧时间 + self._track_frame_time() + # 计算VR FPS import time current_time = time.time() @@ -404,9 +524,22 @@ class VRManager(DirectObject): self.last_fps_check = self.frame_count self.last_fps_time = current_time - # 优化的VR更新顺序: - # 注意:WaitGetPoses现在在渲染回调中调用,避免ATW双重预测 - # 这里仅更新非关键的跟踪设备和交互系统 + # 更新系统性能指标 + self._update_performance_metrics() + + # VR更新策略选择 + if self.pose_strategy == 'update_task': + # 策略1:在更新任务中调用waitGetPoses(推荐用于高性能) + timing = self._start_timing('wait_poses') + self._wait_get_poses_with_prediction() + self._end_timing(timing) + self.poses_updated_in_task = True + + # 更新相机姿态 + self._update_camera_poses() + else: + # 策略2:在渲染回调中调用waitGetPoses(默认策略) + self.poses_updated_in_task = False # 1. 更新手柄和其他跟踪设备 self.update_tracked_devices() @@ -419,8 +552,9 @@ class VRManager(DirectObject): # 注意:纹理提交现在通过渲染回调自动处理 - # 定期输出性能报告 - if self.frame_count % 1800 == 1: # 每30秒@60fps输出一次性能报告 + # 定期输出性能报告 - 默认10秒间隔 + report_interval = getattr(self, 'performance_report_interval', 600) + if self.frame_count % report_interval == 1: self._print_performance_report() except Exception as e: @@ -480,14 +614,21 @@ class VRManager(DirectObject): def _wait_get_poses_immediate(self): """立即获取VR姿态 - 修复ATW闪烁的关键方法(双姿态版本)""" + # 开始计时waitGetPoses操作 + timing = self._start_timing('wait_poses') + try: 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) + # 结束计时 + wait_time = self._end_timing(timing) + # 检查姿态数据的有效性 valid_poses = 0 @@ -527,6 +668,56 @@ class VRManager(DirectObject): 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: @@ -830,7 +1021,7 @@ class VRManager(DirectObject): if texture_context and hasattr(texture_context, 'getNativeId'): native_id = texture_context.getNativeId() if native_id > 0: - print(f"✓ 成功获取纹理 {texture.getName()} 的OpenGL ID: {native_id}") + # print(f"✓ 成功获取纹理 {texture.getName()} 的OpenGL ID: {native_id}") return native_id # 方法2: 备选方案 - 通过prepared_objects获取texture context @@ -839,14 +1030,14 @@ class VRManager(DirectObject): if texture_context and hasattr(texture_context, 'getNativeId'): native_id = texture_context.getNativeId() if native_id > 0: - print(f"✓ 备选方法获取纹理 {texture.getName()} 的OpenGL ID: {native_id}") + # print(f"✓ 备选方法获取纹理 {texture.getName()} 的OpenGL ID: {native_id}") return native_id # 方法3: 尝试texture对象本身的方法 if hasattr(texture, 'getNativeId'): native_id = texture.getNativeId() if native_id > 0: - print(f"✓ 直接获取纹理 {texture.getName()} 的OpenGL ID: {native_id}") + # print(f"✓ 直接获取纹理 {texture.getName()} 的OpenGL ID: {native_id}") return native_id # 如果所有方法都失败 @@ -948,65 +1139,295 @@ class VRManager(DirectObject): def _print_performance_report(self): """输出VR性能报告""" - print("📊 === VR性能报告 ===") - print(f" VR帧率: {self.vr_fps:.1f} FPS") - print(f" 总帧数: {self.frame_count}") - print(f" 提交失败: {self.submit_failures}") - print(f" 姿态失败: {self.pose_failures}") + if 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") + + # VR特定指标 + print(f"🥽 VR指标:") + print(f" 总帧数: {stats['frame_count']}") + print(f" 提交失败: {stats['submit_failures']}") + print(f" 姿态失败: {stats['pose_failures']}") # 计算失败率 - if self.frame_count > 0: - submit_fail_rate = (self.submit_failures / self.frame_count) * 100 - pose_fail_rate = (self.pose_failures / self.frame_count) * 100 + 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}%") - print("========================") + # 渲染管线监控 + 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"🕐 各阶段耗时 (最近{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") + + # 显示姿态策略信息 + strategy_info = self.get_pose_strategy_info() + print(f"🎯 姿态策略:") + print(f" 当前策略: {strategy_info['strategy']}") + print(f" 策略描述: {strategy_info['description']}") + if strategy_info['strategy'] == 'update_task': + print(f" 预测时间: {strategy_info['prediction_time_ms']:.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'] < 72: + recommendations.append(" ⚠️ VR帧率过低,可能影响体验") + + # 帧时间相关建议 + if stats['frame_time_avg'] > 13.9: # 72fps = 13.9ms + 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(" 🔴 显存使用率过高,可能需要降低纹理质量") + + # 失败率建议 + 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" + + # 显示目标帧时间对比 + 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) def left_cb(self, cbdata): """左眼渲染回调 - 修复ATW闪烁问题""" + # 开始计时左眼渲染 + render_timing = self._start_timing('left_render') + try: - # 在渲染开始前立即获取最新姿态,避免ATW双重预测 - if not hasattr(self, '_poses_updated_this_frame') or not self._poses_updated_this_frame: - self._wait_get_poses_immediate() - # 缓存当前姿态供下一帧使用,符合OpenVR时序假设 - self._cache_poses_for_next_frame() - # 使用缓存的姿态更新相机(上一帧的姿态) - self._update_camera_poses_with_cache() - self._poses_updated_this_frame = True - # 在帧结束时重置标记 - self.world.taskMgr.doMethodLater(0.001, self._reset_frame_flag, "reset_frame_flag") + # 根据策略决定是否在渲染回调中获取姿态 + if self.pose_strategy == 'render_callback': + # 传统策略:在渲染开始前立即获取最新姿态 + if not hasattr(self, '_poses_updated_this_frame') or not self._poses_updated_this_frame: + self._wait_get_poses_immediate() + # 缓存当前姿态供下一帧使用,符合OpenVR时序假设 + self._cache_poses_for_next_frame() + # 使用缓存的姿态更新相机(上一帧的姿态) + self._update_camera_poses_with_cache() + self._poses_updated_this_frame = True + # 在帧结束时重置标记 + self.world.taskMgr.doMethodLater(0.001, self._reset_frame_flag, "reset_frame_flag") + elif self.poses_updated_in_task: + # 新策略:使用在更新任务中获取的姿态,无需重新获取 + pass # 执行实际的渲染工作 cbdata.upcall() + # 结束渲染计时 + self._end_timing(render_timing) + # 根据提交策略决定是否立即提交 if not self.submit_together: # 分别提交模式:左眼渲染完成后立即提交 + submit_timing = self._start_timing('submit') self.submit_texture(openvr.Eye_Left, self.vr_left_texture) + self._end_timing(submit_timing) except Exception as e: + self._end_timing(render_timing) print(f"左眼渲染回调错误: {e}") def right_cb(self, cbdata): """右眼渲染回调 - 修复ATW闪烁问题""" + # 开始计时右眼渲染 + render_timing = self._start_timing('right_render') + try: - # 在渲染开始前立即获取最新姿态,避免ATW双重预测 - # 由于左右眼都会调用回调,确保每帧只调用一次WaitGetPoses - if not hasattr(self, '_poses_updated_this_frame') or not self._poses_updated_this_frame: - self._wait_get_poses_immediate() - # 缓存当前姿态供下一帧使用,符合OpenVR时序假设 - self._cache_poses_for_next_frame() - # 使用缓存的姿态更新相机(上一帧的姿态) - self._update_camera_poses_with_cache() - self._poses_updated_this_frame = True - # 在帧结束时重置标记 - self.world.taskMgr.doMethodLater(0.001, self._reset_frame_flag, "reset_frame_flag") + # 根据策略决定是否在渲染回调中获取姿态 + if self.pose_strategy == 'render_callback': + # 传统策略:在渲染开始前立即获取最新姿态 + # 由于左右眼都会调用回调,确保每帧只调用一次WaitGetPoses + if not hasattr(self, '_poses_updated_this_frame') or not self._poses_updated_this_frame: + self._wait_get_poses_immediate() + # 缓存当前姿态供下一帧使用,符合OpenVR时序假设 + self._cache_poses_for_next_frame() + # 使用缓存的姿态更新相机(上一帧的姿态) + self._update_camera_poses_with_cache() + self._poses_updated_this_frame = True + # 在帧结束时重置标记 + self.world.taskMgr.doMethodLater(0.001, self._reset_frame_flag, "reset_frame_flag") + elif self.poses_updated_in_task: + # 新策略:使用在更新任务中获取的姿态,无需重新获取 + pass # 执行实际的渲染工作 cbdata.upcall() + # 结束渲染计时 + self._end_timing(render_timing) + # 根据提交策略决定提交方式 + submit_timing = self._start_timing('submit') if self.submit_together: # 同时提交模式:右眼渲染完成后同时提交左右眼 self.submit_texture(openvr.Eye_Left, self.vr_left_texture) @@ -1014,8 +1435,10 @@ class VRManager(DirectObject): else: # 分别提交模式:只提交右眼 self.submit_texture(openvr.Eye_Right, self.vr_right_texture) + self._end_timing(submit_timing) except Exception as e: + self._end_timing(render_timing) print(f"右眼渲染回调错误: {e}") def submit_texture(self, eye, texture): @@ -1045,7 +1468,8 @@ class VRManager(DirectObject): self.submit_failures += 1 return - print(f"🔍 准备纹理: {texture.getName()}, 大小: {texture.getXSize()}x{texture.getYSize()}") + # 纹理准备调试信息已注释掉以减少输出 + # print(f"🔍 准备纹理: {texture.getName()}, 大小: {texture.getXSize()}x{texture.getYSize()}") texture_context = texture.prepareNow(0, prepared_objects, gsg) if not texture_context: @@ -1054,7 +1478,7 @@ class VRManager(DirectObject): return handle = texture_context.getNativeId() - print(f"🔍 获取OpenGL纹理句柄: {handle}") + # print(f"🔍 获取OpenGL纹理句柄: {handle}") if handle != 0: ovr_texture = openvr.Texture_t() @@ -1063,12 +1487,12 @@ class VRManager(DirectObject): ovr_texture.eColorSpace = openvr.ColorSpace_Gamma eye_name = "左眼" if eye == openvr.Eye_Left else "右眼" - print(f"🔍 提交{eye_name}纹理到VR, 句柄: {handle}") + # print(f"🔍 提交{eye_name}纹理到VR, 句柄: {handle}") # 提交到VR系统 error = self.vr_compositor.submit(eye, ovr_texture) - print(f"🔍 VR提交结果: {error}") + # print(f"🔍 VR提交结果: {error}") # 检查错误 if error and error != openvr.VRCompositorError_None: @@ -1464,4 +1888,474 @@ class VRManager(DirectObject): def disable_async_reprojection_disable(self): """禁用异步重投影禁用选项 - 恢复默认行为""" self.disable_async_reprojection = False - print("📝 异步重投影禁用选项已关闭,将使用默认ATW行为") \ No newline at end of file + print("📝 异步重投影禁用选项已关闭,将使用默认ATW行为") + + def _start_timing(self, operation_name): + """开始计时操作""" + if not self.enable_pipeline_monitoring: + return None + + import time + return { + 'operation': operation_name, + 'start_time': time.perf_counter() + } + + def _end_timing(self, timing_data): + """结束计时并记录结果""" + if not self.enable_pipeline_monitoring 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) + } + + 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), + '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 + }, + '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 + } + } + + def set_pose_strategy(self, strategy): + """设置waitGetPoses调用策略 + + Args: + strategy: 'render_callback' 或 'update_task' + """ + if strategy not in ['render_callback', 'update_task']: + print(f"⚠️ 无效的姿态策略: {strategy},支持的策略: render_callback, update_task") + return False + + old_strategy = self.pose_strategy + self.pose_strategy = strategy + + if old_strategy != strategy: + print(f"✓ VR姿态策略已切换: {old_strategy} → {strategy}") + if strategy == 'update_task': + print(f" 使用预测时间: {self.use_prediction_time*1000:.1f}ms") + print(" 优势:降低VR回调延迟,提高性能") + else: + print(" 使用传统渲染回调策略") + print(" 优势:更低延迟,更精确的姿态") + + # 重置相关标记 + if hasattr(self, '_poses_updated_this_frame'): + delattr(self, '_poses_updated_this_frame') + self.poses_updated_in_task = False + + return True + + 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 get_pose_strategy_info(self): + """获取当前姿态策略信息""" + return { + 'strategy': self.pose_strategy, + 'prediction_time_ms': self.use_prediction_time * 1000, + 'description': { + 'render_callback': '在渲染回调中获取姿态 - 低延迟,精确', + 'update_task': '在更新任务中获取姿态 - 高性能,预测' + }.get(self.pose_strategy, '未知策略') + } + + 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") + + # 测试策略信息 + strategy_info = self.get_pose_strategy_info() + print("🎯 姿态策略:") + print(f" 当前策略: {strategy_info['strategy']}") + print(f" 预测时间: {strategy_info['prediction_time_ms']:.1f}ms") + + # 测试管线统计 + 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 _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 # 转换为毫秒 + + # 添加到帧时间历史 + 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: + stats['frame_time_avg'] = 0 + stats['frame_time_min'] = 0 + stats['frame_time_max'] = 0 + stats['frame_time_95th'] = 0 + + return stats + + def enable_performance_monitoring(self): + """启用性能监控""" + self.performance_monitoring = True + print("✓ VR性能监控已启用") + + def disable_performance_monitoring(self): + """禁用性能监控""" + self.performance_monitoring = False + print("✓ VR性能监控已禁用") + + 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 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 + } \ No newline at end of file diff --git a/ui/main_window.py b/ui/main_window.py index 33e3f247..aa5208ad 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -445,6 +445,60 @@ class MainWindow(QMainWindow): 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(True) # 默认启用 + + 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.vrPipelineMonitorAction = self.vrDebugMenu.addAction('启用管线监控') + self.vrPipelineMonitorAction.setCheckable(True) + self.vrPipelineMonitorAction.setChecked(True) # 默认启用 + + 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('测试管线监控') + + self.vrDebugSettingsAction = self.vrDebugMenu.addAction('调试设置...') # 初始状态下禁用退出VR选项 self.exitVRAction.setEnabled(False) @@ -915,6 +969,17 @@ class MainWindow(QMainWindow): 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.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.vrDebugSettingsAction.triggered.connect(self.onShowVRDebugSettings) + def onCreateCesiumView(self): if hasattr(self.world,'gui_manager') and self.world.gui_manager: @@ -2190,6 +2255,268 @@ class MainWindow(QMainWindow): 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 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 + def setup_main_window(world,path = None): """设置主窗口的便利函数""" app = QApplication.instance() -- 2.45.2 From b067e3a304fffb2fea607124adb28c8038f7dbc1 Mon Sep 17 00:00:00 2001 From: Rowland <975945824@qq.com> Date: Tue, 23 Sep 2025 14:14:13 +0800 Subject: [PATCH 04/11] =?UTF-8?q?vr=E7=94=BB=E9=9D=A2=E6=8A=96=E5=8A=A8?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/vr_manager.py | 51 +++++++++--- vr_actions/bindings_index.json | 133 ++++++++++++++++++++++++++++++++ vr_actions/bindings_oculus.json | 118 ++++++++++++++++++++++++++++ 3 files changed, 291 insertions(+), 11 deletions(-) create mode 100644 vr_actions/bindings_index.json create mode 100644 vr_actions/bindings_oculus.json diff --git a/core/vr_manager.py b/core/vr_manager.py index 37c289ab..8ab68ae9 100644 --- a/core/vr_manager.py +++ b/core/vr_manager.py @@ -144,6 +144,9 @@ class VRManager(DirectObject): # 帧同步标记 - 修复ATW闪烁 self._poses_updated_this_frame = False + # Running Start标记 - Valve最佳实践 + self._waitgetposes_called_this_frame = False + # 姿态缓存 - 修复时序不匹配 self._cached_render_poses = None # 用于渲染的缓存姿态 self._first_frame = True # 首帧标记 @@ -529,16 +532,19 @@ class VRManager(DirectObject): # VR更新策略选择 if self.pose_strategy == 'update_task': - # 策略1:在更新任务中调用waitGetPoses(推荐用于高性能) - timing = self._start_timing('wait_poses') - self._wait_get_poses_with_prediction() - self._end_timing(timing) + # 🚀 新的Running Start策略:WaitGetPoses在Submit后调用,此处只更新相机 + # 不在此处调用WaitGetPoses,避免错过VSync窗口 self.poses_updated_in_task = True - # 更新相机姿态 + # 使用上一帧获取的姿态更新相机 self._update_camera_poses() + + # 输出策略信息(仅第一次) + if not hasattr(self, '_running_start_logged'): + print("✓ Running Start模式已启用 - WaitGetPoses在Submit后调用") + self._running_start_logged = True else: - # 策略2:在渲染回调中调用waitGetPoses(默认策略) + # 策略2:在渲染回调中调用waitGetPoses(传统策略) self.poses_updated_in_task = False # 1. 更新手柄和其他跟踪设备 @@ -750,6 +756,11 @@ class VRManager(DirectObject): self._poses_updated_this_frame = False return task.done + def _reset_waitgetposes_flag(self, task): + """重置WaitGetPoses标记 - 确保下一帧可以调用WaitGetPoses""" + self._waitgetposes_called_this_frame = False + return task.done + def _update_tracking_data(self): """更新VR追踪数据""" try: @@ -1065,6 +1076,12 @@ class VRManager(DirectObject): # 禁用主相机避免干扰VR渲染 self._disable_main_cam() + # 优化VR性能:切换到update_task姿态策略 + print("🚀 正在优化VR性能...") + self.set_pose_strategy('update_task') + self.set_prediction_time(11.0) # 11ms预测时间 + print("✓ VR性能优化完成 - 姿态策略已切换至update_task模式") + print("✅ VR模式已启用") return True @@ -1437,6 +1454,18 @@ class VRManager(DirectObject): self.submit_texture(openvr.Eye_Right, self.vr_right_texture) self._end_timing(submit_timing) + # 🚀 Valve的"Running Start"技术:Submit后立即调用WaitGetPoses + # 这是获取下一帧姿态的最佳时机,避免错过VSync窗口 + if not hasattr(self, '_waitgetposes_called_this_frame') or not self._waitgetposes_called_this_frame: + pose_timing = self._start_timing('wait_poses') + self._wait_get_poses_immediate() + self._end_timing(pose_timing) + self._waitgetposes_called_this_frame = True + # print("✓ Running Start: 在Submit后立即获取下一帧姿态") + + # 重置标记,准备下一帧 + self.world.taskMgr.doMethodLater(0.001, self._reset_waitgetposes_flag, "reset_waitgetposes_flag") + except Exception as e: self._end_timing(render_timing) print(f"右眼渲染回调错误: {e}") @@ -1992,11 +2021,11 @@ class VRManager(DirectObject): if old_strategy != strategy: print(f"✓ VR姿态策略已切换: {old_strategy} → {strategy}") if strategy == 'update_task': - print(f" 使用预测时间: {self.use_prediction_time*1000:.1f}ms") - print(" 优势:降低VR回调延迟,提高性能") + print(" 🚀 Valve Running Start模式:Submit后立即获取姿态") + print(" 优势:避免错过VSync窗口,显著降低WaitGetPoses延迟") else: print(" 使用传统渲染回调策略") - print(" 优势:更低延迟,更精确的姿态") + print(" 缺点:可能在渲染前调用WaitGetPoses,容易错过VSync") # 重置相关标记 if hasattr(self, '_poses_updated_this_frame'): @@ -2025,8 +2054,8 @@ class VRManager(DirectObject): 'strategy': self.pose_strategy, 'prediction_time_ms': self.use_prediction_time * 1000, 'description': { - 'render_callback': '在渲染回调中获取姿态 - 低延迟,精确', - 'update_task': '在更新任务中获取姿态 - 高性能,预测' + 'render_callback': '在渲染回调中获取姿态 - 传统模式,可能错过VSync', + 'update_task': 'Valve Running Start模式 - Submit后立即获取姿态,避免VSync延迟' }.get(self.pose_strategy, '未知策略') } 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 -- 2.45.2 From 76ca736824aed724a54aa561a34d42f23879ddae Mon Sep 17 00:00:00 2001 From: Rowland <975945824@qq.com> Date: Wed, 24 Sep 2025 14:20:26 +0800 Subject: [PATCH 05/11] =?UTF-8?q?=E8=A1=A5=E5=AE=8C=E8=B0=83=E8=AF=95?= =?UTF-8?q?=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- QPanda3D/Panda3DWorld.py | 6 +- core/vr_manager.py | 203 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 204 insertions(+), 5 deletions(-) diff --git a/QPanda3D/Panda3DWorld.py b/QPanda3D/Panda3DWorld.py index 78193075..1afffc65 100644 --- a/QPanda3D/Panda3DWorld.py +++ b/QPanda3D/Panda3DWorld.py @@ -79,8 +79,8 @@ class Panda3DWorld(ShowBase): ShowBase.__init__(self) # 初始化渲染管线并设置可调整大小的标志 - self.render_pipeline.create(self) - _global_render_pipeline = self.render_pipeline + # self.render_pipeline.create(self) + # _global_render_pipeline = self.render_pipeline # 创建 Qt 能读的 RGBA8 贴图 self.qt_output_tex = Texture("qt_output_tex") @@ -138,7 +138,7 @@ class Panda3DWorld(ShowBase): #self.cam = self.render_pipeline._showbase.cam #self.camNode = self.cam.node() #self.camLens = self.camNode.get_lens() - self.render_pipeline._showbase.camera = self.render_pipeline._showbase.cam + # self.render_pipeline._showbase.camera = self.render_pipeline._showbase.cam #self.render_pipeline.daytime_mgr.update() diff --git a/core/vr_manager.py b/core/vr_manager.py index 8ab68ae9..74762612 100644 --- a/core/vr_manager.py +++ b/core/vr_manager.py @@ -119,6 +119,18 @@ class VRManager(DirectObject): self.sync_wait_times = [] self.pipeline_history_size = 30 + # GPU渲染时间监控(OpenVR Frame Timing) + self.enable_gpu_timing = True # 是否启用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) @@ -530,6 +542,10 @@ class VRManager(DirectObject): # 更新系统性能指标 self._update_performance_metrics() + # 更新GPU渲染时间统计 + if self.enable_gpu_timing: + self._get_gpu_frame_timing() + # VR更新策略选择 if self.pose_strategy == 'update_task': # 🚀 新的Running Start策略:WaitGetPoses在Submit后调用,此处只更新相机 @@ -1190,6 +1206,64 @@ class VRManager(DirectObject): 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']}") @@ -1306,6 +1380,17 @@ class VRManager(DirectObject): 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: @@ -1357,6 +1442,17 @@ class VRManager(DirectObject): 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: @@ -1979,18 +2075,41 @@ class VRManager(DirectObject): '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 + '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, @@ -2001,7 +2120,9 @@ class VRManager(DirectObject): '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 + 'motion_smoothing': self.motion_smoothing_enabled, + 'gpu_timing_enabled': self.enable_gpu_timing, + 'gpu_timing_failures': self.gpu_timing_failure_count } } @@ -2130,6 +2251,84 @@ class VRManager(DirectObject): 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: -- 2.45.2 From 432502e9338266880611f97a82f5b276463f4572 Mon Sep 17 00:00:00 2001 From: Rowland <975945824@qq.com> Date: Wed, 24 Sep 2025 15:00:36 +0800 Subject: [PATCH 06/11] =?UTF-8?q?=E8=A1=A5=E5=AE=8C=E8=B0=83=E8=AF=95?= =?UTF-8?q?=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- QPanda3D/Panda3DWorld.py | 7 ++ core/vr_manager.py | 174 +++++++++++++++++++++++++++++++-------- 2 files changed, 147 insertions(+), 34 deletions(-) diff --git a/QPanda3D/Panda3DWorld.py b/QPanda3D/Panda3DWorld.py index 1afffc65..bf6a2524 100644 --- a/QPanda3D/Panda3DWorld.py +++ b/QPanda3D/Panda3DWorld.py @@ -70,6 +70,13 @@ 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("", "gl-debug true") # 调试时可启用OpenGL调试 + if (is_fullscreen): loadPrcFileData("", "fullscreen #t") diff --git a/core/vr_manager.py b/core/vr_manager.py index 74762612..522d81b4 100644 --- a/core/vr_manager.py +++ b/core/vr_manager.py @@ -144,7 +144,7 @@ class VRManager(DirectObject): # waitGetPoses调用策略配置 self.pose_strategy = 'render_callback' # 'render_callback' 或 'update_task' - self.use_prediction_time = 0.011 # 11ms的预测时间(用于update_task策略) + self.use_prediction_time = 0.007 # 7ms的预测时间 - 优化后减少VSync等待 self.poses_updated_in_task = False # 标记是否在更新任务中已获取姿态 # 尝试导入性能监控库 @@ -410,7 +410,7 @@ class VRManager(DirectObject): return texture def _create_vr_buffer(self, name, texture, width, height): - """创建VR渲染缓冲区 - 基于参考实现""" + """创建VR渲染缓冲区 - 基于参考实现 + 性能诊断""" # 设置帧缓冲属性 fbprops = FrameBufferProperties() fbprops.setRgbaBits(1, 1, 1, 1) @@ -421,13 +421,59 @@ class VRManager(DirectObject): 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避免不必要的内存拷贝 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: @@ -1095,7 +1141,7 @@ class VRManager(DirectObject): # 优化VR性能:切换到update_task姿态策略 print("🚀 正在优化VR性能...") self.set_pose_strategy('update_task') - self.set_prediction_time(11.0) # 11ms预测时间 + self.set_prediction_time(7.0) # 7ms预测时间 - 减少VSync等待时间 print("✓ VR性能优化完成 - 姿态策略已切换至update_task模式") print("✅ VR模式已启用") @@ -1306,40 +1352,88 @@ class VRManager(DirectObject): else: print(f" ✓ 帧时间正常: {current_frame_time:.1f}ms (目标:{target_frame_time:.1f}ms)") - 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"🔧 优化诊断:") + current = pipeline_stats.get('current', {}) if self.enable_pipeline_monitoring else {} - 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") + # 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") - # 显示姿态策略信息 - strategy_info = self.get_pose_strategy_info() - print(f"🎯 姿态策略:") - print(f" 当前策略: {strategy_info['strategy']}") - print(f" 策略描述: {strategy_info['description']}") - if strategy_info['strategy'] == 'update_task': - print(f" 预测时间: {strategy_info['prediction_time_ms']:.1f}ms") + # CPU-GPU并行度分析 + gpu_total = current.get('gpu_total_render', 0) + cpu_render = current.get('total_render', 0) + if gpu_total > 0 and cpu_render > 0: + if gpu_total > cpu_render * 3: + print(f" ⚠️ GPU等待CPU: GPU{gpu_total:.1f}ms vs CPU{cpu_render:.1f}ms") + print(f" 建议: 检查OpenGL命令提交是否及时") + elif cpu_render > gpu_total * 2: + print(f" ⚠️ CPU瓶颈: CPU{cpu_render:.1f}ms vs GPU{gpu_total:.1f}ms") + else: + print(f" ✓ CPU-GPU平衡: CPU{cpu_render:.1f}ms GPU{gpu_total:.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)") + # 预测时间诊断 + current_prediction = self.use_prediction_time * 1000 + print(f" 预测时间设置: {current_prediction:.1f}ms") + if current_prediction > 10: + print(f" 建议: 预测时间较高,可能增加waitGetPoses延迟") + elif current_prediction < 5: + print(f" 注意: 预测时间较低,可能影响姿态准确性") - if bottleneck_analysis: - print(f"🚨 瓶颈分析:") - for analysis in bottleneck_analysis: - print(f" ⚠️ {analysis}") + # 优化状态总结 + 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") + + # 显示姿态策略信息 + strategy_info = self.get_pose_strategy_info() + print(f"🎯 姿态策略:") + print(f" 当前策略: {strategy_info['strategy']}") + print(f" 策略描述: {strategy_info['description']}") + if strategy_info['strategy'] == 'update_task': + print(f" 预测时间: {strategy_info['prediction_time_ms']:.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) @@ -1587,6 +1681,18 @@ class VRManager(DirectObject): self.submit_failures += 1 return + # 🚀 安全的OpenGL命令刷新:在纹理提交前确保渲染命令已发送到GPU + try: + if hasattr(gsg, 'flush'): + gsg.flush() + elif hasattr(gsg, 'clear_before_callback'): + # 另一种可能的刷新方法 + pass + except Exception as flush_error: + if not hasattr(self, '_gsg_flush_error_logged'): + print(f"⚠️ GSG刷新失败: {flush_error}") + self._gsg_flush_error_logged = True + # 准备纹理并获取更详细的错误信息 if not texture: print("❌ 纹理对象为空") -- 2.45.2 From bb6eee2250d94bceb7d199ad9d52d3aa2b523931 Mon Sep 17 00:00:00 2001 From: Rowland <975945824@qq.com> Date: Fri, 26 Sep 2025 14:53:27 +0800 Subject: [PATCH 07/11] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=B0=83=E8=AF=95?= =?UTF-8?q?=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- QPanda3D/Panda3DWorld.py | 1 + core/vr_manager.py | 2015 ++++++++++++++++++++++++++++++-------- run_vr_test.sh | 123 +++ ui/main_window.py | 37 + 4 files changed, 1758 insertions(+), 418 deletions(-) create mode 100755 run_vr_test.sh diff --git a/QPanda3D/Panda3DWorld.py b/QPanda3D/Panda3DWorld.py index bf6a2524..f93c74aa 100644 --- a/QPanda3D/Panda3DWorld.py +++ b/QPanda3D/Panda3DWorld.py @@ -75,6 +75,7 @@ class Panda3DWorld(ShowBase): 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): diff --git a/core/vr_manager.py b/core/vr_manager.py index 522d81b4..a9242f9b 100644 --- a/core/vr_manager.py +++ b/core/vr_manager.py @@ -9,6 +9,7 @@ VR管理器模块 """ import sys +import gc import numpy as np from panda3d.core import ( WindowProperties, GraphicsPipe, FrameBufferProperties, @@ -56,6 +57,13 @@ class VRManager(DirectObject): self.vr_right_camera = None self.vr_compositor = None + # 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 = {} @@ -63,12 +71,46 @@ class VRManager(DirectObject): 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 @@ -105,10 +147,14 @@ class VRManager(DirectObject): # 渲染管线详细监控 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同步等待时间 @@ -142,10 +188,25 @@ class VRManager(DirectObject): self.async_reprojection_enabled = False # 异步重投影状态 self.motion_smoothing_enabled = False # 运动平滑状态 - # waitGetPoses调用策略配置 - self.pose_strategy = 'render_callback' # 'render_callback' 或 'update_task' - self.use_prediction_time = 0.007 # 7ms的预测时间 - 优化后减少VSync等待 - self.poses_updated_in_task = 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() @@ -153,8 +214,6 @@ class VRManager(DirectObject): # VR提交策略 - 基于参考实现 self.submit_together = True # 是否在right_cb中同时提交左右眼 - # 帧同步标记 - 修复ATW闪烁 - self._poses_updated_this_frame = False # Running Start标记 - Valve最佳实践 self._waitgetposes_called_this_frame = False @@ -172,14 +231,122 @@ class VRManager(DirectObject): self.controllers = {} # 设备索引到控制器的映射 self.tracked_device_anchors = {} # 跟踪设备锚点 - # VR动作系统 - self.action_manager = VRActionManager(self) + # VR动作系统 - 添加异常保护 + try: + self.action_manager = VRActionManager(self) + print("✓ VR动作管理器初始化完成") + except Exception as e: + print(f"⚠️ VR动作管理器初始化失败: {e}") + self.action_manager = None - # VR交互系统 - self.interaction_manager = VRInteractionManager(self) + # VR交互系统 - 添加异常保护 + try: + self.interaction_manager = VRInteractionManager(self) + print("✓ VR交互管理器初始化完成") + except Exception as e: + print(f"⚠️ VR交互管理器初始化失败: {e}") + self.interaction_manager = None print("✓ VR管理器初始化完成") + 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矩阵 - 基于参考实现 @@ -223,6 +390,11 @@ class VRManager(DirectObject): try: 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: @@ -236,10 +408,25 @@ class VRManager(DirectObject): return False # 获取推荐的渲染目标尺寸 - self.eye_width, self.eye_height = self.vr_system.getRecommendedRenderTargetSize() + base_width, base_height = self.vr_system.getRecommendedRenderTargetSize() + self.base_eye_width = base_width + self.base_eye_height = base_height + + # 应用分辨率缩放 + self.scaled_eye_width = int(base_width * self.resolution_scale) + self.scaled_eye_height = int(base_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) - self.recommended_eye_resolution = (self.eye_width, self.eye_height) - print(f"✓ VR渲染目标尺寸: {self.eye_width}x{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: @@ -324,16 +511,31 @@ class VRManager(DirectObject): print("❌ 设置VR相机失败") return False + # 优化VR渲染管线 + self._optimize_vr_rendering() + # 初始化手柄控制器 self._initialize_controllers() - # 初始化动作系统 - if not self.action_manager.initialize(): - print("⚠️ VR动作系统初始化失败,但VR系统将继续运行") + # 初始化动作系统 - 检查是否存在 + if self.action_manager: + try: + if not self.action_manager.initialize(): + print("⚠️ VR动作系统初始化失败,但VR系统将继续运行") + except Exception as e: + print(f"⚠️ VR动作系统初始化异常: {e}") + else: + print("⚠️ VR动作管理器未创建,跳过动作系统初始化") - # 初始化交互系统 - if not self.interaction_manager.initialize(): - print("⚠️ VR交互系统初始化失败,但VR系统将继续运行") + # 初始化交互系统 - 检查是否存在 + 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交互管理器未创建,跳过交互系统初始化") # 可选:禁用异步重投影(备选方案) if self.disable_async_reprojection: @@ -353,8 +555,14 @@ class VRManager(DirectObject): return False def _create_vr_buffers(self): - """创建VR渲染缓冲区 - 基于参考实现""" + """创建VR渲染缓冲区 - 使用分辨率缩放优化""" try: + 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( @@ -391,8 +599,14 @@ class VRManager(DirectObject): self.vr_right_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) # 深蓝色背景便于调试 self.vr_right_eye_buffer.setActive(True) - print("✓ VR渲染缓冲区创建成功") - return 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"❌ 创建VR缓冲区失败: {e}") @@ -409,6 +623,61 @@ class VRManager(DirectObject): texture.setMagfilter(Texture.FTLinear) return texture + def _prepare_and_cache_textures(self): + """准备纹理并缓存OpenGL ID - 解决重复准备问题""" + try: + # 获取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"❌ 纹理准备和缓存失败: {e}") + import traceback + traceback.print_exc() + return False + def _create_vr_buffer(self, name, texture, width, height): """创建VR渲染缓冲区 - 基于参考实现 + 性能诊断""" # 设置帧缓冲属性 @@ -426,7 +695,9 @@ class VRManager(DirectObject): # 清除默认渲染纹理 buffer.clearRenderTextures() - # 添加我们的纹理 - 使用RTMBindOrCopy避免不必要的内存拷贝 + # 🚀 使用RTMBindOrCopy模式 - 已经是最优选择 + # RTMBindOrCopy会尝试直接绑定到纹理,只有硬件不支持时才回退到复制 + # 这已经是Panda3D中最高效的渲染到纹理模式 buffer.addRenderTexture(texture, GraphicsOutput.RTMBindOrCopy, GraphicsOutput.RTPColor) else: @@ -505,16 +776,18 @@ class VRManager(DirectObject): self.vr_left_camera = self.left_eye_anchor.attachNewNode(left_cam_node) self.vr_right_camera = self.right_eye_anchor.attachNewNode(right_cam_node) - # 设置显示区域并添加渲染回调 + # 设置显示区域使用标准渲染流程 left_dr = self.vr_left_eye_buffer.makeDisplayRegion() left_dr.setCamera(self.vr_left_camera) left_dr.setActive(True) - left_dr.setDrawCallback(PythonCallbackObject(self.left_cb)) + # 恢复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) - right_dr.setDrawCallback(PythonCallbackObject(self.right_cb)) + # 恢复DrawCallback以精确控制渲染时机 + right_dr.setDrawCallback(PythonCallbackObject(self.simple_right_cb)) print("✓ VR相机锚点层级系统设置完成") return True @@ -555,6 +828,71 @@ class VRManager(DirectObject): 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: @@ -572,6 +910,15 @@ class VRManager(DirectObject): # 性能监控 self.frame_count += 1 + # 🚀 手动垃圾回收控制 - 避免16-19帧周期性峰值 + self._manual_gc_control() + + # 🚀 自动启用性能模式 - 减少计时对象创建 + if not self.performance_mode_enabled and self.frame_count >= self.performance_mode_trigger_frame: + self.performance_mode_enabled = True + print(f"🎯 性能模式已启用 (帧#{self.frame_count}) - 禁用详细监控以提升性能") + print(" 现在将减少每帧对象创建,显著提升VR性能稳定性") + # 记录帧时间 self._track_frame_time() @@ -592,22 +939,17 @@ class VRManager(DirectObject): if self.enable_gpu_timing: self._get_gpu_frame_timing() - # VR更新策略选择 - if self.pose_strategy == 'update_task': - # 🚀 新的Running Start策略:WaitGetPoses在Submit后调用,此处只更新相机 - # 不在此处调用WaitGetPoses,避免错过VSync窗口 - self.poses_updated_in_task = True + # 🚀 Running Start策略:WaitGetPoses在Submit后调用,此处只更新相机 + # 不在此处调用WaitGetPoses,避免错过VSync窗口 + self.poses_updated_in_task = True - # 使用上一帧获取的姿态更新相机 - self._update_camera_poses() + # 使用上一帧获取的姿态更新相机 + self._update_camera_poses() - # 输出策略信息(仅第一次) - if not hasattr(self, '_running_start_logged'): - print("✓ Running Start模式已启用 - WaitGetPoses在Submit后调用") - self._running_start_logged = True - else: - # 策略2:在渲染回调中调用waitGetPoses(传统策略) - self.poses_updated_in_task = False + # 输出策略信息(仅第一次) + if not hasattr(self, '_running_start_logged'): + print("✓ Running Start模式已启用 - WaitGetPoses在Submit后调用") + self._running_start_logged = True # 1. 更新手柄和其他跟踪设备 self.update_tracked_devices() @@ -618,7 +960,8 @@ class VRManager(DirectObject): # 3. 更新VR交互系统 self.interaction_manager.update() - # 注意:纹理提交现在通过渲染回调自动处理 + # 🚀 阶段2&3:渲染同步和纹理提交(替代DrawCallback) + self._handle_vr_rendering_and_submit() # 定期输出性能报告 - 默认10秒间隔 report_interval = getattr(self, 'performance_report_interval', 600) @@ -632,6 +975,134 @@ class VRManager(DirectObject): return task.cont + def _handle_vr_rendering_and_submit(self): + """VR帧管理 - DrawCallback现在处理实际渲染和提交""" + try: + import time + + # 记录帧处理开始时间 + frame_start_time = time.perf_counter() + + # DrawCallback会处理实际的渲染和纹理提交 + # 这里主要处理帧管理和诊断 + + # 同步等待GPU完成(如果需要) + gsg = self.world.win.getGsg() + if gsg: + gsg.getEngine().syncFrame() + + # 计算帧处理时间 + frame_time = (time.perf_counter() - frame_start_time) * 1000 + + # Running Start的备用逻辑(DrawCallback可能已处理) + # 确保每帧都有姿态获取 + if not hasattr(self, '_waitgetposes_called_this_frame') or not self._waitgetposes_called_this_frame: + pose_start = time.perf_counter() + self._wait_get_poses_immediate() + self.wait_poses_time = (time.perf_counter() - pose_start) * 1000 + self._waitgetposes_called_this_frame = True + + # 重置标记,准备下一帧 + self.world.taskMgr.doMethodLater(0.001, self._reset_waitgetposes_flag, "reset_waitgetposes_flag") + + # 调试信息(仅在启用时) + if self.debug_output_enabled and self.frame_count % 300 == 1: + print(f"🎯 帧#{self.frame_count} DrawCallback模式:") + print(f" 帧管理时间: {frame_time:.2f}ms") + print(f" 渲染由回调处理: 左眼={self.left_render_count}, 右眼={self.right_render_count}") + print(f" 姿态获取: {self.wait_poses_time:.2f}ms") + + except Exception as e: + print(f"❌ VR帧管理异常: {e}") + import traceback + traceback.print_exc() + + 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 + + # 检查避免重复提交 + current_frame = getattr(self, 'frame_count', 0) + if not hasattr(self, '_left_submitted_frame'): + self._left_submitted_frame = -1 + + if self._left_submitted_frame != current_frame: + self._left_submitted_frame = current_frame + + # 🔧 渐进式VR功能测试:根据调试标志决定是否提交纹理 + should_submit = not self.vr_test_mode or self.test_mode_submit_texture + + 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 + + # 检查避免重复提交 + current_frame = getattr(self, 'frame_count', 0) + if not hasattr(self, '_right_submitted_frame'): + self._right_submitted_frame = -1 + + if self._right_submitted_frame != current_frame: + self._right_submitted_frame = current_frame + + # 🔧 渐进式VR功能测试:根据调试标志决定启用哪些功能 + should_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 should_submit: + # 渲染后立即提交纹理(普通模式或测试模式启用提交时) + if self.vr_compositor and self.vr_right_texture: + self.submit_texture(openvr.Eye_Right, self.vr_right_texture) + + if should_wait_poses: + # Running Start:右眼提交后立即获取姿态(普通模式或测试模式启用时) + if not hasattr(self, '_waitgetposes_called_this_frame') or not self._waitgetposes_called_this_frame: + self._wait_get_poses_immediate() + self._waitgetposes_called_this_frame = True + self.world.taskMgr.doMethodLater(0.001, self._reset_waitgetposes_flag, "reset_waitgetposes_flag") + + 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: @@ -709,15 +1180,29 @@ class VRManager(DirectObject): print("⚠️ HMD姿态数据无效(立即模式)") self._hmd_invalid_warning_shown = True - # 更新控制器姿态 - self.controller_poses.clear() + # 🚀 优化控制器姿态更新:使用缓存,避免每帧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) + + # 检查是否已有此设备的缓存矩阵 + 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] # 调试信息 - 仅在第一次成功时输出 if not hasattr(self, '_dual_pose_mode_logged') and valid_poses > 0: @@ -813,10 +1298,6 @@ class VRManager(DirectObject): except Exception as e: print(f"⚠️ 姿态缓存失败: {e}") - def _reset_frame_flag(self, task): - """重置帧标记 - 确保下一帧可以更新姿态""" - self._poses_updated_this_frame = False - return task.done def _reset_waitgetposes_flag(self, task): """重置WaitGetPoses标记 - 确保下一帧可以调用WaitGetPoses""" @@ -848,7 +1329,7 @@ class VRManager(DirectObject): print(f"更新追踪数据失败: {e}") def _convert_openvr_matrix_to_panda(self, ovr_matrix): - """将OpenVR矩阵转换为Panda3D矩阵 + """将OpenVR矩阵转换为Panda3D矩阵 - 使用对象池优化 坐标系转换: OpenVR: X右, Y上, -Z前(右手坐标系) @@ -859,7 +1340,8 @@ class VRManager(DirectObject): OpenVR Y → Panda3D Z OpenVR -Z → Panda3D Y """ - mat = Mat4() + # 🚀 使用对象池获取预分配的Mat4对象,避免每帧创建新对象 + mat = self._get_pooled_matrix() # 修正的坐标转换矩阵 # OpenVR: X右, Y上, -Z前 → Panda3D: X右, Y前, Z上 @@ -890,7 +1372,7 @@ class VRManager(DirectObject): 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 @@ -898,36 +1380,33 @@ class VRManager(DirectObject): if self._coord_debug_counter % 600 == 1: # 每10秒输出一次@60fps print(f"🔄 坐标系转换调试 (第{self._coord_debug_counter}帧)") - # 输出原始OpenVR矩阵信息 - ovr_pos = Vec3(ovr_matrix[0][3], ovr_matrix[1][3], ovr_matrix[2][3]) - print(f" OpenVR原始位置: {ovr_pos}") + # 直接输出数值,避免创建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})") - # 输出转换后的Panda3D矩阵信息 - # 正确的方法:从矩阵中读取位移(第4列,前3行) - panda_pos = Vec3(mat.getCell(0, 3), mat.getCell(1, 3), mat.getCell(2, 3)) - print(f" Panda3D转换位置: {panda_pos}") + # 直接从矩阵读取数值,避免创建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) - manual_converted_pos = Vec3(ovr_pos.x, -ovr_pos.z, ovr_pos.y) - print(f" 手动转换结果: {manual_converted_pos}") + # 手动验证转换: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})") - # 检查我们的矩阵是否正确设置了位置 - print(f" 矩阵位置元素: [{mat.getCell(0,3)}, {mat.getCell(1,3)}, {mat.getCell(2,3)}]") + # 计算误差,避免创建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 - # 验证转换是否正确 - expected_panda_pos = manual_converted_pos - print(f" 预期Panda3D位置: {expected_panda_pos}") - - # 检查转换是否正确 - diff = panda_pos - expected_panda_pos - diff_magnitude = diff.length() if diff_magnitude < 0.001: print(f" ✅ 坐标转换正确 (误差: {diff_magnitude:.6f})") else: print(f" ⚠️ 坐标转换可能有误 (误差: {diff_magnitude:.6f})") - print(f" 差异向量: {diff}") - print(f" 实际矩阵第4行: [{mat.getCell(3,0)}, {mat.getCell(3,1)}, {mat.getCell(3,2)}, {mat.getCell(3,3)}]") + print(f" 差异: ({diff_x:.6f}, {diff_y:.6f}, {diff_z:.6f})") return mat @@ -1003,125 +1482,6 @@ class VRManager(DirectObject): import traceback traceback.print_exc() - def _submit_frames_to_vr(self): - """将渲染帧提交给VR系统""" - try: - if not self.vr_compositor: - return - - # 使用我们创建的纹理对象 - left_texture = self.vr_left_texture - right_texture = self.vr_right_texture - - if left_texture and right_texture: - # 确保纹理已准备并获取OpenGL纹理ID - gsg = self.world.win.getGsg() - prepared_objects = gsg.getPreparedObjects() - - # 准备纹理 - 使用更简单的方法 - try: - # 方法1: 尝试prepareNow - if hasattr(left_texture, 'prepareNow'): - left_texture.prepareNow(0, prepared_objects, gsg) - if hasattr(right_texture, 'prepareNow'): - right_texture.prepareNow(0, prepared_objects, gsg) - except Exception as prep_error: - print(f"纹理准备失败: {prep_error}") - # 继续尝试,可能纹理已经准备好了 - - # 获取OpenGL纹理ID - left_texture_id = self._get_texture_opengl_id(left_texture, prepared_objects, gsg) - right_texture_id = self._get_texture_opengl_id(right_texture, prepared_objects, gsg) - - # 检查是否成功获取了纹理ID - if left_texture_id is not None and left_texture_id > 0 and right_texture_id is not None and right_texture_id > 0: - try: - # 创建OpenVR纹理结构 - left_eye_texture = openvr.Texture_t() - left_eye_texture.handle = int(left_texture_id) - left_eye_texture.eType = openvr.TextureType_OpenGL - left_eye_texture.eColorSpace = openvr.ColorSpace_Gamma - - right_eye_texture = openvr.Texture_t() - right_eye_texture.handle = int(right_texture_id) - right_eye_texture.eType = openvr.TextureType_OpenGL - right_eye_texture.eColorSpace = openvr.ColorSpace_Gamma - - # 提交到VR系统 - error_left = self.vr_compositor.submit(openvr.Eye_Left, left_eye_texture) - error_right = self.vr_compositor.submit(openvr.Eye_Right, right_eye_texture) - - # 检查提交结果 - 在Python OpenVR中,None表示成功 - left_success = (error_left is None or error_left == openvr.VRCompositorError_None) - right_success = (error_right is None or error_right == openvr.VRCompositorError_None) - - if not left_success: - print(f"⚠️ 左眼纹理提交错误: {error_left}") - self.submit_failures += 1 - if not right_success: - print(f"⚠️ 右眼纹理提交错误: {error_right}") - self.submit_failures += 1 - - # 如果两个都成功了,输出成功信息(仅第一次) - if not hasattr(self, '_first_submit_success'): - if left_success and right_success: - print("✅ VR纹理提交成功!缓冲区应该显示场景内容。") - print(f" 左眼纹理ID: {left_texture_id}, 右眼纹理ID: {right_texture_id}") - self._first_submit_success = True - - except Exception as submit_error: - print(f"❌ VR纹理提交过程失败: {submit_error}") - if "DoNotHaveFocus" in str(submit_error): - print("🔍 这通常意味着另一个VR应用程序正在使用VR系统") - self.submit_failures += 1 - else: - # 只在第一次失败时输出警告,避免太多日志 - if not hasattr(self, '_texture_id_warning_shown'): - print("⚠️ 无法获取有效的纹理OpenGL ID,跳过VR帧提交") - print(" 这可能是因为纹理尚未正确准备到GPU") - self._texture_id_warning_shown = True - - except Exception as e: - print(f"提交VR帧失败: {e}") - import traceback - traceback.print_exc() - - def _get_texture_opengl_id(self, texture, prepared_objects, gsg): - """获取纹理的OpenGL ID""" - try: - # 方法1: 使用prepareNow获取正确的纹理上下文 - texture_context = texture.prepareNow(0, prepared_objects, gsg) - if texture_context and hasattr(texture_context, 'getNativeId'): - native_id = texture_context.getNativeId() - if native_id > 0: - # print(f"✓ 成功获取纹理 {texture.getName()} 的OpenGL ID: {native_id}") - return native_id - - # 方法2: 备选方案 - 通过prepared_objects获取texture context - if hasattr(prepared_objects, 'getTextureContext'): - texture_context = prepared_objects.getTextureContext(texture) - if texture_context and hasattr(texture_context, 'getNativeId'): - native_id = texture_context.getNativeId() - if native_id > 0: - # print(f"✓ 备选方法获取纹理 {texture.getName()} 的OpenGL ID: {native_id}") - return native_id - - # 方法3: 尝试texture对象本身的方法 - if hasattr(texture, 'getNativeId'): - native_id = texture.getNativeId() - if native_id > 0: - # print(f"✓ 直接获取纹理 {texture.getName()} 的OpenGL ID: {native_id}") - return native_id - - # 如果所有方法都失败 - print(f"❌ 无法获取纹理 {texture.getName()} 的OpenGL ID") - return None - - except Exception as e: - print(f"获取纹理OpenGL ID失败: {e}") - import traceback - traceback.print_exc() - return None def enable_vr(self): """启用VR模式""" @@ -1138,11 +1498,17 @@ class VRManager(DirectObject): # 禁用主相机避免干扰VR渲染 self._disable_main_cam() - # 优化VR性能:切换到update_task姿态策略 - print("🚀 正在优化VR性能...") - self.set_pose_strategy('update_task') - self.set_prediction_time(7.0) # 7ms预测时间 - 减少VSync等待时间 - print("✓ VR性能优化完成 - 姿态策略已切换至update_task模式") + # VR性能优化:使用Running Start模式(Valve最佳实践) + print("🚀 VR性能优化:Running Start模式已启用") + print(" 优势:Submit后立即获取姿态,避免错过VSync窗口") + 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 @@ -1154,6 +1520,12 @@ class VRManager(DirectObject): # 恢复主相机 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): @@ -1166,6 +1538,36 @@ class VRManager(DirectObject): 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() @@ -1356,35 +1758,54 @@ class VRManager(DirectObject): 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窗口,建议降低预测时间") + 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并行度分析 + # CPU-GPU并行度分析 - 增强诊断 gpu_total = current.get('gpu_total_render', 0) - cpu_render = current.get('total_render', 0) - if gpu_total > 0 and cpu_render > 0: - if gpu_total > cpu_render * 3: - print(f" ⚠️ GPU等待CPU: GPU{gpu_total:.1f}ms vs CPU{cpu_render:.1f}ms") + 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命令提交是否及时") - elif cpu_render > gpu_total * 2: - print(f" ⚠️ CPU瓶颈: CPU{cpu_render:.1f}ms vs GPU{gpu_total:.1f}ms") else: - print(f" ✓ CPU-GPU平衡: CPU{cpu_render:.1f}ms GPU{gpu_total:.1f}ms") + print(f" ✓ CPU-GPU时间匹配: 比例{ratio:.1f}:1") # 预测时间诊断 current_prediction = self.use_prediction_time * 1000 print(f" 预测时间设置: {current_prediction:.1f}ms") - if current_prediction > 10: + if current_prediction > 15: print(f" 建议: 预测时间较高,可能增加waitGetPoses延迟") - elif current_prediction < 5: + elif current_prediction < 8: print(f" 注意: 预测时间较低,可能影响姿态准确性") + else: + print(f" ✓ 预测时间在合理范围内") # 优化状态总结 optimization_score = 0 @@ -1412,13 +1833,11 @@ class VRManager(DirectObject): print(f" 右眼渲染: {pipeline_stats['current']['right_render']:.2f}ms") print(f" 姿态获取: {pipeline_stats['current']['wait_poses']:.2f}ms") - # 显示姿态策略信息 - strategy_info = self.get_pose_strategy_info() - print(f"🎯 姿态策略:") - print(f" 当前策略: {strategy_info['strategy']}") - print(f" 策略描述: {strategy_info['description']}") - if strategy_info['strategy'] == 'update_task': - print(f" 预测时间: {strategy_info['prediction_time_ms']:.1f}ms") + # 显示Running Start模式信息 + print(f"🎯 Running Start模式:") + print(f" 模式: Valve Running Start - Submit后立即获取姿态") + print(f" 优势: 避免VSync延迟,提升VR性能") + print(f" 预测时间: {self.use_prediction_time * 1000:.1f}ms") # 分析最大瓶颈 current = pipeline_stats['current'] @@ -1565,180 +1984,103 @@ class VRManager(DirectObject): print(summary) - def left_cb(self, cbdata): - """左眼渲染回调 - 修复ATW闪烁问题""" - # 开始计时左眼渲染 - render_timing = self._start_timing('left_render') + # 注意:原来的left_cb和right_cb函数已被删除 + # 它们的功能已集成到_handle_vr_rendering_and_submit方法中 - try: - # 根据策略决定是否在渲染回调中获取姿态 - if self.pose_strategy == 'render_callback': - # 传统策略:在渲染开始前立即获取最新姿态 - if not hasattr(self, '_poses_updated_this_frame') or not self._poses_updated_this_frame: - self._wait_get_poses_immediate() - # 缓存当前姿态供下一帧使用,符合OpenVR时序假设 - self._cache_poses_for_next_frame() - # 使用缓存的姿态更新相机(上一帧的姿态) - self._update_camera_poses_with_cache() - self._poses_updated_this_frame = True - # 在帧结束时重置标记 - self.world.taskMgr.doMethodLater(0.001, self._reset_frame_flag, "reset_frame_flag") - elif self.poses_updated_in_task: - # 新策略:使用在更新任务中获取的姿态,无需重新获取 - pass - - # 执行实际的渲染工作 - cbdata.upcall() - - # 结束渲染计时 - self._end_timing(render_timing) - - # 根据提交策略决定是否立即提交 - if not self.submit_together: - # 分别提交模式:左眼渲染完成后立即提交 - submit_timing = self._start_timing('submit') - self.submit_texture(openvr.Eye_Left, self.vr_left_texture) - self._end_timing(submit_timing) - - except Exception as e: - self._end_timing(render_timing) - print(f"左眼渲染回调错误: {e}") - - def right_cb(self, cbdata): - """右眼渲染回调 - 修复ATW闪烁问题""" - # 开始计时右眼渲染 - render_timing = self._start_timing('right_render') - - try: - # 根据策略决定是否在渲染回调中获取姿态 - if self.pose_strategy == 'render_callback': - # 传统策略:在渲染开始前立即获取最新姿态 - # 由于左右眼都会调用回调,确保每帧只调用一次WaitGetPoses - if not hasattr(self, '_poses_updated_this_frame') or not self._poses_updated_this_frame: - self._wait_get_poses_immediate() - # 缓存当前姿态供下一帧使用,符合OpenVR时序假设 - self._cache_poses_for_next_frame() - # 使用缓存的姿态更新相机(上一帧的姿态) - self._update_camera_poses_with_cache() - self._poses_updated_this_frame = True - # 在帧结束时重置标记 - self.world.taskMgr.doMethodLater(0.001, self._reset_frame_flag, "reset_frame_flag") - elif self.poses_updated_in_task: - # 新策略:使用在更新任务中获取的姿态,无需重新获取 - pass - - # 执行实际的渲染工作 - cbdata.upcall() - - # 结束渲染计时 - self._end_timing(render_timing) - - # 根据提交策略决定提交方式 - submit_timing = self._start_timing('submit') - if self.submit_together: - # 同时提交模式:右眼渲染完成后同时提交左右眼 - self.submit_texture(openvr.Eye_Left, self.vr_left_texture) - self.submit_texture(openvr.Eye_Right, self.vr_right_texture) - else: - # 分别提交模式:只提交右眼 - self.submit_texture(openvr.Eye_Right, self.vr_right_texture) - self._end_timing(submit_timing) - - # 🚀 Valve的"Running Start"技术:Submit后立即调用WaitGetPoses - # 这是获取下一帧姿态的最佳时机,避免错过VSync窗口 - if not hasattr(self, '_waitgetposes_called_this_frame') or not self._waitgetposes_called_this_frame: - pose_timing = self._start_timing('wait_poses') - self._wait_get_poses_immediate() - self._end_timing(pose_timing) - self._waitgetposes_called_this_frame = True - # print("✓ Running Start: 在Submit后立即获取下一帧姿态") - - # 重置标记,准备下一帧 - self.world.taskMgr.doMethodLater(0.001, self._reset_waitgetposes_flag, "reset_waitgetposes_flag") - - except Exception as e: - self._end_timing(render_timing) - print(f"右眼渲染回调错误: {e}") + # 注意:_safe_submit_texture方法已删除 + # VR纹理提交现在完全由Panda3D的renderFrame()自动处理 def submit_texture(self, eye, texture): - """提交纹理到VR - 基于参考实现,增强调试信息""" + """优化的VR纹理提交 - 使用缓存的纹理ID,避免重复prepareNow""" try: if not self.vr_compositor: print("❌ VR compositor不可用") self.submit_failures += 1 return - # 获取graphics state guardian和prepared objects - gsg = self.world.win.getGsg() - if not gsg: - print("❌ 无法获取GraphicsStateGuardian") + # 🚀 关键修复:防止同一帧重复提交 + 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 - prepared_objects = gsg.getPreparedObjects() - if not prepared_objects: - print("❌ 无法获取PreparedObjects") + # 检查缓存的纹理ID是否有效 + if not handle or handle <= 0: + print(f"❌ {eye_name}纹理ID缓存无效: {handle}") + print(" 这可能表示纹理准备失败,需要检查_prepare_and_cache_textures()") self.submit_failures += 1 return - # 🚀 安全的OpenGL命令刷新:在纹理提交前确保渲染命令已发送到GPU - try: - if hasattr(gsg, 'flush'): - gsg.flush() - elif hasattr(gsg, 'clear_before_callback'): - # 另一种可能的刷新方法 - pass - except Exception as flush_error: - if not hasattr(self, '_gsg_flush_error_logged'): - print(f"⚠️ GSG刷新失败: {flush_error}") - self._gsg_flush_error_logged = True + # ❌ 移除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 - # 准备纹理并获取更详细的错误信息 - if not texture: - print("❌ 纹理对象为空") - self.submit_failures += 1 - return + # 🚀 关键优化:使用缓存的OpenVR Texture对象,避免每帧创建 + if eye == openvr.Eye_Left: + ovr_texture = self._left_ovr_texture + else: + ovr_texture = self._right_ovr_texture - # 纹理准备调试信息已注释掉以减少输出 - # print(f"🔍 准备纹理: {texture.getName()}, 大小: {texture.getXSize()}x{texture.getYSize()}") - - texture_context = texture.prepareNow(0, prepared_objects, gsg) - if not texture_context: - print("❌ prepareNow返回空的texture_context") - self.submit_failures += 1 - return - - handle = texture_context.getNativeId() - # print(f"🔍 获取OpenGL纹理句柄: {handle}") - - if handle != 0: + # 检查缓存对象是否存在(向后兼容) + if ovr_texture is None: + # 备用方案:如果缓存对象不存在,创建新的(性能较差) ovr_texture = openvr.Texture_t() - ovr_texture.handle = handle 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 - eye_name = "左眼" if eye == openvr.Eye_Left else "右眼" - # print(f"🔍 提交{eye_name}纹理到VR, 句柄: {handle}") + # 只更新handle,其他属性已预设置 + ovr_texture.handle = handle - # 提交到VR系统 - error = self.vr_compositor.submit(eye, ovr_texture) + # 提交到VR系统 + error = self.vr_compositor.submit(eye, ovr_texture) - # print(f"🔍 VR提交结果: {error}") - - # 检查错误 - if error and error != openvr.VRCompositorError_None: - print(f"⚠️ VR纹理提交错误代码: {error}") - self.submit_failures += 1 - else: - # 只在第一次成功时输出 - if not hasattr(self, '_submit_success_logged'): - print(f"✅ VR纹理提交成功! {eye_name}") - self._submit_success_logged = True - else: - print(f"❌ 无法获取纹理OpenGL句柄: handle = {handle}") - print(f" 纹理状态: 已准备={texture.isPrepared(prepared_objects)}") - print(f" 纹理格式: {texture.getFormat()}") + # 检查错误 + 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}") @@ -1746,6 +2088,59 @@ class VRManager(DirectObject): 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: @@ -2122,19 +2517,20 @@ class VRManager(DirectObject): print("📝 异步重投影禁用选项已关闭,将使用默认ATW行为") def _start_timing(self, operation_name): - """开始计时操作""" - if not self.enable_pipeline_monitoring: + """开始计时操作 - 性能优化版本""" + if not self.enable_pipeline_monitoring or self.performance_mode_enabled: return None import time + # 🚀 性能优化:减少字典创建,只在必要时创建 return { 'operation': operation_name, 'start_time': time.perf_counter() } def _end_timing(self, timing_data): - """结束计时并记录结果""" - if not self.enable_pipeline_monitoring or not timing_data: + """结束计时并记录结果 - 性能优化版本""" + if not self.enable_pipeline_monitoring or self.performance_mode_enabled or not timing_data: return 0.0 import time @@ -2232,34 +2628,6 @@ class VRManager(DirectObject): } } - def set_pose_strategy(self, strategy): - """设置waitGetPoses调用策略 - - Args: - strategy: 'render_callback' 或 'update_task' - """ - if strategy not in ['render_callback', 'update_task']: - print(f"⚠️ 无效的姿态策略: {strategy},支持的策略: render_callback, update_task") - return False - - old_strategy = self.pose_strategy - self.pose_strategy = strategy - - if old_strategy != strategy: - print(f"✓ VR姿态策略已切换: {old_strategy} → {strategy}") - if strategy == 'update_task': - print(" 🚀 Valve Running Start模式:Submit后立即获取姿态") - print(" 优势:避免错过VSync窗口,显著降低WaitGetPoses延迟") - else: - print(" 使用传统渲染回调策略") - print(" 缺点:可能在渲染前调用WaitGetPoses,容易错过VSync") - - # 重置相关标记 - if hasattr(self, '_poses_updated_this_frame'): - delattr(self, '_poses_updated_this_frame') - self.poses_updated_in_task = False - - return True def set_prediction_time(self, prediction_time_ms): """设置预测时间(仅用于update_task策略) @@ -2275,16 +2643,6 @@ class VRManager(DirectObject): self.use_prediction_time = prediction_time_s print(f"✓ VR预测时间已设置: {old_time:.1f}ms → {prediction_time_ms:.1f}ms") - def get_pose_strategy_info(self): - """获取当前姿态策略信息""" - return { - 'strategy': self.pose_strategy, - 'prediction_time_ms': self.use_prediction_time * 1000, - 'description': { - 'render_callback': '在渲染回调中获取姿态 - 传统模式,可能错过VSync', - 'update_task': 'Valve Running Start模式 - Submit后立即获取姿态,避免VSync延迟' - }.get(self.pose_strategy, '未知策略') - } def test_pipeline_monitoring(self): """测试管线监控功能 - 用于调试和验证""" @@ -2297,11 +2655,10 @@ class VRManager(DirectObject): print(f" 显示频率: {self.vr_display_frequency} Hz") print(f" 目标帧时间: {self.target_frame_time_ms:.2f}ms") - # 测试策略信息 - strategy_info = self.get_pose_strategy_info() - print("🎯 姿态策略:") - print(f" 当前策略: {strategy_info['strategy']}") - print(f" 预测时间: {strategy_info['prediction_time_ms']:.1f}ms") + # Running Start模式信息 + print("🎯 Running Start模式:") + print(f" 预测时间: {self.use_prediction_time * 1000:.1f}ms") + print(f" 模式: Valve Running Start - Submit后立即获取姿态") # 测试管线统计 if self.enable_pipeline_monitoring: @@ -2496,19 +2853,21 @@ class VRManager(DirectObject): 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 # 转换为毫秒 - # 添加到帧时间历史 - self.frame_times.append(frame_time) + # 🚀 性能优化:性能模式下跳过列表操作以减少内存分配 + 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) + # 限制历史长度 + if len(self.frame_times) > self.max_frame_time_history: + self.frame_times.pop(0) self._last_frame_time = current_time @@ -2655,6 +3014,127 @@ class VRManager(DirectObject): 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 @@ -2692,4 +3172,703 @@ class VRManager(DirectObject): '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 + + # 设置测试模式 + 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}") + + return True + + except Exception as e: + print(f"❌ 启动VR测试模式失败: {e}") + import traceback + traceback.print_exc() + 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/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/ui/main_window.py b/ui/main_window.py index aa5208ad..b7dfc0f2 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -498,6 +498,11 @@ class MainWindow(QMainWindow): # 测试功能 self.vrTestPipelineAction = self.vrDebugMenu.addAction('测试管线监控') + # VR测试模式 + self.vrTestModeAction = self.vrDebugMenu.addAction('VR测试模式') + self.vrTestModeAction.setCheckable(True) + self.vrTestModeAction.setChecked(False) # 默认关闭 + self.vrDebugSettingsAction = self.vrDebugMenu.addAction('调试设置...') # 初始状态下禁用退出VR选项 @@ -978,6 +983,7 @@ class MainWindow(QMainWindow): 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) self.vrDebugSettingsAction.triggered.connect(self.onShowVRDebugSettings) @@ -2517,6 +2523,37 @@ class MainWindow(QMainWindow): 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 setup_main_window(world,path = None): """设置主窗口的便利函数""" app = QApplication.instance() -- 2.45.2 From 1c356ed6dc84e99350818b8df4157994c4f124dc Mon Sep 17 00:00:00 2001 From: Rowland <975945824@qq.com> Date: Sun, 28 Sep 2025 10:41:44 +0800 Subject: [PATCH 08/11] =?UTF-8?q?vr=E9=AB=98=E5=B8=A7=E7=8E=87=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/vr_manager.py | 380 ++++++++++++++++++++++++++++++++------------- ui/main_window.py | 74 +++++++++ 2 files changed, 348 insertions(+), 106 deletions(-) diff --git a/core/vr_manager.py b/core/vr_manager.py index a9242f9b..7d60bdf9 100644 --- a/core/vr_manager.py +++ b/core/vr_manager.py @@ -132,6 +132,11 @@ class VRManager(DirectObject): 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 = True # 是否启用性能监控 self.debug_output_enabled = True # 是否启用调试输出 @@ -898,26 +903,36 @@ class VRManager(DirectObject): if self.vr_task: self.world.taskMgr.remove(self.vr_task) - self.vr_task = self.world.taskMgr.add(self._update_vr, "update_vr") - print("✓ VR更新任务已启动") + # 设置高优先级(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更新任务 - 每帧调用""" + """VR更新任务 - 每帧调用(参考 panda3d-openvr 架构)""" if not self.vr_enabled or not self.vr_system: return task.cont try: + # 📌 第一件事: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 - # 🚀 手动垃圾回收控制 - 避免16-19帧周期性峰值 - self._manual_gc_control() - # 🚀 自动启用性能模式 - 减少计时对象创建 if not self.performance_mode_enabled and self.frame_count >= self.performance_mode_trigger_frame: self.performance_mode_enabled = True - print(f"🎯 性能模式已启用 (帧#{self.frame_count}) - 禁用详细监控以提升性能") - print(" 现在将减少每帧对象创建,显著提升VR性能稳定性") + if self.frame_count <= self.performance_mode_trigger_frame + 5: # 只输出一次 + print(f"🎯 性能模式已启用 (帧#{self.frame_count}) - 禁用详细监控以提升性能") # 记录帧时间 self._track_frame_time() @@ -932,36 +947,31 @@ class VRManager(DirectObject): self.last_fps_check = self.frame_count self.last_fps_time = current_time - # 更新系统性能指标 - self._update_performance_metrics() - - # 更新GPU渲染时间统计 - if self.enable_gpu_timing: - self._get_gpu_frame_timing() - - # 🚀 Running Start策略:WaitGetPoses在Submit后调用,此处只更新相机 - # 不在此处调用WaitGetPoses,避免错过VSync窗口 - self.poses_updated_in_task = True - - # 使用上一帧获取的姿态更新相机 + # 📌 使用刚获取的姿态更新相机(而不是旧姿态) self._update_camera_poses() # 输出策略信息(仅第一次) - if not hasattr(self, '_running_start_logged'): - print("✓ Running Start模式已启用 - WaitGetPoses在Submit后调用") - self._running_start_logged = True + if not hasattr(self, '_new_architecture_logged'): + print("✅ 新架构已启用 - waitGetPoses 在任务开始阻塞,参考 panda3d-openvr") + print(" 这确保每个 VSync 周期只渲染一次,解决 AlreadySubmitted 错误") + self._new_architecture_logged = True - # 1. 更新手柄和其他跟踪设备 - self.update_tracked_devices() - - # 2. 更新VR动作状态 + # 更新VR动作状态 self.action_manager.update_actions() - # 3. 更新VR交互系统 + # 更新VR交互系统 self.interaction_manager.update() - # 🚀 阶段2&3:渲染同步和纹理提交(替代DrawCallback) - self._handle_vr_rendering_and_submit() + # 更新系统性能指标(减少频率) + 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) @@ -975,47 +985,15 @@ class VRManager(DirectObject): return task.cont - def _handle_vr_rendering_and_submit(self): - """VR帧管理 - DrawCallback现在处理实际渲染和提交""" + def _sync_gpu_if_needed(self): + """可选的GPU同步 - 仅在需要时使用""" try: - import time - - # 记录帧处理开始时间 - frame_start_time = time.perf_counter() - - # DrawCallback会处理实际的渲染和纹理提交 - # 这里主要处理帧管理和诊断 - # 同步等待GPU完成(如果需要) gsg = self.world.win.getGsg() if gsg: gsg.getEngine().syncFrame() - - # 计算帧处理时间 - frame_time = (time.perf_counter() - frame_start_time) * 1000 - - # Running Start的备用逻辑(DrawCallback可能已处理) - # 确保每帧都有姿态获取 - if not hasattr(self, '_waitgetposes_called_this_frame') or not self._waitgetposes_called_this_frame: - pose_start = time.perf_counter() - self._wait_get_poses_immediate() - self.wait_poses_time = (time.perf_counter() - pose_start) * 1000 - self._waitgetposes_called_this_frame = True - - # 重置标记,准备下一帧 - self.world.taskMgr.doMethodLater(0.001, self._reset_waitgetposes_flag, "reset_waitgetposes_flag") - - # 调试信息(仅在启用时) - if self.debug_output_enabled and self.frame_count % 300 == 1: - print(f"🎯 帧#{self.frame_count} DrawCallback模式:") - print(f" 帧管理时间: {frame_time:.2f}ms") - print(f" 渲染由回调处理: 左眼={self.left_render_count}, 右眼={self.right_render_count}") - print(f" 姿态获取: {self.wait_poses_time:.2f}ms") - except Exception as e: - print(f"❌ VR帧管理异常: {e}") - import traceback - traceback.print_exc() + print(f"❌ GPU同步异常: {e}") def simple_left_cb(self, cbdata): """简化的左眼渲染回调 - 精确控制渲染和提交""" @@ -1033,25 +1011,25 @@ class VRManager(DirectObject): # 记录渲染次数 self.left_render_count += 1 - # 检查避免重复提交 - current_frame = getattr(self, 'frame_count', 0) - if not hasattr(self, '_left_submitted_frame'): - self._left_submitted_frame = -1 + # 📌 OpenVR 帧边界检查:防止同一 OpenVR 帧内重复渲染 + if self.left_eye_last_render_frame == self.openvr_frame_id: + return # 已在当前 OpenVR 帧渲染过,跳过 - if self._left_submitted_frame != current_frame: - self._left_submitted_frame = current_frame + # 🔧 OpenVR最佳实践:左眼只渲染,不立即提交 + # 基于官方hellovr示例:两眼都渲染完后再批量提交 + self.left_eye_last_render_frame = self.openvr_frame_id - # 🔧 渐进式VR功能测试:根据调试标志决定是否提交纹理 - should_submit = not self.vr_test_mode or self.test_mode_submit_texture + # 渐进式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 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() + if self.vr_test_mode: + # 测试模式:始终触发屏幕显示更新 + self._update_test_display() except Exception as e: print(f"左眼渲染回调错误: {e}") @@ -1072,33 +1050,56 @@ class VRManager(DirectObject): # 记录渲染次数 self.right_render_count += 1 - # 检查避免重复提交 - current_frame = getattr(self, 'frame_count', 0) - if not hasattr(self, '_right_submitted_frame'): - self._right_submitted_frame = -1 + # 📌 OpenVR 帧边界检查:防止同一 OpenVR 帧内重复渲染 + if self.right_eye_last_render_frame == self.openvr_frame_id: + return # 已在当前 OpenVR 帧渲染过,跳过 - if self._right_submitted_frame != current_frame: - self._right_submitted_frame = current_frame + # 🔧 OpenVR最佳实践:右眼渲染完成后批量提交两眼纹理 + # 基于官方hellovr示例:避免分散提交导致的VSync阻塞 + self.right_eye_last_render_frame = self.openvr_frame_id - # 🔧 渐进式VR功能测试:根据调试标志决定启用哪些功能 - should_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 + # 🔧 渐进式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 should_submit: - # 渲染后立即提交纹理(普通模式或测试模式启用提交时) + # 特殊处理:测试模式单独启用功能时的兼容模式 + 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) - if should_wait_poses: - # Running Start:右眼提交后立即获取姿态(普通模式或测试模式启用时) - if not hasattr(self, '_waitgetposes_called_this_frame') or not self._waitgetposes_called_this_frame: - self._wait_get_poses_immediate() - self._waitgetposes_called_this_frame = True - self.world.taskMgr.doMethodLater(0.001, self._reset_waitgetposes_flag, "reset_waitgetposes_flag") + # 🚀 测试模式也需要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 self.vr_test_mode: - # 测试模式:始终更新性能HUD - self._update_test_performance_hud() + 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}") @@ -1165,6 +1166,9 @@ class VRManager(DirectObject): # 渲染姿态用于绘制,游戏姿态用于逻辑,避免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) @@ -1204,10 +1208,15 @@ class VRManager(DirectObject): 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: @@ -1500,7 +1509,8 @@ class VRManager(DirectObject): # VR性能优化:使用Running Start模式(Valve最佳实践) print("🚀 VR性能优化:Running Start模式已启用") - print(" 优势:Submit后立即获取姿态,避免错过VSync窗口") + print(" 优势:在帧开始时获取姿态,提供VSync前3ms的渲染时间") + print(" 注意:Submit后立即调用WaitGetPoses是错误实现") self.set_prediction_time(11.0) # 11ms预测时间 - OpenVR标准值,平衡准确性和延迟 # 🚀 动态调整Qt Timer频率以支持VR @@ -1835,8 +1845,8 @@ class VRManager(DirectObject): # 显示Running Start模式信息 print(f"🎯 Running Start模式:") - print(f" 模式: Valve Running Start - Submit后立即获取姿态") - print(f" 优势: 避免VSync延迟,提升VR性能") + print(f" 模式: Valve Running Start - 帧开始时获取姿态") + print(f" 优势: VSync前3ms获取姿态,提供充足渲染时间") print(f" 预测时间: {self.use_prediction_time * 1000:.1f}ms") # 分析最大瓶颈 @@ -1866,11 +1876,11 @@ class VRManager(DirectObject): recommendations = [] # FPS相关建议 - if stats['vr_fps'] < 72: + if stats['vr_fps'] < 60: recommendations.append(" ⚠️ VR帧率过低,可能影响体验") # 帧时间相关建议 - if stats['frame_time_avg'] > 13.9: # 72fps = 13.9ms + if stats['frame_time_avg'] > 16.7: # 60fps = 16.7ms recommendations.append(" ⚠️ 平均帧时间过高,建议降低渲染质量") if stats['frame_time_max'] > 50: @@ -2658,7 +2668,7 @@ class VRManager(DirectObject): # Running Start模式信息 print("🎯 Running Start模式:") print(f" 预测时间: {self.use_prediction_time * 1000:.1f}ms") - print(f" 模式: Valve Running Start - Submit后立即获取姿态") + print(f" 模式: Valve Running Start - 帧开始时获取姿态") # 测试管线统计 if self.enable_pipeline_monitoring: @@ -3437,6 +3447,13 @@ class VRManager(DirectObject): 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 @@ -3483,6 +3500,157 @@ class VRManager(DirectObject): 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: diff --git a/ui/main_window.py b/ui/main_window.py index b7dfc0f2..b40718ba 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -503,6 +503,26 @@ class MainWindow(QMainWindow): 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选项 @@ -984,6 +1004,15 @@ class MainWindow(QMainWindow): 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) @@ -2554,6 +2583,51 @@ class MainWindow(QMainWindow): 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() -- 2.45.2 From fa8353c86b8715b68e296d198475ce5065f0e149 Mon Sep 17 00:00:00 2001 From: Rowland <975945824@qq.com> Date: Sun, 28 Sep 2025 10:54:19 +0800 Subject: [PATCH 09/11] =?UTF-8?q?vr=E9=AB=98=E5=B8=A7=E7=8E=87=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/vr_manager.py | 18 ++++++++++++---- ui/main_window.py | 51 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/core/vr_manager.py b/core/vr_manager.py index 7d60bdf9..82929b7e 100644 --- a/core/vr_manager.py +++ b/core/vr_manager.py @@ -137,9 +137,9 @@ class VRManager(DirectObject): self.left_eye_last_render_frame = -1 self.right_eye_last_render_frame = -1 - # 高级性能监控 - self.performance_monitoring = True # 是否启用性能监控 - self.debug_output_enabled = True # 是否启用调试输出 + # 高级性能监控(默认关闭,手动开启) + 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 @@ -171,7 +171,7 @@ class VRManager(DirectObject): self.pipeline_history_size = 30 # GPU渲染时间监控(OpenVR Frame Timing) - self.enable_gpu_timing = True # 是否启用GPU时间监控 + 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时间 @@ -2918,6 +2918,16 @@ class VRManager(DirectObject): 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): """设置性能检查间隔 diff --git a/ui/main_window.py b/ui/main_window.py index b40718ba..541588f6 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -451,7 +451,7 @@ class MainWindow(QMainWindow): self.vrDebugMenu = self.vrMenu.addMenu('VR调试') self.vrDebugToggleAction = self.vrDebugMenu.addAction('启用调试输出') self.vrDebugToggleAction.setCheckable(True) - self.vrDebugToggleAction.setChecked(True) # 默认启用 + self.vrDebugToggleAction.setChecked(False) # 默认关闭(节省资源) self.vrShowPerformanceAction = self.vrDebugMenu.addAction('立即显示性能报告') @@ -473,10 +473,19 @@ class MainWindow(QMainWindow): 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(True) # 默认启用 + self.vrPipelineMonitorAction.setChecked(False) # 默认关闭(节省资源) self.vrDebugMenu.addSeparator() @@ -999,6 +1008,8 @@ class MainWindow(QMainWindow): 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')) @@ -2341,6 +2352,42 @@ class MainWindow(QMainWindow): 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: -- 2.45.2 From e5d2e485b9fc6077b09a6d16dfed43167a2d0884 Mon Sep 17 00:00:00 2001 From: Rowland <975945824@qq.com> Date: Sun, 28 Sep 2025 16:08:22 +0800 Subject: [PATCH 10/11] =?UTF-8?q?=E6=89=8B=E6=9F=84=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- QPanda3D/Panda3DWorld.py | 6 +-- core/vr_visualization.py | 94 ++++++++++++++++++++++++++++++++-------- 2 files changed, 78 insertions(+), 22 deletions(-) diff --git a/QPanda3D/Panda3DWorld.py b/QPanda3D/Panda3DWorld.py index f93c74aa..d7012fc9 100644 --- a/QPanda3D/Panda3DWorld.py +++ b/QPanda3D/Panda3DWorld.py @@ -87,8 +87,8 @@ class Panda3DWorld(ShowBase): ShowBase.__init__(self) # 初始化渲染管线并设置可调整大小的标志 - # self.render_pipeline.create(self) - # _global_render_pipeline = self.render_pipeline + self.render_pipeline.create(self) + _global_render_pipeline = self.render_pipeline # 创建 Qt 能读的 RGBA8 贴图 self.qt_output_tex = Texture("qt_output_tex") @@ -146,7 +146,7 @@ class Panda3DWorld(ShowBase): #self.cam = self.render_pipeline._showbase.cam #self.camNode = self.cam.node() #self.camLens = self.camNode.get_lens() - # self.render_pipeline._showbase.camera = self.render_pipeline._showbase.cam + self.render_pipeline._showbase.camera = self.render_pipeline._showbase.cam #self.render_pipeline.daytime_mgr.update() diff --git a/core/vr_visualization.py b/core/vr_visualization.py index 368cda81..9906d97a 100644 --- a/core/vr_visualization.py +++ b/core/vr_visualization.py @@ -178,36 +178,38 @@ class VRControllerVisualizer: return None def _fix_model_material(self, model): - """修复模型材质,解决纯黑色问题同时保持纹理""" - from panda3d.core import Material, MaterialAttrib + """修复模型材质,使用RenderPipeline兼容的新Material API""" + from panda3d.core import Material, Vec4 # 检查模型是否有纹理 has_texture = model.hasTexture() - # 创建新的材质 + # 创建新的材质,使用RenderPipeline兼容的API material = Material() if has_texture: - # 有纹理时,设置材质以增强纹理效果 - material.setDiffuse((1.0, 1.0, 1.0, 1.0)) # 白色漫反射让纹理完全显示 - material.setAmbient((0.4, 0.4, 0.4, 1.0)) # 适度环境光 - material.setSpecular((0.3, 0.3, 0.3, 1.0)) # 轻度高光 - material.setShininess(16.0) # 中等光泽度 - print(f"🎨 {self.controller.name}手柄:已修复材质(保持纹理效果)") + # 有纹理时,设置白色基础颜色让纹理完全显示 + 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.setDiffuse((0.7, 0.7, 0.8, 1.0)) # 略偏蓝的灰色 - material.setAmbient((0.3, 0.3, 0.3, 1.0)) # 环境光 - material.setSpecular((0.5, 0.5, 0.5, 1.0)) # 高光 - material.setShininess(32.0) # 光泽度 - print(f"🎨 {self.controller.name}手柄:已修复材质(使用颜色)") + # 无纹理时,设置手柄颜色 + 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)") - # 应用材质到模型 - model.setMaterial(material) + # 应用材质到模型,使用优先级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 @@ -494,10 +496,33 @@ class VRControllerVisualizer: # 设置透明度 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}手柄交互射线已创建") + print(f"✓ {self.controller.name}手柄交互射线已创建(含PBR支持)") def _create_sphere_geometry(self, radius): """创建球体几何体""" @@ -530,6 +555,19 @@ class VRControllerVisualizer: 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): @@ -619,6 +657,24 @@ class VRControllerVisualizer: 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 -- 2.45.2 From a16c4bba13102f228ad34101883eb439f5d35441 Mon Sep 17 00:00:00 2001 From: Rowland <975945824@qq.com> Date: Mon, 29 Sep 2025 10:52:39 +0800 Subject: [PATCH 11/11] =?UTF-8?q?=E6=89=8B=E6=9F=84=E4=BA=92=E5=8A=A8?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E6=B7=BB=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/vr_controller.py | 164 ++++++++- core/vr_joystick.py | 701 +++++++++++++++++++++++++++++++++++++ core/vr_joystick_config.py | 268 ++++++++++++++ core/vr_manager.py | 175 ++++++++- core/vr_teleport.py | 411 ++++++++++++++++++++++ core/vr_visualization.py | 17 +- 6 files changed, 1708 insertions(+), 28 deletions(-) create mode 100644 core/vr_joystick.py create mode 100644 core/vr_joystick_config.py create mode 100644 core/vr_teleport.py diff --git a/core/vr_controller.py b/core/vr_controller.py index f9e5fd64..e293d07f 100644 --- a/core/vr_controller.py +++ b/core/vr_controller.py @@ -60,6 +60,12 @@ class VRController(DirectObject): 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 @@ -146,8 +152,9 @@ class VRController(DirectObject): 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: @@ -159,20 +166,71 @@ class VRController(DirectObject): # OpenVR Python绑定中使用ulButtonPressed而不是rButtonPressed self.button_states[i] = (state.ulButtonPressed & button_mask) != 0 - # 更新轴状态(扳机、握把、触摸板) - # OpenVR Python绑定中使用vAxis而不是rAxis - if hasattr(state, 'vAxis') and len(state.vAxis) > 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(state.vAxis) > 1: - self.trigger_value = state.vAxis[1].x + if len(axis_data) > 1: + self.trigger_value = axis_data[1].x - # 触摸板轴通常在axis[0] - if len(state.vAxis) > 0: - self.touchpad_pos = Vec3(state.vAxis[0].x, state.vAxis[0].y, 0) + # 触摸板/摇杆轴通常在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) - # 触摸板触摸状态 - 使用正确的OpenVR属性名 + # 额外检查其他轴(某些控制器可能将摇杆分配到不同轴) + 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'): @@ -260,12 +318,92 @@ class VRController(DirectObject): return Vec3(0, 0, 0) def get_forward_direction(self): - """获取手柄指向的方向向量""" + """获取手柄指向的方向向量(包含视角转向)""" if self.anchor_node: - # Y轴正方向为前方 - return self.anchor_node.getMat().getRow3(1).getXyz().normalized() + # 获取相对于世界坐标系的方向,包含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() 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 82929b7e..ab1bc036 100644 --- a/core/vr_manager.py +++ b/core/vr_manager.py @@ -28,10 +28,12 @@ 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): @@ -236,13 +238,19 @@ class VRManager(DirectObject): self.controllers = {} # 设备索引到控制器的映射 self.tracked_device_anchors = {} # 跟踪设备锚点 - # VR动作系统 - 添加异常保护 - try: - self.action_manager = VRActionManager(self) - print("✓ VR动作管理器初始化完成") - except Exception as e: - print(f"⚠️ VR动作管理器初始化失败: {e}") + # 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: @@ -252,6 +260,22 @@ class VRManager(DirectObject): 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_object_pools(self): @@ -522,15 +546,18 @@ class VRManager(DirectObject): # 初始化手柄控制器 self._initialize_controllers() - # 初始化动作系统 - 检查是否存在 - if self.action_manager: + # 初始化动作系统 - 仅在启用时初始化 + 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: - print("⚠️ VR动作管理器未创建,跳过动作系统初始化") + if self.disable_action_system: + print("🚫 VR动作系统已禁用,跳过初始化") + else: + print("⚠️ VR动作管理器未创建,跳过动作系统初始化") # 初始化交互系统 - 检查是否存在 if self.interaction_manager: @@ -542,6 +569,27 @@ class VRManager(DirectObject): else: print("⚠️ VR交互管理器未创建,跳过交互系统初始化") + # 初始化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传送系统未创建,跳过传送系统初始化") + + # 初始化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摇杆管理器未创建,跳过摇杆系统初始化") + # 可选:禁用异步重投影(备选方案) if self.disable_async_reprojection: self._disable_async_reprojection() @@ -956,11 +1004,29 @@ class VRManager(DirectObject): print(" 这确保每个 VSync 周期只渲染一次,解决 AlreadySubmitted 错误") self._new_architecture_logged = True - # 更新VR动作状态 - self.action_manager.update_actions() + # 更新VR动作状态 - 仅在启用时更新 + if not self.disable_action_system and self.action_manager: + self.action_manager.update_actions() # 更新VR交互系统 - self.interaction_manager.update() + 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帧更新一次,减少开销 @@ -1527,6 +1593,32 @@ class VRManager(DirectObject): """禁用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() @@ -1536,7 +1628,7 @@ class VRManager(DirectObject): self.world.qtWidget.synchronizer.setInterval(int(1000/60)) print("✓ Qt Timer恢复为60Hz") - print("✅ VR模式已禁用") + print("✅ VR模式已禁用,手柄模型已隐藏") def cleanup(self): """清理VR资源""" @@ -1598,6 +1690,61 @@ class VRManager(DirectObject): 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: 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 index 9906d97a..c331a299 100644 --- a/core/vr_visualization.py +++ b/core/vr_visualization.py @@ -708,7 +708,22 @@ class VRControllerVisualizer: 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 + print(f"✅ {self.controller.name}手柄可视化已彻底清理") \ No newline at end of file -- 2.45.2