from panda3d.core import ConfigVariableString, NodePath,Filename,Material from direct.showbase.ShowBase import ShowBase import simplepbr from rpcore.render_pipeline import RenderPipeline import os,sys import p3dimgui # Panda3D 的 ImGui 后端 from imgui_bundle import ImVec2, imgui # ImGui API from tools.picker_ray import PickerRay import panda3d.core as p3d from pathlib import Path sys.path.insert(0,os.path.dirname(__file__)) sys.path.insert(0, str(Path(__file__).resolve().parent / "Builtin")) from TransformGizmo.transform_gizmo import TransformGizmo from tools.camera_orbit_controller import CameraOrbitController from classes.cornerBBox import CornerBBox from classes.colors import Color from rpcore.rpobject import RPObject from classes.gameobject import GameObject from classes.point_marker import PointMarker import panda3d import lui panda3d.lui = lui sys.modules["panda3d.lui"] = lui from Builtin.LUIRegion import LUIRegion from Builtin.LUIInputHandler import LUIInputHandler from Builtin.LUIButton import LUIButton from Builtin.LUISkin import LUIDefaultSkin class MainApp(ShowBase): def __init__(self): ConfigVariableString('win-size').set_value('1280 720') # super().__init__() RPObject._OUTPUT_LEVEL = 1 self.render_pipeline = RenderPipeline() self.render_pipeline.create(self) self.setFrameRateMeter(True) # 关闭窗口时确保主循环退出 if self.win: self.win.setCloseRequestEvent("app-exit") self.accept("app-exit", self.on_exit) # 初始化 ImGui:挂在 pixel2d 上并注册渲染回调 p3dimgui.init(window=self.win, parent=self.pixel2d, style='dark') self.accept("imgui-new-frame", self.draw_imgui) self.font_en = None self.font_cn = None self.node_icon = None self.node_icon_size = ImVec2(16, 16) self._init_imgui_fonts() self._load_node_icon() self.controller = CameraOrbitController(self) self.gizmo = TransformGizmo(self) self.models_root = self.render.attach_new_node("models_root") self.picker = PickerRay(self,self.models_root) model_path = "assets/Fox.glb" infile = Filename.from_os_specific(os.path.abspath(model_path)) if os.path.exists(infile): model: NodePath = self.loader.load_model(infile,noCache = True) model.reparent_to(self.models_root) GameObject.set_model_auto_scale(model) mat = Material('unlit') model.set_material(mat) self.controller.set_target(model) else: print(f"模型路径不存在 -> {model_path}") self.accept("mouse1", self.picker_evt) self.corner_box:CornerBBox = None panel:NodePath = GameObject.create_panel() panel.wrtReparentTo(self.models_root) self.marker = PointMarker(default_disable=True) self._init_lui_button() # region_width = 1.0 # region_height = (self.win.getYSize() - 48) / self.win.getYSize() # dr = self.camNode.getDisplayRegion(0) # dr.setDimensions(0, region_width, 0, region_height) # # 鼠标坐标系跟随显示区域,否则拾取会偏移 # self.mouseWatcherNode.setDisplayRegion(dr) # self._apply_region_aspect(dr) # # 窗口变化时同步刷新镜头纵横比,避免再次挤压/拉伸 # self.accept("window-event", lambda win: self._apply_region_aspect(dr)) self.selected_node = None self.hierarchy_size = ImVec2(300,0) self.last_mouse_cursor = imgui.get_mouse_cursor() # props = p3d.WindowProperties() # props.setCursorHidden(True) # self.win.requestProperties(props) # imgui.get_io().mouse_draw_cursor = True def _init_imgui_fonts(self): # 为 ImGui 加载中英文字体,避免中文显示为方块 io = imgui.get_io() # 英文字体:优先项目内提供,其次默认 font_en_path = Path("fonts/monaco.ttf") if font_en_path.exists(): self.font_en = io.fonts.add_font_from_file_ttf(str(font_en_path), 18) else: self.font_en = io.fonts.add_font_default() print(f'找不到英文字体文件 -> {font_en_path},已使用默认字体') # 中文字体:尝试多个常见路径,选第一个可用的 candidate_cn_fonts = [ Path("fonts/msyh.ttc"), Path("fonts/SourceHanSansCN-Regular.otf"), Path("fonts/SourceHanSansSC-Regular.otf"), Path("fonts/simhei.ttf"), Path("fonts/simsun.ttc"), Path("C:/Windows/Fonts/msyh.ttc"), Path("C:/Windows/Fonts/SimHei.ttf"), Path("C:/Windows/Fonts/simsun.ttc"), ] cn_path = next((p for p in candidate_cn_fonts if p.exists()), None) if cn_path: cfg = imgui.ImFontConfig() cfg.oversample_h = 2 cfg.oversample_v = 2 self.font_cn = io.fonts.add_font_from_file_ttf(str(cn_path), 18, cfg) else: self.font_cn = None print("未找到可用的中文字体(尝试了项目 fonts/ 下和系统字体),中文可能显示为方框,请放入中文字体到 fonts/ 并重启程序。") def _init_lui_button(self): """在右上角添加一个 LUI 按钮,演示 LUI 与现有管线共存。""" # 创建 LUI 区域和输入处理 self.lui_region = LUIRegion.make("LUI", self.win) self.lui_handler = LUIInputHandler() self.mouseWatcher.attach_new_node(self.lui_handler) self.lui_region.set_input_handler(self.lui_handler) # 提高显示区域排序,确保覆盖在 RenderPipeline 的最终合成之上 self.lui_region.set_sort(8) # 加载默认皮肤,保证按钮资源可用 self.lui_skin = LUIDefaultSkin() self.lui_skin.load() # 在右上角放置按钮 self.lui_button = LUIButton(parent=self.lui_region.root, text="LUI按钮", template="ButtonGreen") self.lui_button.top = 20 self.lui_button.right = 20 self.lui_button.bind("click", lambda event: print("按钮被点击了", event)) def _load_node_icon(self): icon_path = Path("assets/icons/model-color.png") if not icon_path.exists(): print(f"找不到层级树图标 -> {icon_path}") return try: self.node_icon = base.imgui.loadTexture(str(icon_path)) except Exception as exc: self.node_icon = None print(f"加载层级树图标失败 -> {icon_path}: {exc}") def _apply_region_aspect(self, dr:p3d.DisplayRegion): """根据显示区域的实际宽高比调整镜头,保持 3D 画面比例不变。""" if not self.win or not dr: return win_w = self.win.getXSize() win_h = self.win.getYSize() if win_h == 0: return region_w = dr.getRight() - dr.getLeft() region_h = dr.getTop() - dr.getBottom() if region_h == 0: return region_aspect = (win_w / win_h) * (region_w / region_h) self.camLens.setAspectRatio(region_aspect) def picker_evt(self): # 如果 ImGui 捕获鼠标,则不执行 3D 拾取,避免 UI 上点击触发场景操作 try: if base.imgui.isMouseCaptured(): # print("ImGui 捕获鼠标,不执行 3D 拾取") return except AttributeError: pass if self.gizmo.is_hovering: return if self.corner_box: self.corner_box.remove() node,point = self.picker.pick_object() self.controller.set_target(node) if node: self.corner_box = CornerBBox(node) self.gizmo.attach(node) else: self.gizmo.detach() if point: self.marker.update_point(point) else: self.marker.hide() def draw_imgui(self): io = imgui.get_io() display_w, display_h = io.display_size imgui.set_next_window_pos((0, 0), imgui.Cond_.always) imgui.set_next_window_size((min(500,self.hierarchy_size.x), display_h), imgui.Cond_.always) imgui.set_next_window_bg_alpha(1.0) imgui.begin("Hierarchy", flags=imgui.WindowFlags_.no_collapse | imgui.WindowFlags_.no_move) imgui.push_style_var(imgui.StyleVar_.item_spacing, (4, 2)) # item之间间距 imgui.push_style_var(imgui.StyleVar_.frame_padding, (0, 0)) # item内部padding imgui.push_style_var(imgui.StyleVar_.indent_spacing, 10) # 缩进更紧 imgui.push_font(self.font_cn,16) self.draw_tree_node(self.render) imgui.pop_font() imgui.pop_style_var(3) self.hierarchy_size = imgui.get_window_size() # if imgui.get_mouse_cursor() != self.last_mouse_cursor: # self.last_mouse_cursor = imgui.get_mouse_cursor() # print(f"设置鼠标光标 -> {self.last_mouse_cursor}") imgui.end() def draw_tree_node(self, node: NodePath): children = node.getChildren() has_children = len(children) > 0 # --- 图标 --- if self.node_icon: imgui.image(self.node_icon, self.node_icon_size, (1, 1), (0, 0)) imgui.same_line() label = f'{node.getName()} [{GameObject.get_node_type(node)}]' # ✅ 叶子节点:不用 tree_node_ex,完全无占位 if not has_children: clicked, _ = imgui.selectable( label, p_selected=(node == self.selected_node), flags=imgui.SelectableFlags_.span_all_columns | imgui.SelectableFlags_.allow_double_click ) if clicked: self.selected_node = node if imgui.is_mouse_double_clicked(imgui.MouseButton_.left): # print(f"双击了节点 -> {node.getName()}") pass return # --- 有子节点的才使用 tree node --- flags = ( imgui.TreeNodeFlags_.open_on_arrow | imgui.TreeNodeFlags_.span_avail_width | imgui.TreeNodeFlags_.open_on_double_click ) if node == self.selected_node: flags |= imgui.TreeNodeFlags_.selected if node.getName() == 'render': imgui.set_next_item_open(True, imgui.Cond_.first_use_ever) opened = imgui.tree_node_ex(label, flags) if imgui.is_item_clicked(imgui.MouseButton_.left): self.selected_node = node if opened: for child in children: self.draw_tree_node(child) imgui.tree_pop() def on_exit(self): """关闭窗口时清理 ImGui 上下文并退出应用。""" try: self.imgui.cleanup() except Exception: pass self.userExit() MainApp().run()