1572 lines
62 KiB
Python
1572 lines
62 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
|
||
"""
|
||
打包
|
||
"""
|
||
|
||
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()
|
||
|
||
|