1
0
forked from Rowland/EG
EG/core/vr_interaction.py
2025-09-15 16:41:35 +08:00

432 lines
15 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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交互管理器已清理")