视频屏幕可播放视频流,修改部分名称过长问题
This commit is contained in:
parent
5e68ff5ee6
commit
4ca5068b2c
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -157,17 +157,23 @@ class GUIManager:
|
|||||||
# 父节点是GUI元素 - 作为子GUI挂载
|
# 父节点是GUI元素 - 作为子GUI挂载
|
||||||
gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node)
|
gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node)
|
||||||
parent_gui_node = parent_node # 直接挂载到GUI元素
|
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:
|
else:
|
||||||
# 父节点是普通3D节点 - 使用屏幕坐标
|
# 父节点是普通3D节点 - 使用屏幕坐标
|
||||||
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
|
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
|
||||||
parent_gui_node = None # 使用默认的aspect2d
|
parent_gui_node = None # 使用默认的aspect2d
|
||||||
|
relative_scale = size
|
||||||
print(f"📎 挂载到3D父节点: {parent_item.text(0)}")
|
print(f"📎 挂载到3D父节点: {parent_item.text(0)}")
|
||||||
|
|
||||||
button = DirectButton(
|
button = DirectButton(
|
||||||
text=text,
|
text=text,
|
||||||
pos=gui_pos,
|
pos=gui_pos,
|
||||||
scale=size,
|
scale=relative_scale,
|
||||||
command=self.onGUIButtonClick,
|
command=self.onGUIButtonClick,
|
||||||
extraArgs=[f"button_{len(self.gui_elements)}"],
|
extraArgs=[f"button_{len(self.gui_elements)}"],
|
||||||
frameColor=(0.2, 0.6, 0.8, 1),
|
frameColor=(0.2, 0.6, 0.8, 1),
|
||||||
@ -268,17 +274,22 @@ class GUIManager:
|
|||||||
# 父节点是GUI元素 - 作为子GUI挂载
|
# 父节点是GUI元素 - 作为子GUI挂载
|
||||||
gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node)
|
gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node)
|
||||||
parent_gui_node = 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:
|
else:
|
||||||
# 父节点是普通3D节点 - 使用屏幕坐标
|
# 父节点是普通3D节点 - 使用屏幕坐标
|
||||||
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
|
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
|
||||||
parent_gui_node = None
|
parent_gui_node = None
|
||||||
print(f"📎 挂载到3D父节点: {parent_item.text(0)}")
|
relative_scale = size
|
||||||
|
|
||||||
label = DirectLabel(
|
label = DirectLabel(
|
||||||
text=text,
|
text=text,
|
||||||
pos=gui_pos,
|
pos=gui_pos,
|
||||||
scale=size,
|
scale=relative_scale,
|
||||||
frameColor=(0, 0, 0, 0), # 透明背景
|
frameColor=(0, 0, 0, 0), # 透明背景
|
||||||
text_fg=(1, 1, 1, 1),
|
text_fg=(1, 1, 1, 1),
|
||||||
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None,
|
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None,
|
||||||
@ -862,10 +873,9 @@ class GUIManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def createVideoScreen(self, pos=(0, 0, 0), size=0.2, video_path=None):
|
def createVideoScreen(self, pos=(0, 0, 0), size=0.2, video_path=None):
|
||||||
"""创建视频播放屏幕 - 改进版本"""
|
"""创建3D视频播放屏幕 - 添加占位符纹理支持"""
|
||||||
try:
|
try:
|
||||||
from panda3d.core import CardMaker, TransparencyAttrib, Texture, TextureStage
|
from panda3d.core import CardMaker, TransparencyAttrib, Texture, TextureStage
|
||||||
from PyQt5.QtCore import Qt
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
# 确保 pos 是有效的三维坐标元组
|
# 确保 pos 是有效的三维坐标元组
|
||||||
@ -896,7 +906,7 @@ class GUIManager:
|
|||||||
return None
|
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:
|
if not target_parents:
|
||||||
print("❌ 没有找到有效的父节点")
|
print("❌ 没有找到有效的父节点")
|
||||||
return None
|
return None
|
||||||
@ -925,6 +935,40 @@ class GUIManager:
|
|||||||
# 设置初始颜色为白色,确保纹理能正确显示
|
# 设置初始颜色为白色,确保纹理能正确显示
|
||||||
video_screen.setColor(1, 1, 1, 1)
|
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
|
movie_texture = None
|
||||||
if video_path and os.path.exists(video_path):
|
if video_path and os.path.exists(video_path):
|
||||||
@ -941,6 +985,9 @@ class GUIManager:
|
|||||||
|
|
||||||
print(f"✅ 视频纹理加载成功: {video_path}")
|
print(f"✅ 视频纹理加载成功: {video_path}")
|
||||||
|
|
||||||
|
# 保存视频纹理引用以便后续控制
|
||||||
|
video_screen.setPythonTag("movie_texture", movie_texture)
|
||||||
|
|
||||||
# 尝试自动播放视频(如果支持)
|
# 尝试自动播放视频(如果支持)
|
||||||
try:
|
try:
|
||||||
if hasattr(movie_texture, 'play'):
|
if hasattr(movie_texture, 'play'):
|
||||||
@ -966,19 +1013,6 @@ class GUIManager:
|
|||||||
else:
|
else:
|
||||||
print("ℹ️ 未提供视频文件,显示默认占位符")
|
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:
|
if movie_texture:
|
||||||
video_screen.setPythonTag("movie_texture", movie_texture)
|
video_screen.setPythonTag("movie_texture", movie_texture)
|
||||||
@ -1201,8 +1235,6 @@ class GUIManager:
|
|||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def setVideoTime(self, video_screen, time_seconds):
|
def setVideoTime(self, video_screen, time_seconds):
|
||||||
"""设置视频播放时间"""
|
"""设置视频播放时间"""
|
||||||
try:
|
try:
|
||||||
@ -1434,8 +1466,25 @@ class GUIManager:
|
|||||||
video_screen.setTag("is_scene_element", "1")
|
video_screen.setTag("is_scene_element", "1")
|
||||||
video_screen.setTag("created_by_user", "1")
|
video_screen.setTag("created_by_user", "1")
|
||||||
|
|
||||||
|
# 设置视频路径标签
|
||||||
if video_path and os.path.exists(video_path):
|
if video_path and os.path.exists(video_path):
|
||||||
video_screen.setTag("video_path", 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
|
movie_texture = None
|
||||||
@ -1445,7 +1494,7 @@ class GUIManager:
|
|||||||
# 加载视频纹理
|
# 加载视频纹理
|
||||||
movie_texture = self._loadMovieTexture(video_path)
|
movie_texture = self._loadMovieTexture(video_path)
|
||||||
if movie_texture:
|
if movie_texture:
|
||||||
# 应用纹理到视频屏幕
|
# 应用纹理到视频屏幕(替换占位符)
|
||||||
video_screen["frameTexture"] = movie_texture
|
video_screen["frameTexture"] = movie_texture
|
||||||
print(f"✅ 2D视频纹理加载成功: {video_path}")
|
print(f"✅ 2D视频纹理加载成功: {video_path}")
|
||||||
|
|
||||||
@ -1546,143 +1595,349 @@ class GUIManager:
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return False
|
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):
|
def play2DVideo(self, video_screen):
|
||||||
"""播放2D视频"""
|
"""播放2D视频"""
|
||||||
try:
|
try:
|
||||||
# 获取视频纹理
|
if video_screen.hasPythonTag("video_capture"):
|
||||||
movie_texture = video_screen.getPythonTag("movie_texture")
|
print("视频已在播放中")
|
||||||
|
|
||||||
# 备用获取方法
|
|
||||||
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
|
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:
|
else:
|
||||||
print(f"⚠️ 2D视频屏幕没有可播放的视频纹理: {video_screen.getName()}")
|
print("❌ 没有找到视频源")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ 播放2D视频失败: {e}")
|
print(f"❌ 播放视频失败: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def pause2DVideo(self, video_screen):
|
def pause2DVideo(self, video_screen):
|
||||||
"""暂停2D视频"""
|
"""暂停2D视频"""
|
||||||
try:
|
try:
|
||||||
movie_texture = video_screen.getPythonTag("movie_texture")
|
# 在OpenCV中没有直接的暂停功能,我们通过停止线程来模拟暂停
|
||||||
|
if video_screen.hasPythonTag("video_capture"):
|
||||||
# 备用获取方法
|
cap = video_screen.getPythonTag("video_capture")
|
||||||
if not movie_texture:
|
if cap:
|
||||||
# 尝试从frameTexture获取
|
cap.release()
|
||||||
if hasattr(video_screen, 'frameTexture'):
|
video_screen.clearPythonTag("video_capture")
|
||||||
movie_texture = video_screen.frameTexture
|
print("⏸️ 视频已暂停")
|
||||||
|
|
||||||
if movie_texture and hasattr(movie_texture, 'stop'): # MovieTexture使用stop来暂停
|
|
||||||
try:
|
|
||||||
movie_texture.stop()
|
|
||||||
print(f"⏸️ 2D视频已暂停: {video_screen.getName()}")
|
|
||||||
return True
|
return True
|
||||||
except Exception as stop_error:
|
|
||||||
print(f"⚠️ 暂停2D视频时出错: {stop_error}")
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
print(f"⚠️ 2D视频屏幕没有可暂停的视频纹理: {video_screen.getName()}")
|
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ 暂停2D视频失败: {e}")
|
print(f"❌ 暂停视频失败: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def stop2DVideo(self, video_screen):
|
def stop2DVideo(self, video_screen):
|
||||||
"""停止2D视频(回到开头)"""
|
"""停止2D视频"""
|
||||||
try:
|
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:
|
if video_screen.hasPythonTag("video_texture"):
|
||||||
# 尝试从frameTexture获取
|
video_screen.clearPythonTag("video_texture")
|
||||||
if hasattr(video_screen, 'frameTexture'):
|
|
||||||
movie_texture = video_screen.frameTexture
|
|
||||||
|
|
||||||
if movie_texture:
|
# 清理视频路径标签
|
||||||
try:
|
video_screen.clearTag("video_path")
|
||||||
if hasattr(movie_texture, 'stop'):
|
|
||||||
movie_texture.stop()
|
print("⏹️ 视频已停止")
|
||||||
# 重置到开头
|
|
||||||
if hasattr(movie_texture, 'setTime'):
|
|
||||||
movie_texture.setTime(0.0)
|
|
||||||
print(f"⏹️ 2D视频已停止: {video_screen.getName()}")
|
|
||||||
return True
|
return True
|
||||||
except Exception as stop_error:
|
except Exception as e:
|
||||||
print(f"⚠️ 停止2D视频时出错: {stop_error}")
|
print(f"❌ 停止视频失败: {e}")
|
||||||
return False
|
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:
|
else:
|
||||||
print(f"⚠️ 2D视频屏幕没有视频纹理: {video_screen.getName()}")
|
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ 停止2D视频失败: {e}")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"):
|
def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"):
|
||||||
@ -2570,13 +2825,13 @@ class GUIManager:
|
|||||||
transform_layout.addWidget(z_label, 0, 2)
|
transform_layout.addWidget(z_label, 0, 2)
|
||||||
|
|
||||||
xPos = QDoubleSpinBox()
|
xPos = QDoubleSpinBox()
|
||||||
xPos.setRange(-50, 50)
|
xPos.setRange(-1000, 1000)
|
||||||
xPos.setValue(logical_x)
|
xPos.setValue(logical_x)
|
||||||
xPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "x", v))
|
xPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "x", v))
|
||||||
transform_layout.addWidget(xPos, 1, 1)
|
transform_layout.addWidget(xPos, 1, 1)
|
||||||
|
|
||||||
zPos = QDoubleSpinBox()
|
zPos = QDoubleSpinBox()
|
||||||
zPos.setRange(-50, 50)
|
zPos.setRange(-1000, 1000)
|
||||||
zPos.setValue(logical_z)
|
zPos.setValue(logical_z)
|
||||||
zPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "z", v))
|
zPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "z", v))
|
||||||
transform_layout.addWidget(zPos, 1, 2)
|
transform_layout.addWidget(zPos, 1, 2)
|
||||||
@ -2930,19 +3185,13 @@ class GUIManager:
|
|||||||
if axis == "x":
|
if axis == "x":
|
||||||
# 转换逻辑坐标到屏幕坐标
|
# 转换逻辑坐标到屏幕坐标
|
||||||
screen_x = value * 0.1
|
screen_x = value * 0.1
|
||||||
# 应用边界约束
|
new_pos = (screen_x,current_pos[1],current_pos[2])
|
||||||
constrained_pos = self.constrain2DPosition(gui_element, new_x=screen_x)
|
|
||||||
new_pos = (constrained_pos.getX(), constrained_pos.getY(), constrained_pos.getZ())
|
|
||||||
elif axis == "z":
|
elif axis == "z":
|
||||||
screen_z = value * 0.1
|
screen_z = value * 0.1
|
||||||
# 应用边界约束
|
new_pos = (current_pos[0],current_pos[1],screen_z)
|
||||||
constrained_pos = self.constrain2DPosition(gui_element, new_z=screen_z)
|
|
||||||
new_pos = (constrained_pos.getX(), constrained_pos.getY(), constrained_pos.getZ())
|
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
gui_element.setPos(*new_pos)
|
gui_element.setPos(*new_pos)
|
||||||
print(f"✓ 更新2D GUI元素位置: {axis}={value} (约束后位置: {new_pos})")
|
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
print(f"✗ 不支持的GUI类型进行2D位置编辑: {gui_type}")
|
print(f"✗ 不支持的GUI类型进行2D位置编辑: {gui_type}")
|
||||||
|
|||||||
13
main.py
13
main.py
@ -242,6 +242,19 @@ class MyWorld(CoreWorld):
|
|||||||
"""创建2D视频屏幕"""
|
"""创建2D视频屏幕"""
|
||||||
return self.gui_manager.createGUI2DVideoScreen(pos,size,video_path)
|
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)):
|
def createSpotLight(self,pos=(0,0,5)):
|
||||||
"""创建聚光灯"""
|
"""创建聚光灯"""
|
||||||
return self.scene_manager.createSpotLight(pos)
|
return self.scene_manager.createSpotLight(pos)
|
||||||
|
|||||||
@ -89,16 +89,8 @@ class SceneManager:
|
|||||||
# ==================== 模型导入和处理 ====================
|
# ==================== 模型导入和处理 ====================
|
||||||
|
|
||||||
def importModel(self, filepath, apply_unit_conversion=False, normalize_scales=True, auto_convert_to_glb=True):
|
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:
|
try:
|
||||||
|
|
||||||
# 预处理文件路径和转换
|
# 预处理文件路径和转换
|
||||||
filepath = util.normalize_model_path(filepath)
|
filepath = util.normalize_model_path(filepath)
|
||||||
original_filepath = filepath
|
original_filepath = filepath
|
||||||
@ -107,11 +99,24 @@ class SceneManager:
|
|||||||
print(f"缩放标准化: {'开启' if normalize_scales else '关闭'}")
|
print(f"缩放标准化: {'开启' if normalize_scales else '关闭'}")
|
||||||
print(f"自动转换GLB: {'开启' if auto_convert_to_glb 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
|
# 检查是否需要转换为GLB
|
||||||
if auto_convert_to_glb and self._shouldConvertToGLB(filepath):
|
if auto_convert_to_glb and self._shouldConvertToGLB(filepath):
|
||||||
print(f"🔄 检测到需要转换的格式,尝试转换为GLB...")
|
print(f"🔄 检测到需要转换的格式,尝试转换为GLB...")
|
||||||
converted_path = self._convertToGLBWithProgress(filepath)
|
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}")
|
print(f"✅ 转换成功: {converted_path}")
|
||||||
filepath = converted_path
|
filepath = converted_path
|
||||||
# 显示成功消息
|
# 显示成功消息
|
||||||
@ -123,16 +128,64 @@ class SceneManager:
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
else:
|
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根节点下创建模型
|
# 直接在render根节点下创建模型
|
||||||
print("--- 在根节点下创建模型实例 ---")
|
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)
|
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:
|
if not model:
|
||||||
print("❌ 加载模型失败")
|
print("❌ 所有加载尝试都失败了")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# 设置模型名称
|
# 设置模型名称
|
||||||
@ -154,17 +207,6 @@ class SceneManager:
|
|||||||
model.setTag("converted_from", os.path.splitext(original_filepath)[1])
|
model.setTag("converted_from", os.path.splitext(original_filepath)[1])
|
||||||
model.setTag("converted_to_glb", "true")
|
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文件,通常不需要单位转换,因为它们已经是标准单位
|
# 对于GLB文件,通常不需要单位转换,因为它们已经是标准单位
|
||||||
if apply_unit_conversion and filepath.lower().endswith(
|
if apply_unit_conversion and filepath.lower().endswith(
|
||||||
@ -228,6 +270,49 @@ class SceneManager:
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return None
|
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):
|
# def importAnimatedFBX(self, filepath, scale=0.01, auto_play=True):
|
||||||
# """导入带动画的FBX模型(使用assimp)
|
# """导入带动画的FBX模型(使用assimp)
|
||||||
#
|
#
|
||||||
@ -289,8 +374,10 @@ class SceneManager:
|
|||||||
|
|
||||||
def _applyMaterialsToModel(self, model):
|
def _applyMaterialsToModel(self, model):
|
||||||
"""递归应用材质到模型的所有GeomNode"""
|
"""递归应用材质到模型的所有GeomNode"""
|
||||||
|
|
||||||
def apply_material(node_path, depth=0):
|
def apply_material(node_path, depth=0):
|
||||||
indent = " " * depth
|
indent = " " * depth
|
||||||
|
try:
|
||||||
print(f"{indent}处理节点: {node_path.getName()}")
|
print(f"{indent}处理节点: {node_path.getName()}")
|
||||||
print(f"{indent}节点类型: {node_path.node().__class__.__name__}")
|
print(f"{indent}节点类型: {node_path.node().__class__.__name__}")
|
||||||
|
|
||||||
@ -322,19 +409,24 @@ class SceneManager:
|
|||||||
# 如果还没找到颜色,检查几何体
|
# 如果还没找到颜色,检查几何体
|
||||||
if not has_color:
|
if not has_color:
|
||||||
for i in range(geom_node.getNumGeoms()):
|
for i in range(geom_node.getNumGeoms()):
|
||||||
|
try:
|
||||||
geom = geom_node.getGeom(i)
|
geom = geom_node.getGeom(i)
|
||||||
state = geom_node.getGeomState(i)
|
state = geom_node.getGeomState(i)
|
||||||
|
|
||||||
# 检查顶点颜色
|
# 检查顶点颜色
|
||||||
vdata = geom.getVertexData()
|
vdata = geom.getVertexData()
|
||||||
|
if vdata:
|
||||||
format = vdata.getFormat()
|
format = vdata.getFormat()
|
||||||
|
if format:
|
||||||
for j in range(format.getNumColumns()):
|
for j in range(format.getNumColumns()):
|
||||||
|
try:
|
||||||
column = format.getColumn(j)
|
column = format.getColumn(j)
|
||||||
# InternalName对象需要使用getName()转换为字符串
|
# InternalName对象需要使用getName()转换为字符串
|
||||||
column_name = column.getName().getName()
|
column_name = column.getName().getName()
|
||||||
if "color" in column_name.lower():
|
if "color" in column_name.lower():
|
||||||
print(f"{indent}发现顶点颜色数据: {column_name}")
|
print(f"{indent}发现顶点颜色数据: {column_name}")
|
||||||
# 这里可以读取顶点颜色,但先记录发现
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
# 检查材质属性
|
# 检查材质属性
|
||||||
if state.hasAttrib(MaterialAttrib.getClassType()):
|
if state.hasAttrib(MaterialAttrib.getClassType()):
|
||||||
@ -360,14 +452,21 @@ class SceneManager:
|
|||||||
has_color = True
|
has_color = True
|
||||||
print(f"{indent}从颜色属性获取: {color}")
|
print(f"{indent}从颜色属性获取: {color}")
|
||||||
break
|
break
|
||||||
|
except Exception as geom_error:
|
||||||
|
print(f"{indent}处理几何体 {i} 时出错: {geom_error}")
|
||||||
|
continue
|
||||||
|
|
||||||
# 创建新材质
|
# 创建新材质
|
||||||
material = Material()
|
material = Material()
|
||||||
if has_color:
|
if has_color:
|
||||||
print(f"{indent}应用找到的颜色: {color}")
|
print(f"{indent}应用找到的颜色: {color}")
|
||||||
|
try:
|
||||||
material.setDiffuse(color)
|
material.setDiffuse(color)
|
||||||
material.setBaseColor(color) # 同时设置基础颜色
|
material.setBaseColor(color) # 同时设置基础颜色
|
||||||
node_path.setColor(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:
|
else:
|
||||||
print(f"{indent}使用默认颜色")
|
print(f"{indent}使用默认颜色")
|
||||||
material.setDiffuse((0.8, 0.8, 0.8, 1.0))
|
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.setAmbient((0.2, 0.2, 0.2, 1.0))
|
||||||
material.setSpecular((0.5, 0.5, 0.5, 1.0))
|
material.setSpecular((0.5, 0.5, 0.5, 1.0))
|
||||||
material.setShininess(32.0)
|
material.setShininess(32.0)
|
||||||
#material.set_metallic(1)
|
|
||||||
#material.set_roughness(0)
|
|
||||||
|
|
||||||
# 应用材质
|
# 应用材质
|
||||||
node_path.setMaterial(material)
|
node_path.setMaterial(material)
|
||||||
print(f"{indent}几何体数量: {geom_node.getNumGeoms()}")
|
print(f"{indent}几何体数量: {geom_node.getNumGeoms()}")
|
||||||
|
|
||||||
|
except Exception as node_error:
|
||||||
|
print(f"{indent}处理节点 {node_path.getName()} 时出错: {node_error}")
|
||||||
|
|
||||||
# 递归处理子节点
|
# 递归处理子节点
|
||||||
child_count = node_path.getNumChildren()
|
child_count = node_path.getNumChildren()
|
||||||
print(f"{indent}子节点数量: {child_count}")
|
print(f"{indent}子节点数量: {child_count}")
|
||||||
for i in range(child_count):
|
for i in range(child_count):
|
||||||
|
try:
|
||||||
child = node_path.getChild(i)
|
child = node_path.getChild(i)
|
||||||
apply_material(child, depth + 1)
|
apply_material(child, depth + 1)
|
||||||
|
except Exception as child_error:
|
||||||
|
print(f"{indent}处理子节点 {i} 时出错: {child_error}")
|
||||||
|
continue
|
||||||
|
|
||||||
# 应用材质
|
# 应用材质
|
||||||
print("\n开始递归应用材质...")
|
print("\n开始递归应用材质...")
|
||||||
|
try:
|
||||||
apply_material(model)
|
apply_material(model)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"应用材质时出错: {e}")
|
||||||
print("=== 材质设置完成 ===\n")
|
print("=== 材质设置完成 ===\n")
|
||||||
|
|
||||||
def _adjustModelToGround(self, model):
|
def _adjustModelToGround(self, model):
|
||||||
|
|||||||
@ -429,7 +429,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.createImageAction.triggered.connect(lambda: self.world.createGUI2DImage())
|
self.createImageAction.triggered.connect(lambda: self.world.createGUI2DImage())
|
||||||
self.createVideoScreen.triggered.connect(self.world.createVideoScreen)
|
self.createVideoScreen.triggered.connect(self.world.createVideoScreen)
|
||||||
self.create2DVideoScreen.triggered.connect(self.world.create2DVideoScreen)
|
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.createVirtualScreenAction.triggered.connect(lambda: self.world.createGUIVirtualScreen())
|
||||||
self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight())
|
self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight())
|
||||||
self.createPointLightAction.triggered.connect(lambda :self.world.createPointLight())
|
self.createPointLightAction.triggered.connect(lambda :self.world.createPointLight())
|
||||||
@ -573,10 +573,10 @@ class MainWindow(QMainWindow):
|
|||||||
self.addDockWidget(Qt.BottomDockWidgetArea, self.bottomDock)
|
self.addDockWidget(Qt.BottomDockWidgetArea, self.bottomDock)
|
||||||
|
|
||||||
# 创建底部停靠控制台
|
# 创建底部停靠控制台
|
||||||
self.consoleDock = QDockWidget("控制台", self)
|
# self.consoleDock = QDockWidget("控制台", self)
|
||||||
self.consoleView = CustomConsoleDockWidget(self.world)
|
# self.consoleView = CustomConsoleDockWidget(self.world)
|
||||||
self.consoleDock.setWidget(self.consoleView)
|
# self.consoleDock.setWidget(self.consoleView)
|
||||||
self.addDockWidget(Qt.BottomDockWidgetArea, self.consoleDock)
|
# self.addDockWidget(Qt.BottomDockWidgetArea, self.consoleDock)
|
||||||
|
|
||||||
def setupToolbar(self):
|
def setupToolbar(self):
|
||||||
"""创建工具栏"""
|
"""创建工具栏"""
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1445,28 +1445,21 @@ class CustomTreeWidget(QTreeWidget):
|
|||||||
event.ignore()
|
event.ignore()
|
||||||
return
|
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}")
|
print(f"dragged_node: {dragged_node}, target_node: {target_node}")
|
||||||
|
|
||||||
# 记录拖拽前的父节点
|
# 记录拖拽前的父节点
|
||||||
old_parent_item = dragged_item.parent()
|
old_parent_item = dragged_item.parent()
|
||||||
old_parent_node = old_parent_item.data(0, Qt.UserRole) if old_parent_item else None
|
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默认拖拽
|
# 执行Qt默认拖拽
|
||||||
super().dropEvent(event)
|
super().dropEvent(event)
|
||||||
|
|
||||||
@ -1483,11 +1476,6 @@ class CustomTreeWidget(QTreeWidget):
|
|||||||
# 保存世界坐标位置
|
# 保存世界坐标位置
|
||||||
world_pos = dragged_node.getPos(self.world.render)
|
world_pos = dragged_node.getPos(self.world.render)
|
||||||
world_hpr = dragged_node.getHpr(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:
|
if new_parent_node:
|
||||||
@ -1513,16 +1501,18 @@ class CustomTreeWidget(QTreeWidget):
|
|||||||
else:
|
else:
|
||||||
dragged_node.reparentTo(self.world.render)
|
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.setPos(self.world.render, world_pos)
|
||||||
dragged_node.setHpr(self.world.render, world_hpr)
|
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父子关系已更新")
|
print(f"✅ Panda3D父子关系已更新")
|
||||||
else:
|
else:
|
||||||
@ -1537,28 +1527,6 @@ class CustomTreeWidget(QTreeWidget):
|
|||||||
self.world.property_panel._syncEffectiveVisibility(dragged_node)
|
self.world.property_panel._syncEffectiveVisibility(dragged_node)
|
||||||
event.accept()
|
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):
|
def _ensureUnderSceneRoot(self, item):
|
||||||
"""确保节点在场景根节点下,如果不是则自动修正"""
|
"""确保节点在场景根节点下,如果不是则自动修正"""
|
||||||
if not item:
|
if not item:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user