""" VR管理器模块 负责VR功能的初始化、渲染和交互: - OpenVR/SteamVR集成 - VR头显跟踪和渲染 - VR控制器交互 - VR模式切换 """ import sys import numpy as np from panda3d.core import ( WindowProperties, GraphicsPipe, FrameBufferProperties, GraphicsOutput, Texture, Camera, PerspectiveLens, MatrixLens, Mat4, Vec3, TransformState, RenderState, CardMaker, BitMask32, PandaNode, NodePath, LMatrix4, LVector3, LVector4, CS_yup_right, CS_default, PythonCallbackObject ) from direct.task import Task from direct.showbase.DirectObject import DirectObject try: import openvr OPENVR_AVAILABLE = True except ImportError: OPENVR_AVAILABLE = False print("警告: OpenVR未安装,VR功能将不可用") # 导入手柄控制器、动作系统和交互系统 from .vr_controller import LeftController, RightController from .vr_actions import VRActionManager from .vr_interaction import VRInteractionManager class VRManager(DirectObject): """VR管理器类 - 处理所有VR相关功能""" def __init__(self, world): """初始化VR管理器 Args: world: 主世界对象引用 """ super().__init__() self.world = world self.vr_system = None self.vr_enabled = False self.vr_initialized = False # VR渲染相关 self.vr_left_eye_buffer = None self.vr_right_eye_buffer = None self.vr_left_camera = None self.vr_right_camera = None self.vr_compositor = None # VR跟踪数据 self.hmd_pose = Mat4.identMat() self.controller_poses = {} self.tracked_device_poses = [] self.poses = None # OpenVR渲染姿态数组 self.game_poses = None # OpenVR游戏逻辑姿态数组 # VR渲染参数 self.eye_width = 1080 self.eye_height = 1200 self.near_clip = 0.1 self.far_clip = 1000.0 # VR任务 self.vr_task = None # VR锚点层级系统 self.tracking_space = None self.hmd_anchor = None self.left_eye_anchor = None self.right_eye_anchor = None # 坐标系转换矩阵 - 使用Panda3D内置方法 self.coord_mat = LMatrix4.convert_mat(CS_yup_right, CS_default) self.coord_mat_inv = LMatrix4.convert_mat(CS_default, CS_yup_right) # 性能监控 self.frame_count = 0 self.last_fps_check = 0 self.last_fps_time = 0 self.vr_fps = 0 self.submit_failures = 0 self.pose_failures = 0 # VR提交策略 - 基于参考实现 self.submit_together = True # 是否在right_cb中同时提交左右眼 # 帧同步标记 - 修复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 self.controllers = {} # 设备索引到控制器的映射 self.tracked_device_anchors = {} # 跟踪设备锚点 # VR动作系统 self.action_manager = VRActionManager(self) # VR交互系统 self.interaction_manager = VRInteractionManager(self) print("✓ VR管理器初始化完成") def convert_mat(self, mat): """ 将OpenVR矩阵转换为Panda3D矩阵 - 基于参考实现 """ if len(mat.m) == 4: result = LMatrix4( mat.m[0][0], mat.m[1][0], mat.m[2][0], mat.m[3][0], mat.m[0][1], mat.m[1][1], mat.m[2][1], mat.m[3][1], mat.m[0][2], mat.m[1][2], mat.m[2][2], mat.m[3][2], mat.m[0][3], mat.m[1][3], mat.m[2][3], mat.m[3][3]) elif len(mat.m) == 3: result = LMatrix4( mat.m[0][0], mat.m[1][0], mat.m[2][0], 0.0, mat.m[0][1], mat.m[1][1], mat.m[2][1], 0.0, mat.m[0][2], mat.m[1][2], mat.m[2][2], 0.0, mat.m[0][3], mat.m[1][3], mat.m[2][3], 1.0) return result def is_vr_available(self): """检查VR系统是否可用""" if not OPENVR_AVAILABLE: return False try: # 检查SteamVR是否运行 return openvr.isRuntimeInstalled() and openvr.isHmdPresent() except Exception as e: print(f"VR检查失败: {e}") return False def initialize_vr(self): """初始化VR系统""" if not OPENVR_AVAILABLE: print("❌ OpenVR不可用,无法初始化VR") return False if self.vr_initialized: print("VR系统已经初始化") return True try: print("🔄 正在初始化VR系统...") # 初始化OpenVR - 使用Scene应用类型确保正确的焦点管理 self.vr_system = openvr.init(openvr.VRApplication_Scene) if not self.vr_system: print("❌ 无法初始化OpenVR系统") return False # 获取compositor self.vr_compositor = openvr.VRCompositor() if not self.vr_compositor: print("❌ 无法获取VR Compositor") return False # 获取推荐的渲染目标尺寸 self.eye_width, self.eye_height = self.vr_system.getRecommendedRenderTargetSize() print(f"✓ VR渲染目标尺寸: {self.eye_width}x{self.eye_height}") # 创建OpenVR姿态数组 - 分离渲染和游戏姿态 poses_t = openvr.TrackedDevicePose_t * openvr.k_unMaxTrackedDeviceCount self.poses = poses_t() # 渲染姿态(预测的) self.game_poses = poses_t() # 游戏逻辑姿态(当前的) print("✓ VR渲染和游戏姿态数组已创建") # 创建VR渲染缓冲区 if not self._create_vr_buffers(): print("❌ 创建VR渲染缓冲区失败") return False # 设置VR相机 if not self._setup_vr_cameras(): print("❌ 设置VR相机失败") return False # 初始化手柄控制器 self._initialize_controllers() # 初始化动作系统 if not self.action_manager.initialize(): print("⚠️ VR动作系统初始化失败,但VR系统将继续运行") # 初始化交互系统 if not self.interaction_manager.initialize(): print("⚠️ VR交互系统初始化失败,但VR系统将继续运行") # 可选:禁用异步重投影(备选方案) if self.disable_async_reprojection: self._disable_async_reprojection() # 启动VR更新任务 self._start_vr_task() self.vr_initialized = True print("✅ VR系统初始化成功") return True except Exception as e: print(f"❌ VR初始化失败: {e}") import traceback traceback.print_exc() return False def _create_vr_buffers(self): """创建VR渲染缓冲区 - 基于参考实现""" try: # 创建左眼纹理和缓冲区 self.vr_left_texture = self._create_vr_texture("VR Left Eye Texture") self.vr_left_eye_buffer = self._create_vr_buffer( "VR Left Eye", self.vr_left_texture, self.eye_width, self.eye_height ) if not self.vr_left_eye_buffer: print("❌ 创建左眼缓冲区失败") return False # 设置左眼缓冲区属性 self.vr_left_eye_buffer.setSort(-100) self.vr_left_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) # 深蓝色背景便于调试 self.vr_left_eye_buffer.setActive(True) # 创建右眼纹理和缓冲区 self.vr_right_texture = self._create_vr_texture("VR Right Eye Texture") self.vr_right_eye_buffer = self._create_vr_buffer( "VR Right Eye", self.vr_right_texture, self.eye_width, self.eye_height ) if not self.vr_right_eye_buffer: print("❌ 创建右眼缓冲区失败") return False # 设置右眼缓冲区属性 self.vr_right_eye_buffer.setSort(-99) self.vr_right_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) # 深蓝色背景便于调试 self.vr_right_eye_buffer.setActive(True) print("✓ VR渲染缓冲区创建成功") return True except Exception as e: print(f"❌ 创建VR缓冲区失败: {e}") import traceback traceback.print_exc() return False def _create_vr_texture(self, name): """创建VR纹理对象 - 基于参考实现""" texture = Texture(name) texture.setWrapU(Texture.WMClamp) texture.setWrapV(Texture.WMClamp) texture.setMinfilter(Texture.FTLinear) texture.setMagfilter(Texture.FTLinear) return texture def _create_vr_buffer(self, name, texture, width, height): """创建VR渲染缓冲区 - 基于参考实现""" # 设置帧缓冲属性 fbprops = FrameBufferProperties() fbprops.setRgbaBits(1, 1, 1, 1) # 可以在这里添加多重采样抗锯齿 # fbprops.setMultisamples(4) # 创建缓冲区 buffer = self.world.win.makeTextureBuffer(name, width, height, to_ram=False, fbp=fbprops) if buffer: # 清除默认渲染纹理 buffer.clearRenderTextures() # 添加我们的纹理 buffer.addRenderTexture(texture, GraphicsOutput.RTMBindOrCopy, GraphicsOutput.RTPColor) return buffer def _setup_vr_cameras(self): """设置VR相机 - 使用锚点层级系统""" try: # 创建VR追踪空间锚点层级 self.tracking_space = self.world.render.attachNewNode('tracking-space') self.hmd_anchor = self.tracking_space.attachNewNode('hmd-anchor') self.left_eye_anchor = self.hmd_anchor.attachNewNode('left-eye') self.right_eye_anchor = self.hmd_anchor.attachNewNode('right-eye') # 获取投影矩阵 projection_left = self.coord_mat_inv * self.convert_mat( self.vr_system.getProjectionMatrix(openvr.Eye_Left, self.near_clip, self.far_clip)) projection_right = self.coord_mat_inv * self.convert_mat( self.vr_system.getProjectionMatrix(openvr.Eye_Right, self.near_clip, self.far_clip)) # 创建左眼相机节点 left_cam_node = Camera('left-cam') left_lens = MatrixLens() left_lens.setUserMat(projection_left) left_cam_node.setLens(left_lens) # 创建右眼相机节点 right_cam_node = Camera('right-cam') right_lens = MatrixLens() right_lens.setUserMat(projection_right) right_cam_node.setLens(right_lens) # 附加相机到眼睛锚点 self.vr_left_camera = self.left_eye_anchor.attachNewNode(left_cam_node) self.vr_right_camera = self.right_eye_anchor.attachNewNode(right_cam_node) # 设置显示区域并添加渲染回调 left_dr = self.vr_left_eye_buffer.makeDisplayRegion() left_dr.setCamera(self.vr_left_camera) left_dr.setActive(True) left_dr.setDrawCallback(PythonCallbackObject(self.left_cb)) right_dr = self.vr_right_eye_buffer.makeDisplayRegion() right_dr.setCamera(self.vr_right_camera) right_dr.setActive(True) right_dr.setDrawCallback(PythonCallbackObject(self.right_cb)) print("✓ VR相机锚点层级系统设置完成") return True except Exception as e: print(f"❌ 设置VR相机失败: {e}") import traceback traceback.print_exc() return False def _get_eye_offset(self, eye): """获取眼睛相对于头显的偏移""" try: if not self.vr_system: # 使用标准IPD(瞳距)估算值 ipd = 0.064 # 64mm,平均IPD if eye == openvr.Eye_Left: return Vec3(-ipd/2, 0, 0) else: return Vec3(ipd/2, 0, 0) # 从OpenVR获取眼睛到头显的变换矩阵 eye_transform = self.vr_system.getEyeToHeadTransform(eye) # 提取位移信息 x = eye_transform[0][3] y = eye_transform[1][3] z = eye_transform[2][3] return Vec3(x, y, z) except Exception as e: print(f"❌ 获取眼睛偏移失败: {e}") # 返回默认值 ipd = 0.064 if eye == openvr.Eye_Left: return Vec3(-ipd/2, 0, 0) else: return Vec3(ipd/2, 0, 0) def _start_vr_task(self): """启动VR更新任务""" if self.vr_task: self.world.taskMgr.remove(self.vr_task) self.vr_task = self.world.taskMgr.add(self._update_vr, "update_vr") print("✓ VR更新任务已启动") def _update_vr(self, task): """VR更新任务 - 每帧调用""" if not self.vr_enabled or not self.vr_system: return task.cont try: # 性能监控 self.frame_count += 1 # 计算VR FPS import time current_time = time.time() if self.last_fps_time == 0: self.last_fps_time = current_time elif current_time - self.last_fps_time >= 1.0: # 每秒更新一次FPS self.vr_fps = (self.frame_count - self.last_fps_check) / (current_time - self.last_fps_time) self.last_fps_check = self.frame_count self.last_fps_time = current_time # 优化的VR更新顺序: # 注意:WaitGetPoses现在在渲染回调中调用,避免ATW双重预测 # 这里仅更新非关键的跟踪设备和交互系统 # 1. 更新手柄和其他跟踪设备 self.update_tracked_devices() # 2. 更新VR动作状态 self.action_manager.update_actions() # 3. 更新VR交互系统 self.interaction_manager.update() # 注意:纹理提交现在通过渲染回调自动处理 # 定期输出性能报告 if self.frame_count % 1800 == 1: # 每30秒@60fps输出一次性能报告 self._print_performance_report() except Exception as e: print(f"VR更新错误: {e}") import traceback traceback.print_exc() return task.cont def _wait_get_poses(self): """调用VRCompositor的waitGetPoses来获取焦点和姿态数据""" try: if not self.vr_compositor or not self.poses: return # 调用waitGetPoses获取焦点和姿态数据 # 这个调用可能会阻塞直到下一个VR同步点 result = self.vr_compositor.waitGetPoses(self.poses, None) # 检查姿态数据的有效性 valid_poses = 0 # 更新HMD姿态(设备0通常是头显) if len(self.poses) > 0 and self.poses[0].bPoseIsValid: valid_poses += 1 else: # 如果HMD姿态无效,不要频繁输出错误信息 if not hasattr(self, '_hmd_invalid_warning_shown'): print("⚠️ HMD姿态数据无效") self._hmd_invalid_warning_shown = True # 更新控制器姿态 self.controller_poses.clear() for device_id in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): if self.poses[device_id].bPoseIsValid: device_class = self.vr_system.getTrackedDeviceClass(device_id) if device_class == openvr.TrackedDeviceClass_Controller: controller_matrix = self.poses[device_id].mDeviceToAbsoluteTracking self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) valid_poses += 1 # 性能监控 - 偶尔输出姿态状态 if self.frame_count % 600 == 1: # 每10秒输出一次@60fps print(f"📊 VR姿态状态 - 有效姿态数: {valid_poses}, 总帧数: {self.frame_count}") except Exception as e: # 限制错误输出频率 if not hasattr(self, '_last_error_frame'): self._last_error_frame = 0 if self.frame_count - self._last_error_frame > 300: # 每5秒最多输出一次错误 print(f"waitGetPoses失败: {e}") self._last_error_frame = self.frame_count # 记录姿态失败次数 self.pose_failures += 1 def _wait_get_poses_immediate(self): """立即获取VR姿态 - 修复ATW闪烁的关键方法(双姿态版本)""" try: if not self.vr_compositor or not self.poses or not self.game_poses: return # 关键修复:传递渲染姿态和游戏姿态数组 # 渲染姿态用于绘制,游戏姿态用于逻辑,避免ATW过度补偿 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_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, '_dual_pose_mode_logged') and valid_poses > 0: print(f"✅ 双姿态立即获取模式启用 - 有效姿态数: {valid_poses}") print(" 渲染姿态用于绘制,游戏姿态用于逻辑,防止ATW过度补偿") self._dual_pose_mode_logged = True except Exception as e: # 限制错误输出频率 if not hasattr(self, '_last_immediate_error_frame'): self._last_immediate_error_frame = 0 if self.frame_count - self._last_immediate_error_frame > 300: # 每5秒最多输出一次错误 print(f"立即姿态获取失败: {e}") self._last_immediate_error_frame = self.frame_count self.pose_failures += 1 def _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 return task.done def _update_tracking_data(self): """更新VR追踪数据""" try: # 获取设备姿态 poses = self.vr_system.getDeviceToAbsoluteTrackingPose( openvr.TrackingUniverseStanding, 0.0, openvr.k_unMaxTrackedDeviceCount ) # 更新HMD姿态(设备0通常是头显) if poses[0].bPoseIsValid: hmd_matrix = poses[0].mDeviceToAbsoluteTracking self.hmd_pose = self._convert_openvr_matrix_to_panda(hmd_matrix) # 更新控制器姿态 for device_id in range(1, openvr.k_unMaxTrackedDeviceCount): if poses[device_id].bPoseIsValid: device_class = self.vr_system.getTrackedDeviceClass(device_id) if device_class == openvr.TrackedDeviceClass_Controller: controller_matrix = poses[device_id].mDeviceToAbsoluteTracking self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) except Exception as e: print(f"更新追踪数据失败: {e}") def _convert_openvr_matrix_to_panda(self, ovr_matrix): """将OpenVR矩阵转换为Panda3D矩阵 坐标系转换: OpenVR: X右, Y上, -Z前(右手坐标系) Panda3D: X右, Y前, Z上(右手坐标系) 转换规则: OpenVR X → Panda3D X OpenVR Y → Panda3D Z OpenVR -Z → Panda3D Y """ mat = Mat4() # 修正的坐标转换矩阵 # OpenVR: X右, Y上, -Z前 → Panda3D: X右, Y前, Z上 # 转换规则: (ovr_x, ovr_y, ovr_z) → (panda_x, panda_y, panda_z) # (ovr_x, ovr_y, ovr_z) → (ovr_x, -ovr_z, ovr_y) # X轴行:Panda3D的X轴对应OpenVR的X轴 mat.setCell(0, 0, ovr_matrix[0][0]) # X_x → X_x mat.setCell(0, 1, ovr_matrix[0][1]) # X_y → X_y mat.setCell(0, 2, ovr_matrix[0][2]) # X_z → X_z mat.setCell(0, 3, ovr_matrix[0][3]) # 位移X分量 # Y轴行:Panda3D的Y轴对应OpenVR的-Z轴 mat.setCell(1, 0, -ovr_matrix[2][0]) # -Z_x → Y_x mat.setCell(1, 1, -ovr_matrix[2][1]) # -Z_y → Y_y mat.setCell(1, 2, -ovr_matrix[2][2]) # -Z_z → Y_z mat.setCell(1, 3, -ovr_matrix[2][3]) # 位移Y分量(-Z位移) # Z轴行:Panda3D的Z轴对应OpenVR的Y轴 mat.setCell(2, 0, ovr_matrix[1][0]) # Y_x → Z_x mat.setCell(2, 1, ovr_matrix[1][1]) # Y_y → Z_y mat.setCell(2, 2, ovr_matrix[1][2]) # Y_z → Z_z mat.setCell(2, 3, ovr_matrix[1][3]) # 位移Z分量(Y位移) # 齐次坐标 mat.setCell(3, 0, 0) mat.setCell(3, 1, 0) mat.setCell(3, 2, 0) mat.setCell(3, 3, 1) # 调试信息 - 验证坐标系转换 if not hasattr(self, '_coord_debug_counter'): self._coord_debug_counter = 0 self._coord_debug_counter += 1 if self._coord_debug_counter % 600 == 1: # 每10秒输出一次@60fps print(f"🔄 坐标系转换调试 (第{self._coord_debug_counter}帧)") # 输出原始OpenVR矩阵信息 ovr_pos = Vec3(ovr_matrix[0][3], ovr_matrix[1][3], ovr_matrix[2][3]) print(f" OpenVR原始位置: {ovr_pos}") # 输出转换后的Panda3D矩阵信息 # 正确的方法:从矩阵中读取位移(第4列,前3行) panda_pos = Vec3(mat.getCell(0, 3), mat.getCell(1, 3), mat.getCell(2, 3)) print(f" Panda3D转换位置: {panda_pos}") # 检查矩阵设置是否正确 # 手动验证位置转换:OpenVR (x,y,z) → Panda3D (x,-z,y) manual_converted_pos = Vec3(ovr_pos.x, -ovr_pos.z, ovr_pos.y) print(f" 手动转换结果: {manual_converted_pos}") # 检查我们的矩阵是否正确设置了位置 print(f" 矩阵位置元素: [{mat.getCell(0,3)}, {mat.getCell(1,3)}, {mat.getCell(2,3)}]") # 验证转换是否正确 expected_panda_pos = manual_converted_pos print(f" 预期Panda3D位置: {expected_panda_pos}") # 检查转换是否正确 diff = panda_pos - expected_panda_pos diff_magnitude = diff.length() if diff_magnitude < 0.001: print(f" ✅ 坐标转换正确 (误差: {diff_magnitude:.6f})") else: print(f" ⚠️ 坐标转换可能有误 (误差: {diff_magnitude:.6f})") print(f" 差异向量: {diff}") print(f" 实际矩阵第4行: [{mat.getCell(3,0)}, {mat.getCell(3,1)}, {mat.getCell(3,2)}, {mat.getCell(3,3)}]") return mat def update_hmd(self, pose): """ 更新HMD锚点 - 基于参考实现 """ try: # 将OpenVR姿态转换为Panda3D矩阵 modelview = self.convert_mat(pose.mDeviceToAbsoluteTracking) # 应用坐标系转换并设置HMD锚点 self.hmd_anchor.setMat(self.coord_mat_inv * modelview * self.coord_mat) # 获取眼睛到头部的变换 view_left = self.convert_mat(self.vr_system.getEyeToHeadTransform(openvr.Eye_Left)) view_right = self.convert_mat(self.vr_system.getEyeToHeadTransform(openvr.Eye_Right)) # 设置眼睛锚点 self.left_eye_anchor.setMat(self.coord_mat_inv * view_left * self.coord_mat) self.right_eye_anchor.setMat(self.coord_mat_inv * view_right * self.coord_mat) except Exception as e: print(f"更新HMD姿态失败: {e}") def _update_camera_poses(self): """更新相机姿态 - 使用锚点系统简化处理""" try: # 使用锚点系统后,相机位置自动跟随锚点 # 只需要获取HMD姿态并更新锚点即可 # 从poses数组中获取HMD姿态 if hasattr(self, 'poses') and len(self.poses) > 0: hmd_pose = self.poses[openvr.k_unTrackedDeviceIndex_Hmd] if hmd_pose.bPoseIsValid: self.update_hmd(hmd_pose) else: print("⚠️ HMD姿态数据无效") except Exception as e: print(f"更新相机姿态失败: {e}") import traceback traceback.print_exc() def _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: if not self.vr_compositor: return # 使用我们创建的纹理对象 left_texture = self.vr_left_texture right_texture = self.vr_right_texture if left_texture and right_texture: # 确保纹理已准备并获取OpenGL纹理ID gsg = self.world.win.getGsg() prepared_objects = gsg.getPreparedObjects() # 准备纹理 - 使用更简单的方法 try: # 方法1: 尝试prepareNow if hasattr(left_texture, 'prepareNow'): left_texture.prepareNow(0, prepared_objects, gsg) if hasattr(right_texture, 'prepareNow'): right_texture.prepareNow(0, prepared_objects, gsg) except Exception as prep_error: print(f"纹理准备失败: {prep_error}") # 继续尝试,可能纹理已经准备好了 # 获取OpenGL纹理ID left_texture_id = self._get_texture_opengl_id(left_texture, prepared_objects, gsg) right_texture_id = self._get_texture_opengl_id(right_texture, prepared_objects, gsg) # 检查是否成功获取了纹理ID if left_texture_id is not None and left_texture_id > 0 and right_texture_id is not None and right_texture_id > 0: try: # 创建OpenVR纹理结构 left_eye_texture = openvr.Texture_t() left_eye_texture.handle = int(left_texture_id) left_eye_texture.eType = openvr.TextureType_OpenGL left_eye_texture.eColorSpace = openvr.ColorSpace_Gamma right_eye_texture = openvr.Texture_t() right_eye_texture.handle = int(right_texture_id) right_eye_texture.eType = openvr.TextureType_OpenGL right_eye_texture.eColorSpace = openvr.ColorSpace_Gamma # 提交到VR系统 error_left = self.vr_compositor.submit(openvr.Eye_Left, left_eye_texture) error_right = self.vr_compositor.submit(openvr.Eye_Right, right_eye_texture) # 检查提交结果 - 在Python OpenVR中,None表示成功 left_success = (error_left is None or error_left == openvr.VRCompositorError_None) right_success = (error_right is None or error_right == openvr.VRCompositorError_None) if not left_success: print(f"⚠️ 左眼纹理提交错误: {error_left}") self.submit_failures += 1 if not right_success: print(f"⚠️ 右眼纹理提交错误: {error_right}") self.submit_failures += 1 # 如果两个都成功了,输出成功信息(仅第一次) if not hasattr(self, '_first_submit_success'): if left_success and right_success: print("✅ VR纹理提交成功!缓冲区应该显示场景内容。") print(f" 左眼纹理ID: {left_texture_id}, 右眼纹理ID: {right_texture_id}") self._first_submit_success = True except Exception as submit_error: print(f"❌ VR纹理提交过程失败: {submit_error}") if "DoNotHaveFocus" in str(submit_error): print("🔍 这通常意味着另一个VR应用程序正在使用VR系统") self.submit_failures += 1 else: # 只在第一次失败时输出警告,避免太多日志 if not hasattr(self, '_texture_id_warning_shown'): print("⚠️ 无法获取有效的纹理OpenGL ID,跳过VR帧提交") print(" 这可能是因为纹理尚未正确准备到GPU") self._texture_id_warning_shown = True except Exception as e: print(f"提交VR帧失败: {e}") import traceback traceback.print_exc() def _get_texture_opengl_id(self, texture, prepared_objects, gsg): """获取纹理的OpenGL ID""" try: # 方法1: 使用prepareNow获取正确的纹理上下文 texture_context = texture.prepareNow(0, prepared_objects, gsg) if texture_context and hasattr(texture_context, 'getNativeId'): native_id = texture_context.getNativeId() if native_id > 0: print(f"✓ 成功获取纹理 {texture.getName()} 的OpenGL ID: {native_id}") return native_id # 方法2: 备选方案 - 通过prepared_objects获取texture context if hasattr(prepared_objects, 'getTextureContext'): texture_context = prepared_objects.getTextureContext(texture) if texture_context and hasattr(texture_context, 'getNativeId'): native_id = texture_context.getNativeId() if native_id > 0: print(f"✓ 备选方法获取纹理 {texture.getName()} 的OpenGL ID: {native_id}") return native_id # 方法3: 尝试texture对象本身的方法 if hasattr(texture, 'getNativeId'): native_id = texture.getNativeId() if native_id > 0: print(f"✓ 直接获取纹理 {texture.getName()} 的OpenGL ID: {native_id}") return native_id # 如果所有方法都失败 print(f"❌ 无法获取纹理 {texture.getName()} 的OpenGL ID") return None except Exception as e: print(f"获取纹理OpenGL ID失败: {e}") import traceback traceback.print_exc() return None def enable_vr(self): """启用VR模式""" if not self.is_vr_available(): print("❌ VR系统不可用") return False if not self.vr_initialized: if not self.initialize_vr(): return False self.vr_enabled = True # 禁用主相机避免干扰VR渲染 self._disable_main_cam() print("✅ VR模式已启用") return True def disable_vr(self): """禁用VR模式""" self.vr_enabled = False # 恢复主相机 self._enable_main_cam() print("✅ VR模式已禁用") def cleanup(self): """清理VR资源""" try: print("🔄 正在清理VR资源...") # 停止VR任务 if self.vr_task: self.world.taskMgr.remove(self.vr_task) self.vr_task = None # 清理渲染缓冲区 if self.vr_left_eye_buffer: self.vr_left_eye_buffer.removeAllDisplayRegions() self.world.graphicsEngine.removeWindow(self.vr_left_eye_buffer) self.vr_left_eye_buffer = None if self.vr_right_eye_buffer: self.vr_right_eye_buffer.removeAllDisplayRegions() self.world.graphicsEngine.removeWindow(self.vr_right_eye_buffer) self.vr_right_eye_buffer = None # 清理相机 if self.vr_left_camera: self.vr_left_camera.removeNode() self.vr_left_camera = None if self.vr_right_camera: self.vr_right_camera.removeNode() self.vr_right_camera = None # 关闭OpenVR if self.vr_system and OPENVR_AVAILABLE: try: openvr.shutdown() except: pass self.vr_system = None self.vr_enabled = False self.vr_initialized = False print("✅ VR资源清理完成") except Exception as e: print(f"⚠️ VR清理过程中出错: {e}") def get_vr_status(self): """获取VR状态信息""" return { 'available': self.is_vr_available(), 'initialized': self.vr_initialized, 'enabled': self.vr_enabled, 'eye_resolution': (self.eye_width, self.eye_height), 'device_count': len(self.controller_poses) + (1 if self.vr_enabled else 0), 'vr_fps': self.vr_fps, 'frame_count': self.frame_count, 'submit_failures': self.submit_failures, 'pose_failures': self.pose_failures } def _print_performance_report(self): """输出VR性能报告""" print("📊 === VR性能报告 ===") print(f" VR帧率: {self.vr_fps:.1f} FPS") print(f" 总帧数: {self.frame_count}") print(f" 提交失败: {self.submit_failures}") print(f" 姿态失败: {self.pose_failures}") # 计算失败率 if self.frame_count > 0: submit_fail_rate = (self.submit_failures / self.frame_count) * 100 pose_fail_rate = (self.pose_failures / self.frame_count) * 100 print(f" 提交失败率: {submit_fail_rate:.2f}%") print(f" 姿态失败率: {pose_fail_rate:.2f}%") print("========================") def left_cb(self, cbdata): """左眼渲染回调 - 修复ATW闪烁问题""" 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") # 执行实际的渲染工作 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): """右眼渲染回调 - 修复ATW闪烁问题""" 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") # 执行实际的渲染工作 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 - 基于参考实现,增强调试信息""" try: if not self.vr_compositor: print("❌ VR compositor不可用") self.submit_failures += 1 return # 获取graphics state guardian和prepared objects gsg = self.world.win.getGsg() if not gsg: print("❌ 无法获取GraphicsStateGuardian") self.submit_failures += 1 return prepared_objects = gsg.getPreparedObjects() if not prepared_objects: print("❌ 无法获取PreparedObjects") self.submit_failures += 1 return # 准备纹理并获取更详细的错误信息 if not texture: print("❌ 纹理对象为空") self.submit_failures += 1 return print(f"🔍 准备纹理: {texture.getName()}, 大小: {texture.getXSize()}x{texture.getYSize()}") texture_context = texture.prepareNow(0, prepared_objects, gsg) if not texture_context: print("❌ prepareNow返回空的texture_context") self.submit_failures += 1 return handle = texture_context.getNativeId() print(f"🔍 获取OpenGL纹理句柄: {handle}") if handle != 0: ovr_texture = openvr.Texture_t() ovr_texture.handle = handle ovr_texture.eType = openvr.TextureType_OpenGL ovr_texture.eColorSpace = openvr.ColorSpace_Gamma eye_name = "左眼" if eye == openvr.Eye_Left else "右眼" print(f"🔍 提交{eye_name}纹理到VR, 句柄: {handle}") # 提交到VR系统 error = self.vr_compositor.submit(eye, ovr_texture) print(f"🔍 VR提交结果: {error}") # 检查错误 if error and error != openvr.VRCompositorError_None: print(f"⚠️ VR纹理提交错误代码: {error}") self.submit_failures += 1 else: # 只在第一次成功时输出 if not hasattr(self, '_submit_success_logged'): print(f"✅ VR纹理提交成功! {eye_name}") self._submit_success_logged = True else: print(f"❌ 无法获取纹理OpenGL句柄: handle = {handle}") print(f" 纹理状态: 已准备={texture.isPrepared(prepared_objects)}") print(f" 纹理格式: {texture.getFormat()}") self.submit_failures += 1 except Exception as e: print(f"❌ VR纹理提交异常: {e}") import traceback traceback.print_exc() self.submit_failures += 1 def _disable_main_cam(self): """禁用主相机 - 基于参考实现""" try: # 保存原始相机状态 if not hasattr(self, '_original_camera_parent'): self._original_camera_parent = self.world.camera.getParent() # 创建空节点并将主相机重新附加到它 self._empty_world = NodePath("empty_world") self.world.camera.reparentTo(self._empty_world) print("✓ 主相机已禁用") except Exception as e: print(f"⚠️ 禁用主相机失败: {e}") def _enable_main_cam(self): """恢复主相机 - 基于参考实现""" try: # 恢复原始相机状态 if hasattr(self, '_original_camera_parent') and self._original_camera_parent: self.world.camera.reparentTo(self._original_camera_parent) else: # 如果没有保存的父节点,重新附加到render self.world.camera.reparentTo(self.world.render) # 清理空世界节点 if hasattr(self, '_empty_world'): self._empty_world.removeNode() delattr(self, '_empty_world') print("✓ 主相机已恢复") except Exception as e: print(f"⚠️ 恢复主相机失败: {e}") def _initialize_controllers(self): """初始化VR手柄控制器""" try: print("🎮 正在初始化VR手柄控制器...") # 创建左右手柄控制器实例 self.left_controller = LeftController(self) self.right_controller = RightController(self) # 检测现有连接的控制器 self._detect_controllers() print("✓ VR手柄控制器初始化完成") except Exception as e: print(f"⚠️ VR手柄初始化失败: {e}") import traceback traceback.print_exc() def _detect_controllers(self): """检测并连接VR控制器""" if not self.vr_system: return try: for device_index in range(openvr.k_unMaxTrackedDeviceCount): # 检查设备是否已连接 if not self.vr_system.isTrackedDeviceConnected(device_index): continue # 获取设备类型 device_class = self.vr_system.getTrackedDeviceClass(device_index) if device_class == openvr.TrackedDeviceClass_Controller: # 获取控制器角色 role = self.vr_system.getControllerRoleForTrackedDeviceIndex(device_index) if role == openvr.TrackedControllerRole_LeftHand and self.left_controller: self.left_controller.set_device_index(device_index) self.controllers[device_index] = self.left_controller # 为设备创建锚点 self._create_tracked_device_anchor(device_index, 'left_controller') elif role == openvr.TrackedControllerRole_RightHand and self.right_controller: self.right_controller.set_device_index(device_index) self.controllers[device_index] = self.right_controller # 为设备创建锚点 self._create_tracked_device_anchor(device_index, 'right_controller') print(f"🎮 检测到 {len(self.controllers)} 个控制器") except Exception as e: print(f"⚠️ 控制器检测失败: {e}") def _create_tracked_device_anchor(self, device_index, name): """为跟踪设备创建锚点节点""" if not self.tracking_space: print(f"⚠️ 无法为设备 {device_index} 创建锚点 - tracking_space未初始化") return try: # 获取设备模型名称 if self.vr_system: model_name = self.vr_system.getStringTrackedDeviceProperty( device_index, openvr.Prop_RenderModelName_String ) anchor_name = f"{device_index}:{model_name}:{name}" else: anchor_name = f"{device_index}:{name}" # 创建锚点节点 device_anchor = self.tracking_space.attachNewNode(anchor_name) self.tracked_device_anchors[device_index] = device_anchor print(f"✓ 为设备 {device_index} 创建锚点: {anchor_name}") except Exception as e: print(f"⚠️ 创建设备锚点失败: {e}") def update_tracked_devices(self): """更新所有跟踪设备的姿态 - 基于参考实现""" if not self.poses or not self.vr_system: return try: # 更新每个已连接的控制器 for device_index, controller in self.controllers.items(): if device_index < len(self.poses): pose_data = self.poses[device_index] # 更新控制器姿态 controller.update_pose(pose_data) # 更新控制器输入状态 controller.update_input_state(self.vr_system) # 更新其他跟踪设备的锚点 for device_index in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): if device_index in self.tracked_device_anchors: pose_data = self.poses[device_index] if pose_data.bPoseIsValid: # 转换姿态矩阵 modelview = self.convert_mat(pose_data.mDeviceToAbsoluteTracking) final_matrix = self.coord_mat_inv * modelview * self.coord_mat # 更新锚点变换 anchor = self.tracked_device_anchors[device_index] anchor.setMat(final_matrix) anchor.show() else: # 姿态无效,隐藏锚点 self.tracked_device_anchors[device_index].hide() except Exception as e: if self.frame_count % 300 == 0: # 每5秒输出一次错误 print(f"⚠️ 更新跟踪设备失败: {e}") def get_controller_by_role(self, role): """根据角色获取控制器 Args: role: 'left' 或 'right' Returns: VRController实例或None """ if role == 'left': return self.left_controller elif role == 'right': return self.right_controller return None def are_controllers_connected(self): """检查是否有控制器连接""" return len(self.controllers) > 0 def get_connected_controllers(self): """获取所有连接的控制器列表""" return list(self.controllers.values()) def trigger_controller_haptic(self, role, duration=0.001, strength=1.0): """触发控制器震动反馈 Args: role: 'left', 'right' 或 'both' duration: 震动持续时间(秒) strength: 震动强度 (0.0-1.0) """ if role in ['left', 'both'] and self.left_controller: self.left_controller.trigger_haptic_feedback(duration, strength) if role in ['right', 'both'] and self.right_controller: self.right_controller.trigger_haptic_feedback(duration, strength) # VR动作系统便捷方法 def is_trigger_pressed(self, hand='any'): """检查扳机是否被按下 Args: hand: 'left', 'right', 'any' """ device_path = None if hand == 'left': device_path = '/user/hand/left' elif hand == 'right': device_path = '/user/hand/right' pressed, _ = self.action_manager.is_digital_action_pressed('trigger', device_path) return pressed def is_trigger_just_pressed(self, hand='any'): """检查扳机是否刚刚被按下""" device_path = None if hand == 'left': device_path = '/user/hand/left' elif hand == 'right': device_path = '/user/hand/right' pressed, _ = self.action_manager.is_digital_action_just_pressed('trigger', device_path) return pressed def is_grip_pressed(self, hand='any'): """检查握把是否被按下""" device_path = None if hand == 'left': device_path = '/user/hand/left' elif hand == 'right': device_path = '/user/hand/right' pressed, _ = self.action_manager.is_digital_action_pressed('grip', device_path) return pressed def is_grip_just_pressed(self, hand='any'): """检查握把是否刚刚被按下""" device_path = None if hand == 'left': device_path = '/user/hand/left' elif hand == 'right': device_path = '/user/hand/right' pressed, _ = self.action_manager.is_digital_action_just_pressed('grip', device_path) return pressed def is_menu_pressed(self, hand='any'): """检查菜单按钮是否被按下""" device_path = None if hand == 'left': device_path = '/user/hand/left' elif hand == 'right': device_path = '/user/hand/right' pressed, _ = self.action_manager.is_digital_action_pressed('menu', device_path) return pressed def is_trackpad_touched(self, hand='any'): """检查触摸板是否被触摸""" device_path = None if hand == 'left': device_path = '/user/hand/left' elif hand == 'right': device_path = '/user/hand/right' touched, _ = self.action_manager.is_digital_action_pressed('trackpad_touch', device_path) return touched def get_trackpad_position(self, hand='any'): """获取触摸板位置 Returns: Vec2或None: 触摸板位置 (-1到1的范围) """ device_path = None if hand == 'left': device_path = '/user/hand/left' elif hand == 'right': device_path = '/user/hand/right' value, _ = self.action_manager.get_analog_action_value('trackpad', device_path) return value # VR交互系统便捷方法 def get_selected_object(self, hand='any'): """获取指定手选中的对象 Args: hand: 'left', 'right', 'any' Returns: 选中的对象节点或None """ if hand == 'any': # 返回任意手选中的对象 for controller in self.get_connected_controllers(): selected = self.interaction_manager.get_selected_object(controller.name) if selected: return selected return None else: return self.interaction_manager.get_selected_object(hand) def get_grabbed_object(self, hand='any'): """获取指定手抓取的对象 Args: hand: 'left', 'right', 'any' Returns: 抓取的对象节点或None """ if hand == 'any': # 返回任意手抓取的对象 for controller in self.get_connected_controllers(): grabbed = self.interaction_manager.get_grabbed_object(controller.name) if grabbed: return grabbed return None else: return self.interaction_manager.get_grabbed_object(hand) def is_grabbing_object(self, hand='any'): """检查是否正在抓取对象 Args: hand: 'left', 'right', 'any' Returns: bool: 是否正在抓取 """ if hand == 'any': # 检查任意手是否正在抓取 for controller in self.get_connected_controllers(): if self.interaction_manager.is_grabbing(controller.name): return True return False else: return self.interaction_manager.is_grabbing(hand) def force_release_all_grabs(self): """强制释放所有抓取的对象""" self.interaction_manager.force_release_all() def add_interactable_object(self, object_node): """将对象标记为可交互 Args: object_node: 要标记的对象节点 """ self.interaction_manager._add_collision_to_object(object_node) def _disable_async_reprojection(self): """禁用异步重投影 - 备选修复方案""" try: # 尝试通过OpenVR设置禁用异步重投影 if hasattr(openvr, 'VRSettings'): settings = openvr.VRSettings() if settings: # 禁用异步重投影 error = settings.setBool("steamvr", "enableAsyncReprojection", False) if error == openvr.VRSettingsError_None: print("✅ 异步重投影已禁用") else: print(f"⚠️ 禁用异步重投影失败: 设置错误 {error}") else: print("⚠️ 无法获取VR设置接口") else: print("⚠️ OpenVR设置接口不可用") except Exception as e: print(f"⚠️ 禁用异步重投影失败: {e}") def enable_async_reprojection_disable(self): """启用异步重投影禁用选项 - 用户可调用的方法""" self.disable_async_reprojection = True print("📝 异步重投影禁用选项已启用,将在下次VR初始化时生效") def disable_async_reprojection_disable(self): """禁用异步重投影禁用选项 - 恢复默认行为""" self.disable_async_reprojection = False print("📝 异步重投影禁用选项已关闭,将使用默认ATW行为")