295 lines
11 KiB
Python
295 lines
11 KiB
Python
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()
|