EG/templates/main_template.py
2025-12-12 16:16:15 +08:00

1573 lines
62 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
测试动画模型 - Panda3D应用程序
使用Panda3D引擎编辑器创建
"""
from __future__ import print_function
import json
from direct.actor.Actor import Actor
from direct.showbase.ShowBaseGlobal import globalClock
from panda3d.core import TextNode, CardMaker, TextureStage, NodePath, Texture, TransparencyAttrib, CollisionTraverser, \
Point3
from core.InfoPanelManager import InfoPanelManager
# 获取渲染管线路径
# 在文件开头添加sys导入如果还没有的话
import sys
import os
# 修改工作目录设置部分
if getattr(sys, 'frozen', False):
# 打包后的环境
project_root = os.path.dirname(sys.executable)
else:
# 开发环境
try:
project_root = os.path.dirname(os.path.abspath(__file__))
except NameError:
project_root = os.getcwd()
os.chdir(project_root)
render_pipeline_path = 'RenderPipelineFile'
sys.path.insert(0, render_pipeline_path)
# 改进路径设置逻辑
pipeline_path = os.path.join(project_root, "RenderPipelineFile")
if os.path.exists(pipeline_path) and pipeline_path not in sys.path:
sys.path.insert(0, pipeline_path)
# 同时添加子目录以确保所有模块都能正确导入
for root, dirs, files in os.walk(pipeline_path):
if root not in sys.path:
sys.path.insert(0, root)
import math
from random import random, randint, seed
from panda3d.core import Vec3, load_prc_file_data, Filename
from direct.showbase.ShowBase import ShowBase
from direct.task.TaskManagerGlobal import taskMgr
# os.chdir(os.path.dirname(os.path.realpath(__file__)))
from core.script_system import ScriptManager
from core.CustomMouseController import CustomMouseController
from panda3d.core import CollisionTraverser
class MainApp(ShowBase):
def __init__(self):
# 在调用父类构造函数前确保必要的属性存在
if not hasattr(self, 'appRunner'):
self.appRunner = None
load_prc_file_data("", """
win-size 1380 750
window-title
support-threads #t
""")
# 简化 sys.path 设置逻辑
pipeline_path = os.path.join(project_root, "RenderPipelineFile")
if os.path.exists(pipeline_path):
if pipeline_path not in sys.path:
sys.path.insert(0, pipeline_path)
else:
print(f"错误: 找不到渲染管线目录: {pipeline_path}")
return
try:
from rpcore import RenderPipeline
self.render_pipeline = RenderPipeline()
self.render_pipeline.create(self)
except ImportError as e:
print(f"导入RenderPipeline模块失败: {e}")
import traceback
traceback.print_exc()
ShowBase.__init__(self)
self.render_pipeline = None
return
self.script_manager = ScriptManager(self)
self.script_manager.start_system()
# 加载所有脚本e
self.script_manager.load_all_scripts_from_directory()
self.info_panel_manager = InfoPanelManager(self)
try:
# 再导入controller模块
from rpcore.util.movement_controller import MovementController
self.render_pipeline._showbase.camera = self.render_pipeline._showbase.cam
self.controller = MovementController(self)
self.camLens.set_fov(80)
self.controller.set_initial_position(
Vec3(0, -50, 20), Vec3(0, 0, 0))
self.controller.setup()
except ImportError as e:
print(f"导入MovementController失败: {e}")
self.controller = None
self._last_click_time = 0
self._last_clicked_node = None
self._double_click_threshold = 0.3
self.cameraSpeed = 20.0
self.cameraRotateSpeed=10.0
self.lastMouseX=0
self.lastMouseY=0
self.mouseRightPressed=False
self._loadFont()
self.loadFullScene()
self.loadGUIFromJSON()
self.setupMouseClickHandler()
self.cTrav = CollisionTraverser()
self.createFrameRateDisplay()
self.women_actor = None
self.current_actor = None
if hasattr(self, 'accept'):
base.accept("l", self.tour)
def _loadFont(self):
"""加载中文字体"""
self.chinese_font = None
try:
self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc')
if not self.chinese_font:
print("警告: 无法加载中文字体,将使用默认字体")
else:
print("✓ 中文字体加载成功")
except:
print("警告: 无法加载中文字体,将使用默认字体")
self.chinese_font = None
def getChineseFont(self):
"""获取中文字体"""
return self.chinese_font
def getResourcePath(self,relative_path):
if getattr(sys,'frozen',False):
base_path = os.path.dirname(sys.executable)
else:
base_path = os.path.dirname(os.path.abspath(__file__))
return os.path.join(base_path,relative_path)
def loadFullScene(self):
if not hasattr(self, 'loader') or not hasattr(self, 'render'):
print("错误: Panda3D核心组件未正确初始化")
return
try:
scene_file = self.getResourcePath("scene.bam")
if os.path.exists(scene_file):
# 使用readBamFile加载完整场景
from panda3d.core import BamCache
BamCache.getGlobalPtr().setActive(False) # 禁用缓存以避免问题
scene = self.loader.loadModel(Filename.fromOsSpecific(scene_file))
if scene:
scene.reparentTo(self.render)
self.render_pipeline.prepare_scene(scene)
print("✓ 完整场景加载成功")
# 检测并播放模型动画
#self._processModelAnimations(scene)
# 处理场景中的各种元素
self.processSceneElements(scene)
else:
print("⚠️ 场景文件加载失败")
else:
print("⚠️ 未找到场景文件")
except Exception as e:
print(f"加载完整场景时出错: {str(e)}")
import traceback
traceback.print_exc()
def checkAnimationStatus(self):
"""检查场景中所有Actor的动画状态"""
try:
all_actors = self.render.findAllMatches("**/+ActorNode")
print(f"=== 动画状态检查 ===")
print(f"场景中总共有 {len(all_actors)} 个Actor")
for i, actor_np in enumerate(all_actors):
actor_node = actor_np.node()
if isinstance(actor_node, Actor):
# 确保Actor可见
actor_np.show()
current_anim = actor_node.getCurrentAnim()
anim_names = actor_node.getAnimNames()
print(f"Actor {i + 1} ({actor_np.getName()}):")
print(f" 位置: {actor_np.getPos()}")
print(f" 可见性: {actor_np.isHidden()}")
print(f" 可用动画: {anim_names}")
print(f" 当前播放: {current_anim}")
if current_anim:
frame = actor_node.getCurrentFrame(current_anim)
num_frames = actor_node.getNumFrames(current_anim)
play_rate = actor_node.getPlayRate(current_anim)
print(f" 播放进度: {frame}/{num_frames}")
print(f" 播放速度: {play_rate}")
else:
print(f"节点 {actor_np.getName()} 不是Actor类型")
print("==================")
except Exception as e:
print(f"检查动画状态时出错: {str(e)}")
def _processModelAnimations(self, node_path):
"""处理节点中的动画模型"""
try:
# 查找场景中所有可能的动画模型
char_nodes = node_path.findAllMatches("**/+Character")
for char_node in char_nodes:
try:
# 获取父节点(通常是模型根节点)
model_root = char_node.getParent()
model_name = model_root.getName()
print(f"检测到可能的动画模型: {model_name}")
# 尝试创建Actor
actor = Actor(model_root)
actor.reparentTo(self.render)
actor.setPos(node_path.getPos())
actor.setHpr(node_path.getHpr())
actor.setScale(node_path.getScale())
# 获取动画名称
anim_names = actor.getAnimNames()
if anim_names:
print(f"✓ 成功创建动画模型: {model_name}")
print(f" 可用动画: {anim_names}")
# 循环播放所有动画
for anim_name in anim_names:
print(f" 循环播放动画: {anim_name}")
actor.loop(anim_name)
break # 只播放第一个动画,避免同时播放多个动画
# 替换原始模型
model_root.detachNode()
else:
# 没有动画,使用原始模型
print(f"模型 {model_name} 不包含动画")
actor.detachNode() # 移除创建的Actor
except Exception as e:
print(f"处理动画模型 {char_node.getName()} 时出错: {str(e)}")
except Exception as e:
print(f"处理模型动画时出错: {str(e)}")
import traceback
traceback.print_exc()
def processSceneElements(self, scene):
"""处理场景中的各种元素"""
try:
processed_lights = []
loaded_nodes = {}
def processNode(nodePath, depth=0):
loaded_nodes[nodePath.getName()] = nodePath
# 为模型添加碰撞体 - 修复版本
if nodePath.hasTag("is_scene_element") and not nodePath.hasTag("is_gizmo"):
if nodePath.hasTag("gui_type"):
gui_type = nodePath.getTag("gui_type")
if gui_type in ["video_screen"]:
print(f"移除GUI视频节点: {nodePath.getName()}")
nodePath.removeNode() # 移除视频屏幕节点
return
# 使用更精确的包围盒
from panda3d.core import CollisionNode, CollisionBox
bounds = nodePath.getBounds()
if not bounds.isEmpty():
min_point = bounds.getMin()
max_point = bounds.getMax()
# 创建碰撞节点
collision_node = CollisionNode(f'collision_{nodePath.getName()}')
collision_node.addSolid(CollisionBox(min_point, max_point))
collision_np = nodePath.attachNewNode(collision_node)
# 隐藏碰撞体
collision_np.hide()
if nodePath.hasTag("scripts_info"):
try:
import json
scripts_info = json.loads(nodePath.getTag("scripts_info"))
self.processScripts(nodePath, scripts_info)
except Exception as e:
print(f"处理节点 {nodePath.getName()} 的脚本时出错: {str(e)}")
if nodePath.hasTag("light_type"):
light_type = nodePath.getTag("light_type")
if nodePath.hasTag("is_auxiliary_light") and nodePath.getTag("is_auxiliary_light").lower() == "true":
print(f"跳过辅助灯光节点:{nodePath.getName()}")
return
if nodePath not in processed_lights:
if light_type == "spot_light":
self._recreateSpotLight(nodePath)
elif light_type == "point_light":
self._recreatePointLight(nodePath)
processed_lights.append(nodePath)
for child in nodePath.getChildren():
processNode(child, depth + 1)
processNode(scene)
except Exception as e:
print(f"处理场景元素时出错: {str(e)}")
def _recreateSpotLight(self,light_node):
try:
from RenderPipelineFile.rpcore import SpotLight
from panda3d.core import Vec3
light = SpotLight()
light.direction = Vec3(0,0,-1)
light.fov = 70
light.set_color_from_temperature(5*1000.0)
if light_node.hasTag("light_energy"):
light.energy = float(light_node.getTag("light_energy"))
else:
light.energy = 5000
light.radius = 1000
light.casts_shadows = True
light.shadow_map_resolution = 256
light_pos = light_node.getPos()
light.setPos(light_pos)
self.render_pipeline.add_light(light)
except Exception as e:
print(f"创建点光源 {light_node.getName()} 失败: {str(e)}")
import traceback
traceback.print_exc()
def _recreatePointLight(self,light_node):
try:
from RenderPipelineFile.rpcore import PointLight
light = PointLight()
if light_node.hasTag("light_energy"):
light.energy = float(light_node.getTag("light_energy"))
else:
light.energy = 5000
light.radius = 1000
light.inner_radius = 0.4
light.set_color_from_temperature(5*1000.0)
light.casts_shadows = True
light.shadow_map_resolution = 256
light.setPos(light_node.getPos())
self.render_pipeline.add_light(light)
except Exception as e:
print(f"创建点光源 {light_node.getName()} 失败: {str(e)}")
import traceback
traceback.print_exc()
def processGUIElements(self, scene):
"""处理场景中的GUI元素"""
try:
# 查找并处理2D图像
images_2d = scene.findAllMatches("**/=gui_type=image_2d")
for img_node in images_2d:
try:
# GUI元素通常在场景加载时自动处理
print(f"✓ 2D图像 {img_node.getName()} 已加载")
except Exception as e:
print(f"处理2D图像 {img_node.getName()} 失败: {str(e)}")
except Exception as e:
print(f"处理GUI元素时出错: {str(e)}")
def tour(self):
mopath = (
(Vec3(-10.8645000458, 9.76458263397, 2.13306283951), Vec3(-133.556228638, -4.23447799683, 0.0)),
(Vec3(-10.6538448334, -5.98406457901, 1.68028640747), Vec3(-59.3999938965, -3.32706642151, 0.0)),
(Vec3(9.58458328247, -5.63625621796, 2.63269257545), Vec3(58.7906494141, -9.40668964386, 0.0)),
(Vec3(6.8135137558, 11.0153560638, 2.25509500504), Vec3(148.762527466, -6.41223621368, 0.0)),
(Vec3(-9.07093334198, 3.65908527374, 1.42396306992), Vec3(245.362503052, -3.59927511215, 0.0)),
(Vec3(-8.75390911102, -3.82727789879, 0.990055501461), Vec3(296.090484619, -0.604830980301, 0.0)),
)
self.controller.play_motion_path(mopath, 3.0)
def loadGUIFromJSON(self):
try:
gui_json_path = self.getResourcePath("gui/gui_elements.json")
if os.path.exists(gui_json_path):
with open(gui_json_path, 'r', encoding='utf-8') as f:
content = f.read().strip()
if content:
gui_data = json.loads(content)
self.createGUIElement(gui_data)
else:
print("GUI配置文件为空 ")
except Exception as e:
print(f"加载GUI元素时出错: {str(e)}")
import traceback
traceback.print_exc()
def createGUIElement(self, element_data):
try:
processed_names = set()
element_original_data = {}
for i, gui_info in enumerate(element_data):
name = gui_info.get("name", f"gui_element_{i}")
element_original_data[name] = {
"scale": gui_info.get("scale", [1, 1, 1]),
"position": gui_info.get("position", [0, 0, 0]),
"parent_name": gui_info.get("parent_name")
}
valid_parents = set()
for gui_info in element_data:
name = gui_info.get("name", f"gui_element_{gui_info.get('index', 0)}")
valid_parents.add(name)
for i, gui_info in enumerate(element_data):
try:
gui_type = gui_info.get("type", "unknown")
name = gui_info.get("name", f"gui_element_{i}")
position = gui_info.get("position", [0, 0, 0])
scale = gui_info.get("scale", [1, 1, 1])
tags = gui_info.get("tags", {})
text = gui_info.get("text", "")
image_path = gui_info.get("image_path", "")
video_path = gui_info.get("video_path", "")
bg_image_path = gui_info.get("bg_image_path", "")
parent_name = gui_info.get("parent_name")
if name in processed_names:
continue
processed_names.add(name)
absolute_position = list(position)
absolute_scale = list(scale)
if parent_name and parent_name in element_original_data:
parent_data = element_original_data[parent_name]
parent_scale = parent_data["scale"]
if gui_type in ["3d_text", "3d_image", "button", "label", "entry", "2d_image",
"2d_video_screen"]:
# 位置需要乘以父级缩放来得到绝对位置
for j in range(min(len(absolute_position), len(parent_scale))):
absolute_position[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0]
# 缩放需要乘以父级缩放来得到绝对缩放
for j in range(min(len(absolute_scale), len(parent_scale))):
absolute_scale[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0]
new_element = None
if gui_type == "3d_text":
size = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 0.5
new_element = self.createGUI3DText(
pos=tuple(absolute_position),
text=text,
size=size
)
elif gui_type == "button":
new_element = self.createGUIButton(
pos=tuple(absolute_position),
text=text,
size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0,
command=self.playModelAnimation
)
elif gui_type == "label":
new_element = self.createGUILabel(
pos=tuple(absolute_position),
text=text,
size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0
)
elif gui_type == "entry":
new_element = self.createGUIEntry(
pos=tuple(absolute_position),
placeholder=text,
size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0,
command=self.onGUIEntrySubmit
)
elif gui_type == "2d_image":
scale_value = absolute_scale[0]
print(f"2d_image{scale_value}")
new_element = self.createGUI2DImage(
pos=tuple(absolute_position),
image_path=image_path,
size=absolute_scale
)
elif gui_type == "2d_video_screen":
new_element = self.createGUI2DVideoScreen(
pos=tuple(absolute_position),
video_path=video_path,
size=absolute_scale
)
elif gui_type == "video_screen":
new_element = self.createGUIVideoScreen(
pos=tuple(absolute_position),
video_path=video_path,
size=absolute_scale
)
elif gui_type == "info_panel":
new_element = self.info_panel_manager.onCreateSampleInfoPanel()
if "scripts" in gui_info and new_element:
self.processScripts(new_element,gui_info["scripts"])
except Exception as e:
print(f"重建GUI元素失败 {name}: {e}")
import traceback
traceback.print_exc()
continue
except Exception as e:
print(f"创建GUI元素时出错: {str(e)}")
def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1, command=None):
from direct.gui.DirectGui import DirectButton
button = DirectButton(
text=text,
pos=(pos[0], pos[1], pos[2]), # 保持正确的坐标格式
scale=size, # size 应该是数值而不是元组
frameColor=(0.2, 0.6, 0.8, 1),
text_font=self.getChineseFont() if self.getChineseFont() else None,
rolloverSound=None,
clickSound=None,
parent=None,
command=command
)
def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5):
"""创建3D文本GUI元素"""
try:
# 创建文本节点
text_node = TextNode("gui_3d_text")
text_node.setText(text)
text_node.setAlign(TextNode.ACenter)
# 设置字体(如果可用)
if self.getChineseFont():
text_node.setFont(self.getChineseFont())
# 创建节点路径并添加到场景
text_np = self.render.attachNewNode(text_node)
# 设置位置和大小
text_np.setPos(Vec3(pos[0], pos[1], pos[2]))
text_np.setScale(size)
# 设置面向摄像机
# text_np.setBillboardPointEye()
# 设置渲染属性
text_np.setBin("fixed", 40)
text_np.setDepthWrite(False)
return text_np
except Exception as e:
print(f"❌ 创建3D文本失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08):
from direct.gui.DirectGui import DirectLabel
label = DirectLabel(
text=text,
pos=(pos[0], pos[1], pos[2]),
scale=size,
frameColor=(0, 0, 0, 0),
text_fg=(1, 1, 1, 1),
text_font=self.getChineseFont() if self.getChineseFont() else None,
text_align=TextNode.ACenter,
text_wordwrap=None,
text_mayChange=True,
parent=None
)
def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08, command=None):
from direct.gui.DirectGui import DirectEntry
entry = DirectEntry(
text="",
pos=(pos[0], pos[1], pos[2]),
scale=size,
command=command,
initialText=placeholder,
numLines=1,
width=12,
focus=0,
frameColor=(0, 0, 0, 0),
text_fg=(1, 1, 1, 1),
text_font=self.getChineseFont() if self.getChineseFont() else None,
text_align=TextNode.ACenter,
text_wordwrap=None,
text_mayChange=True,
parent=None,
rolloverSound=None,
clickSound=None,
# 添加焦点管理命令
focusInCommand=self.disableCameraControl,
focusOutCommand=self.enableCameraControl,
# 确保输入框能正确捕获所有键盘事件
suppressKeys=True, # 这个参数很重要,它会阻止按键事件传播到其他处理器
suppressMouse=True
)
return entry
def disableCameraControl(self):
"""禁用相机控制"""
try:
if hasattr(self, 'controller'):
# 如果控制器有内置的禁用方法
if hasattr(self.controller, 'disable'):
self.controller.disable()
else:
# 否则手动禁用事件监听
self.controller.ignoreAll() # 忽略所有已注册的事件
print("相机控制已禁用")
except Exception as e:
print(f"禁用相机控制时出错: {e}")
def enableCameraControl(self):
"""启用相机控制"""
try:
if hasattr(self, 'controller'):
# 如果控制器有内置的启用方法
if hasattr(self.controller, 'enable'):
self.controller.enable()
else:
# 重新设置控制器
self.controller.setup()
print("相机控制已启用")
except Exception as e:
print(f"启用相机控制时出错: {e}")
def onGUIEntrySubmit(self, text, entry_id=None):
"""GUI输入框提交事件处理"""
try:
print(f"GUI输入框提交: {entry_id} = {text}")
# 重新启用相机控制
self.enableCameraControl()
# 清除输入框焦点(如果需要)
# base.win.focus()
# 在这里添加您需要的文本处理逻辑
# 例如保存文本、更新UI等
except Exception as e:
print(f"处理输入框提交时出错: {e}")
import traceback
traceback.print_exc()
def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=0.2):
# 添加属性检查
if not hasattr(self, 'aspect2d'):
print("错误: aspect2d未初始化")
return None
if image_path and not os.path.isabs(image_path):
image_path = self.getResourcePath(image_path)
# 处理非均匀缩放
if isinstance(size, (list, tuple)) and len(size) >= 2:
# 分别处理宽度和高度的缩放
width_scale = size[0] * 0.2
height_scale = size[2] * 0.2
else:
# 如果只提供了一个缩放值,则使用相同值
width_scale = size * 0.1 if isinstance(size, (int, float)) else 0.2
height_scale = width_scale
cm = CardMaker("gui-2d-image")
cm.setFrame(-width_scale, width_scale, -height_scale, height_scale)
#cm.setFrame(-size, size, -size, size)
image_node = self.aspect2d.attachNewNode(cm.generate())
image_node.setPos(pos)
image_node.setBin("fixed", 0)
image_node.setDepthWrite(False)
image_node.setDepthTest(False)
image_node.setColor(1, 1, 1, 1)
# 设置透明度支持
image_node.setTransparency(TransparencyAttrib.MAlpha)
if image_path:
try:
texture = self.loader.loadTexture(image_path)
if texture:
image_node.setTexture(texture, 1)
texture.setWrapU(Texture.WM_clamp)
texture.setWrapV(Texture.WM_clamp)
texture.setMinfilter(Texture.FT_linear)
texture.setMagfilter(Texture.FT_linear)
image_node.setColor(1, 1, 1, 1)
else:
print(f"无法加载图片: {image_path}")
except Exception as e:
print(f"加载图片时出错: {e}")
return image_node
def createGUIVideoScreen(self,pos=(0,0,0),size=1,video_path=None):
import os
from panda3d.core import TransparencyAttrib,Texture,TextureStage
if isinstance(size,(list,tuple)) and len(size) >= 2:
width_scale = size[0]
height_scale = size[2]
else:
width_scale = size * 0.1 if isinstance(size, (int, float)) else 0.2
height_scale = width_scale
cm = CardMaker("gui-video-screen")
cm.setFrame(-width_scale,width_scale,-height_scale,height_scale)
video_screen = self.render.attachNewNode(cm.generate())
video_screen.setPos(pos)
video_screen.setBin("fixed", 0)
#video_screen.setTransparency(TransparencyAttrib.MAlpha)
video_screen.setColor(1, 1, 1, 1)
self._ensureVideoScreenMaterial(video_screen)
if video_path:
if video_path.startswith(("http://", "https://")):
try:
success = self._loadVideoFromURLWithOpenCV3D(video_screen, video_path)
if success:
print("视频已从URL加载成功")
else:
print("从URL加载视频时出错")
except Exception as e:
print(f"从URL加载视频时出错: {e}")
elif os.path.exists(video_path):
movie_texture = self._loadMovieTexture(video_path)
if movie_texture:
video_screen.setTexture(movie_texture, 1)
else:
print(f"视频文件不存在: {video_path}")
else:
# 设置占位符纹理
placeholder_texture = Texture()
placeholder_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb)
placeholder_data = b'\x19\x19\x4c' # 深蓝色占位符
placeholder_texture.setRamImage(placeholder_data)
video_screen.setTexture(placeholder_texture, 1)
return video_screen
def createGUI2DVideoScreen(self, pos=(0, 0, 0), size=0.2, video_path=None):
import os
from direct.gui.DirectGui import DirectFrame
from panda3d.core import TransparencyAttrib, Texture, TextureStage
if isinstance(size,(list,tuple)) and len(size) >= 2:
width_scale = size[0]*0.2
height_scale = size[2]*0.2
else:
width_scale = size*0.1 if isinstance(size,(int,float)) else 0.2
height_scale = width_scale
video_screen = DirectFrame(
frameSize=(-width_scale, width_scale, -height_scale, height_scale),
frameColor=(1, 1, 1, 1),
pos=pos,
parent=None,
suppressMouse=True
)
video_screen.setTransparency(TransparencyAttrib.MAlpha)
placeholder_texture = Texture()
placeholder_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb)
placeholder_data = b'\x19\x19\x4c'
placeholder_texture.setRamImage(placeholder_data)
if video_path:
if video_path.startswith(("http://","https://")):
try:
success = self._loadVideoFromURLWithOpenCV(video_screen,video_path)
if success:
print("视频已从URL加载成功")
else:
print("从URL加载视频时出错")
except Exception as e:
print(f"从URL加载视频时出错: {e}")
elif os.path.exists(video_path):
movie_texture = self._loadMovieTexture(video_path)
if movie_texture:
video_screen["frameTexture"] = movie_texture
return video_screen
def _loadMovieTexture(self, video_path):
from panda3d.core import Texture, MovieTexture, Filename
import os
# Convert Windows path to Panda3D compatible path format
panda_path = Filename.fromOsSpecific(video_path)
converted_path = str(panda_path)
movie_texture = MovieTexture(converted_path)
if movie_texture.read(converted_path):
self._configureVideoTexture(movie_texture)
return movie_texture
def _configureVideoTexture(self, texture):
from panda3d.core import Texture
texture.setWrapU(Texture.WM_clamp)
texture.setWrapV(Texture.WM_clamp)
texture.setMinfilter(Texture.FT_linear)
texture.setMagfilter(Texture.FT_linear)
texture.setFormat(Texture.FRgb8)
if hasattr(texture, 'set_loop') and hasattr(texture, 'set_play_rate'):
texture.set_loop(True)
texture.set_play_rate(1.0)
def _loadVideoFromURLWithOpenCV(self, video_screen, url):
try:
import cv2
import threading
from panda3d.core import Texture,PNMImage
import numpy as np
import time
def video_stream_thread():
cap = cv2.VideoCapture(url)
if not cap.isOpened():
print(f"无法打开视频流: {url}")
return False
cap.set(cv2.CAP_PROP_BUFFERSIZE,1)
fps = cap.get(cv2.CAP_PROP_FPS)
frame_delay = 1.0 / fps if fps > 0 else 0.033 # 默认30fps
while hasattr(self, 'video_stream_active') and self.video_stream_active:
ret, frame = cap.read()
if not ret:
print("视频流读取失败,尝试重新连接...")
cap.release()
time.sleep(1)
cap = cv2.VideoCapture(url)
continue
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))
frame_rgb = cv2.cvtColor(frame,cv2.COLOR_BGR2RGB)
height,width = frame_rgb.shape[:2]
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 = Texture("video_texture")
texture.setMagfilter(Texture.FTLinear)
texture.setMinfilter(Texture.FTLinear)
texture.setWrapU(Texture.WMClamp)
texture.setWrapV(Texture.WMClamp)
texture.load(img)
# 在主线程中更新GUI纹理
def update_texture():
if hasattr(self, 'aspect2d') and not video_screen.isEmpty():
video_screen["frameTexture"] = texture
# 使用taskMgr在主线程中更新纹理
if hasattr(self, 'taskMgr'):
self.taskMgr.doMethodLater(0, lambda task: update_texture() or task.done,
f"updateVideoTexture_{time.time()}")
# 控制帧率
time.sleep(frame_delay)
cap.release()
print("视频流线程结束")
return True
# 启动视频流线程
self.video_stream_active = True
self.video_thread = threading.Thread(target=video_stream_thread, daemon=True)
self.video_thread.start()
return True
except Exception as e:
print(f"加载视频流失败: {e}")
import traceback
traceback.print_exc()
return False
def _loadVideoFromURLWithOpenCV3D(self,video_screen,url):
try:
import cv2
import threading
from panda3d.core import Texture,PNMImage
import time
def video_stream_thread():
cap = cv2.VideoCapture(url)
if not cap.isOpened():
return False
cap.set(cv2.CAP_PROP_BUFFERSIZE,1)
fps = cap.get(cv2.CAP_PROP_FPS)
frame_delay = 1.0 / fps if fps > 0 else 0.033
while hasattr(self,'video_stream_active') and self.video_stream_active:
ret,frame = cap.read()
if not ret:
cap.release()
time.sleep(1)
cap = cv2.VideoCapture(url)
continue
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))
frame_rgb = cv2.cvtColor(frame,cv2.COLOR_BGR2RGB)
height,width = frame_rgb.shape[:2]
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 = Texture("video_texture_3d")
texture.setMagfilter(Texture.FTLinear)
texture.setMinfilter(Texture.FTLinear)
texture.setWrapU(Texture.WMClamp)
texture.setWrapV(Texture.WMClamp)
texture.load(img)
def update_texture():
if hasattr(self,'render') and not video_screen.isEmpty():
video_screen.setTexture(texture,1)
if hasattr(self,'taskMgr'):
self.taskMgr.doMethodLater(0,lambda task:update_texture() or task.done,f"updateVideoTexture_{time.time()}")
time.sleep(frame_delay)
cap.release()
return True
self.video_stream_active = True
self.video_thread_3d = threading.Thread(target=video_stream_thread,daemon=True)
self.video_thread_3d.start()
return True
except Exception as e:
print(f"加载3D视频流失败: {e}")
import traceback
traceback.print_exc()
return False
def _ensureVideoScreenMaterial(self, video_screen):
"""确保视频屏幕有正确的材质设置"""
try:
from panda3d.core import Material, LColor
if not video_screen.hasMaterial():
material = Material(f"video-material-{video_screen.getName()}")
material.setBaseColor(LColor(1, 1, 1, 1))
material.setDiffuse(LColor(1, 1, 1, 1))
material.setAmbient(LColor(1, 1, 1, 1)) # 确保环境光为白色
material.setEmission(LColor(0, 0, 0, 1))
material.setSpecular(LColor(0, 0, 0, 1))
material.setShininess(0)
video_screen.setMaterial(material, 1)
else:
# 更新现有材质确保正确设置
material = video_screen.getMaterial()
material.setBaseColor(LColor(1, 1, 1, 1))
material.setAmbient(LColor(1, 1, 1, 1)) # 确保环境光为白色
video_screen.setMaterial(material, 1)
except Exception as e:
print(f"⚠️ 设置视频屏幕材质时出错: {e}")
def playModelAnimation(self):
"""播放场景中所有 .glb 模型的动画(仅转换有动画的模型为 Actor"""
try:
glb_models = self.render.findAllMatches("**/*.glb*")
# 修复过滤逻辑,确保正确排除碰撞体节点
filtered_models = []
for model in glb_models:
model_name = model.getName().lower()
# 排除碰撞体节点
if ("collision_" not in model_name and
"modelcollision_" not in model_name and
not model_name.endswith("_bounds")):
filtered_models.append(model)
if not filtered_models:
print("⚠️ 场景中没有找到 .glb 模型")
return
print(f"找到 {len(filtered_models)} 个 glb 模型")
self.actors = [] # 存储所有 Actor避免被垃圾回收
for model in filtered_models:
print(f"正在处理模型: {model.getName()}")
actor = None
# 尝试把模型当作Actor加载判断是否有动画
try:
# 在创建Actor之前先检查节点是否可能包含动画
if not self._isValidAnimationNode(model):
print(f"⚠️ {model.getName()} 不是有效的动画节点,跳过")
continue
test_actor = Actor(model)
anim_names = test_actor.getAnimNames()
except Exception as e:
print(f"⚠️ {model.getName()} 无法作为Actor加载: {str(e)}")
anim_names = []
if anim_names: # ✅ 只有有动画才转为Actor
if not isinstance(model, Actor):
model_parent = model.getParent()
model_pos = model.getPos()
model_hpr = model.getHpr()
model_scale = model.getScale()
actor = Actor(model)
actor.reparentTo(model_parent)
actor.setPos(model_pos)
actor.setHpr(model_hpr)
actor.setScale(model_scale)
model.detachNode()
else:
actor = model
# 播放动画
actor.show()
self.actors.append(actor)
print(f"{actor.getName()} 可用动画: {anim_names}")
first_anim = anim_names[0]
actor.stop()
actor.setPlayRate(1.0, first_anim)
actor.loop(first_anim)
print(f"{actor.getName()} 正在播放动画: {first_anim}")
actor.update()
else: # 没有动画
print(f"⚠️ {model.getName()} 没有动画不转为Actor")
except Exception as e:
print(f"播放模型动画时出错: {str(e)}")
import traceback
traceback.print_exc()
def _isValidAnimationNode(self, nodePath):
"""检查节点是否可能是有效的动画节点"""
# 排除明显不是动画节点的节点
name = nodePath.getName().lower()
if ("collision_" in name or
"modelcollision_" in name or
"bound" in name or
name.endswith("_bounds")):
return False
# 检查节点是否有网格数据(简单判断)
from panda3d.core import GeomNode
geom_nodes = nodePath.findAllMatches("**/+GeomNode")
if not geom_nodes:
return False
return True
def focusOnWomenModel(self):
"""定位并聚焦到Women模型"""
try:
women_models = self.render.findAllMatches("**/Women_1.glb*")
if women_models:
model = women_models[0]
# 确保模型可见
model.show()
# 将模型放置在原点附近
model.setPos(0, 0, 0)
model.setScale(1.0)
# 确保是Actor的话设置动画
if isinstance(model, Actor):
anim_names = model.getAnimNames()
if anim_names:
model.loop(anim_names[0])
model.setPlayRate(1.0, anim_names[0])
print(f"为模型设置动画: {anim_names[0]}")
print(f"已定位模型: {model.getName()}")
else:
print("未找到Women模型")
except Exception as e:
print(f"定位模型时出错: {str(e)}")
def processScripts(self, element, script_info_list):
"""处理元素上挂载的脚本 - 使用新的脚本系统"""
try:
print(f"正在为元素 {element.getName()} 挂载脚本")
print(f"可用脚本列表: {self.script_manager.get_available_scripts()}")
if not hasattr(self,'script_manager'):
print("脚本管理器未初始化")
return
for script_info in script_info_list:
script_name = script_info["name"]
script_file = script_info.get("file", "")
if script_name:
script_component = self.script_manager.add_script_to_object(element,script_name)
if script_component:
print(f"✓ 脚本 {script_name} 已挂载到元素 {element.getName()}")
else:
print(f"⚠️ 脚本 {script_name} 挂载失败")
# 列出可用脚本帮助调试
available_scripts = self.script_manager.get_available_scripts()
print(f"当前可用脚本: {available_scripts}")
else:
print(f"⚠️ 脚本信息不完整: {script_info}")
# # 从文件路径中提取脚本类名
# if script_file:
# # 获取脚本文件名(不含路径和扩展名)
# script_filename = os.path.basename(script_file)
# script_class_name = os.path.splitext(script_filename)[0]
# print(f"尝试挂载脚本: {script_class_name} (来自文件: {script_file})")
#
# # 使用脚本管理器为元素添加脚本
# script_component = self.script_manager.add_script_to_object(element, script_class_name)
#
# if script_component:
# print(f"✓ 脚本 {script_class_name} 已挂载到元素 {element.getName()}")
# else:
# print(f"⚠️ 脚本 {script_class_name} 挂载失败")
# # 列出可用脚本帮助调试
# available_scripts = self.script_manager.get_available_scripts()
# print(f"当前可用脚本: {available_scripts}")
# else:
# print(f"⚠️ 脚本信息不完整: {script_name}")
except Exception as e:
print(f"挂载脚本到元素 {element.getName()} 失败: {str(e)}")
import traceback
traceback.print_exc()
def checkDoubleClick(self, nodePath):
"""检查是否为双击,返回布尔值 - 改进版本"""
try:
import time
current_time = time.time()
# 必须是同一节点且在时间阈值内
is_double_click = (self._last_clicked_node is not None and
self._last_clicked_node == nodePath and
nodePath is not None and
current_time - self._last_click_time < self._double_click_threshold)
if is_double_click:
# 双击成功,重置状态
self._last_click_time = 0
self._last_clicked_node = None
print(f"✓ 检测到双击: {nodePath.getName()}")
return True
else:
# 单击,更新状态
self._last_click_time = current_time
self._last_clicked_node = nodePath
return False
except Exception as e:
print(f"双击检测失败: {e}")
return False
def focusCameraOnNode(self, nodePath):
"""带动画效果的聚焦到节点方法"""
try:
if not nodePath or nodePath.isEmpty():
print("无效的节点")
return
minPoint = Point3()
maxPoint = Point3()
if not nodePath.calcTightBounds(minPoint, maxPoint,self.render):
print("无法计算选中节点的边界框,使用节点为位置作为替代方案")
node_pos = nodePath.getPos(self.render)
optimal_distance = 10.0
current_cam_pos = self.cam.getPos()
view_direction = node_pos - current_cam_pos
if view_direction.length()<0.001:
view_direction = Vec3(5,-5,2)
view_direction.normalize()
target_cam_pos = node_pos-(view_direction * optimal_distance)
temp_node = self.render.attachNewNode("temp_lookat_target")
temp_node.setPos(node_pos)
dummy_cam = self.render.attachNewNode("dummy_camera")
dummy_cam.setPos(target_cam_pos)
dummy_cam.lookAt(temp_node)
target_cam_hpr = Vec3(dummy_cam.getHpr())
temp_node.removeNode()
dummy_cam.removeNode()
current_cam_pos = Point3(self.cam.getPos())
current_cam_hpr = Vec3(self.cam.getHpr())
self._startCameraFocusAnimation(current_cam_pos,target_cam_pos,current_cam_hpr,target_cam_hpr)
print(f"开始聚焦到节点(使用位置): {nodePath.getName()}")
return
center = Point3(
(minPoint.x + maxPoint.x)*0.5,
(minPoint.y+maxPoint.y)*0.5,
(minPoint.z+maxPoint.z)*0.5
)
size = (maxPoint - minPoint).length()
if size < 0.01:
size = 1.0
current_cam_pos = Point3(self.cam.getPos())
current_cam_hpr = Vec3(self.cam.getHpr())
view_direction = current_cam_pos - center
if view_direction.length() < 0.001:
view_direction = Vec3(5,-5,2)
view_direction.normalize()
optimal_distance = max(size * 2.5,1.0)
target_cam_pos = center + (view_direction * optimal_distance)
temp_node = self.render.attachNewNode("temp_lookat_target")
temp_node.setPos(center)
dummy_cam = self.render.attachNewNode("dummy_camera")
dummy_cam.setPos(target_cam_pos)
dummy_cam.lookAt(temp_node)
target_cam_hpr = Vec3(dummy_cam.getHpr())
temp_node.removeNode()
dummy_cam.removeNode()
self._startCameraFocusAnimation(current_cam_pos,target_cam_pos,
current_cam_hpr,target_cam_hpr)
print(f"开始聚焦到节点: {nodePath.getName()}")
return True
except Exception as e:
print(f"聚焦相机失败: {str(e)}")
import traceback
traceback.print_exc()
def _startCameraFocusAnimation(self, start_pos, end_pos, start_hpr, end_hpr):
"""启动相机聚焦动画 - 改进版本"""
try:
class CameraFocusData:
def __init__(self, start_pos, end_pos, start_hpr, end_hpr):
self.start_pos = Point3(start_pos)
self.end_pos = Point3(end_pos)
self.start_hpr = Vec3(start_hpr)
self.end_hpr = Vec3(end_hpr)
self.elapsed_time = 0.0
self.duration = 0.5 # 增加动画时间使移动更平滑
self._camera_focus_data = CameraFocusData(start_pos, end_pos, start_hpr, end_hpr)
from direct.task.TaskManagerGlobal import taskMgr
taskMgr.remove("cameraFocusTask")
taskMgr.add(self._cameraFocusTask, "cameraFocusTask")
print(f"开始相机聚焦动画: {start_pos} -> {end_pos}")
except Exception as e:
print(f"相机聚焦动画启动失败: {str(e)}")
def _normalizeAngle(self, angle):
"""规范化角度到-180到180度之间"""
while angle > 180:
angle -= 360
while angle < -180:
angle += 360
return angle
def _cameraFocusTask(self, task):
"""摄像机聚焦动画任务"""
try:
if not hasattr(self, '_camera_focus_data'):
return task.done
data = self._camera_focus_data
from direct.showbase.ShowBaseGlobal import globalClock
dt = globalClock.getDt()
if dt <= 0:
dt = 0.016
data.elapsed_time += dt
t = min(1.0, data.elapsed_time / data.duration)
# 使用平滑插值
smooth_t = t * t * (3 - 2 * t)
# 插值位置和朝向
current_pos = data.start_pos + (data.end_pos - data.start_pos) * smooth_t
current_hpr = data.start_hpr + (data.end_hpr - data.start_hpr) * smooth_t
self.cam.setPos(current_pos)
self.cam.setHpr(current_hpr)
if t >= 1.0:
self.cam.setPos(data.end_pos)
self.cam.setHpr(data.end_hpr)
print(f"聚焦完成: 位置 {data.end_pos}, 朝向 {data.end_hpr}")
if hasattr(self, '_camera_focus_data'):
delattr(self, '_camera_focus_data')
return task.done
return task.cont
except Exception as e:
print(f"摄像机聚焦动画任务失败: {e}")
import traceback
traceback.print_exc()
return task.done
def setupMouseClickHandler(self):
"""设置鼠标点击处理器"""
try:
# 设置鼠标监听
self.accept("mouse1", self.onMouseClick)
# 启用鼠标观察器
if not hasattr(self, 'mouseWatcherNode'):
from panda3d.core import MouseWatcher
self.mouseWatcherNode = MouseWatcher()
except Exception as e:
print(f"设置鼠标点击处理器失败: {e}")
def onMouseClick(self):
"""处理鼠标左键点击"""
try:
if not hasattr(self, 'mouseWatcherNode') or not self.mouseWatcherNode.hasMouse():
return
# 获取鼠标位置
mouse_pos = self.mouseWatcherNode.getMouse()
# 创建射线进行点击检测
from panda3d.core import CollisionRay, CollisionNode, CollisionHandlerQueue, BitMask32
# 创建射线
ray = CollisionRay()
ray.setFromLens(self.camNode, mouse_pos.x, mouse_pos.y)
# 创建碰撞节点
ray_node = CollisionNode('mouseRay')
ray_node.addSolid(ray)
ray_node.setFromCollideMask(BitMask32.allOn())
# 附加到相机
ray_np = self.camera.attachNewNode(ray_node)
# 创建碰撞队列
handler = CollisionHandlerQueue()
# 确保碰撞遍历器存在
if not hasattr(self, 'cTrav'):
self.cTrav = CollisionTraverser()
self.cTrav.addCollider(ray_np, handler)
self.cTrav.traverse(self.render)
# 检查碰撞结果
target_node = None
if handler.getNumEntries() > 0:
# 按距离排序
handler.sortEntries()
# 遍历所有碰撞结果,找到最近的有效节点
for i in range(handler.getNumEntries()):
entry = handler.getEntry(i)
clicked_node = entry.getIntoNodePath()
# 向上遍历找到可选择的父节点
current_node = clicked_node
found_valid_node = False
while current_node and not current_node.isEmpty():
# 检查节点是否应该被选择
if (current_node.hasTag("is_scene_element") and
not current_node.hasTag("is_gizmo") and
current_node.getName() not in ["render", "camera", "ambient_light",
"directional_light", "mouseRay"] and
not current_node.getName().startswith("collision_") and
"ground" not in current_node.getName().lower()):
target_node = current_node
found_valid_node = True
break
# 如果当前节点是碰撞体,获取其父节点作为目标
if current_node.getName().startswith("collision_"):
parent = current_node.getParent()
if (parent.hasTag("is_scene_element") and
not parent.hasTag("is_gizmo") and
parent.getName() not in ["render", "camera", "ambient_light",
"directional_light"] and
"ground" not in parent.getName().lower()):
target_node = parent
found_valid_node = True
break
current_node = current_node.getParent()
if current_node.getName() == "render":
break
if found_valid_node:
break
# 如果找到了目标节点,则进行双击检测
if target_node:
print(f"找到可选择节点: {target_node.getName()}")
print(f"节点位置: {target_node.getPos()}")
print(f"节点边界: {target_node.getBounds()}")
# 检查是否为双击
if self.checkDoubleClick(target_node):
print(f"双击节点: {target_node.getName()}")
self.focusCameraOnNode(target_node)
else:
print(f"单击节点: {target_node.getName()}")
else:
print("未找到有效的目标节点")
# 清理碰撞器
ray_np.removeNode()
except Exception as e:
print(f"处理鼠标点击失败: {e}")
import traceback
traceback.print_exc()\
def createFrameRateDisplay(self):
from direct.gui.DirectGui import DirectLabel
from panda3d.core import TextNode
self.fps_label = DirectLabel(
text="FPS:0",
pos=(-1.8,0,0.9),
scale=0.05,
text_fg=(1,1,1,1),
text_align=TextNode.ALeft,
frameSize=(-0.1,0.3,-0.05,0.05),
frameColor=(0,0,0,0.5),
text_font=self.getChineseFont() if self.getChineseFont() else None,
#parent=self.a2dTopLeft
)
taskMgr.add(self.updateFrameRate,"updateFrameRateTask")
def updateFrameRate(self,task):
try:
fps = globalClock.getAverageFrameRate()
if self.fps_label:
self.fps_label.setText(f"FPS:{fps:.1f}")
except Exception as e:
print(f"更新帧率失败: {e}")
return task.cont
# 在 main.py 的最后部分,修改为:
if __name__ == "__main__":
try:
app = MainApp()
if hasattr(app, 'run'):
app.run()
else:
print("应用程序初始化失败")
except Exception as e:
print(f"应用程序启动失败: {str(e)}")
import traceback
traceback.print_exc()