1
0
forked from Rowland/EG

项目保存时剔除坐标轴模型以及项目不会加载多余天空盒

This commit is contained in:
Hector 2025-09-05 10:33:38 +08:00
parent 4ca5068b2c
commit bc90a4521e
5 changed files with 507 additions and 141 deletions

View File

@ -538,8 +538,8 @@ class SelectionSystem:
self.gizmo.setShaderOff() # 禁用着色器
self.gizmo.setFogOff() # 禁用雾效
self.gizmo.setBin("fixed", 40) # 设置为fixed渲染层级数值越大越优先
#self.gizmo.setDepthWrite(False) # 禁用深度写入
#self.gizmo.setDepthTest(False) # 禁用深度测试,确保始终可见
self.gizmo.setDepthWrite(False) # 禁用深度写入
self.gizmo.setDepthTest(False) # 禁用深度测试,确保始终可见
# 设置各轴节点的渲染属性
for axis_node in axis_nodes:
@ -548,8 +548,8 @@ class SelectionSystem:
axis_node.setShaderOff()
axis_node.setFogOff()
axis_node.setBin("fixed", 40) # 与主节点相同优先级
#axis_node.setDepthWrite(False) # 禁用深度写入
#axis_node.setDepthTest(False) # 禁用深度测试
axis_node.setDepthWrite(False) # 禁用深度写入
axis_node.setDepthTest(False) # 禁用深度测试
# 设置旋转轴节点的渲染属性
for axis_rotnode in axis_Rotnodes:
@ -558,8 +558,8 @@ class SelectionSystem:
axis_rotnode.setShaderOff()
axis_rotnode.setFogOff()
axis_rotnode.setBin("fixed", 40) # 与主节点相同优先级
#axis_rotnode.setDepthWrite(False) # 禁用深度写入
#axis_rotnode.setDepthTest(False) # 禁用深度测试
axis_rotnode.setDepthWrite(False) # 禁用深度写入
axis_rotnode.setDepthTest(False) # 禁用深度测试
# 收集所有handle节点
arrow_nodes = []
@ -597,8 +597,8 @@ class SelectionSystem:
arrow_node.setShaderOff()
arrow_node.setFogOff()
arrow_node.setBin("fixed", 41) # 略高于主节点,确保最优先显示
#arrow_node.setDepthWrite(False)
#arrow_node.setDepthTest(False)
arrow_node.setDepthWrite(False)
arrow_node.setDepthTest(False)
# 启用透明度支持
arrow_node.setTransparency(TransparencyAttrib.MAlpha)
@ -608,8 +608,8 @@ class SelectionSystem:
rot_node.setShaderOff()
rot_node.setFogOff()
rot_node.setBin("fixed", 41) # 略高于主节点,确保最优先显示
#rot_node.setDepthWrite(False)
#rot_node.setDepthTest(False)
rot_node.setDepthWrite(False)
rot_node.setDepthTest(False)
# 启用透明度支持
rot_node.setTransparency(TransparencyAttrib.MAlpha)
@ -920,8 +920,8 @@ class SelectionSystem:
handle_node.setFogOff() # 禁用雾效果
handle_node.setBin("fixed",41)
#handle_node.setDepthWrite(False)
#handle_node.setDepthTest(False)
handle_node.setDepthWrite(False)
handle_node.setDepthTest(False)
# 保存材质引用以便后续修改
if axis == "x":
@ -935,8 +935,8 @@ class SelectionSystem:
axis_node.setShaderOff()
axis_node.setFogOff()
axis_node.setBin("fixed", 40)
#axis_node.setDepthWrite(False)
#axis_node.setDepthTest(False)
axis_node.setDepthWrite(False)
axis_node.setDepthTest(False)
except Exception as e:
print(f"设置坐标轴颜色失败: {str(e)}")
@ -1015,8 +1015,8 @@ class SelectionSystem:
handle_node.setFogOff() # 禁用雾效果
handle_node.setBin("fixed",41)
#handle_node.setDepthWrite(False)
#handle_node.setDepthTest(False)
handle_node.setDepthWrite(False)
handle_node.setDepthTest(False)
# 保存材质引用以便后续修改
if axis == "x":
@ -1030,8 +1030,8 @@ class SelectionSystem:
axis_node.setShaderOff()
axis_node.setFogOff()
axis_node.setBin("fixed", 40)
#axis_node.setDepthWrite(False)
#axis_node.setDepthTest(False)
axis_node.setDepthWrite(False)
axis_node.setDepthTest(False)
except Exception as e:
print(f"设置坐标轴颜色失败: {str(e)}")

View File

