diff --git a/RenderPipelineFile/config/daytime.yaml b/RenderPipelineFile/config/daytime.yaml index af9861ed..f422041e 100644 --- a/RenderPipelineFile/config/daytime.yaml +++ b/RenderPipelineFile/config/daytime.yaml @@ -17,7 +17,7 @@ control_points: scattering: sun_intensity: [[[0.0000000000,0.0000000000],[0.0041666667,0.0000000000],[0.0083333333,0.0000000000],[0.0125000000,0.0000000000],[0.0166666667,0.0000000000],[0.0208333333,0.0000000000],[0.0250000000,0.0000000000],[0.0291666667,0.0000000000],[0.0333333333,0.0000000000],[0.0375000000,0.0000000000],[0.0416666667,0.0000000000],[0.0458333333,0.0000000000],[0.0500000000,0.0000000000],[0.0541666667,0.0000000000],[0.0583333333,0.0000000000],[0.0625000000,0.0000000000],[0.0666666667,0.0000000000],[0.0708333333,0.0000000000],[0.0750000000,0.0000000000],[0.0791666667,0.0000000000],[0.0833333333,0.0000000000],[0.0875000000,0.0000000000],[0.0916666667,0.0000000000],[0.0958333333,0.0000000000],[0.1000000000,0.0000000000],[0.1041666667,0.0000000000],[0.1083333333,0.0000000000],[0.1125000000,0.0000000000],[0.1166666667,0.0000000000],[0.1208333333,0.0000000000],[0.1250000000,0.0000000000],[0.1291666667,0.0000000000],[0.1333333333,0.0000000000],[0.1375000000,0.0000000000],[0.1416666667,0.0000000000],[0.1458333333,0.0000000000],[0.1500000000,0.0000000000],[0.1541666667,0.0000000000],[0.1583333333,0.0000028805],[0.1625000000,0.0003577724],[0.1666666667,0.0013331400],[0.1708333333,0.0029671803],[0.1750000000,0.0052963381],[0.1791666667,0.0083550556],[0.1833333333,0.0121755589],[0.1875000000,0.0167876159],[0.1916666667,0.0222183530],[0.1958333333,0.0284919947],[0.2000000000,0.0356297193],[0.2041666667,0.0436494349],[0.2083333333,0.0525656099],[0.2125000000,0.0623891610],[0.2166666667,0.0731272461],[0.2208333333,0.0847831708],[0.2250000000,0.0973563167],[0.2291666667,0.1108419698],[0.2333333333,0.1252313631],[0.2375000000,0.1405115250],[0.2416666667,0.1566653434],[0.2458333333,0.1736715009],[0.2500000000,0.1915046014],[0.2541666667,0.2101350464],[0.2583333333,0.2295292930],[0.2625000000,0.2496498145],[0.2666666667,0.2704552670],[0.2708333333,0.2919006662],[0.2750000000,0.3139375192],[0.2791666667,0.3365139497],[0.2833333333,0.3595750662],[0.2875000000,0.3830630359],[0.2916666667,0.4069173972],[0.2958333333,0.4310753462],[0.3000000000,0.4554720417],[0.3041666667,0.4800408236],[0.3083333333,0.5047136020],[0.3125000000,0.5294212108],[0.3166666667,0.5540936424],[0.3208333333,0.5786605298],[0.3250000000,0.6030514553],[0.3291666667,0.6271963182],[0.3333333333,0.6510256858],[0.3375000000,0.6744711982],[0.3416666667,0.6974659988],[0.3458333333,0.7199450163],[0.3500000000,0.7418453485],[0.3541666667,0.7631067095],[0.3583333333,0.7836717291],[0.3625000000,0.8034862953],[0.3666666667,0.8224999302],[0.3708333333,0.8406661079],[0.3750000000,0.8579425235],[0.3791666667,0.8742914270],[0.3833333333,0.8896799131],[0.3875000000,0.9040801386],[0.3916666667,0.9174695289],[0.3958333333,0.9298310650],[0.4000000000,0.9411533765],[0.4041666667,0.9514309312],[0.4083333333,0.9606641691],[0.4125000000,0.9688595571],[0.4166666667,0.9760296330],[0.4208333333,0.9821930708],[0.4250000000,0.9873746114],[0.4291666667,0.9916050060],[0.4333333333,0.9949209310],[0.4375000000,0.9973647924],[0.4416666667,0.9989845508],[0.4458333333,0.9998334497],[0.4500000000,0.9999696949],[0.4541666667,0.9994560801],[0.4583333333,0.9983595429],[0.4625000000,0.9967506613],[0.4666666667,0.9947030614],[0.4708333333,0.9922927758],[0.4750000000,0.9895975125],[0.4791666667,0.9866958610],[0.4833333333,0.9836664262],[0.4875000000,0.9805868867],[0.4916666667,0.9775330316],[0.4958333333,0.9745777179],[0.5000000000,0.9717898417],[0.5041666667,0.9692332877],[0.5083333333,0.9669658924],[0.5125000000,0.9650384806],[0.5089595376,0.9690650222],[0.5208333333,0.9623666659],[0.5250000000,0.9616814371],[0.5291666667,0.9614534423],[0.5333333333,0.9616877089],[0.5375000000,0.9623790807],[0.5416666667,0.9635123329],[0.5458333333,0.9650624244],[0.5500000000,0.9669949804],[0.5541666667,0.9692669864],[0.5583333333,0.9718275065],[0.5625000000,0.9746185969],[0.5666666667,0.9775762863],[0.5708333333,0.9806315864],[0.5750000000,0.9837115661],[0.5791666667,0.9867403433],[0.5833333333,0.9896401655],[0.5875000000,0.9923323562],[0.5916666667,0.9947382579],[0.5958333333,0.9967800977],[0.6000000000,0.9983817820],[0.6041666667,0.9994696263],[0.6083333333,0.9999730028],[0.6125000000,0.9998249266],[0.6166666667,0.9989625601],[0.6208333333,0.9973276624],[0.6250000000,0.9948669567],[0.6291666667,0.9915324664],[0.6333333333,0.9872817545],[0.6375000000,0.9820781426],[0.6416666667,0.9758908775],[0.6458333333,0.9686952146],[0.6500000000,0.9604725211],[0.6541666667,0.9512102537],[0.6583333333,0.9409019858],[0.6625000000,0.9295473441],[0.6666666667,0.9171518878],[0.6708333333,0.9037270619],[0.6750000000,0.8892899902],[0.6791666667,0.8738633008],[0.6833333333,0.8574749656],[0.6875000000,0.8401579787],[0.6916666667,0.8219502453],[0.6958333333,0.8028941798],[0.7000000000,0.7830364456],[0.7041666667,0.7624277344],[0.7083333333,0.7411222520],[0.7125000000,0.7191776044],[0.7166666667,0.6966542563],[0.7208333333,0.6736152714],[0.7250000000,0.6501259629],[0.7291666667,0.6262533880],[0.7333333333,0.6020661121],[0.7375000000,0.5776338043],[0.7416666667,0.5530267796],[0.7458333333,0.5283156992],[0.7500000000,0.5035711751],[0.7541666667,0.4788634341],[0.7583333333,0.4542618347],[0.7625000000,0.4298347613],[0.7666666667,0.4056490351],[0.7708333333,0.3817697830],[0.7750000000,0.3582600107],[0.7791666667,0.3351803495],[0.7833333333,0.3125888445],[0.7875000000,0.2905406366],[0.7916666667,0.2690876955],[0.7958333333,0.2482787388],[0.8000000000,0.2281588906],[0.8041666667,0.2087696425],[0.8083333333,0.1901486315],[0.8125000000,0.1723295359],[0.8166666667,0.1553419918],[0.8208333333,0.1392115328],[0.8250000000,0.1239595144],[0.8291666667,0.1096030703],[0.8333333333,0.0961551918],[0.8375000000,0.0836246599],[0.8416666667,0.0720161369],[0.8458333333,0.0613302273],[0.8500000000,0.0515635598],[0.8541666667,0.0427088803],[0.8583333333,0.0347551990],[0.8625000000,0.0276878920],[0.8666666667,0.0214889271],[0.8708333333,0.0161369711],[0.8750000000,0.0116076130],[0.8791666667,0.0078735477],[0.8833333333,0.0049047927],[0.8875000000,0.0026688977],[0.8916666667,0.0011311782],[0.8958333333,0.0002549473],[0.9000000000,0.0000000000],[0.9041666667,0.0000000000],[0.9083333333,0.0000000000],[0.9125000000,0.0000000000],[0.9166666667,0.0000000000],[0.9208333333,0.0000000000],[0.9250000000,0.0000000000],[0.9291666667,0.0000000000],[0.9333333333,0.0000000000],[0.9375000000,0.0000000000],[0.9416666667,0.0000000000],[0.9458333333,0.0000000000],[0.9500000000,0.0000000000],[0.9541666667,0.0000000000],[0.9583333333,0.0000000000],[0.9625000000,0.0000000000],[0.9666666667,0.0000000000],[0.9708333333,0.0000000000],[0.9750000000,0.0000000000],[0.9791666667,0.0000000000],[0.9833333333,0.0000000000],[0.9875000000,0.0000000000],[0.9916666667,0.0000000000],[0.9958333333,0.0000000000]]] sun_color: [[[0.5010435645,0.5818710306],[0.0433100000,0.8999700000],[0.8635787716,0.9130000000],[0.1785000000,0.8973600000],[0.8099800000,0.8651100000],[0.2360800000,0.7712700000],[0.6583432177,0.8485126184],[0.1266806142,0.9648102053],[0.9558541267,0.9090909091],[0.5568400771,0.7353760446]],[[0.5001318426,0.5160300000],[0.0572700000,0.6541600000],[0.2395000000,0.5976800000],[0.8104600000,0.6009000000],[0.6967400000,0.5483900000]],[[0.0862400000,0.4257800000],[0.4955600000,0.4033000000],[0.8234200000,0.4340200000]]] - sun_azimuth: [[[0.5000000000,0.5000000000]]] + sun_azimuth: [[[0.5000000000,0.8527777778]]] sun_altitude: [[[0.5000000000,1.0000000000]]] extinction: [[[0.4913294798,0.6378830084]]] volumetrics: diff --git a/Resources/model/v.glb b/Resources/model/v.glb deleted file mode 100644 index 79ebe8d2..00000000 Binary files a/Resources/model/v.glb and /dev/null differ diff --git a/gui/gui_manager.py b/gui/gui_manager.py index e2ebab6a..2ae54393 100644 --- a/gui/gui_manager.py +++ b/gui/gui_manager.py @@ -157,17 +157,23 @@ class GUIManager: # 父节点是GUI元素 - 作为子GUI挂载 gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node) parent_gui_node = parent_node # 直接挂载到GUI元素 - print(f"📎 挂载到GUI父节点: {parent_node.getName()}") + parent_scale=parent_node.getScale() + relative_scale = ( + size/parent_scale[0] if parent_scale[0]!=0 else size, + size/parent_scale[1] if parent_scale[1]!=0 else size, + size/parent_scale[2] if parent_scale[2]!=0 else size + ) else: # 父节点是普通3D节点 - 使用屏幕坐标 gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) parent_gui_node = None # 使用默认的aspect2d + relative_scale = size print(f"📎 挂载到3D父节点: {parent_item.text(0)}") button = DirectButton( text=text, pos=gui_pos, - scale=size, + scale=relative_scale, command=self.onGUIButtonClick, extraArgs=[f"button_{len(self.gui_elements)}"], frameColor=(0.2, 0.6, 0.8, 1), @@ -268,17 +274,22 @@ class GUIManager: # 父节点是GUI元素 - 作为子GUI挂载 gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node) parent_gui_node = parent_node - print(f"📎 挂载到GUI父节点: {parent_node.getName()}") + + parent_scale = parent_node.getScale() + relative_scale = ( + size/parent_scale[0] if parent_scale[0]!= 0 else size, + size/parent_scale[1] if parent_scale[1]!= 0 else size, + size/parent_scale[2] if parent_scale[2]!= 0 else size + ) else: # 父节点是普通3D节点 - 使用屏幕坐标 gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) parent_gui_node = None - print(f"📎 挂载到3D父节点: {parent_item.text(0)}") - + relative_scale = size label = DirectLabel( text=text, pos=gui_pos, - scale=size, + scale=relative_scale, frameColor=(0, 0, 0, 0), # 透明背景 text_fg=(1, 1, 1, 1), text_font=self.world.getChineseFont() if self.world.getChineseFont() else None, @@ -862,10 +873,9 @@ class GUIManager: return None def createVideoScreen(self, pos=(0, 0, 0), size=0.2, video_path=None): - """创建视频播放屏幕 - 改进版本""" + """创建3D视频播放屏幕 - 添加占位符纹理支持""" try: from panda3d.core import CardMaker, TransparencyAttrib, Texture, TextureStage - from PyQt5.QtCore import Qt import os # 确保 pos 是有效的三维坐标元组 @@ -896,7 +906,7 @@ class GUIManager: return None # 获取目标父节点列表 - target_parents = tree_widget.get_target_parents_for_gui_creation() + target_parents = tree_widget.get_target_parents_for_creation() if not target_parents: print("❌ 没有找到有效的父节点") return None @@ -925,6 +935,40 @@ class GUIManager: # 设置初始颜色为白色,确保纹理能正确显示 video_screen.setColor(1, 1, 1, 1) + # 确保视频屏幕有正确的材质 + self._ensureVideoScreenMaterial(video_screen) + + # 设置节点标签 + video_screen.setTag("gui_type", "video_screen") + video_screen.setTag("gui_id", f"video_screen_{len(self.gui_elements)}") + video_screen.setTag("gui_text", f"视频屏幕_{len(self.gui_elements)}") + video_screen.setTag("is_gui_element", "1") + video_screen.setTag("is_scene_element", "1") + video_screen.setTag("created_by_user", "1") + + # 设置视频路径标签 + if video_path and os.path.exists(video_path): + video_screen.setTag("video_path", video_path) + else: + video_screen.setTag("video_path", "") + + # 关键修改:预先创建一个占位符纹理,为后续视频播放做准备 + placeholder_texture = Texture(f"placeholder_video_texture_3d_{len(self.gui_elements)}") + placeholder_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb) + placeholder_data = b'\x19\x19\x4c' # 深蓝色占位符颜色 (25, 25, 76) + placeholder_texture.setRamImage(placeholder_data) + + # 创建纹理阶段并应用占位符纹理到视频屏幕 + texture_stage = TextureStage("video_placeholder") + texture_stage.setSort(0) + texture_stage.setMode(TextureStage.MModulate) + video_screen.setTexture(texture_stage, placeholder_texture) + + # 保存占位符纹理引用 + video_screen.setPythonTag("placeholder_texture", placeholder_texture) + + print(f"🔧 为3D视频屏幕创建了占位符纹理环境: {screen_name}") + # 如果提供了视频路径,则加载视频纹理 movie_texture = None if video_path and os.path.exists(video_path): @@ -941,6 +985,9 @@ class GUIManager: print(f"✅ 视频纹理加载成功: {video_path}") + # 保存视频纹理引用以便后续控制 + video_screen.setPythonTag("movie_texture", movie_texture) + # 尝试自动播放视频(如果支持) try: if hasattr(movie_texture, 'play'): @@ -966,19 +1013,6 @@ class GUIManager: else: print("ℹ️ 未提供视频文件,显示默认占位符") - # 确保视频屏幕有正确的材质 - self._ensureVideoScreenMaterial(video_screen) - - # 设置节点标签 - video_screen.setTag("gui_type", "video_screen") - video_screen.setTag("gui_id", f"video_screen_{len(self.gui_elements)}") - video_screen.setTag("gui_text", f"视频屏幕_{len(self.gui_elements)}") - video_screen.setTag("is_gui_element", "1") - video_screen.setTag("is_scene_element", "1") - video_screen.setTag("created_by_user", "1") - if video_path and os.path.exists(video_path): - video_screen.setTag("video_path", video_path) - # 保存视频纹理引用以便后续控制 if movie_texture: video_screen.setPythonTag("movie_texture", movie_texture) @@ -1201,8 +1235,6 @@ class GUIManager: import traceback traceback.print_exc() return False - - def setVideoTime(self, video_screen, time_seconds): """设置视频播放时间""" try: @@ -1434,8 +1466,25 @@ class GUIManager: video_screen.setTag("is_scene_element", "1") video_screen.setTag("created_by_user", "1") + # 设置视频路径标签 if video_path and os.path.exists(video_path): video_screen.setTag("video_path", video_path) + else: + video_screen.setTag("video_path", "") + + # 关键修改:预先创建一个占位符纹理,为后续视频播放做准备 + placeholder_texture = Texture(f"placeholder_video_texture_{len(self.gui_elements)}") + placeholder_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb) + placeholder_data = b'\x19\x19\x4c' # 深蓝色占位符颜色 (25, 25, 76) + placeholder_texture.setRamImage(placeholder_data) + + # 应用占位符纹理到视频屏幕 + video_screen["frameTexture"] = placeholder_texture + + # 保存占位符纹理引用 + video_screen.setPythonTag("placeholder_texture", placeholder_texture) + + print(f"🔧 为2D视频屏幕创建了占位符纹理环境: {screen_name}") # 如果提供了视频路径,则加载视频纹理 movie_texture = None @@ -1445,7 +1494,7 @@ class GUIManager: # 加载视频纹理 movie_texture = self._loadMovieTexture(video_path) if movie_texture: - # 应用纹理到视频屏幕 + # 应用纹理到视频屏幕(替换占位符) video_screen["frameTexture"] = movie_texture print(f"✅ 2D视频纹理加载成功: {video_path}") @@ -1546,143 +1595,349 @@ class GUIManager: traceback.print_exc() return False - def _loadMovieTexture(self, video_path): - """加载视频纹理的兼容方法""" - try: - from panda3d.core import Texture, MovieTexture - import os - - # 检查文件是否存在 - if not os.path.exists(video_path): - print(f"❌ 视频文件不存在: {video_path}") - return None - - print(f"🔍 尝试加载视频文件: {video_path}") - - # 方法1: 尝试使用 MovieTexture(专门用于视频) - try: - movie_texture = MovieTexture(video_path) - if movie_texture.read(video_path): - print("✅ 使用 MovieTexture 成功加载视频") - self._configureVideoTexture(movie_texture) - return movie_texture - else: - print("⚠️ MovieTexture.read() 返回失败") - except Exception as e: - print(f"⚠️ MovieTexture 方法失败: {e}") - - # 方法2: 尝试使用 loader.loadTexture - try: - movie_texture = self.world.loader.loadTexture(video_path) - if movie_texture and hasattr(movie_texture, 'is_playable') and movie_texture.is_playable(): - print("✅ 使用 loader.loadTexture 成功加载视频纹理") - self._configureVideoTexture(movie_texture) - return movie_texture - else: - print("⚠️ loader.loadTexture 加载的不是可播放的视频纹理") - except Exception as e: - print(f"⚠️ loader.loadTexture 方法失败: {e}") - - # 方法3: 尝试使用 Texture.read(作为最后备选) - try: - texture = Texture() - if texture.read(video_path): - print("✅ 使用 Texture.read 成功加载(可能作为静态纹理)") - self._configureVideoTexture(texture) - return texture - except Exception as e: - print(f"⚠️ Texture.read 方法失败: {e}") - - print("❌ 所有视频纹理加载方法都失败") - return None - - except Exception as e: - print(f"❌ 加载视频纹理时发生未知错误: {e}") - import traceback - traceback.print_exc() - return None - def play2DVideo(self, video_screen): """播放2D视频""" try: - # 获取视频纹理 - movie_texture = video_screen.getPythonTag("movie_texture") + if video_screen.hasPythonTag("video_capture"): + print("视频已在播放中") + return True - # 备用获取方法 - if not movie_texture: - # 尝试从frameTexture获取 - if hasattr(video_screen, 'frameTexture'): - movie_texture = video_screen.frameTexture - - if movie_texture and hasattr(movie_texture, 'play'): - try: - movie_texture.play() - print(f"▶️ 继续播放2D视频: {video_screen.getName()}") - return True - except Exception as play_error: - print(f"⚠️ 播放2D视频时出错: {play_error}") - return False + # 获取视频路径并重新开始播放 + video_path = video_screen.getTag("video_path") + if video_path: + return self.world.property_manager._loadVideoFromURL(video_screen, video_path) else: - print(f"⚠️ 2D视频屏幕没有可播放的视频纹理: {video_screen.getName()}") + print("❌ 没有找到视频源") return False + except Exception as e: - print(f"❌ 播放2D视频失败: {e}") + print(f"❌ 播放视频失败: {e}") return False def pause2DVideo(self, video_screen): """暂停2D视频""" try: - movie_texture = video_screen.getPythonTag("movie_texture") - - # 备用获取方法 - if not movie_texture: - # 尝试从frameTexture获取 - if hasattr(video_screen, 'frameTexture'): - movie_texture = video_screen.frameTexture - - if movie_texture and hasattr(movie_texture, 'stop'): # MovieTexture使用stop来暂停 - try: - movie_texture.stop() - print(f"⏸️ 2D视频已暂停: {video_screen.getName()}") - return True - except Exception as stop_error: - print(f"⚠️ 暂停2D视频时出错: {stop_error}") - return False - else: - print(f"⚠️ 2D视频屏幕没有可暂停的视频纹理: {video_screen.getName()}") - return False + # 在OpenCV中没有直接的暂停功能,我们通过停止线程来模拟暂停 + if video_screen.hasPythonTag("video_capture"): + cap = video_screen.getPythonTag("video_capture") + if cap: + cap.release() + video_screen.clearPythonTag("video_capture") + print("⏸️ 视频已暂停") + return True + return False except Exception as e: - print(f"❌ 暂停2D视频失败: {e}") + print(f"❌ 暂停视频失败: {e}") return False def stop2DVideo(self, video_screen): - """停止2D视频(回到开头)""" + """停止2D视频""" try: - movie_texture = video_screen.getPythonTag("movie_texture") + # 停止视频捕获 + if video_screen.hasPythonTag("video_capture"): + cap = video_screen.getPythonTag("video_capture") + if cap: + cap.release() + video_screen.clearPythonTag("video_capture") - # 备用获取方法 - if not movie_texture: - # 尝试从frameTexture获取 - if hasattr(video_screen, 'frameTexture'): - movie_texture = video_screen.frameTexture + # 清理纹理 + if video_screen.hasPythonTag("video_texture"): + video_screen.clearPythonTag("video_texture") - if movie_texture: - try: - if hasattr(movie_texture, 'stop'): - movie_texture.stop() - # 重置到开头 - if hasattr(movie_texture, 'setTime'): - movie_texture.setTime(0.0) - print(f"⏹️ 2D视频已停止: {video_screen.getName()}") - return True - except Exception as stop_error: - print(f"⚠️ 停止2D视频时出错: {stop_error}") - return False + # 清理视频路径标签 + video_screen.clearTag("video_path") + + print("⏹️ 视频已停止") + return True + except Exception as e: + print(f"❌ 停止视频失败: {e}") + return False + + def createSphericalVideo(self, pos=(0, 0, 0), radius=5.0, video_path=None): + """创建球形视频(360度视频)- 支持无初始视频文件""" + try: + from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter + from panda3d.core import Geom, GeomTriangles, GeomNode + from panda3d.core import TextureStage, Texture + import math + import os + + # 确保 pos 是有效的三维坐标元组 + if not isinstance(pos, (tuple, list)) or len(pos) != 3: + print(f"⚠️ 位置参数无效,使用默认值 (0, 0, 0),原始值: {pos}") + pos = (0, 0, 0) + else: + # 确保所有坐标都是数值类型 + try: + pos = (float(pos[0]), float(pos[1]), float(pos[2])) + except (ValueError, TypeError): + print(f"⚠️ 位置参数包含非数值,使用默认值 (0, 0, 0),原始值: {pos}") + pos = (0, 0, 0) + + # 确保 radius 是有效数值 + try: + radius = float(radius) + except (ValueError, TypeError): + print(f"⚠️ 半径参数无效,使用默认值 5.0,原始值: {radius}") + radius = 5.0 + + print(f"🌍 开始创建球形视频,位置: {pos}, 半径: {radius}, 视频路径: {video_path}") + + # 不再强制检查视频文件是否存在,允许创建空的球形视频 + if video_path and not os.path.exists(video_path): + print(f"⚠️ 视频文件不存在,将创建空的球形视频: {video_path}") + + # 获取树形控件 + tree_widget = self._get_tree_widget() + if not tree_widget: + print("❌ 无法访问树形控件") + return None + + # 获取目标父节点列表 + target_parents = tree_widget.get_target_parents_for_creation() + if not target_parents: + print("❌ 没有找到有效的父节点") + return None + + created_spherical_videos = [] + + # 为每个有效的父节点创建球形视频 + for parent_item, parent_node in target_parents: + try: + # 生成唯一名称 + sphere_name = f"SphericalVideo_{len(self.gui_elements)}" + + # 创建球形几何体 + sphere_geom = self._createSphereGeometry(radius, segments=32) + + # 创建几何节点 + sphere_node = GeomNode('sphere_video') + sphere_node.addGeom(sphere_geom) + + # 创建节点路径并挂载 + sphere_np = parent_node.attachNewNode(sphere_node) + sphere_np.setPos(*pos) # 现在 pos 已经是有效的元组 + sphere_np.setName(sphere_name) + + # 翻转法线,使视频在球体内部显示 + sphere_np.setTwoSided(True) + sphere_np.setBin('background', 0) # 确保在背景层 + sphere_np.setDepthWrite(False) # 不写入深度缓冲 + + # 设置初始颜色为占位符颜色 + sphere_np.setColor(0.1, 0.1, 0.3, 0.8) # 深蓝色占位符 + + # 如果提供了视频路径且文件存在,则加载视频纹理 + movie_texture = None + if video_path and os.path.exists(video_path): + try: + # 加载视频纹理 + movie_texture = self._loadMovieTexture(video_path) + if movie_texture: + # 设置视频纹理属性 + movie_texture.setWrapU(Texture.WM_clamp) + movie_texture.setWrapV(Texture.WM_clamp) + movie_texture.setMinfilter(Texture.FT_linear) + movie_texture.setMagfilter(Texture.FT_linear) + + # 为视频纹理创建专用的纹理阶段 + texture_stage = TextureStage("spherical_video") + texture_stage.setMode(TextureStage.MModulate) + + # 应用纹理到球体 + sphere_np.setTexture(texture_stage, movie_texture) + + # 设置为白色以正确显示视频 + sphere_np.setColor(1, 1, 1, 1) + + print(f"✅ 视频纹理已应用到球形视频: {video_path}") + + # 尝试自动播放 + try: + if hasattr(movie_texture, 'play'): + movie_texture.play() + print("▶️ 球形视频已开始播放") + except Exception as play_error: + print(f"⚠️ 球形视频自动播放失败: {play_error}") + else: + print(f"⚠️ 无法加载视频纹理: {video_path}") + # 保持占位符颜色 + except Exception as e: + print(f"❌ 加载视频纹理失败: {e}") + import traceback + traceback.print_exc() + # 保持占位符颜色 + else: + if video_path: + print(f"⚠️ 视频文件不存在: {video_path}") + else: + print("ℹ️ 未提供视频文件,创建空的球形视频") + + # 设置标签以便识别和管理 + sphere_np.setTag("gui_type", "spherical_video") + sphere_np.setTag("is_gui_element", "1") + sphere_np.setTag("video_path", video_path or "") + sphere_np.setTag("original_radius", str(radius)) + + # 保存视频纹理引用 + if movie_texture: + sphere_np.setPythonTag("movie_texture", movie_texture) + + # 添加到GUI元素列表 + self.gui_elements.append(sphere_np) + + print(f"✅ 为 {parent_item.text(0)} 创建球形视频成功: {sphere_name}") + + # 在Qt树形控件中添加对应节点 + qt_item = tree_widget.add_node_to_tree_widget(sphere_np, parent_item, "GUI_SPHERICAL_VIDEO") + if qt_item: + created_spherical_videos.append((sphere_np, qt_item)) + else: + created_spherical_videos.append((sphere_np, None)) + print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") + + except Exception as e: + print(f"❌ 为 {parent_item.text(0)} 创建球形视频失败: {str(e)}") + import traceback + traceback.print_exc() + continue + + # 处理创建结果 + if not created_spherical_videos: + print("❌ 没有成功创建任何球形视频") + return None + + # 选中最后创建的球形视频 + if created_spherical_videos: + last_sphere_np, last_qt_item = created_spherical_videos[-1] + if last_qt_item: + tree_widget.setCurrentItem(last_qt_item) + # 更新选择和属性面板 + tree_widget.update_selection_and_properties(last_sphere_np, last_qt_item) + + print(f"🎉 总共创建了 {len(created_spherical_videos)} 个球形视频") + + # 返回值处理 + if len(created_spherical_videos) == 1: + return created_spherical_videos[0][0] # 单个球形视频返回NodePath + else: + return [sphere_np for sphere_np, _ in created_spherical_videos] # 多个球形视频返回列表 + + except Exception as e: + print(f"❌ 创建球形视频过程失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def _createSphereGeometry(self, radius=5.0, segments=32): + """创建球形几何体""" + try: + from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter + from panda3d.core import Geom, GeomTriangles + import math + + # 创建顶点数据 + format = GeomVertexFormat.getV3n3t2() + vdata = GeomVertexData('sphere', format, Geom.UHStatic) + + vertex = GeomVertexWriter(vdata, 'vertex') + normal = GeomVertexWriter(vdata, 'normal') + texcoord = GeomVertexWriter(vdata, 'texcoord') + + # 生成球体顶点 + vertices = [] + for i in range(segments + 1): + phi = math.pi * i / segments # 从0到π + for j in range(segments * 2 + 1): + theta = 2 * math.pi * j / (segments * 2) # 从0到2π + + x = radius * math.sin(phi) * math.cos(theta) + y = radius * math.cos(phi) + z = radius * math.sin(phi) * math.sin(theta) + + # 纹理坐标 + u = j / (segments * 2) + v = i / segments + + # 修复:将坐标作为一个元组添加到列表中 + vertices.append((x, y, z, u, v)) + + # 添加顶点数据 + for vert in vertices: + vertex.addData3f(vert[0], vert[1], vert[2]) + # 法线(标准化顶点位置) + length = math.sqrt(vert[0] ** 2 + vert[1] ** 2 + vert[2] ** 2) + if length > 0: + normal.addData3f(vert[0] / length, vert[1] / length, vert[2] / length) + else: + normal.addData3f(0, 1, 0) + texcoord.addData2f(vert[3], vert[4]) + + # 创建几何体 + geom = Geom(vdata) + prim = GeomTriangles(Geom.UHStatic) + + # 生成三角形面 + for i in range(segments): + for j in range(segments * 2): + # 计算顶点索引 + v1 = i * (segments * 2 + 1) + j + v2 = (i + 1) * (segments * 2 + 1) + j + v3 = (i + 1) * (segments * 2 + 1) + (j + 1) + v4 = i * (segments * 2 + 1) + (j + 1) + + # 添加两个三角形 + prim.addVertices(v1, v2, v3) + prim.addVertices(v1, v3, v4) + + prim.closePrimitive() + geom.addPrimitive(prim) + + return geom + + except Exception as e: + print(f"❌ 创建球形几何体失败: {e}") + import traceback + traceback.print_exc() + raise + + def playSphericalVideo(self,spherical_video_node): + try: + if not spherical_video_node: + return False + texture = spherical_video_node.getTexture() + if texture and hasattr(texture,'play'): + texture.play() + return True + else: + return False + except Exception as e: + return False + + def pauseSphericalVideo(self,spherical_video_node): + try: + if not spherical_video_node: + return False + texture = spherical_video_node.getTexture() + if texture and hasattr(texture,'stop'): + texture.stop() + return True + else: + return False + except Exception as e: + return False + + def setSphericalVideoTime(self,spherical_video_node,time_seconds): + try: + if not spherical_video_node: + return False + texture = spherical_video_node.getTexture() + if texture and hasattr(texture,'set_time'): + texture.setTime(time_seconds) + return True else: - print(f"⚠️ 2D视频屏幕没有视频纹理: {video_screen.getName()}") return False except Exception as e: - print(f"❌ 停止2D视频失败: {e}") return False def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"): @@ -2570,13 +2825,13 @@ class GUIManager: transform_layout.addWidget(z_label, 0, 2) xPos = QDoubleSpinBox() - xPos.setRange(-50, 50) + xPos.setRange(-1000, 1000) xPos.setValue(logical_x) xPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "x", v)) transform_layout.addWidget(xPos, 1, 1) zPos = QDoubleSpinBox() - zPos.setRange(-50, 50) + zPos.setRange(-1000, 1000) zPos.setValue(logical_z) zPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "z", v)) transform_layout.addWidget(zPos, 1, 2) @@ -2930,19 +3185,13 @@ class GUIManager: if axis == "x": # 转换逻辑坐标到屏幕坐标 screen_x = value * 0.1 - # 应用边界约束 - constrained_pos = self.constrain2DPosition(gui_element, new_x=screen_x) - new_pos = (constrained_pos.getX(), constrained_pos.getY(), constrained_pos.getZ()) + new_pos = (screen_x,current_pos[1],current_pos[2]) elif axis == "z": screen_z = value * 0.1 - # 应用边界约束 - constrained_pos = self.constrain2DPosition(gui_element, new_z=screen_z) - new_pos = (constrained_pos.getX(), constrained_pos.getY(), constrained_pos.getZ()) + new_pos = (current_pos[0],current_pos[1],screen_z) else: return False - gui_element.setPos(*new_pos) - print(f"✓ 更新2D GUI元素位置: {axis}={value} (约束后位置: {new_pos})") return True else: print(f"✗ 不支持的GUI类型进行2D位置编辑: {gui_type}") diff --git a/main.py b/main.py index b930bef5..54ed988f 100644 --- a/main.py +++ b/main.py @@ -242,6 +242,19 @@ class MyWorld(CoreWorld): """创建2D视频屏幕""" return self.gui_manager.createGUI2DVideoScreen(pos,size,video_path) + def createSphericalVideo(self,pos=(0,0,0),radius=5.0,video_path=None): + """创建360度视频""" + return self.gui_manager.createSphericalVideo(pos,radius,video_path) + + def playSphericalVideo(self,spherical_video_node): + return self.gui_manager.playSphericalVideo(spherical_video_node) + + def pauseSphericalVideo(self,spherical_video_node): + return self.gui_manager.pauseSphericalVideo(spherical_video_node) + + def setSphericalVideoTime(self,spherical_video_node,time_seconds): + return self.gui_manager.setSphericalVideoTime(spherical_video_node,time_seconds) + def createSpotLight(self,pos=(0,0,5)): """创建聚光灯""" return self.scene_manager.createSpotLight(pos) diff --git a/scene/scene_manager.py b/scene/scene_manager.py index 5ba53bf0..064394e1 100644 --- a/scene/scene_manager.py +++ b/scene/scene_manager.py @@ -89,16 +89,8 @@ class SceneManager: # ==================== 模型导入和处理 ==================== def importModel(self, filepath, apply_unit_conversion=False, normalize_scales=True, auto_convert_to_glb=True): - """导入模型到场景 - 只在根节点下创建 - - Args: - filepath: 模型文件路径 - apply_unit_conversion: 是否应用单位转换(主要针对FBX文件) - normalize_scales: 是否标准化子节点缩放(推荐开启) - auto_convert_to_glb: 是否自动将非GLB格式转换为GLB以获得更好的动画支持 - """ + """导入模型到场景 - 增强错误处理""" try: - # 预处理文件路径和转换 filepath = util.normalize_model_path(filepath) original_filepath = filepath @@ -107,34 +99,95 @@ class SceneManager: print(f"缩放标准化: {'开启' if normalize_scales else '关闭'}") print(f"自动转换GLB: {'开启' if auto_convert_to_glb else '关闭'}") + # 检查文件是否存在 + if not os.path.exists(filepath): + print(f"❌ 模型文件不存在: {filepath}") + return None + + # 检查文件大小 + file_size = os.path.getsize(filepath) + if file_size == 0: + print(f"❌ 模型文件为空: {filepath}") + return None + + print(f"文件大小: {file_size} 字节") + # 检查是否需要转换为GLB if auto_convert_to_glb and self._shouldConvertToGLB(filepath): print(f"🔄 检测到需要转换的格式,尝试转换为GLB...") converted_path = self._convertToGLBWithProgress(filepath) - if converted_path: + if converted_path and os.path.exists(converted_path) and os.path.getsize(converted_path) > 0: print(f"✅ 转换成功: {converted_path}") filepath = converted_path # 显示成功消息 try: from PyQt5.QtWidgets import QMessageBox original_ext = os.path.splitext(original_filepath)[1].upper() - QMessageBox.information(None, "转换成功", - f"已将 {original_ext} 格式自动转换为 GLB 格式\n以获得更好的动画支持!") + QMessageBox.information(None, "转换成功", + f"已将 {original_ext} 格式自动转换为 GLB 格式\n以获得更好的动画支持!") except: pass else: - print(f"⚠️ 转换失败,使用原始文件") + print(f"⚠️ 转换失败或文件无效,使用原始文件") + # 验证原始文件是否有效 + if not os.path.exists(original_filepath) or os.path.getsize(original_filepath) == 0: + print(f"❌ 原始文件也无效: {original_filepath}") + return None + filepath = original_filepath # 直接在render根节点下创建模型 print("--- 在根节点下创建模型实例 ---") - # 加载模型 - print("直接从文件加载模型...") - model = self.world.loader.loadModel(filepath) + # 添加模型加载前的安全检查 + print("准备加载模型...") + + # 使用try-except包装模型加载过程 + model = None + max_retry_attempts = 3 # 最大重试次数 + + for attempt in range(max_retry_attempts): + try: + print(f"直接从文件加载模型... (尝试 {attempt + 1}/{max_retry_attempts})") + model = self.world.loader.loadModel(filepath) + + if model: + print(f"✅ 模型加载成功: {model}") + break + else: + print(f"⚠️ 加载模型返回空对象 (尝试 {attempt + 1})") + if attempt < max_retry_attempts - 1: + import time + time.sleep(0.1) # 短暂延迟后重试 + + except AssertionError as ae: + error_msg = str(ae) + if "mismatched number of frames" in error_msg: + print(f"⚠️ 动画帧数不匹配错误 (尝试 {attempt + 1}): {error_msg}") + # 清理模型池并重试 + if attempt < max_retry_attempts - 1: + self._clearModelCache(filepath) + import time + time.sleep(0.2) # 增加延迟后重试 + else: + # 最后一次尝试失败,尝试无动画加载 + print("🔄 尝试无动画加载...") + model = self._loadModelWithoutAnimations(filepath) + else: + raise ae # 其他断言错误直接抛出 + + except Exception as load_error: + print(f"❌ 模型加载过程出错 (尝试 {attempt + 1}): {str(load_error)}") + if attempt < max_retry_attempts - 1: + import time + time.sleep(0.1) # 短暂延迟后重试 + else: + import traceback + traceback.print_exc() + if not model: - print("❌ 加载模型失败") + print("❌ 所有加载尝试都失败了") return None - + # 设置模型名称 model_name = os.path.basename(filepath) model.setName(model_name) @@ -153,17 +206,6 @@ class SceneManager: if filepath != original_filepath: model.setTag("converted_from", os.path.splitext(original_filepath)[1]) model.setTag("converted_to_glb", "true") - - # # 应用处理选项 - # if apply_unit_conversion and filepath.lower().endswith('.fbx'): - # print("应用FBX单位转换(厘米到米)...") - # self._applyUnitConversion(model, 0.01) - # model.setTag("unit_conversion_applied", "true") - # - # if normalize_scales and filepath.lower().endswith('.fbx'): - # print("标准化FBX模型缩放层级...") - # self._normalizeModelScales(model) - # model.setTag("scale_normalization_applied", "true") # 应用处理选项 # 对于GLB文件,通常不需要单位转换,因为它们已经是标准单位 @@ -180,11 +222,11 @@ class SceneManager: # 调整模型位置到地面 self._adjustModelToGround(model) - + # 创建并设置基础材质 print("设置材质...") self._applyMaterialsToModel(model) - + # 设置碰撞检测(重要!用于选择功能) print("设置碰撞检测...") self.setupCollision(model) @@ -228,6 +270,49 @@ class SceneManager: traceback.print_exc() return None + def _clearModelCache(self, filepath): + """清理模型缓存以解决加载问题""" + try: + # 清理特定文件的缓存 + filename = Filename.fromOsSpecific(filepath) + if ModelPool.hasModel(filename): + ModelPool.releaseModel(filename) + print(f"🧹 已清理模型缓存: {filepath}") + + # 清理所有动画缓存 + from panda3d.core import AnimBundleNode + AnimBundleNode.clearAllBundleCache() + print("🧹 已清理动画缓存") + + except Exception as e: + print(f"⚠️ 清理缓存时出错: {e}") + + def _loadModelWithoutAnimations(self, filepath): + """尝试无动画加载模型""" + try: + from panda3d.core import LoaderOptions + + print("🔄 尝试无动画加载模型...") + + # 创建不加载动画的加载选项 + loader_options = LoaderOptions() + loader_options.setFlags(LoaderOptions.LF_no_cache) # 不使用缓存 + loader_options.setAllowInstance(False) # 不允许实例化 + + # 尝试加载不带动画的模型 + model = self.world.loader.loadModel(filepath, loaderOptions=loader_options) + + if model: + print("✅ 无动画加载成功") + return model + else: + print("❌ 无动画加载也失败了") + return None + + except Exception as e: + print(f"❌ 无动画加载失败: {e}") + return None + # def importAnimatedFBX(self, filepath, scale=0.01, auto_play=True): # """导入带动画的FBX模型(使用assimp) # @@ -289,110 +374,132 @@ class SceneManager: def _applyMaterialsToModel(self, model): """递归应用材质到模型的所有GeomNode""" + def apply_material(node_path, depth=0): indent = " " * depth - print(f"{indent}处理节点: {node_path.getName()}") - print(f"{indent}节点类型: {node_path.node().__class__.__name__}") + try: + print(f"{indent}处理节点: {node_path.getName()}") + print(f"{indent}节点类型: {node_path.node().__class__.__name__}") - if isinstance(node_path.node(), GeomNode): - print(f"{indent}发现GeomNode,处理材质") - geom_node = node_path.node() + if isinstance(node_path.node(), GeomNode): + print(f"{indent}发现GeomNode,处理材质") + geom_node = node_path.node() - # 检查所有几何体的状态 - has_color = False - color = None + # 检查所有几何体的状态 + has_color = False + color = None - # 首先检查节点自身的状态 - node_state = node_path.getState() - if node_state.hasAttrib(MaterialAttrib.getClassType()): - mat_attrib = node_state.getAttrib(MaterialAttrib.getClassType()) - node_material = mat_attrib.getMaterial() - if node_material and node_material.hasDiffuse(): - color = node_material.getDiffuse() - has_color = True - print(f"{indent}从节点材质获取颜色: {color}") + # 首先检查节点自身的状态 + node_state = node_path.getState() + if node_state.hasAttrib(MaterialAttrib.getClassType()): + mat_attrib = node_state.getAttrib(MaterialAttrib.getClassType()) + node_material = mat_attrib.getMaterial() + if node_material and node_material.hasDiffuse(): + color = node_material.getDiffuse() + has_color = True + print(f"{indent}从节点材质获取颜色: {color}") - # 检查FBX特有的属性 - for tag_key in node_path.getTagKeys(): - print(f"{indent}发现标签: {tag_key}") - if "color" in tag_key.lower() or "diffuse" in tag_key.lower(): - tag_value = node_path.getTag(tag_key) - print(f"{indent}颜色相关标签: {tag_key} = {tag_value}") + # 检查FBX特有的属性 + for tag_key in node_path.getTagKeys(): + print(f"{indent}发现标签: {tag_key}") + if "color" in tag_key.lower() or "diffuse" in tag_key.lower(): + tag_value = node_path.getTag(tag_key) + print(f"{indent}颜色相关标签: {tag_key} = {tag_value}") - # 如果还没找到颜色,检查几何体 - if not has_color: - for i in range(geom_node.getNumGeoms()): - geom = geom_node.getGeom(i) - state = geom_node.getGeomState(i) + # 如果还没找到颜色,检查几何体 + if not has_color: + for i in range(geom_node.getNumGeoms()): + try: + geom = geom_node.getGeom(i) + state = geom_node.getGeomState(i) - # 检查顶点颜色 - vdata = geom.getVertexData() - format = vdata.getFormat() - for j in range(format.getNumColumns()): - column = format.getColumn(j) - # InternalName对象需要使用getName()转换为字符串 - column_name = column.getName().getName() - if "color" in column_name.lower(): - print(f"{indent}发现顶点颜色数据: {column_name}") - # 这里可以读取顶点颜色,但先记录发现 + # 检查顶点颜色 + vdata = geom.getVertexData() + if vdata: + format = vdata.getFormat() + if format: + for j in range(format.getNumColumns()): + try: + column = format.getColumn(j) + # InternalName对象需要使用getName()转换为字符串 + column_name = column.getName().getName() + if "color" in column_name.lower(): + print(f"{indent}发现顶点颜色数据: {column_name}") + except Exception: + continue - # 检查材质属性 - if state.hasAttrib(MaterialAttrib.getClassType()): - mat_attrib = state.getAttrib(MaterialAttrib.getClassType()) - orig_material = mat_attrib.getMaterial() - if orig_material: - if orig_material.hasBaseColor(): - color = orig_material.getBaseColor() - has_color = True - print(f"{indent}从基础颜色获取: {color}") - break - elif orig_material.hasDiffuse(): - color = orig_material.getDiffuse() - has_color = True - print(f"{indent}从漫反射颜色获取: {color}") - break + # 检查材质属性 + if state.hasAttrib(MaterialAttrib.getClassType()): + mat_attrib = state.getAttrib(MaterialAttrib.getClassType()) + orig_material = mat_attrib.getMaterial() + if orig_material: + if orig_material.hasBaseColor(): + color = orig_material.getBaseColor() + has_color = True + print(f"{indent}从基础颜色获取: {color}") + break + elif orig_material.hasDiffuse(): + color = orig_material.getDiffuse() + has_color = True + print(f"{indent}从漫反射颜色获取: {color}") + break - # 检查颜色属性 - if not has_color and state.hasAttrib(ColorAttrib.getClassType()): - color_attrib = state.getAttrib(ColorAttrib.getClassType()) - if not color_attrib.isOff(): - color = color_attrib.getColor() - has_color = True - print(f"{indent}从颜色属性获取: {color}") - break + # 检查颜色属性 + if not has_color and state.hasAttrib(ColorAttrib.getClassType()): + color_attrib = state.getAttrib(ColorAttrib.getClassType()) + if not color_attrib.isOff(): + color = color_attrib.getColor() + has_color = True + print(f"{indent}从颜色属性获取: {color}") + break + except Exception as geom_error: + print(f"{indent}处理几何体 {i} 时出错: {geom_error}") + continue - # 创建新材质 - material = Material() - if has_color: - print(f"{indent}应用找到的颜色: {color}") - material.setDiffuse(color) - material.setBaseColor(color) # 同时设置基础颜色 - node_path.setColor(color) - else: - print(f"{indent}使用默认颜色") - material.setDiffuse((0.8, 0.8, 0.8, 1.0)) + # 创建新材质 + material = Material() + if has_color: + print(f"{indent}应用找到的颜色: {color}") + try: + material.setDiffuse(color) + material.setBaseColor(color) # 同时设置基础颜色 + node_path.setColor(color) + except Exception as color_error: + print(f"{indent}设置颜色时出错: {color_error}") + material.setDiffuse((0.8, 0.8, 0.8, 1.0)) + else: + print(f"{indent}使用默认颜色") + material.setDiffuse((0.8, 0.8, 0.8, 1.0)) - # 设置其他材质属性 - material.setAmbient((0.2, 0.2, 0.2, 1.0)) - material.setSpecular((0.5, 0.5, 0.5, 1.0)) - material.setShininess(32.0) - #material.set_metallic(1) - #material.set_roughness(0) + # 设置其他材质属性 + material.setAmbient((0.2, 0.2, 0.2, 1.0)) + material.setSpecular((0.5, 0.5, 0.5, 1.0)) + material.setShininess(32.0) - # 应用材质 - node_path.setMaterial(material) - print(f"{indent}几何体数量: {geom_node.getNumGeoms()}") + # 应用材质 + node_path.setMaterial(material) + print(f"{indent}几何体数量: {geom_node.getNumGeoms()}") + + except Exception as node_error: + print(f"{indent}处理节点 {node_path.getName()} 时出错: {node_error}") # 递归处理子节点 child_count = node_path.getNumChildren() print(f"{indent}子节点数量: {child_count}") for i in range(child_count): - child = node_path.getChild(i) - apply_material(child, depth + 1) + try: + child = node_path.getChild(i) + apply_material(child, depth + 1) + except Exception as child_error: + print(f"{indent}处理子节点 {i} 时出错: {child_error}") + continue # 应用材质 print("\n开始递归应用材质...") - apply_material(model) + try: + apply_material(model) + except Exception as e: + print(f"应用材质时出错: {e}") print("=== 材质设置完成 ===\n") def _adjustModelToGround(self, model): diff --git a/ui/main_window.py b/ui/main_window.py index f9891d46..f83115e9 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -429,7 +429,7 @@ class MainWindow(QMainWindow): self.createImageAction.triggered.connect(lambda: self.world.createGUI2DImage()) self.createVideoScreen.triggered.connect(self.world.createVideoScreen) self.create2DVideoScreen.triggered.connect(self.world.create2DVideoScreen) - # self.createSphericalVideo.triggered.connect(self.world.createSphericalVideo) + self.createSphericalVideo.triggered.connect(self.world.createSphericalVideo) self.createVirtualScreenAction.triggered.connect(lambda: self.world.createGUIVirtualScreen()) self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight()) self.createPointLightAction.triggered.connect(lambda :self.world.createPointLight()) @@ -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 7355478b..0501cb96 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -24,6 +24,7 @@ class PropertyPanelManager: self.world = world self._propertyLayout = None self._actor_cache={} + self._spherical_video_controls = {} # 初始化地形编辑参数 if not hasattr(self.world, 'terrain_edit_radius'): @@ -1325,13 +1326,13 @@ class PropertyPanelManager: transform_layout.addWidget(z_label, 0, 2) self.pos_x = QDoubleSpinBox() - self.pos_x.setRange(-100, 100) + self.pos_x.setRange(-1000, 1000) self.pos_x.setValue(logical_x) self.pos_x.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "x", v)) transform_layout.addWidget(self.pos_x, 1, 1) self.pos_z = QDoubleSpinBox() - self.pos_z.setRange(-100, 100) + self.pos_z.setRange(-1000, 1000) self.pos_z.setValue(logical_z) self.pos_z.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "z", v)) transform_layout.addWidget(self.pos_z, 1, 2) @@ -1610,6 +1611,8 @@ class PropertyPanelManager: self._addVideoScreenProperties(gui_element) if gui_type == "2d_video_screen": self._add2DVideoScreenProperties(gui_element) + if gui_type == "spherical_video": + self._addSphericalVideoProperties(gui_element) self._propertyLayout.addStretch() @@ -1620,11 +1623,314 @@ class PropertyPanelManager: if propertyWidget: propertyWidget.update() + def _addSphericalVideoProperties(self, spherical_video): + """为球形视频添加属性控制面板""" + try: + from PyQt5.QtWidgets import (QGroupBox, QGridLayout, QPushButton, QLabel, + QDoubleSpinBox, QFileDialog, QHBoxLayout, QSlider) + from PyQt5.QtCore import Qt + import os + + # 视频控制组 + video_group = QGroupBox("视频控制") + video_layout = QGridLayout() + + # 播放控制按钮 + control_layout = QHBoxLayout() + + self.play_button = QPushButton("▶️ 播放") + self.play_button.clicked.connect(lambda: self._toggleSphericalVideoPlayback(spherical_video)) + + self.stop_button = QPushButton("⏹️ 停止") + self.stop_button.clicked.connect(lambda: self._stopSphericalVideo(spherical_video)) + + control_layout.addWidget(self.play_button) + control_layout.addWidget(self.stop_button) + video_layout.addLayout(control_layout, 0, 0, 1, 2) + + # 视频进度控制 + progress_layout = QHBoxLayout() + self.progress_slider = QSlider(Qt.Horizontal) + self.progress_slider.setRange(0, 100) + self.progress_slider.setValue(0) + self.progress_slider.sliderMoved.connect(lambda value: self._seekSphericalVideo(spherical_video, value)) + + self.time_label = QLabel("00:00 / 00:00") + self.time_label.setStyleSheet("font-size: 10px; color: gray;") + + progress_layout.addWidget(self.progress_slider) + progress_layout.addWidget(self.time_label) + video_layout.addLayout(progress_layout, 1, 0, 1, 2) + + # 视频文件选择 + file_layout = QHBoxLayout() + self.select_video_button = QPushButton("选择视频文件") + self.select_video_button.clicked.connect(lambda: self._selectSphericalVideoFile(spherical_video)) + + # 显示当前视频文件 + current_video = spherical_video.getTag("video_path") if spherical_video.hasTag("video_path") else "" + self.video_file_label = QLabel(os.path.basename(current_video) if current_video else "未选择视频") + self.video_file_label.setStyleSheet("font-size: 9px; color: gray;") + self.video_file_label.setWordWrap(True) + + file_layout.addWidget(self.select_video_button) + file_layout.addWidget(self.video_file_label) + video_layout.addLayout(file_layout, 2, 0, 1, 2) + + video_group.setLayout(video_layout) + self._propertyLayout.addWidget(video_group) + + # 球体属性组 + sphere_group = QGroupBox("球体属性") + sphere_layout = QGridLayout() + + # 半径控制 + sphere_layout.addWidget(QLabel("半径:"), 0, 0) + radius_spin = QDoubleSpinBox() + radius_spin.setRange(0.1, 100) + radius_spin.setSingleStep(0.1) + current_radius = float(spherical_video.getTag("original_radius") or "5.0") + radius_spin.setValue(current_radius) + radius_spin.valueChanged.connect(lambda value: self._updateSphericalVideoRadius(spherical_video, value)) + sphere_layout.addWidget(radius_spin, 0, 1) + + sphere_group.setLayout(sphere_layout) + self._propertyLayout.addWidget(sphere_group) + + # 存储控件引用 + self._spherical_video_controls = { + 'play_button': self.play_button, + 'stop_button': self.stop_button, + 'progress_slider': self.progress_slider, + 'time_label': self.time_label, + 'select_button': self.select_video_button, + 'file_label': self.video_file_label + } + + print("✅ 球形视频属性面板已添加") + + except Exception as e: + print(f"❌ 添加球形视频属性面板失败: {e}") + import traceback + traceback.print_exc() + + def _toggleSphericalVideoPlayback(self, spherical_video): + """切换球形视频播放状态""" + try: + # 获取视频纹理 + movie_texture = spherical_video.getPythonTag("movie_texture") + if not movie_texture: + # 尝试从节点获取纹理 + texture = spherical_video.getTexture() + if texture: + movie_texture = texture + + if not movie_texture: + print("❌ 未找到视频纹理") + return + + # 检查当前播放状态 + if not hasattr(self, '_video_playing'): + self._video_playing = {} + + video_id = id(spherical_video) + is_playing = self._video_playing.get(video_id, False) + + if is_playing: + # 暂停视频 + if hasattr(movie_texture, 'stop'): + movie_texture.stop() + if hasattr(self, '_spherical_video_controls') and 'play_button' in self._spherical_video_controls: + play_button = self._spherical_video_controls['play_button'] + play_button.setText("▶️ 播放") + print("⏸️ 球形视频已暂停") + else: + # 播放视频 + if hasattr(movie_texture, 'play'): + movie_texture.play() + if hasattr(self, '_spherical_video_controls') and 'play_button' in self._spherical_video_controls: + play_button = self._spherical_video_controls['play_button'] + play_button.setText("⏸️ 暂停") + print("▶️ 球形视频开始播放") + + # 更新播放状态 + self._video_playing[video_id] = not is_playing + + except Exception as e: + print(f"❌ 切换球形视频播放状态失败: {e}") + + def _stopSphericalVideo(self, spherical_video): + """停止球形视频""" + try: + # 获取视频纹理 + movie_texture = spherical_video.getPythonTag("movie_texture") + if not movie_texture: + # 尝试从节点获取纹理 + texture = spherical_video.getTexture() + if texture: + movie_texture = texture + + if movie_texture: + if hasattr(movie_texture, 'stop'): + movie_texture.stop() + # 重置到开头 + if hasattr(movie_texture, 'setTime'): + movie_texture.setTime(0.0) + + # 更新播放按钮 + if hasattr(self, '_spherical_video_controls') and 'play_button' in self._spherical_video_controls: + play_button = self._spherical_video_controls['play_button'] + play_button.setText("▶️ 播放") + + # 更新播放状态 + if hasattr(self, '_video_playing'): + video_id = id(spherical_video) + self._video_playing[video_id] = False + + print("⏹️ 球形视频已停止") + else: + print("❌ 视频纹理不支持停止操作") + else: + print("❌ 未找到视频纹理") + + except Exception as e: + print(f"❌ 停止球形视频失败: {e}") + + def _seekSphericalVideo(self, spherical_video, slider_value): + """跳转到指定时间位置""" + try: + # 获取视频纹理 + movie_texture = spherical_video.getPythonTag("movie_texture") + if not movie_texture: + # 尝试从节点获取纹理 + texture = spherical_video.getTexture() + if texture: + movie_texture = texture + + if movie_texture and hasattr(movie_texture, 'setTime'): + # 假设视频总时长为100秒(实际应该根据视频时长计算) + time_seconds = slider_value + movie_texture.setTime(time_seconds) + print(f"⏰ 跳转到时间: {time_seconds}秒") + + # 更新时间标签 + if hasattr(self, '_spherical_video_controls') and 'time_label' in self._spherical_video_controls: + time_label = self._spherical_video_controls['time_label'] + current_time = f"{int(time_seconds // 60):02d}:{int(time_seconds % 60):02d}" + total_time = "01:40" # 假设总时长100秒 + time_label.setText(f"{current_time} / {total_time}") + + else: + print("❌ 视频纹理不支持时间控制") + + except Exception as e: + print(f"❌ 球形视频跳转失败: {e}") + + def _updateSphericalVideoRadius(self, spherical_video, new_radius): + """更新球形视频半径""" + try: + from panda3d.core import GeomNode + + # 移除旧的几何体 + spherical_video.node().removeAllGeoms() + + # 创建新的球形几何体 + new_geom = self.world.gui_manager._createSphereGeometry(new_radius, segments=32) + spherical_video.node().addGeom(new_geom) + + # 更新标签 + spherical_video.setTag("original_radius", str(new_radius)) + + print(f"🔄 球形视频半径已更新为: {new_radius}") + + except Exception as e: + print(f"❌ 更新球形视频半径失败: {e}") + + def loadSphericalVideoFile(self, spherical_video, video_path): + """为球形视频加载新的视频文件""" + try: + from panda3d.core import TextureStage, Texture + import os + + if not os.path.exists(video_path): + print(f"❌ 球形视频文件不存在: {video_path}") + return False + + # 加载新的视频纹理 + movie_texture = self.world.gui_manager._loadMovieTexture(video_path) + if movie_texture: + # 清除现有的纹理 + spherical_video.clearTexture() + + # 设置视频纹理属性 + movie_texture.setWrapU(Texture.WM_clamp) + movie_texture.setWrapV(Texture.WM_clamp) + movie_texture.setMinfilter(Texture.FT_linear) + movie_texture.setMagfilter(Texture.FT_linear) + + # 为视频纹理创建专用的纹理阶段 + texture_stage = TextureStage("spherical_video") + texture_stage.setMode(TextureStage.MModulate) + + # 应用纹理到球体 + spherical_video.setTexture(texture_stage, movie_texture) + + # 保存视频纹理引用 + spherical_video.setPythonTag("movie_texture", movie_texture) + spherical_video.setTag("video_path", video_path) + + # 设置为白色以正确显示视频 + spherical_video.setColor(1, 1, 1, 1) + + print(f"✅ 成功加载新球形视频: {video_path}") + return True + else: + print(f"❌ 无法加载球形视频文件: {video_path}") + return False + + except Exception as e: + print(f"❌ 加载球形视频文件失败: {e}") + import traceback + traceback.print_exc() + return False + + def _selectSphericalVideoFile(self, spherical_video): + """选择球形视频文件""" + try: + from PyQt5.QtWidgets import QFileDialog + import os + + # 打开文件选择对话框 + file_path, _ = QFileDialog.getOpenFileName( + None, + "选择360度视频文件", + "", + "视频文件 (*.mp4 *.avi *.mov *.wmv *.flv *.mkv *.webm);;所有文件 (*)" + ) + + if file_path and os.path.exists(file_path): + # 加载新视频文件 - 修复方法调用 + success = self.loadSphericalVideoFile(spherical_video, file_path) + if success: + # 更新文件标签显示 + if hasattr(self, '_spherical_video_controls') and 'file_label' in self._spherical_video_controls: + file_label = self._spherical_video_controls['file_label'] + file_label.setText(os.path.basename(file_path)) + print(f"✅ 球形视频文件已更新为: {file_path}") + else: + print(f"❌ 无法加载球形视频文件: {file_path}") + else: + print("❌ 未选择有效的视频文件") + + except Exception as e: + print(f"❌ 选择球形视频文件失败: {e}") + import traceback + traceback.print_exc() def _add2DVideoScreenProperties(self, video_screen): """为2D视频屏幕添加属性控制面板""" try: from PyQt5.QtWidgets import (QGroupBox, QGridLayout, QPushButton, QLabel, - QFileDialog, QHBoxLayout) + QFileDialog, QHBoxLayout, QLineEdit) import os # 视频信息组 @@ -1632,19 +1938,40 @@ class PropertyPanelManager: video_info_layout = QGridLayout() # 显示当前视频文件路径 + # 在显示视频信息时区分本地文件和网络URL video_path = video_screen.getTag("video_path") - if video_path and 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) + if video_path: + 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.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.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("无视频文件" if not video_path else "文件不存在") + status_label = QLabel("无视频文件") status_label.setStyleSheet("color: red;") video_info_layout.addWidget(status_label, 0, 1) - video_screen.setBin("fixed",0) + + video_screen.setBin("fixed", 0) video_info_group.setLayout(video_info_layout) self._propertyLayout.addWidget(video_info_group) @@ -1655,17 +1982,17 @@ class PropertyPanelManager: # 播放按钮 play_btn = QPushButton("▶️ 播放") - play_btn.clicked.connect(lambda: self.world.gui_manager.play2DVideo(video_screen)) + play_btn.clicked.connect(lambda: self.play2DVideo(video_screen)) video_control_layout.addWidget(play_btn) # 暂停按钮 pause_btn = QPushButton("⏸️ 暂停") - pause_btn.clicked.connect(lambda: self.world.gui_manager.pause2DVideo(video_screen)) + pause_btn.clicked.connect(lambda: self.pause2DVideo(video_screen)) video_control_layout.addWidget(pause_btn) # 停止按钮 stop_btn = QPushButton("⏹️ 停止") - stop_btn.clicked.connect(lambda: self.world.gui_manager.stop2DVideo(video_screen)) + stop_btn.clicked.connect(lambda: self._stopVideo(video_screen)) video_control_layout.addWidget(stop_btn) video_control_group.setLayout(video_control_layout) @@ -1676,9 +2003,295 @@ class PropertyPanelManager: 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") + 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") + # 修复:直接调用 _loadVideoFromURL 方法并传递 video_screen 和 URL 文本 + load_url_btn.clicked.connect(lambda: self._loadVideoFromURL(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"添加2D视频屏幕属性失败: {e}") + def _loadVideoFromURL(self, video_screen, url): + """从URL加载视频流并在2D视频屏幕上显示(使用MovieTexture)""" + if not url: + print("❌ URL不能为空") + return False + + try: + # 停止之前可能正在播放的视频 + self._stopVideo(video_screen) + + # 检查是本地文件还是网络URL + if url.startswith("http://") or url.startswith("https://"): + # 对于网络流,我们仍然使用OpenCV方式 + return self._loadVideoFromURLWithOpenCV(video_screen, url) + else: + # 对于本地文件,使用与_loadNew2DVideo相同的方式 + return self.world.gui_manager.load2DVideoFile(video_screen, url) + + except Exception as e: + print(f"❌ 加载视频失败: {e}") + import traceback + traceback.print_exc() + return False + + def _loadVideoFromURLWithOpenCV(self, video_screen, url): + """使用OpenCV从URL加载视频流并在2D视频屏幕上显示(稳定版)""" + try: + import cv2 + import threading + from panda3d.core import Texture, PNMImage + import time + + # 停止之前可能正在播放的视频 + self._stopVideo(video_screen) + + # 使用 OpenCV 打开视频流 + cap = cv2.VideoCapture(url) + if not cap.isOpened(): + print(f"❌ 无法打开视频流: {url}") + return False + + # 优化视频参数设置 + cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 减少缓冲以降低延迟 + cap.set(cv2.CAP_PROP_FPS, 20) # 设置合理的帧率 + + # 创建纹理对象 + texture = Texture("video_texture") + texture.setMagfilter(Texture.FTLinear) + texture.setMinfilter(Texture.FTLinear) + texture.setWrapU(Texture.WMClamp) + texture.setWrapV(Texture.WMClamp) + + # 保存视频信息 + video_info = { + 'capture': cap, + 'texture': texture, + 'playing': True, + 'target_fps': 60, + 'last_frame_time': time.time() + } + + # 应用纹理到视频屏幕 + video_screen.setTexture(texture, 1) + video_screen.setPythonTag("video_info", video_info) + video_screen.setTag("video_path", url) + + # 启动视频播放线程 + def update_video_texture(): + frame_skip_counter = 0 + #frame_skip_rate = 1 # 每处理1帧就跳过2帧 + + while True: + try: + current_time = time.time() + + # 检查是否应该继续播放 + 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'] + 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_height, frame_width = frame.shape[:2] + if frame_width > 256: # 限制最大宽度 + scale = 256.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) + + 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 + + print("视频播放线程结束") + + # 在单独线程中处理视频流 + thread = threading.Thread(target=update_video_texture, daemon=True) + thread.start() + + print(f"✅ 开始在2D视频屏幕上播放网络视频流: {url}") + return True + + except ImportError: + print("❌ 缺少必要的库,请安装: pip install opencv-python") + return False + except Exception as e: + print(f"❌ 加载网络视频失败: {e}") + import traceback + traceback.print_exc() + return False + + def play2DVideo(self, video_screen): + """播放2D视频""" + try: + # 检查是否已经有视频信息(OpenCV方式) + if video_screen.hasPythonTag("video_info"): + video_info = video_screen.getPythonTag("video_info") + if video_info: + video_info['playing'] = True + print("▶️ 视频已恢复播放") + return True + + # 检查是否是 MovieTexture 方式 + movie_texture = video_screen.getPythonTag("movie_texture") + if movie_texture and hasattr(movie_texture, 'play'): + try: + movie_texture.play() + print("▶️ MovieTexture 视频开始播放") + return True + 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) + else: + return self.world.gui_manager.load2DVideoFile(video_screen,url) + else: + print("没有找到视频源") + return False + + except Exception as e: + print(f"❌ 播放视频失败: {e}") + import traceback + traceback.print_exc() + return False + + def pause2DVideo(self, video_screen): + """暂停2D视频""" + try: + if video_screen.hasPythonTag("video_info"): + video_info = video_screen.getPythonTag("video_info") + if video_info: + video_info['playing'] = False + print("⏸️ 视频已暂停") + return True + print("没有正在播放的视频") + return False + except Exception as e: + print(f"❌ 暂停视频失败: {e}") + return False + + def _stopVideo(self, video_screen): + """停止2D视频(内部方法)""" + 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: + # 在单独的线程中释放资源,避免阻塞 + import threading + def release_capture(): + try: + video_info['capture'].release() + except: + pass + + release_thread = threading.Thread(target=release_capture, daemon=True) + release_thread.start() + 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("⏹️ 视频已停止") + return True + except Exception as e: + print(f"❌ 停止视频失败: {e}") + return False def _loadNew2DVideo(self, video_screen): """为2D视频屏幕加载新视频文件""" try: @@ -1693,6 +2306,7 @@ class PropertyPanelManager: if success: print(f"成功加载新视频: {file_path}") # 刷新属性面板以显示新视频信息 + self._stopVideo(video_screen) self.updateGUIPropertyPanel(video_screen) return True except Exception as e: @@ -1704,8 +2318,19 @@ class PropertyPanelManager: try: import os + # 处理空路径情况 - 显示空白 + if not video_path or video_path == "": + print("ℹ️ 空视频路径,显示空白") + video_screen["frameColor"] = (0, 0, 0, 0) # 透明背景 + video_screen.clearPythonTag("movie_texture") + video_screen.setTag("video_path", "") + return True + + # 检查文件是否存在 if not os.path.exists(video_path): print(f"❌ 2D视频文件不存在: {video_path}") + # 显示空白而不是红色错误提示 + video_screen["frameColor"] = (0, 0, 0, 0) # 透明背景 return False # 加载新的视频纹理 @@ -1713,6 +2338,8 @@ class PropertyPanelManager: if movie_texture: # 应用纹理到2D视频屏幕 video_screen["frameTexture"] = movie_texture + # 设置白色背景以正确显示视频 + video_screen["frameColor"] = (1, 1, 1, 1) # 保存视频纹理引用 video_screen.setPythonTag("movie_texture", movie_texture) @@ -1722,37 +2349,59 @@ class PropertyPanelManager: return True else: print(f"❌ 无法加载2D视频文件: {video_path}") + # 显示空白而不是红色错误提示 + video_screen["frameColor"] = (0, 0, 0, 0) # 透明背景 return False except Exception as e: print(f"❌ 加载2D视频文件失败: {e}") import traceback traceback.print_exc() + # 显示空白而不是红色错误提示 + video_screen["frameColor"] = (0, 0, 0, 0) # 透明背景 return False - def _addVideoScreenProperties(self,video_screen): + def _addVideoScreenProperties(self, video_screen): try: - from PyQt5.QtWidgets import (QGroupBox,QGridLayout,QPushButton,QLabel,QSlider,QFileDialog,QHBoxLayout,QCheckBox) + 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_path and 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) + # 显示当前视频文件路径 - 改进版本 + 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) + 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.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("无视频文件"if not video_path else "文件不存在") - 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_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) @@ -1779,11 +2428,61 @@ class PropertyPanelManager: self._propertyLayout.addWidget(video_control_group) # 加载新视频按钮 - load_btn = QPushButton("📁 加载新视频...") + load_btn = QPushButton("📁 加载视频...") load_btn.clicked.connect(lambda: self._loadNewVideo(video_screen)) self._propertyLayout.addWidget(load_btn) + + # 添加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)) + url_layout.addWidget(load_url_btn) + + url_group.setLayout(url_layout) + self._propertyLayout.addWidget(url_group) + except Exception as e: - print(f"添加视频屏幕属性失败{e}") + print(f"添加视频屏幕属性失败: {e}") + def _directLoadURL(self, video_screen): + """直接加载URL - 修复版""" + try: + # 获取URL文本 + url = self.url_input.text().strip() if hasattr(self, 'url_input') and 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) + + except Exception as e: + print(f"❌ 直接加载URL失败: {e}") + import traceback + traceback.print_exc() + return False def _loadNewVideo(self,video_screen): try: @@ -2438,7 +3137,6 @@ class PropertyPanelManager: return unique_names - def _updateModelMaterialPanel(self,model): """模型材质属性""" if model.is_empty(): @@ -2504,7 +3202,11 @@ class PropertyPanelManager: material_layout = QGridLayout() material_layout.addWidget(QLabel("名称:"), 0, 0) - name_label = QLabel(display_name) + if len(display_name) > 25: + limited_name = display_name[:22] + "..." + else: + limited_name = display_name + name_label = QLabel(limited_name) # name_label.setStyleSheet("color: #FF6B6B; font-weight: bold;") material_layout.addWidget(name_label, 0, 1, 1, 3) @@ -2667,46 +3369,8 @@ 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) @@ -2714,17 +3378,6 @@ 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: @@ -5082,7 +5735,7 @@ class PropertyPanelManager: if preset["shading_model"]==3: self._apply_transparent_effect() - material._applied_preset = preset_name + #material._applied_preset = preset_name self._refreshMaterialUI() print(f"已应用材质预设: {preset_name}") @@ -5114,38 +5767,6 @@ class PropertyPanelManager: # 触发属性面板更新 self.updatePropertyPanel(current_item) - def _addColorSpacePanel(self, material): - """添加颜色空间选择面板""" - from PyQt5.QtWidgets import QButtonGroup, QRadioButton, QHBoxLayout, QWidget - - # color_space_title = QLabel("颜色空间") - # color_space_title.setStyleSheet("color: #FF9800; font-weight:bold;") - # self._propertyLayout.addRow(color_space_title) - # - # color_space_widget = QWidget() - # color_space_layout = QHBoxLayout(color_space_widget) - # color_space_group = QButtonGroup() - - # rgb_radio = QRadioButton("RGB") - # srgb_radio = QRadioButton("sRGB") - # hsv_radio = QRadioButton("HSV") - # - # rgb_radio.setChecked(True) - # - # color_space_group.addButton(rgb_radio, 0) - # color_space_group.addButton(srgb_radio, 1) - # color_space_group.addButton(hsv_radio, 2) - # - # color_space_layout.addWidget(rgb_radio) - # color_space_layout.addWidget(srgb_radio) - # color_space_layout.addWidget(hsv_radio) - # - # color_space_group.buttonClicked.connect( - # lambda button: self._updateColorSpace(material, color_space_group.id(button)) - # ) - # - # self._propertyLayout.addRow("颜色空间:", color_space_widget) - def _addBatchOperationsPanel(self, model): """添加批量操作面板""" batch_title = QLabel("批量操作") diff --git a/ui/widgets.py b/ui/widgets.py index 80dac573..b0d584b6 100644 --- a/ui/widgets.py +++ b/ui/widgets.py @@ -1445,28 +1445,21 @@ class CustomTreeWidget(QTreeWidget): event.ignore() return - # # 检查是否是有效的父子关系 - # if self.isValidParentChild(dragged_item, target_item): - # # 保存当前的世界坐标 - # world_pos = dragged_node.getPos(self.world.render) - # - # # 更新场景图中的父子关系 - # dragged_node.wrtReparentTo(target_node) - # - # # 接受拖放事件,更新树形控件 - # super().dropEvent(event) - # - # #self.world.property_panel.updateNodeVisibilityAfterDrag(dragged_item) - # # 更新属性面板 - # self.world.updatePropertyPanel(dragged_item) - # self.world.property_panel._syncEffectiveVisibility(dragged_node) - print(f"dragged_node: {dragged_node}, target_node: {target_node}") # 记录拖拽前的父节点 old_parent_item = dragged_item.parent() old_parent_node = old_parent_item.data(0, Qt.UserRole) if old_parent_item else None + # 获取拖拽前的缩放值(2D GUI节点需要特别处理) + is_2d_gui = dragged_item.data(0, Qt.UserRole + 1) in self.gui_2d_types + if is_2d_gui: + # 对于2D GUI,直接记录节点自身的缩放(不考虑父节点缩放) + original_scale = dragged_node.getScale() + else: + # 对于3D节点,记录世界缩放 + original_scale = dragged_node.getScale(self.world.render) + # 执行Qt默认拖拽 super().dropEvent(event) @@ -1483,11 +1476,6 @@ class CustomTreeWidget(QTreeWidget): # 保存世界坐标位置 world_pos = dragged_node.getPos(self.world.render) world_hpr = dragged_node.getHpr(self.world.render) - world_scale = dragged_node.getScale(self.world.render) - - # 检查是否是2D GUI元素 - dragged_type = dragged_item.data(0, Qt.UserRole + 1) - is_2d_gui = dragged_type in self.gui_2d_types # 重新父化到新的父节点 if new_parent_node: @@ -1513,16 +1501,18 @@ class CustomTreeWidget(QTreeWidget): else: dragged_node.reparentTo(self.world.render) - # 恢复世界坐标位置(对于2D GUI可能需要调整) + # 恢复世界坐标位置和方向 + dragged_node.setPos(self.world.render, world_pos) + dragged_node.setHpr(self.world.render, world_hpr) + + # 恢复缩放值 if is_2d_gui: - # 2D GUI元素使用屏幕坐标系,可能需要特殊处理 - dragged_node.setPos(world_pos) - dragged_node.setHpr(world_hpr) - dragged_node.setScale(world_scale) + # 对于2D GUI,直接使用原始缩放值(不考虑父节点缩放) + dragged_node.setScale(original_scale) + print(f"✅ 2D GUI {dragged_item.text(0)} 缩放已恢复为原始值: {original_scale}") else: - dragged_node.setPos(self.world.render, world_pos) - dragged_node.setHpr(self.world.render, world_hpr) - dragged_node.setScale(self.world.render, world_scale) + # 对于3D节点,使用世界缩放恢复 + dragged_node.setScale(self.world.render, original_scale) print(f"✅ Panda3D父子关系已更新") else: @@ -1537,28 +1527,6 @@ class CustomTreeWidget(QTreeWidget): self.world.property_panel._syncEffectiveVisibility(dragged_node) event.accept() - # try: - # world_pos = dragged_node.getPos(self.world.render) - # - # parent_of_dragged = dragged_node.getParent() - # target_node.wrtReparentTo(parent_of_dragged) - # - # # 拖动节点到目标节点下 - # dragged_node.wrtReparentTo(target_node) - # dragged_node.setPos(self.world.render, world_pos) - # - # # 更新 Qt 树控件 - # super().dropEvent(event) - # - # # 更新属性面板 - # self.world.updatePropertyPanel(dragged_item) - # - # event.accept() - # - # except Exception as e: - # print(f"重设父节点失败: {e}") - # event.ignore() - def _ensureUnderSceneRoot(self, item): """确保节点在场景根节点下,如果不是则自动修正""" if not item: