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

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 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}")

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,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):

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

File diff suppressed because it is too large Load Diff

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可能需要调整
# 恢复世界坐标位置和方向
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: