""" VR交互系统模块 提供VR手柄与3D场景的交互功能: - 射线投射和碰撞检测 - 对象选择和高亮 - 对象抓取和移动 - UI交互 - 距离抓取 """ from panda3d.core import ( Vec3, Vec4, Mat4, Point3, CollisionRay, CollisionTraverser, CollisionNode, CollisionHandlerQueue, BitMask32, NodePath, CollisionSphere, CollisionTube, RenderState, TransparencyAttrib, ColorAttrib ) from direct.showbase.DirectObject import DirectObject class VRInteractionManager(DirectObject): """VR交互管理器 - 处理手柄与场景的交互""" def __init__(self, vr_manager): """初始化VR交互管理器 Args: vr_manager: VR管理器实例 """ super().__init__() self.vr_manager = vr_manager self.world = vr_manager.world if hasattr(vr_manager, 'world') else None # 碰撞检测系统 self.collision_traverser = CollisionTraverser() self.collision_queue = CollisionHandlerQueue() # 射线投射节点 self.left_ray_node = None self.right_ray_node = None self.ray_collision_nodes = {} # 选择和抓取状态 self.selected_objects = {} # 控制器 -> 选中对象 self.grabbed_objects = {} # 控制器 -> 抓取对象 self.grab_offsets = {} # 控制器 -> 抓取偏移 # 交互参数 self.selection_range = 50.0 # 选择距离 self.grab_threshold = 0.5 # 抓取扳机阈值 self.selection_color = Vec4(0.9, 0.9, 0.2, 1.0) # 选择高亮颜色 self.grab_color = Vec4(0.2, 0.9, 0.2, 1.0) # 抓取高亮颜色 # 高亮状态 self.highlighted_objects = set() self.original_colors = {} # 存储对象原始颜色 print("✓ VR交互管理器初始化完成") def initialize(self): """初始化交互系统""" try: print("🔧 正在初始化VR交互系统...") # 创建射线投射节点 self._create_ray_casters() # 设置碰撞检测 self._setup_collision_detection() print("✅ VR交互系统初始化成功") return True except Exception as e: print(f"❌ VR交互系统初始化失败: {e}") import traceback traceback.print_exc() return False def _create_ray_casters(self): """创建射线投射节点""" # 为左手控制器创建射线 if self.vr_manager.left_controller and self.vr_manager.left_controller.anchor_node: self.left_ray_node = self._create_controller_ray('left', self.vr_manager.left_controller.anchor_node) # 为右手控制器创建射线 if self.vr_manager.right_controller and self.vr_manager.right_controller.anchor_node: self.right_ray_node = self._create_controller_ray('right', self.vr_manager.right_controller.anchor_node) def _create_controller_ray(self, controller_name, anchor_node): """为控制器创建射线投射节点""" # 创建射线碰撞体 ray = CollisionRay() ray.setOrigin(0, 0, 0) # 从控制器原点开始 ray.setDirection(0, 1, 0) # 沿Y轴正方向 # 创建碰撞节点 ray_collision_node = CollisionNode(f'{controller_name}_ray') ray_collision_node.addSolid(ray) # 设置碰撞掩码 ray_collision_node.setFromCollideMask(BitMask32.bit(0)) # 射线掩码 ray_collision_node.setIntoCollideMask(BitMask32.allOff()) # 不接受碰撞 # 附加到控制器锚点 ray_node = anchor_node.attachNewNode(ray_collision_node) self.ray_collision_nodes[controller_name] = ray_collision_node # 注册到碰撞遍历器 self.collision_traverser.addCollider(ray_node, self.collision_queue) print(f"✓ {controller_name}手控制器射线投射已创建") return ray_node def _setup_collision_detection(self): """设置碰撞检测系统""" if self.world: # 使用世界的碰撞系统 if hasattr(self.world, 'render'): # 为所有可交互对象设置碰撞体 self._setup_scene_collision_objects() else: print("⚠️ 无法访问世界对象,跳过场景碰撞设置") def _setup_scene_collision_objects(self): """为场景对象设置碰撞体""" if not self.world or not hasattr(self.world, 'render'): return try: # 遍历场景中的所有节点,为它们添加碰撞体 for node_path in self.world.render.findAllMatches("**/+GeomNode"): self._add_collision_to_object(node_path) except Exception as e: print(f"⚠️ 设置场景碰撞对象失败: {e}") def _add_collision_to_object(self, node_path): """为对象添加碰撞体""" try: # 获取对象的边界框 bounds = node_path.getBounds() if bounds.isEmpty(): return # 计算边界球 center = bounds.getCenter() radius = bounds.getRadius() # 创建球形碰撞体 collision_sphere = CollisionSphere(center, radius) # 创建碰撞节点 collision_node = CollisionNode(f'{node_path.getName()}_collision') collision_node.addSolid(collision_sphere) # 设置碰撞掩码 collision_node.setIntoCollideMask(BitMask32.bit(0)) # 接受射线碰撞 collision_node.setFromCollideMask(BitMask32.allOff()) # 不发射射线 # 附加碰撞节点 collision_node_path = node_path.attachNewNode(collision_node) # 标记为可交互对象 node_path.setTag('interactable', 'true') node_path.setTag('original_name', node_path.getName()) except Exception as e: print(f"⚠️ 为对象 {node_path.getName()} 添加碰撞体失败: {e}") def update(self): """更新交互系统 - 每帧调用""" if not self.vr_manager.are_controllers_connected(): return # 执行碰撞检测 self._perform_collision_detection() # 更新选择状态 self._update_selections() # 更新抓取状态 self._update_grabbing() def _perform_collision_detection(self): """执行碰撞检测""" if self.world and hasattr(self.world, 'render'): self.collision_traverser.traverse(self.world.render) def _update_selections(self): """更新对象选择状态""" # 清除之前的选择高亮 self._clear_selection_highlights() # 检查每个控制器的选择 for controller in self.vr_manager.get_connected_controllers(): if not controller: continue # 获取最近的碰撞对象 hit_object = self._get_closest_hit_object(controller.name) if hit_object: # 高亮选中的对象 self._highlight_object(hit_object, self.selection_color) self.selected_objects[controller.name] = hit_object # 显示控制器射线 controller.show_ray(True) controller.set_ray_color([0.9, 0.9, 0.2, 0.8]) # 黄色 else: # 没有选中对象 if controller.name in self.selected_objects: del self.selected_objects[controller.name] # 隐藏射线(除非正在抓取) if controller.name not in self.grabbed_objects: controller.show_ray(False) def _get_closest_hit_object(self, controller_name): """获取指定控制器射线最近的碰撞对象""" if controller_name not in self.ray_collision_nodes: return None closest_object = None closest_distance = float('inf') # 检查碰撞队列中的条目 for i in range(self.collision_queue.getNumEntries()): entry = self.collision_queue.getEntry(i) # 检查是否是该控制器的射线 from_node = entry.getFromNodePath() if from_node.node() == self.ray_collision_nodes[controller_name]: # 获取碰撞的对象 hit_node_path = entry.getIntoNodePath() # 获取实际的几何对象(父节点) geom_object = hit_node_path.getParent() if geom_object and geom_object.hasTag('interactable'): distance = entry.getSurfacePoint(geom_object).length() if distance < closest_distance and distance <= self.selection_range: closest_distance = distance closest_object = geom_object return closest_object def _update_grabbing(self): """更新对象抓取状态""" for controller in self.vr_manager.get_connected_controllers(): if not controller: continue controller_name = controller.name # 检查是否按下抓取按钮 if controller.is_trigger_pressed(threshold=self.grab_threshold): # 如果还没有抓取对象 if controller_name not in self.grabbed_objects: # 尝试抓取选中的对象 if controller_name in self.selected_objects: selected_obj = self.selected_objects[controller_name] self._start_grab(controller, selected_obj) # 如果正在抓取,更新对象位置 if controller_name in self.grabbed_objects: self._update_grabbed_object(controller) else: # 释放抓取 if controller_name in self.grabbed_objects: self._release_grab(controller) def _start_grab(self, controller, obj): """开始抓取对象""" controller_name = controller.name try: # 计算抓取偏移(对象相对于控制器的位置) controller_pos = controller.get_world_position() object_pos = obj.getPos(self.world.render if self.world else obj.getParent()) offset = object_pos - controller_pos self.grab_offsets[controller_name] = offset # 记录抓取状态 self.grabbed_objects[controller_name] = obj # 改变对象颜色表示抓取状态 self._highlight_object(obj, self.grab_color) # 触发震动反馈 controller.trigger_haptic_feedback(0.01, 0.8) # 显示绿色射线表示抓取 controller.show_ray(True) controller.set_ray_color([0.2, 0.9, 0.2, 0.8]) print(f"🤏 {controller_name}手开始抓取对象: {obj.getName()}") except Exception as e: print(f"⚠️ 开始抓取失败: {e}") def _update_grabbed_object(self, controller): """更新被抓取对象的位置""" controller_name = controller.name if controller_name not in self.grabbed_objects: return try: grabbed_obj = self.grabbed_objects[controller_name] grab_offset = self.grab_offsets.get(controller_name, Vec3(0, 0, 0)) # 计算新位置 controller_pos = controller.get_world_position() new_pos = controller_pos + grab_offset # 更新对象位置 grabbed_obj.setPos(self.world.render if self.world else grabbed_obj.getParent(), new_pos) # 可选:同步旋转 if hasattr(controller, 'get_world_rotation'): controller_rot = controller.get_world_rotation() grabbed_obj.setHpr(self.world.render if self.world else grabbed_obj.getParent(), controller_rot) except Exception as e: print(f"⚠️ 更新抓取对象失败: {e}") def _release_grab(self, controller): """释放抓取的对象""" controller_name = controller.name if controller_name not in self.grabbed_objects: return try: grabbed_obj = self.grabbed_objects[controller_name] # 恢复对象原始颜色 self._restore_object_color(grabbed_obj) # 清理抓取状态 del self.grabbed_objects[controller_name] if controller_name in self.grab_offsets: del self.grab_offsets[controller_name] # 触发震动反馈 controller.trigger_haptic_feedback(0.005, 0.4) print(f"🫳 {controller_name}手释放对象: {grabbed_obj.getName()}") except Exception as e: print(f"⚠️ 释放抓取失败: {e}") def _highlight_object(self, obj, color): """高亮显示对象""" if obj in self.highlighted_objects: return try: # 保存原始颜色 if obj not in self.original_colors: self.original_colors[obj] = obj.getColor() # 设置高亮颜色 obj.setColor(color) self.highlighted_objects.add(obj) except Exception as e: print(f"⚠️ 高亮对象失败: {e}") def _restore_object_color(self, obj): """恢复对象原始颜色""" if obj not in self.highlighted_objects: return try: # 恢复原始颜色 if obj in self.original_colors: obj.setColor(self.original_colors[obj]) del self.original_colors[obj] self.highlighted_objects.discard(obj) except Exception as e: print(f"⚠️ 恢复对象颜色失败: {e}") def _clear_selection_highlights(self): """清除所有选择高亮""" for obj in list(self.highlighted_objects): # 只清除非抓取状态的对象 is_grabbed = any(obj == grabbed_obj for grabbed_obj in self.grabbed_objects.values()) if not is_grabbed: self._restore_object_color(obj) def get_selected_object(self, controller_name): """获取指定控制器选中的对象""" return self.selected_objects.get(controller_name) def get_grabbed_object(self, controller_name): """获取指定控制器抓取的对象""" return self.grabbed_objects.get(controller_name) def is_grabbing(self, controller_name): """检查指定控制器是否正在抓取对象""" return controller_name in self.grabbed_objects def force_release_all(self): """强制释放所有抓取的对象""" for controller in self.vr_manager.get_connected_controllers(): if controller and controller.name in self.grabbed_objects: self._release_grab(controller) def cleanup(self): """清理资源""" self.ignoreAll() # 释放所有抓取 self.force_release_all() # 清理碰撞系统 self.collision_traverser.clearColliders() self.ray_collision_nodes.clear() # 清理高亮状态 for obj in list(self.highlighted_objects): self._restore_object_color(obj) print("🧹 VR交互管理器已清理")