@ -890,6 +890,27 @@ class SceneManager:
try:
print(f"\n=== 开始保存场景到: {filename} ===")
# 存储需要临时隐藏的节点,以便保存后恢复
nodes_to_restore = []
# 查找并隐藏所有坐标轴和选择框节点
gizmo_nodes = self.world.render.findAllMatches("**/gizmo*")
selection_box_nodes = self.world.render.findAllMatches("**/selectionBox*")
# 隐藏坐标轴节点
for node in gizmo_nodes:
if not node.isHidden():
nodes_to_restore.append((node, True)) # (节点, 原先是否可见)
node.hide()
print(f"临时隐藏坐标轴节点: {node.getName()}")
# 隐藏选择框节点
for node in selection_box_nodes:
if not node.isHidden():
nodes_to_restore.append((node, True))
node.hide()
print(f"临时隐藏选择框节点: {node.getName()}")
# 遍历所有模型,保存材质状态和变换信息
for model in self.models:
# 保存变换信息(关键!)
@ -924,12 +945,31 @@ class SceneManager:
if not color_attrib.isOff():
model.setTag("color", str(color_attrib.getColor()))
# 保存场景
success = self.world.render.writeBamFile(filename)
return success
try:
# 保存场景
success = self.world.render.writeBamFile(filename)
if success:
print(f"✓ 场景保存成功: {filename}")
else:
print("✗ 场景保存失败")
return success
finally:
# 恢复之前隐藏的节点
for item in nodes_to_restore:
node, was_visible = item
if was_visible and not node.isEmpty():
node.show()
print(f"恢复显示节点: {node.getName()}")
if nodes_to_restore:
print(f"已恢复 {len(nodes_to_restore)} 个辅助节点的显示")
except Exception as e:
print(f"保存场景时发生错误: {str(e)}")
import traceback
traceback.print_exc()
return False
def loadScene(self, filename):
@ -943,6 +983,9 @@ class SceneManager:
model.removeNode()
self.models.clear()
# 清理可能存在的辅助节点(坐标轴、选择框等)
self._cleanupAuxiliaryNodes()
# 加载场景
scene = self.world.loader.loadModel(filename)
if not scene:
@ -968,6 +1011,16 @@ class SceneManager:
print(f"{indent}跳过相机节点: {nodePath.getName()}")
return
# 跳过辅助节点(坐标轴和选择框)
if nodePath.getName().startswith(("gizmo", "selectionBox")):
print(f"{indent}跳过辅助节点: {nodePath.getName()}")
return
if nodePath.getName() in ['SceneRoot'] or \
any(keyword in nodePath.getName() for keyword in ["Skybox","skybox"]):
print(f"{indent}跳过环境节点:{nodePath.getName()}")
return
if isinstance(nodePath.node(), ModelRoot):
print(f"{indent}找到模型根节点!")
@ -1062,8 +1115,36 @@ class SceneManager:
except Exception as e:
print(f"加载场景时发生错误: {str(e)}")
import traceback
traceback.print_exc()
return False
def _cleanupAuxiliaryNodes(self):
"""清理场景中可能存在的辅助节点"""
try:
# 查找并移除所有坐标轴节点
gizmo_nodes = self.world.render.findAllMatches("**/gizmo*")
for node in gizmo_nodes:
if not node.isEmpty():
node.removeNode()
print(f"清理坐标轴节点: {node.getName()}")
# 查找并移除所有选择框节点
selection_box_nodes = self.world.render.findAllMatches("**/selectionBox*")
for node in selection_box_nodes:
if not node.isEmpty():
node.removeNode()
print(f"清理选择框节点: {node.getName()}")
# 停止相关的更新任务
from direct.task.TaskManagerGlobal import taskMgr
taskMgr.remove("updateGizmo")
taskMgr.remove("updateSelectionBox")
print("辅助节点清理完成")
except Exception as e:
print(f"清理辅助节点时出错: {e}")
# ==================== 模型管理 ====================
def deleteModel(self, model):
@ -1174,7 +1255,6 @@ class SceneManager:
light.radius = 1000
light.casts_shadows = True
light.shadow_map_resolution = 256
light.setPos(*pos)
# 添加到渲染管线
@ -1204,6 +1284,8 @@ class SceneManager:
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 创建聚光灯失败: {str(e)}")
import traceback
traceback.print_exc()
continue
# 处理创建结果

View File

