# -*- coding: utf-8-*- """ Module : QPanda3DWidget Author : Saifeddine ALOUI Description : This is the QWidget to be inserted in your standard PyQt5 application. It takes a Panda3DWorld object at init time. You should first create the Panda3DWorkd object before creating this widget. """ # PyQt imports from PyQt5 import QtWidgets, QtGui from PyQt5.QtCore import * from PyQt5.QtGui import * from PyQt5.QtWidgets import * from direct.task.TaskManagerGlobal import taskMgr # Panda imports from panda3d.core import Texture, WindowProperties, CallbackGraphicsWindow from panda3d.core import loadPrcFileData from QPanda3D.QPanda3D_Buttons_Translation import QPanda3D_Button_translation from QPanda3D.QPanda3D_Keys_Translation import QPanda3D_Key_translation from QPanda3D.QPanda3D_Modifiers_Translation import QPanda3D_Modifier_translation __all__ = ["QPanda3DWidget"] class QPanda3DSynchronizer(QTimer): def __init__(self, qPanda3DWidget, FPS=60): QTimer.__init__(self) self.qPanda3DWidget = qPanda3DWidget dt = 1000 / FPS self.setInterval(int(dt)) self.timeout.connect(self.tick) def tick(self): try: # 在渲染前清理可能损坏的TransformState对象 from panda3d.core import TransformState TransformState.clear_cache() taskMgr.step() self.qPanda3DWidget.update() except AssertionError as e: # 专门处理 TransformState has_mat() 断言错误 if "has_mat" in str(e): print(f"警告: 检测到TransformState断言错误,已静默处理: {e}") # 尝试恢复渲染状态 try: # 强制清理缓存并重试 from panda3d.core import TransformState, RenderState TransformState.clear_cache() RenderState.clear_cache() taskMgr.step() self.qPanda3DWidget.update() except: pass else: # 重新抛出其他断言错误 raise except Exception as e: # 静默处理其他所有异常 print(f"警告: 检测到异常,已静默处理: {e}") def get_panda_key_modifiers(evt): panda_mods = [] qt_mods = evt.modifiers() for qt_mod, panda_mod in QPanda3D_Modifier_translation.items(): if (qt_mods & qt_mod) == qt_mod: panda_mods.append(panda_mod) return panda_mods def get_panda_key_modifiers_prefix(evt): # join all modifiers (except NoModifier, which is None) with '-' prefix = "-".join([mod for mod in get_panda_key_modifiers(evt) if mod is not None]) # if the prefix is not empty, append a '-' if prefix: prefix += '-' return prefix class QPanda3DWidget(QWidget): """ An interactive panda3D QWidget Parent : Parent QT Widget FPS : Number of frames per socond to refresh the screen debug: Switch printing key events to console on/off """ def __init__(self, panda3DWorld, parent=None, FPS=60, debug=False): QWidget.__init__(self, parent) self.rp_sync_requested = False # set fixed geometry self.panda3DWorld = panda3DWorld self.panda3DWorld.set_parent(self) # Setup a timer in Qt that runs taskMgr.step() to simulate Panda's own main loop # pandaTimer = QTimer(self) # pandaTimer.timeout.connect() # pandaTimer.start(0) self.setFocusPolicy(Qt.StrongFocus) # Setup another timer that redraws this widget in a specific FPS # redrawTimer = QTimer(self) # redrawTimer.timeout.connect(self.update) # redrawTimer.start(1000/FPS) self.paintSurface = QPainter() self.rotate = QTransform() self.rotate.rotate(180) self.out_image = QImage() size = self.panda3DWorld.cam.node().get_lens().get_film_size() self.initial_film_size = QSizeF(size.x, size.y) self.initial_size = self.size() self.synchronizer = QPanda3DSynchronizer(self, FPS) self.synchronizer.start() self.debug = debug def mousePressEvent(self, evt): button = evt.button() try: b = "{}{}".format(get_panda_key_modifiers_prefix(evt), QPanda3D_Button_translation[button]) if self.debug: print(b) messenger.send(b,[{"x":evt.x(),"y":evt.y()}]) except: print("Unimplemented button. Please send an issue on github to fix this problem") def mouseMoveEvent(self, evt:QtGui.QMouseEvent): button = evt.button() try: b = "mouse-move" if self.debug: print(b) messenger.send(b,[{"x":evt.x(),"y":evt.y()}]) except: print("Unimplemented button. Please send an issue on github to fix this problem") def mouseReleaseEvent(self, evt): button = evt.button() try: b = "{}{}-up".format(get_panda_key_modifiers_prefix(evt), QPanda3D_Button_translation[button]) if self.debug: print(b) messenger.send(b,[{"x":evt.x(),"y":evt.y()}]) except: print("Unimplemented button. Please send an issue on github to fix this problem") def wheelEvent(self, evt): delta = evt.angleDelta().y() try: if self.debug: print(f"wheel {delta}") messenger.send('wheel',[{"delta":delta}]) except: print("Unimplemented button. Please send an issue on github to fix this problem") def keyPressEvent(self, evt): key = evt.key() try: k = "{}{}".format(get_panda_key_modifiers_prefix(evt), QPanda3D_Key_translation[key]) if self.debug: print(k) messenger.send(k) except: print("Unimplemented key. Please send an issue on github to fix this problem") def keyReleaseEvent(self, evt): key = evt.key() try: k = "{}{}-up".format(get_panda_key_modifiers_prefix(evt), QPanda3D_Key_translation[key]) if self.debug: print(k) messenger.send(k) except: print("Unimplemented key. Please send an issue on github to fix this problem") def resizeEvent(self, evt): width = evt.size().width() height = evt.size().height() #print(f"width:{width}") #print(f"height:{height}") from Panda3DWorld import resize_buffer #resize_buffer(width, height) lens = self.panda3DWorld.cam.node().get_lens() # lens.set_film_size(self.initial_film_size.width() * evt.size().width() / self.initial_size.width(), # self.initial_film_size.height() * evt.size().height() / self.initial_size.height()) #self.sync_panda3d_window_size(width, height) #self.panda3DWorld.buff.setSize(evt.size().width(), evt.size().height()) def minimumSizeHint(self): return QSize(400, 300) # Use the paint event to pull the contents of the panda texture to the widget # def paintEvent(self, event): # if self.panda3DWorld.screenTexture.mightHaveRamImage(): # tex = self.panda3DWorld.screenTexture # gsg = base.win.getGsg() # #self.panda3DWorld.screenTexture.store(gsg) # base.graphicsEngine.extractTextureData(tex, gsg) # self.panda3DWorld.screenTexture.setFormat(Texture.FRgba32) # data = self.panda3DWorld.screenTexture.getRamImage().getData() # img = QImage(data, self.panda3DWorld.screenTexture.getXSize(), self.panda3DWorld.screenTexture.getYSize(), # QImage.Format_ARGB32).mirrored() # self.paintSurface.begin(self) # self.paintSurface.drawImage(0, 0, img) # self.paintSurface.end() # def paintEvent(self, event): # tex = self.panda3DWorld.qt_output_tex # # gsg = base.win.getGsg() # # # if not self.rp_sync_requested: # base.graphicsEngine.extractTextureData(tex, gsg) # self.rp_sync_requested = True # self.update() # return # # if tex.hasRamImage(): # data = tex.getRamImage().getData() # width = tex.getXSize() # height = tex.getYSize() # # # ✅ 应该 data 长度 = width * height * 4 # img = QImage(data, width, height, QImage.Format_ARGB32).mirrored() # # painter = QPainter(self) # painter.drawImage(0, 0, img) # painter.end() # # self.rp_sync_requested = False # else: # print("⚠️ Texture has no RAM image yet, retrying next frame") # self.rp_sync_requested = False # self.update() # def paintEvent(self, event): # tex = self.panda3DWorld.qt_output_tex # # gsg = base.win.getGsg() # # if not tex.hasRamImage(): # base.graphicsEngine.extractTextureData(tex, gsg) # self.update() # 请求下一帧更新 # return # # data = tex.getRamImage().getData() # width = tex.getXSize() # height = tex.getYSize() # expected_len = width * height * 4 # # if len(data) != expected_len: # print(f"⚠️ 像素数据长度异常({len(data)} != {expected_len}),跳过绘制") # self.update() # return # # # 一切正常才绘制 # img = QImage(data, width, height, QImage.Format_ARGB32).mirrored() # # painter = QPainter(self) # painter.drawImage(0, 0, img) # painter.end() def paintEvent(self, event): tex = self.panda3DWorld.qt_output_tex gsg = base.win.getGsg() if not gsg: self.update() return if not tex.hasRamImage(): base.graphicsEngine.extractTextureData(tex, gsg) self.update() return data = tex.getRamImage().getData() tex_width = tex.getXSize() tex_height = tex.getYSize() expected_len = tex_width * tex_height * 4 if len(data) != expected_len: print(f"⚠️ 像素数据长度异常({len(data)} != {expected_len}),跳过绘制") self.update() return img = QImage(data, tex_width, tex_height, QImage.Format_ARGB32).mirrored() widget_width = self.width() widget_height = self.height() painter = QPainter(self) # 【保持宽高比的缩放】 scaled_img = img.scaled(widget_width, widget_height, Qt.KeepAspectRatio, Qt.SmoothTransformation) # 居中绘制 x_offset = (widget_width - scaled_img.width()) // 2 y_offset = (widget_height - scaled_img.height()) // 2 painter.drawImage(x_offset, y_offset, scaled_img) painter.end() def sync_panda3d_window_size(self, width, height): """同步 Panda3D 窗口尺寸到 Qt 窗口尺寸""" try: from panda3d.core import WindowProperties, LVecBase2i # 确保尺寸是4的倍数(RenderPipeline 要求) adjusted_width = width - width % 4 adjusted_height = height - height % 4 # 更新窗口属性 props = WindowProperties() props.setSize(adjusted_width, adjusted_height) # 对于 offscreen 渲染,直接更新窗口属性 if self.panda3DWorld.win: self.panda3DWorld.win.request_properties(props) # 手动触发 RenderPipeline 的尺寸更新逻辑 if hasattr(self.panda3DWorld, 'render_pipeline'): # 更新全局分辨率 from RenderPipelineFile.rpcore.globals import Globals Globals.native_resolution = LVecBase2i(adjusted_width, adjusted_height) # 重新计算渲染分辨率 self.panda3DWorld.render_pipeline._compute_render_resolution() # 通知各个管理器处理尺寸变化 self.panda3DWorld.render_pipeline.light_mgr.compute_tile_size() self.panda3DWorld.render_pipeline.stage_mgr.handle_window_resize() if hasattr(self.panda3DWorld.render_pipeline, 'debugger'): self.panda3DWorld.render_pipeline.debugger.handle_window_resize() # 触发插件的窗口尺寸变化钩子 self.panda3DWorld.render_pipeline.plugin_mgr.trigger_hook("window_resized") print(f"Panda3D 窗口尺寸已同步为: {adjusted_width} x {adjusted_height}") except Exception as e: print(f"同步 Panda3D 窗口尺寸失败: {str(e)}")