diff --git a/core/selection.py b/core/selection.py index e0d2629d..7cef719c 100644 --- a/core/selection.py +++ b/core/selection.py @@ -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)}") diff --git a/scene/scene_manager.py b/scene/scene_manager.py index 064394e1..e81325e7 100644 --- a/scene/scene_manager.py +++ b/scene/scene_manager.py @@ -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 # 处理创建结果 diff --git a/ui/main_window.py b/ui/main_window.py index f83115e9..5286e327 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -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): """创建工具栏""" diff --git a/ui/property_panel.py b/ui/property_panel.py index 0501cb96..165324c1 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -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: diff --git a/ui/widgets.py b/ui/widgets.py index b0d584b6..cba839b2 100644 --- a/ui/widgets.py +++ b/ui/widgets.py @@ -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'):