336 lines
12 KiB
Python
336 lines
12 KiB
Python
# -*- 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 *
|
||
|
||
# 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):
|
||
taskMgr.step()
|
||
self.qPanda3DWidget.update()
|
||
|
||
|
||
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)}")
|
||
|
||
|
||
|
||
|
||
|
||
|