""" VR可视化模块 提供VR手柄和交互元素的高级可视化功能: - 手柄3D模型渲染 - 交互射线显示 - 按钮状态可视化 - 触摸板和扳机反馈 """ from panda3d.core import ( NodePath, GeomNode, LineSegs, CardMaker, Geom, GeomVertexData, GeomVertexFormat, GeomVertexWriter, GeomTriangles, GeomPoints, Vec3, Vec4, Mat4, TransparencyAttrib, RenderState, ColorAttrib, InternalName, loadPrcFileData ) from panda3d.core import Texture, Material, TextureStage # 启用Assimp支持OBJ文件加载 loadPrcFileData("", "load-file-type p3assimp") class VRControllerVisualizer: """VR手柄可视化器""" def __init__(self, controller, render_node): """初始化手柄可视化器 Args: controller: VRController实例 render_node: 渲染节点 """ self.controller = controller self.render = render_node # 可视化节点 self.visual_node = None self.model_node = None self.ray_node = None self.button_indicator_node = None # 射线参数 self.ray_length = 10.0 self.ray_color = Vec4(0.9, 0.9, 0.2, 0.8) self.ray_hit_color = Vec4(0.2, 0.9, 0.2, 0.8) # 按钮指示器参数 self.button_colors = { 'normal': Vec4(0.3, 0.3, 0.8, 1.0), 'left': Vec4(0.2, 0.6, 0.9, 1.0), 'right': Vec4(0.9, 0.3, 0.3, 1.0), 'trigger': Vec4(0.9, 0.6, 0.2, 1.0), 'grip': Vec4(0.6, 0.9, 0.3, 1.0), 'menu': Vec4(0.9, 0.2, 0.9, 1.0), 'trackpad': Vec4(0.2, 0.9, 0.9, 1.0) } self._create_visual_components() def _create_visual_components(self): """创建可视化组件""" if not self.controller.anchor_node: return # 创建主可视化节点 self.visual_node = self.controller.anchor_node.attachNewNode(f'{self.controller.name}_visual') # 创建手柄模型 self._create_controller_model() # 创建交互射线 self._create_interaction_ray() # 暂时注释按钮指示器功能,避免额外几何体造成悬空零件 # self._create_button_indicators() def _create_controller_model(self): """创建手柄3D模型""" if not self.visual_node: return # 创建模型节点 self.model_node = self.visual_node.attachNewNode(f'{self.controller.name}_model') # 尝试加载SteamVR官方模型 steamvr_model = self._load_steamvr_model() if steamvr_model: # 使用SteamVR官方模型 steamvr_model.reparentTo(self.model_node) # 应用SteamVR配置中的正确旋转值,绕Y轴(俯仰轴)旋转90度 # body组件的rotate_xyz: [5.037,0.0,0.0],再加上绕Y轴旋转90度 # 右手正确,左手需要反向 if self.controller.name == 'left': # 左手控制器:绕Y轴俯仰+90度(修正反向) steamvr_model.setHpr(0, 5.037 + 90, 0) else: # 右手控制器:绕Y轴俯仰+90度(保持不变) steamvr_model.setHpr(0, 5.037 + 90, 0) # 设置合适的缩放值 steamvr_model.setScale(1.0) # 打印实际应用的旋转值 if self.controller.name == 'left': print(f"🔧 {self.controller.name}手柄:缩放: 1.0,旋转: (0, {5.037 + 90}, 0) [Y轴俯仰+90度]") else: print(f"🔧 {self.controller.name}手柄:缩放: 1.0,旋转: (0, {5.037 + 90}, 0) [Y轴俯仰+90度]") # 修复纯黑色问题:重新设置材质属性 self._fix_model_material(steamvr_model) # 暂时注释身份标记功能,避免额外几何体造成悬空零件 # self._apply_controller_identity_marker(steamvr_model) # 根据渲染模式设置渲染属性 self._apply_render_mode_settings(steamvr_model) print(f"✅ {self.controller.name}手柄已加载SteamVR官方模型(缩放: 1.0,实体渲染模式)") else: # 降级到改进的程序化模型 self._create_fallback_model() print(f"⚠️ {self.controller.name}手柄使用程序化模型(未找到SteamVR模型)") def _load_steamvr_model(self): """加载SteamVR官方手柄模型""" import os from panda3d.core import Filename, Texture, TextureStage # SteamVR模型基础路径 steamvr_base_paths = [ "/home/hello/.local/share/Steam/steamapps/common/SteamVR/resources/rendermodels/vr_controller_vive_1_5", "/home/hello/.steam/steam/steamapps/common/SteamVR/resources/rendermodels/vr_controller_vive_1_5", "~/.local/share/Steam/steamapps/common/SteamVR/resources/rendermodels/vr_controller_vive_1_5" ] for base_path in steamvr_base_paths: expanded_base_path = os.path.expanduser(base_path) if os.path.exists(expanded_base_path): print(f"🔍 找到SteamVR模型目录: {expanded_base_path}") # 不再添加目录到搜索路径,避免自动加载多余组件 # from panda3d.core import getModelPath # getModelPath().appendDirectory(expanded_base_path) # 尝试加载不同的模型文件,按优先级排序 model_files = [ ("body.obj", "手柄主体"), # 最重要的部分 ("vr_controller_vive_1_5.obj", "完整手柄模型"), # 组合模型 ] for model_file, description in model_files: model_path = os.path.join(expanded_base_path, model_file) if os.path.exists(model_path): try: print(f"🎮 尝试加载{description}: {model_file}") # 加载主模型 model = loader.loadModel(Filename.fromOsSpecific(model_path)) if model: # 先应用纹理,再修复材质(保持纹理效果) self._apply_steamvr_textures(model, expanded_base_path) print(f"✅ 成功加载{description}") return model else: print(f"⚠️ 模型文件存在但加载失败: {model_file}") except Exception as e: print(f"❌ 加载{description}失败: {e}") continue # 不再尝试组合加载多个部件,避免悬空零件问题 print("⚠️ 单个模型文件加载失败,跳过组合加载以避免悬空零件") break print("❌ 未找到任何SteamVR模型目录") return None def _fix_model_material(self, model): """修复模型材质,使用RenderPipeline兼容的新Material API""" from panda3d.core import Material, Vec4 # 检查模型是否有纹理 has_texture = model.hasTexture() # 创建新的材质,使用RenderPipeline兼容的API material = Material() if has_texture: # 有纹理时,设置白色基础颜色让纹理完全显示 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.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)") # 应用材质到模型,使用优先级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 # 创建一个小的标识几何体 marker_geom = self._create_box_geometry(0.005, 0.005, 0.02) marker_node = model.attachNewNode(marker_geom) # 根据左右手设置不同位置和颜色(现在都是Y轴+90度俯仰) if self.controller.name == 'left': # 左手控制器:左侧标记 marker_node.setPos(-0.03, 0.05, 0.02) marker_node.setColor(0.2, 0.4, 1.0, 1.0) # 蓝色 print(f"🔵 {self.controller.name}手柄已添加蓝色身份标记") else: # 右手控制器:右侧标记 marker_node.setPos(0.03, 0.05, 0.02) marker_node.setColor(1.0, 0.2, 0.2, 1.0) # 红色 print(f"🔴 {self.controller.name}手柄已添加红色身份标记") # 让标记发光以便更容易看到 marker_node.setLightOff() # 不受光照影响,保持明亮 # 添加轻微的色彩调整(非常微弱,不影响主要纹理) if self.controller.name == 'left': model.setColorScale(0.98, 0.98, 1.02, 1.0) # 极轻微的蓝色调 else: model.setColorScale(1.02, 0.98, 0.98, 1.0) # 极轻微的红色调 def _apply_steamvr_textures(self, model, base_path): """为SteamVR模型应用纹理""" import os from panda3d.core import Texture, TextureStage # SteamVR纹理文件 texture_files = { 'diffuse': 'onepointfive_texture.png', 'specular': 'onepointfive_spec.png' } textures_applied = 0 for texture_type, texture_file in texture_files.items(): texture_path = os.path.join(base_path, texture_file) if os.path.exists(texture_path): try: texture = loader.loadTexture(texture_path) if texture: # 确保纹理能正确加载 texture.setWrapU(Texture.WMClamp) texture.setWrapV(Texture.WMClamp) texture.setMinfilter(Texture.FTLinearMipmapLinear) texture.setMagfilter(Texture.FTLinear) if texture_type == 'diffuse': # 应用主要漫反射纹理 model.setTexture(texture) print(f"✅ 应用了主纹理: {texture_file}") textures_applied += 1 elif texture_type == 'specular': # 应用高光纹理 ts = TextureStage('specular') ts.setMode(TextureStage.MModulate) model.setTexture(ts, texture) print(f"✅ 应用了高光纹理: {texture_file}") textures_applied += 1 except Exception as e: print(f"⚠️ 应用纹理失败 {texture_file}: {e}") if textures_applied == 0: print(f"⚠️ {self.controller.name}手柄未能加载任何纹理,将使用材质颜色") else: print(f"🎨 {self.controller.name}手柄成功应用了 {textures_applied} 个纹理") def _load_combined_steamvr_model(self, base_path): """尝试组合加载多个SteamVR模型部件""" import os from panda3d.core import NodePath # 重要的模型部件 important_components = [ "body.obj", "trigger.obj", "trackpad.obj", "l_grip.obj" if self.controller.name == 'left' else "r_grip.obj" ] combined_model = NodePath("combined_controller") has_components = False for component in important_components: component_path = os.path.join(base_path, component) if os.path.exists(component_path): try: part = loader.loadModel(component_path) if part: part.reparentTo(combined_model) has_components = True print(f"✅ 加载了部件: {component}") except Exception as e: print(f"⚠️ 加载部件失败 {component}: {e}") if has_components: self._apply_steamvr_textures(combined_model, base_path) return combined_model return None def _create_fallback_model(self): """创建改进的程序化手柄模型作为后备方案""" # 主体(长条形状) main_body = self._create_box_geometry(0.025, 0.15, 0.04) main_node = self.model_node.attachNewNode(main_body) # 根据左右手设置不同颜色 if self.controller.name == 'left': color = self.button_colors['left'] else: color = self.button_colors['right'] main_node.setColor(color) # 启用光照响应 from panda3d.core import RenderState, MaterialAttrib, Material material = Material() material.setShininess(32) material.setAmbient((0.2, 0.2, 0.2, 1)) material.setDiffuse(color) material.setSpecular((0.5, 0.5, 0.5, 1)) main_node.setMaterial(material) # 扳机区域(小突起) trigger = self._create_box_geometry(0.015, 0.03, 0.02) trigger_node = self.model_node.attachNewNode(trigger) trigger_node.setPos(0, -0.08, 0.03) trigger_node.setColor(self.button_colors['trigger']) trigger_node.setMaterial(material) # 握把区域 grip = self._create_box_geometry(0.02, 0.06, 0.03) grip_node = self.model_node.attachNewNode(grip) grip_node.setPos(0, 0.05, -0.03) grip_node.setColor(self.button_colors['grip']) grip_node.setMaterial(material) # 触摸板区域(圆盘) trackpad = self._create_disc_geometry(0.015, 0.005) trackpad_node = self.model_node.attachNewNode(trackpad) trackpad_node.setPos(0, -0.02, 0.04) trackpad_node.setColor(self.button_colors['trackpad']) trackpad_node.setMaterial(material) # 应用渲染模式设置 self._apply_render_mode_settings(self.model_node) def _create_box_geometry(self, width, length, height): """创建立方体几何体""" # 创建顶点格式 format = GeomVertexFormat.getV3n3() vdata = GeomVertexData('box', format, Geom.UHStatic) vertex = GeomVertexWriter(vdata, 'vertex') normal = GeomVertexWriter(vdata, 'normal') # 定义立方体的8个顶点 vertices = [ Vec3(-width/2, -length/2, -height/2), Vec3( width/2, -length/2, -height/2), Vec3( width/2, length/2, -height/2), Vec3(-width/2, length/2, -height/2), Vec3(-width/2, -length/2, height/2), Vec3( width/2, -length/2, height/2), Vec3( width/2, length/2, height/2), Vec3(-width/2, length/2, height/2) ] # 立方体的6个面,每个面4个顶点 faces = [ # 底面 (z = -height/2) [0, 1, 2, 3, Vec3(0, 0, -1)], # 顶面 (z = height/2) [7, 6, 5, 4, Vec3(0, 0, 1)], # 前面 (y = -length/2) [4, 5, 1, 0, Vec3(0, -1, 0)], # 后面 (y = length/2) [3, 2, 6, 7, Vec3(0, 1, 0)], # 左面 (x = -width/2) [0, 3, 7, 4, Vec3(-1, 0, 0)], # 右面 (x = width/2) [5, 6, 2, 1, Vec3(1, 0, 0)] ] # 添加顶点和法线 for face in faces: for i in range(4): vertex.addData3(vertices[face[i]]) normal.addData3(face[4]) # 法线向量 # 创建几何体 geom = Geom(vdata) # 为每个面创建三角形 for face_idx in range(6): base_idx = face_idx * 4 prim = GeomTriangles(Geom.UHStatic) # 第一个三角形 prim.addVertices(base_idx, base_idx + 1, base_idx + 2) # 第二个三角形 prim.addVertices(base_idx, base_idx + 2, base_idx + 3) geom.addPrimitive(prim) # 创建几何体节点 geom_node = GeomNode('box') geom_node.addGeom(geom) return geom_node def _create_disc_geometry(self, radius, thickness): """创建圆盘几何体(用于触摸板)""" format = GeomVertexFormat.getV3n3() vdata = GeomVertexData('disc', format, Geom.UHStatic) vertex = GeomVertexWriter(vdata, 'vertex') normal = GeomVertexWriter(vdata, 'normal') # 创建圆盘顶点 segments = 16 import math # 中心点 vertex.addData3(0, 0, thickness/2) normal.addData3(0, 0, 1) # 圆周点 for i in range(segments): angle = 2 * math.pi * i / segments x = radius * math.cos(angle) y = radius * math.sin(angle) vertex.addData3(x, y, thickness/2) normal.addData3(0, 0, 1) # 创建几何体 geom = Geom(vdata) prim = GeomTriangles(Geom.UHStatic) # 创建扇形三角形 for i in range(segments): next_i = (i + 1) % segments prim.addVertices(0, i + 1, next_i + 1) geom.addPrimitive(prim) # 创建几何体节点 geom_node = GeomNode('disc') geom_node.addGeom(geom) return geom_node def _create_interaction_ray(self): """创建交互射线""" if not self.visual_node: return # 创建射线几何 line_segs = LineSegs() line_segs.setThickness(3) line_segs.setColor(self.ray_color) # 射线主体 line_segs.moveTo(0, 0, 0) line_segs.drawTo(0, self.ray_length, 0) # 射线端点(小球) end_point = self._create_sphere_geometry(0.02) # 创建射线节点 geom_node = line_segs.create() self.ray_node = self.visual_node.attachNewNode(geom_node) # 添加端点球 end_node = self.ray_node.attachNewNode(end_point) end_node.setPos(0, self.ray_length, 0) end_node.setColor(self.ray_color) # 设置透明度 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}手柄交互射线已创建(含PBR支持)") def _create_sphere_geometry(self, radius): """创建球体几何体""" # 简单的立方体作为球体替代 return self._create_box_geometry(radius, radius, radius) def _create_button_indicators(self): """创建按钮状态指示器""" if not self.visual_node: return # 创建按钮指示器容器 self.button_indicator_node = self.visual_node.attachNewNode(f'{self.controller.name}_indicators') # 扳机指示器 trigger_indicator = self._create_box_geometry(0.005, 0.01, 0.005) self.trigger_indicator = self.button_indicator_node.attachNewNode(trigger_indicator) self.trigger_indicator.setPos(0.02, -0.08, 0.03) self.trigger_indicator.setColor(0.2, 0.2, 0.2, 1.0) # 握把指示器 grip_indicator = self._create_box_geometry(0.005, 0.02, 0.005) self.grip_indicator = self.button_indicator_node.attachNewNode(grip_indicator) self.grip_indicator.setPos(-0.02, 0.05, -0.03) self.grip_indicator.setColor(0.2, 0.2, 0.2, 1.0) # 触摸板指示器 trackpad_indicator = self._create_disc_geometry(0.003, 0.002) self.trackpad_indicator = self.button_indicator_node.attachNewNode(trackpad_indicator) 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): """更新可视化状态""" if not self.controller.is_connected: self.hide() return self.show() # 更新按钮指示器状态 self._update_button_indicators() # 更新射线显示状态 self._update_ray_display() def _update_button_indicators(self): """更新按钮指示器状态""" if not hasattr(self, 'trigger_indicator'): return # 扳机指示器 if self.controller.is_trigger_pressed(): self.trigger_indicator.setColor(self.button_colors['trigger']) # 根据扳机值调整位置 trigger_offset = self.controller.trigger_value * 0.01 self.trigger_indicator.setPos(0.02, -0.08 + trigger_offset, 0.03) else: self.trigger_indicator.setColor(0.2, 0.2, 0.2, 1.0) self.trigger_indicator.setPos(0.02, -0.08, 0.03) # 握把指示器 if self.controller.is_grip_pressed(): self.grip_indicator.setColor(self.button_colors['grip']) else: self.grip_indicator.setColor(0.2, 0.2, 0.2, 1.0) # 触摸板指示器 if self.controller.touchpad_touched: self.trackpad_indicator.setColor(self.button_colors['trackpad']) # 根据触摸位置调整指示器位置 if hasattr(self.controller, 'touchpad_pos'): offset_x = self.controller.touchpad_pos.x * 0.01 offset_y = self.controller.touchpad_pos.y * 0.01 self.trackpad_indicator.setPos(offset_x, -0.02 + offset_y, 0.045) else: self.trackpad_indicator.setColor(0.2, 0.2, 0.2, 1.0) self.trackpad_indicator.setPos(0, -0.02, 0.045) def _update_ray_display(self): """更新射线显示""" if not self.ray_node: return # 根据交互状态显示/隐藏射线 # 这里可以添加更复杂的逻辑,比如只在指向对象时显示 show_ray = (self.controller.is_trigger_pressed(threshold=0.1) or self.controller.touchpad_touched) if show_ray: self.show_ray() else: self.hide_ray() def show(self): """显示手柄可视化""" if self.visual_node: self.visual_node.show() def hide(self): """隐藏手柄可视化""" if self.visual_node: self.visual_node.hide() def show_ray(self): """显示交互射线""" if self.ray_node: self.ray_node.show() def hide_ray(self): """隐藏交互射线""" if self.ray_node: self.ray_node.hide() def set_ray_color(self, color): """设置射线颜色""" 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 # 重新创建射线(简单的实现) if self.ray_node: self.ray_node.removeNode() self._create_interaction_ray() def _apply_render_mode_settings(self, model_node): """根据当前渲染模式应用渲染设置 Args: model_node: 手柄模型节点 """ if not model_node: return # 检测是否启用RenderPipeline模式 is_render_pipeline = False try: # 通过VR管理器获取渲染模式 vr_manager = self.controller.vr_manager if hasattr(vr_manager, 'vr_render_mode'): from core.vr_manager import VRRenderMode is_render_pipeline = (vr_manager.vr_render_mode == VRRenderMode.RENDER_PIPELINE and vr_manager.render_pipeline_enabled) except Exception as e: print(f"⚠️ 检测渲染模式失败: {e}") if is_render_pipeline: # RenderPipeline模式:使用正常深度测试,添加着色器标签 print(f"🎨 {self.controller.name}手柄:应用RenderPipeline渲染模式") # 设置着色器标签,使模型通过RenderPipeline的GBuffer渲染 model_node.setTag("RenderPipeline", "1") # 使用正常的深度测试和深度写入 model_node.setDepthTest(True) model_node.setDepthWrite(True) # 设置合适的渲染bin(transparent bin用于透明度支持) # 使用默认的opaque bin确保正常渲染 model_node.clearBin() # 递归设置所有子节点 for child in model_node.findAllMatches("**"): child.setTag("RenderPipeline", "1") child.setDepthTest(True) child.setDepthWrite(True) child.clearBin() print(f"✅ {self.controller.name}手柄已配置RenderPipeline渲染") else: # 普通模式:使用always-on-top设置 print(f"🎨 {self.controller.name}手柄:应用普通渲染模式(always-on-top)") self._set_always_on_top(model_node) def _set_always_on_top(self, model_node): """设置手柄模型始终显示在上层,不被其他物体遮挡(仅普通渲染模式)""" if not model_node: return from panda3d.core import RenderState # 设置为固定渲染bin,优先级设为较高值(1000) # fixed bin中的对象按sort值从小到大渲染,越大越后渲染(越在上层) model_node.setBin("fixed", 1000) # 禁用深度测试和深度写入,确保始终可见 model_node.setDepthTest(False) model_node.setDepthWrite(False) # 递归设置所有子节点的渲染属性 for child in model_node.findAllMatches("**"): child.setBin("fixed", 1000) child.setDepthTest(False) child.setDepthWrite(False) print(f"🔝 {self.controller.name}手柄已设置为始终显示在上层") 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}手柄可视化已彻底清理")