项目保存时剔除坐标轴模型以及项目不会加载多余天空盒
This commit is contained in:
parent
4ca5068b2c
commit
bc90a4521e
@ -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)}")
|
||||
|
||||
@ -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
|
||||
|
||||
# 处理创建结果
|
||||
|
||||
@ -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):
|
||||
"""创建工具栏"""
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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'):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user