@ -573,10 +573,10 @@ class MainWindow(QMainWindow):
self.addDockWidget(Qt.BottomDockWidgetArea, self.bottomDock)
# 创建底部停靠控制台
# self.consoleDock = QDockWidget("控制台", self)
# self.consoleView = CustomConsoleDockWidget(self.world)
# self.consoleDock.setWidget(self.consoleView)
# self.addDockWidget(Qt.BottomDockWidgetArea, self.consoleDock)
self.consoleDock = QDockWidget("控制台", self)
self.consoleView = CustomConsoleDockWidget(self.world)
self.consoleDock.setWidget(self.consoleView)
self.addDockWidget(Qt.BottomDockWidgetArea, self.consoleDock)
def setupToolbar(self):
"""创建工具栏"""

View File

@ -1926,6 +1926,96 @@ class PropertyPanelManager:
print(f"❌ 选择球形视频文件失败: {e}")
import traceback
traceback.print_exc()
def _addVideoScreenProperties(self, video_screen):
"""添加视频屏幕属性面板"""
try:
from PyQt5.QtWidgets import (QGroupBox, QGridLayout, QPushButton, QLabel,
QFileDialog, QHBoxLayout, QLineEdit)
from PyQt5.QtCore import Qt
import os
video_info_group = QGroupBox("视频信息")
video_info_layout = QGridLayout()
# 显示当前视频文件路径 - 改进版本
video_path = video_screen.getTag("video_path") if video_screen.hasTag("video_path") else ""
if video_path:
if video_path.startswith("http://") or video_path.startswith("https://"):
# 显示URL信息
video_info_layout.addWidget(QLabel("视频流URL:"), 0, 0)
path_label = QLabel(video_path)
path_label.setWordWrap(True)
path_label.setStyleSheet("color: #00AAFF;")
video_info_layout.addWidget(path_label, 0, 1)
elif os.path.exists(video_path):
# 显示本地文件信息
video_info_layout.addWidget(QLabel("视频文件:"), 0, 0)
path_label = QLabel(os.path.basename(video_path))
path_label.setWordWrap(True)
path_label.setStyleSheet("color: #00AAFF;")
video_info_layout.addWidget(path_label, 0, 1)
else:
# 文件不存在
video_info_layout.addWidget(QLabel("状态:"), 0, 0)
status_label = QLabel("文件不存在")
status_label.setStyleSheet("color: red;")
video_info_layout.addWidget(status_label, 0, 1)
else:
# 无视频
video_info_layout.addWidget(QLabel("状态:"), 0, 0)
status_label = QLabel("无视频文件")
status_label.setStyleSheet("color: red;")
video_info_layout.addWidget(status_label, 0, 1)
video_info_group.setLayout(video_info_layout)
self._propertyLayout.addWidget(video_info_group)
video_control_group = QGroupBox("视频控制")
video_control_layout = QHBoxLayout()
# 播放按钮
play_btn = QPushButton("▶️ 播放")
play_btn.clicked.connect(lambda: self.world.gui_manager.playVideo(video_screen))
video_control_layout.addWidget(play_btn)
# 暂停按钮
pause_btn = QPushButton("⏸️ 暂停")
pause_btn.clicked.connect(lambda: self.world.gui_manager.pauseVideo(video_screen))
video_control_layout.addWidget(pause_btn)
# 停止按钮
stop_btn = QPushButton("⏹️ 停止")
stop_btn.clicked.connect(lambda: self.world.gui_manager.stopVideo(video_screen))
video_control_layout.addWidget(stop_btn)
video_control_group.setLayout(video_control_layout)
self._propertyLayout.addWidget(video_control_group)
# 加载新视频按钮
load_btn = QPushButton("📁 加载新视频...")
load_btn.clicked.connect(lambda: self._loadNew2DVideo(video_screen))
self._propertyLayout.addWidget(load_btn)
# 添加URL输入区域
url_group = QGroupBox("网络视频")
url_layout = QHBoxLayout()
self.url_input = QLineEdit()
self.url_input.setPlaceholderText("输入视频流URL")
url_layout.addWidget(self.url_input)
load_url_btn = QPushButton("加载URL")
# 修改: 直接绑定到 _loadVideoFromURLWithOpenCV_3D 方法并传递URL参数
load_url_btn.clicked.connect(lambda: self._loadVideoFromURLWithOpenCV_3D(video_screen, self.url_input.text().strip()))
url_layout.addWidget(load_url_btn)
url_group.setLayout(url_layout)
self._propertyLayout.addWidget(url_group)
except Exception as e:
print(f"添加视频屏幕属性失败: {e}")
def _add2DVideoScreenProperties(self, video_screen):
"""为2D视频屏幕添加属性控制面板"""
try:
@ -1944,17 +2034,14 @@ class PropertyPanelManager:
if video_path.startswith("http://") or video_path.startswith("https://"):
# 显示URL信息
video_info_layout.addWidget(QLabel("视频流URL:"), 0, 0)
display_url = video_path if len(video_path) <= 50 else video_path[:47]+"..."
path_label = QLabel(display_url)
path_label = QLabel(video_path)
path_label.setWordWrap(True)
path_label.setStyleSheet("color: #00AAFF;")
video_info_layout.addWidget(path_label, 0, 1)
elif os.path.exists(video_path):
# 显示本地文件信息
video_info_layout.addWidget(QLabel("视频文件:"), 0, 0)
filename = os.path.basename(video_path)
display_filename = filename if len(filename) <= 30 else filename[:27]+"..."
path_label = QLabel(display_filename)
path_label = QLabel(os.path.basename(video_path))
path_label.setWordWrap(True)
path_label.setStyleSheet("color: #00AAFF;")
video_info_layout.addWidget(path_label, 0, 1)
@ -1992,7 +2079,7 @@ class PropertyPanelManager:
# 停止按钮
stop_btn = QPushButton("⏹️ 停止")
stop_btn.clicked.connect(lambda: self._stopVideo(video_screen))
stop_btn.clicked.connect(lambda: self.stop2DVideo(video_screen))
video_control_layout.addWidget(stop_btn)
video_control_group.setLayout(video_control_layout)
@ -2009,8 +2096,6 @@ class PropertyPanelManager:
self.url_input = QLineEdit()
self.url_input.setPlaceholderText("输入视频流URL")
if video_path and (video_path.startswith("http://") or video_path.startswith("https://")):
self.url_input.setText(video_path)
url_layout.addWidget(self.url_input)
load_url_btn = QPushButton("加载URL")
@ -2032,7 +2117,7 @@ class PropertyPanelManager:
try:
# 停止之前可能正在播放的视频
self._stopVideo(video_screen)
self._stop2DVideo(video_screen)
# 检查是本地文件还是网络URL
if url.startswith("http://") or url.startswith("https://"):
@ -2049,15 +2134,16 @@ class PropertyPanelManager:
return False
def _loadVideoFromURLWithOpenCV(self, video_screen, url):
"""使用OpenCV从URL加载视频流并在2D视频屏幕上显示稳定版)"""
"""使用OpenCV从URL加载视频流并在2D视频屏幕上显示推荐版)"""
try:
import cv2
import threading
from panda3d.core import Texture, PNMImage
import numpy as np
import time
# 停止之前可能正在播放的视频
self._stopVideo(video_screen)
self._stop2DVideo(video_screen)
# 使用 OpenCV 打开视频流
cap = cv2.VideoCapture(url)
@ -2065,9 +2151,8 @@ class PropertyPanelManager:
print(f"❌ 无法打开视频流: {url}")
return False
# 优化视频参数设置
# 设置视频参数以提高性能
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 减少缓冲以降低延迟
cap.set(cv2.CAP_PROP_FPS, 20) # 设置合理的帧率
# 创建纹理对象
texture = Texture("video_texture")
@ -2080,9 +2165,7 @@ class PropertyPanelManager:
video_info = {
'capture': cap,
'texture': texture,
'playing': True,
'target_fps': 60,
'last_frame_time': time.time()
'playing': True
}
# 应用纹理到视频屏幕
@ -2092,13 +2175,14 @@ class PropertyPanelManager:
# 启动视频播放线程
def update_video_texture():
frame_skip_counter = 0
#frame_skip_rate = 1 # 每处理1帧就跳过2帧
target_fps = 25 # 降低目标帧率以减少CPU使用
frame_time = 1.0 / target_fps
frame_count = 0
skip_frames = 2 # 每隔几帧处理一帧以降低CPU使用率
while True:
try:
current_time = time.time()
# 检查是否应该继续播放
if not (video_screen.hasPythonTag("video_info") and
video_screen.getPythonTag("video_info").get('playing', False)):
@ -2106,34 +2190,23 @@ class PropertyPanelManager:
video_info = video_screen.getPythonTag("video_info")
cap = video_info['capture']
target_fps = video_info.get('target_fps', 20)
frame_time = 1.0 / target_fps
# 控制帧率,避免过度处理
if current_time - video_info['last_frame_time'] < frame_time:
time.sleep(0.005) # 短暂休眠
continue
# 读取帧
ret, frame = cap.read()
if not ret:
# 视频结束,重新开始播放
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
continue
# 帧跳过机制
# frame_skip_counter = (frame_skip_counter + 1) % frame_skip_rate
# if frame_skip_counter != 0:
# time.sleep(0.01) # 短暂休眠
# continue
# 更新最后处理时间
video_info['last_frame_time'] = current_time
frame_count += 1
# 跳过一些帧以降低CPU使用率
if frame_count % skip_frames != 0:
time.sleep(frame_time)
continue
# 调整帧大小以降低处理负担
frame_height, frame_width = frame.shape[:2]
if frame_width > 256: # 限制最大宽度
scale = 256.0 / frame_width
if frame_width > 640: # 限制最大宽度
scale = 640.0 / frame_width
new_width = int(frame_width * scale)
new_height = int(frame_height * scale)
frame = cv2.resize(frame, (new_width, new_height))
@ -2159,20 +2232,14 @@ class PropertyPanelManager:
if texture:
texture.load(img)
# 控制帧率
time.sleep(frame_time)
except Exception as e:
print(f"视频帧更新错误: {e}")
import traceback
traceback.print_exc()
# 出现错误时短暂休眠防止CPU占用过高
time.sleep(0.1)
continue
# 线程结束时释放资源
try:
if 'capture' in video_info and video_info['capture']:
video_info['capture'].release()
except:
pass
break
print("视频播放线程结束")
@ -2213,19 +2280,17 @@ class PropertyPanelManager:
except Exception as play_error:
print(f"⚠️ MovieTexture 播放视频时出错: {play_error}")
url = None
if hasattr(self,'url_input') and self.url_input:
url = self.url_input.text().strip()
if not url or not url.startswith(("http://","https://")):
url = video_screen.getTag("video_path")
if url:
if url.startswith("http://") or url.startswith("htpps://"):
return self._loadVideoFromURLWithOpenCV(video_screen,url)
# 如果没有视频信息尝试从URL加载
video_path = video_screen.getTag("video_path")
if video_path:
# 根据路径判断是本地文件还是URL
if video_path.startswith("http://") or video_path.startswith("https://"):
return self._loadVideoFromURLWithOpenCV(video_screen, video_path)
else:
return self.world.gui_manager.load2DVideoFile(video_screen,url)
# 本地文件使用 MovieTexture 方式
return self.world.gui_manager.load2DVideoFile(video_screen, video_path)
else:
print("没有找到视频源")
print("没有找到视频源")
return False
except Exception as e:
@ -2249,7 +2314,7 @@ class PropertyPanelManager:
print(f"❌ 暂停视频失败: {e}")
return False
def _stopVideo(self, video_screen):
def _stop2DVideo(self, video_screen):
"""停止2D视频内部方法"""
try:
# 停止视频播放
@ -2262,16 +2327,7 @@ class PropertyPanelManager:
# 释放视频捕获资源
if 'capture' in video_info and video_info['capture']:
try:
# 在单独的线程中释放资源,避免阻塞
import threading
def release_capture():
try:
video_info['capture'].release()
except:
pass
release_thread = threading.Thread(target=release_capture, daemon=True)
release_thread.start()
video_info['capture'].release()
except:
pass
@ -2306,7 +2362,7 @@ class PropertyPanelManager:
if success:
print(f"成功加载新视频: {file_path}")
# 刷新属性面板以显示新视频信息
self._stopVideo(video_screen)
self._stop2DVideo(video_screen)
self.updateGUIPropertyPanel(video_screen)
return True
except Exception as e:
@ -2362,6 +2418,7 @@ class PropertyPanelManager:
return False
def _addVideoScreenProperties(self, video_screen):
"""添加视频屏幕属性面板"""
try:
from PyQt5.QtWidgets import (QGroupBox, QGridLayout, QPushButton, QLabel,
QFileDialog, QHBoxLayout, QLineEdit)
@ -2375,18 +2432,16 @@ class PropertyPanelManager:
video_path = video_screen.getTag("video_path") if video_screen.hasTag("video_path") else ""
if video_path:
if video_path.startswith("http://") or video_path.startswith("https://"):
video_info_layout.addWidget(QLabel("视频流URL:"),0,0)
display_url = video_path if len(video_path) <= 50 else video_path[:47]+"..."
path_label = QLabel(display_url)
# 显示URL信息
video_info_layout.addWidget(QLabel("视频流URL:"), 0, 0)
path_label = QLabel(video_path)
path_label.setWordWrap(True)
path_label.setStyleSheet("color: #00AAFF;")
video_info_layout.addWidget(path_label, 0, 1)
elif os.path.exists(video_path):
# 显示本地文件信息
video_info_layout.addWidget(QLabel("视频文件:"), 0, 0)
filename = os.path.basename(video_path)
display_filename = filename if len(filename) <= 30 else filename[:27]+"..."
path_label = QLabel(display_filename)
path_label = QLabel(os.path.basename(video_path))
path_label.setWordWrap(True)
path_label.setStyleSheet("color: #00AAFF;")
video_info_layout.addWidget(path_label, 0, 1)
@ -2428,25 +2483,21 @@ class PropertyPanelManager:
self._propertyLayout.addWidget(video_control_group)
# 加载新视频按钮
load_btn = QPushButton("📁 加载视频...")
load_btn.clicked.connect(lambda: self._loadNewVideo(video_screen))
load_btn = QPushButton("📁 加载视频...")
load_btn.clicked.connect(lambda: self._loadNew2DVideo(video_screen))
self._propertyLayout.addWidget(load_btn)
# 添加URL输入区域 - 新增功能
# 添加URL输入区域
url_group = QGroupBox("网络视频")
url_layout = QHBoxLayout()
self.url_input = QLineEdit()
self.url_input.setPlaceholderText("输入视频流URL")
# 如果已有URL则预填充
if video_path and (video_path.startswith("http://") or video_path.startswith("https://")):
self.url_input.setText(video_path)
url_layout.addWidget(self.url_input)
load_url_btn = QPushButton("加载URL")
# 修复:直接绑定处理函数,避免中间层
load_url_btn.clicked.connect(lambda: self._directLoadURL(video_screen))
# 修改: 直接绑定到 _loadVideoFromURLWithOpenCV_3D_direct 方法
load_url_btn.clicked.connect(lambda: self._loadVideoFromURLWithOpenCV_3D_direct(video_screen))
url_layout.addWidget(load_url_btn)
url_group.setLayout(url_layout)
@ -2454,36 +2505,194 @@ class PropertyPanelManager:
except Exception as e:
print(f"添加视频屏幕属性失败: {e}")
def _directLoadURL(self, video_screen):
"""直接加载URL - 修复版"""
def _loadVideoFromURLWithOpenCV_3D_direct(self, video_screen):
"""直接从URL输入框加载视频流 - 修复版"""
try:
# 获取URL文本
url = self.url_input.text().strip() if hasattr(self, 'url_input') and self.url_input else ""
# 从输入框获取URL
url = self.url_input.text().strip() if hasattr(self, 'url_input') else ""
if not url:
print("❌ URL不能为空")
return False
print(f"🔍 开始加载视频URL: {url}")
# 停止当前播放的任何视频
self._stopVideo(video_screen)
# 直接处理URL不通过中间方法
if url.startswith("http://") or url.startswith("https://"):
# 网络流使用OpenCV方式 - 直接调用
#return self._loadVideoFromURLWithOpenCV_3D_direct(video_screen, url)
return self._loadVideoFromURLWithOpenCV(video_screen, url)
else:
# 本地文件使用MovieTexture方式
return self.world.gui_manager.loadVideoFile(video_screen, url)
# 直接调用OpenCV处理方法
return self._loadVideoFromURLWithOpenCV_3D(video_screen, url)
except Exception as e:
print(f"❌ 直接加载URL失败: {e}")
print(f"❌ 直接加载视频失败: {e}")
import traceback
traceback.print_exc()
return False
def _loadVideoFromURLWithOpenCV_3D(self, video_screen, url):
"""使用OpenCV从URL加载视频流并在3D视频屏幕上显示"""
try:
import cv2
import threading
from panda3d.core import Texture, PNMImage, TextureStage
import time
# 停止之前可能正在播放的视频
self._stop3DVideo(video_screen)
# 使用 OpenCV 打开视频流
cap = cv2.VideoCapture(url)
if not cap.isOpened():
print(f"❌ 无法打开视频流: {url}")
return False
# 设置视频参数以提高性能
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 减少缓冲以降低延迟
# 创建纹理对象
texture = Texture("video_texture_3d")
texture.setMagfilter(Texture.FTLinear)
texture.setMinfilter(Texture.FTLinear)
texture.setWrapU(Texture.WMClamp)
texture.setWrapV(Texture.WMClamp)
# 保存视频信息
video_info = {
'capture': cap,
'texture': texture,
'playing': True
}
# 创建纹理阶段并应用到3D视频屏幕
texture_stage = TextureStage("video_3d")
texture_stage.setSort(0)
texture_stage.setMode(TextureStage.MModulate)
video_screen.setTexture(texture_stage, texture)
# 设置为白色以正确显示视频
video_screen.setColor(1, 1, 1, 1)
# 保存视频信息到PythonTag
video_screen.setPythonTag("video_info", video_info)
video_screen.setTag("video_path", url)
# 启动视频播放线程
def update_video_texture():
target_fps = 25 # 降低目标帧率以减少CPU使用
frame_time = 1.0 / target_fps
frame_count = 0
skip_frames = 2 # 每隔几帧处理一帧以降低CPU使用率
while True:
try:
# 检查是否应该继续播放
if not (video_screen.hasPythonTag("video_info") and
video_screen.getPythonTag("video_info").get('playing', False)):
break
video_info = video_screen.getPythonTag("video_info")
cap = video_info['capture']
ret, frame = cap.read()
if not ret:
# 视频结束,重新开始播放
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
continue
frame_count += 1
# 跳过一些帧以降低CPU使用率
if frame_count % skip_frames != 0:
time.sleep(frame_time)
continue
# 调整帧大小以降低处理负担
frame_height, frame_width = frame.shape[:2]
if frame_width > 640: # 限制最大宽度
scale = 640.0 / frame_width
new_width = int(frame_width * scale)
new_height = int(frame_height * scale)
frame = cv2.resize(frame, (new_width, new_height))
# 转换颜色格式 (BGR to RGB)
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# 获取帧尺寸
height, width = frame_rgb.shape[:2]
# 创建 PNMImage 并设置数据
img = PNMImage(width, height, 3)
img.set_maxval(255)
# 使用逐行设置方法(稳定且兼容性好)
for y in range(height):
for x in range(width):
r, g, b = frame_rgb[y, x]
img.setXelVal(x, y, r, g, b)
# 更新纹理
texture = video_info['texture']
if texture:
texture.load(img)
# 控制帧率
time.sleep(frame_time)
except Exception as e:
print(f"3D视频帧更新错误: {e}")
import traceback
traceback.print_exc()
break
print("3D视频播放线程结束")
# 在单独线程中处理视频流
thread = threading.Thread(target=update_video_texture, daemon=True)
thread.start()
print(f"✅ 开始在3D视频屏幕上播放网络视频流: {url}")
return True
except ImportError:
print("❌ 缺少必要的库,请安装: pip install opencv-python")
return False
except Exception as e:
print(f"❌ 加载3D网络视频失败: {e}")
import traceback
traceback.print_exc()
return False
def _stop3DVideo(self, video_screen):
"""停止3D视频内部方法"""
try:
# 停止视频播放
if video_screen.hasPythonTag("video_info"):
video_info = video_screen.getPythonTag("video_info")
if video_info:
# 停止播放
video_info['playing'] = False
# 释放视频捕获资源
if 'capture' in video_info and video_info['capture']:
try:
video_info['capture'].release()
except:
pass
# 清理纹理
try:
video_screen.clearTexture()
except:
pass
video_screen.clearPythonTag("video_info")
# 清理视频路径标签
if video_screen.hasTag("video_path"):
video_screen.clearTag("video_path")
print("⏹️ 3D视频已停止")
return True
except Exception as e:
print(f"❌ 停止3D视频失败: {e}")
return False
def _loadNewVideo(self,video_screen):
try:
file_path, _ = QFileDialog.getOpenFileName(
@ -3137,6 +3346,7 @@ class PropertyPanelManager:
return unique_names
def _updateModelMaterialPanel(self,model):
"""模型材质属性"""
if model.is_empty():
@ -3202,11 +3412,7 @@ class PropertyPanelManager:
material_layout = QGridLayout()
material_layout.addWidget(QLabel("名称:"), 0, 0)
if len(display_name) > 25:
limited_name = display_name[:22] + "..."
else:
limited_name = display_name
name_label = QLabel(limited_name)
name_label = QLabel(display_name)
# name_label.setStyleSheet("color: #FF6B6B; font-weight: bold;")
material_layout.addWidget(name_label, 0, 1, 1, 3)
@ -3369,8 +3575,46 @@ class PropertyPanelManager:
metallic_button.clicked.connect(lambda checked, mat=material: self._selectMetallicTexture(mat))
material_layout.addWidget(metallic_button, current_row, 2, 1, 2)
# #IOR贴图
# ior_button = QPushButton("选择IOR贴图")
# ior_button.clicked.connect(lambda checked,mat = material:self._selectIORTexture(mat))
# self._propertyLayout.addRow("IOR贴图",ior_button)
# # 视差贴图
# parallax_button = QPushButton("选择视差贴图")
# parallax_button.clicked.connect(lambda checked, mat=material: self._selectParallaxTexture(mat))
# self._propertyLayout.addRow("视差贴图:", parallax_button)
#
# # 自发光贴图
# emission_button = QPushButton("选择自发光贴图")
# emission_button.clicked.connect(lambda checked, mat=material: self._selectEmissionTexture(mat))
# self._propertyLayout.addRow("自发光贴图:", emission_button)
#
# # 环境光遮蔽贴图
# ao_button = QPushButton("选择AO贴图")
# ao_button.clicked.connect(lambda checked, mat=material: self._selectAOTexture(mat))
# self._propertyLayout.addRow("AO贴图", ao_button)
# # 透明度贴图
# alpha_button = QPushButton("选择透明度贴图")
# alpha_button.clicked.connect(lambda checked, mat=material: self._selectAlphaTexture(mat))
# self._propertyLayout.addRow("透明度贴图:", alpha_button)
#
# # 细节贴图
# detail_button = QPushButton("选择细节贴图")
# detail_button.clicked.connect(lambda checked, mat=material: self._selectDetailTexture(mat))
# self._propertyLayout.addRow("细节贴图:", detail_button)
#
# # 光泽贴图
# gloss_button = QPushButton("选择光泽贴图")
# gloss_button.clicked.connect(lambda checked, mat=material: self._selectGlossTexture(mat))
# self._propertyLayout.addRow("光泽贴图:", gloss_button)
# 在纹理按钮后添加当前贴图信息显示
current_row = self._displayCurrentTextures(material, material_layout, current_row)
current_row = self._addShadingModelPanel(material, material_layout, current_row)
current_row = self._addEmissionPanel(material, material_layout, current_row)
current_row = self._addMaterialPresetPanel(material, material_layout, current_row)
@ -3378,6 +3622,17 @@ class PropertyPanelManager:
material_group.setLayout(material_layout)
self._propertyLayout.addWidget(material_group)
# # 添加太阳方位角控制面板(只在第一个材质时添加,避免重复)
# # if i == 0:
# # self._addSunAzimuthPanel()
#
#
# # 分隔线
# if i < len(materials) - 1:
# separator = QLabel("─" * 30)
# separator.setStyleSheet("color: lightgray;")
# self._propertyLayout.addRow(separator)
def _updateMaterialBaseColor(self, material, component, value):
"""更新材质基础颜色(智能版本)"""
try:

View File

@ -1809,18 +1809,45 @@ class CustomTreeWidget(QTreeWidget):
if hasattr(panda_node, 'getPythonTag'):
light_object = panda_node.getPythonTag('rp_light_object')
if light_object and hasattr(self.world, 'render_pipeline'):
self.world.render_pipeline.remove_light(light_object)
try:
self.world.render_pipeline.remove_light(light_object)
print(f"移除灯光{panda_node.getName()}")
except Exception as e:
print(f"移除灯光失败: {str(e)}")
panda_node.clearPythonTag('rp_light_object')
#self.world.render_pipeline.remove_light(light_object)
if hasattr(self.world,'gui_manager') and hasattr(self.world.gui_manager,'gui_elements'):
if panda_node in self.world.gui_manager.gui_elements:
self.world.gui_manager.gui_elements.remove(panda_node)
print(f"从gui_elements列表中移除{panda_node.getName()}")
# 从world列表中移除
if hasattr(self.world, 'models') and panda_node in self.world.models:
self.world.models.remove(panda_node)
if hasattr(self.world, 'Spotlight') and panda_node in self.world.Spotlight:
self.world.Spotlight.remove(panda_node)
if hasattr(self.world, 'Pointlight') and panda_node in self.world.Pointlight:
self.world.Pointlight.remove(panda_node)
# if hasattr(self.world, 'Spotlight') and panda_node in self.world.Spotlight:
# self.world.Spotlight.remove(panda_node)
if hasattr(self.world,'Spotlight'):
self.world.Spotlight = [light for light in self.world.Spotlight if light != panda_node]
if panda_node in self.world.Spotlight:
print(f"从Spotlight列表中移除{panda_node.getName()}")
# if hasattr(self.world, 'Pointlight') and panda_node in self.world.Pointlight:
# self.world.Pointlight.remove(panda_node)
if hasattr(self.world,'Pointlight'):
self.world.Pointlight = [light for light in self.world.Pointlight if light != panda_node]
if panda_node in self.world.Pointlight:
print(f"从Pointlight列表中移除{panda_node.getName()}")
# 从Panda3D场景中移除
panda_node.removeNode()
try:
if not panda_node.isEmpty():
panda_node.removeNode()
except Exception as e:
print(f"❌ 删除节点 {item.text(0)} 失败: {str(e)}")
# 从Qt树中移除
parent_item = item.parent()
@ -1836,6 +1863,8 @@ class CustomTreeWidget(QTreeWidget):
except Exception as e:
print(f"❌ 删除节点 {item.text(0)} 失败: {str(e)}")
import traceback
traceback.print_exc()
# 最终清理
# if hasattr(self.world, 'property_panel'):