forked from Rowland/EG
432 lines
15 KiB
Python
432 lines
15 KiB
Python
"""
|
||
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交互管理器已清理") |