视频屏幕可播放视频流,修改部分名称过长问题

This commit is contained in:
Hector 2025-09-03 17:22:28 +08:00
parent 5e68ff5ee6
commit 4ca5068b2c
8 changed files with 1395 additions and 437 deletions

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -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 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()}")
if video_screen.hasPythonTag("video_capture"):
print("视频已在播放中")
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()}")
# 在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
except Exception as stop_error:
print(f"⚠️ 暂停2D视频时出错: {stop_error}")
return False
else:
print(f"⚠️ 2D视频屏幕没有可暂停的视频纹理: {video_screen.getName()}")
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()}")
# 清理视频路径标签
video_screen.clearTag("video_path")
print("⏹️ 视频已停止")
return True
except Exception as stop_error:
print(f"⚠️ 停止2D视频时出错: {stop_error}")
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}")

13
main.py
View File

@ -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)

View File

@ -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,11 +99,24 @@ 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
# 显示成功消息
@ -123,16 +128,64 @@ class SceneManager:
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("直接从文件加载模型...")
# 添加模型加载前的安全检查
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
# 设置模型名称
@ -154,17 +207,6 @@ class SceneManager:
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文件通常不需要单位转换因为它们已经是标准单位
if apply_unit_conversion and filepath.lower().endswith(
@ -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,8 +374,10 @@ class SceneManager:
def _applyMaterialsToModel(self, model):
"""递归应用材质到模型的所有GeomNode"""
def apply_material(node_path, depth=0):
indent = " " * depth
try:
print(f"{indent}处理节点: {node_path.getName()}")
print(f"{indent}节点类型: {node_path.node().__class__.__name__}")
@ -322,19 +409,24 @@ class SceneManager:
# 如果还没找到颜色,检查几何体
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()
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()):
@ -360,14 +452,21 @@ class SceneManager:
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}")
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))
@ -376,23 +475,31 @@ class SceneManager:
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)
# 应用材质
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):
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开始递归应用材质...")
try:
apply_material(model)
except Exception as e:
print(f"应用材质时出错: {e}")
print("=== 材质设置完成 ===\n")
def _adjustModelToGround(self, model):

View File

@ -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):
"""创建工具栏"""

View File

@ -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,18 +1938,39 @@ class PropertyPanelManager:
video_info_layout = QGridLayout()
# 显示当前视频文件路径
# 在显示视频信息时区分本地文件和网络URL
video_path = video_screen.getTag("video_path")
if video_path and os.path.exists(video_path):
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)
path_label = QLabel(os.path.basename(video_path))
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("无视频文件" if not video_path else "文件不存在")
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_screen.setBin("fixed", 0)
video_info_group.setLayout(video_info_layout)
@ -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):
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))
# 显示当前视频文件路径 - 改进版本
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("无视频文件"if not video_path 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_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("批量操作")

View File

@ -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可能需要调整
if is_2d_gui:
# 2D GUI元素使用屏幕坐标系可能需要特殊处理
dragged_node.setPos(world_pos)
dragged_node.setHpr(world_hpr)
dragged_node.setScale(world_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)
# 恢复缩放值
if is_2d_gui:
# 对于2D GUI直接使用原始缩放值不考虑父节点缩放
dragged_node.setScale(original_scale)
print(f"✅ 2D GUI {dragged_item.text(0)} 缩放已恢复为原始值: {original_scale}")
else:
# 对于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: