830 lines
32 KiB
Python
830 lines
32 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""EG packaged project runtime template."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import json
|
|
import os
|
|
import sys
|
|
import traceback
|
|
|
|
from direct.showbase.ShowBase import ShowBase
|
|
from direct.showbase.ShowBaseGlobal import globalClock
|
|
from panda3d.core import (
|
|
CardMaker,
|
|
Filename,
|
|
Material,
|
|
MaterialAttrib,
|
|
MovieTexture,
|
|
Vec4,
|
|
TextNode,
|
|
TransparencyAttrib,
|
|
Vec3,
|
|
load_prc_file_data,
|
|
)
|
|
|
|
PROJECT_NAME = "__EG_PROJECT_NAME__"
|
|
|
|
|
|
def _bootstrap_paths():
|
|
if getattr(sys, "frozen", False):
|
|
project_root = getattr(sys, "_MEIPASS", "") or os.path.join(os.path.dirname(sys.executable), "_internal")
|
|
if not os.path.isdir(project_root):
|
|
project_root = os.path.dirname(sys.executable)
|
|
else:
|
|
source_root = os.path.dirname(os.path.abspath(__file__))
|
|
assets_root = os.path.normpath(os.path.join(source_root, "..", "assets"))
|
|
project_root = assets_root if os.path.isdir(assets_root) else source_root
|
|
|
|
os.chdir(project_root)
|
|
|
|
search_paths = [
|
|
project_root,
|
|
os.path.join(project_root, "third_party"),
|
|
os.path.join(project_root, "RenderPipelineFile"),
|
|
]
|
|
for path in search_paths:
|
|
if os.path.isdir(path) and path not in sys.path:
|
|
sys.path.insert(0, path)
|
|
|
|
return project_root
|
|
|
|
|
|
PROJECT_ROOT = _bootstrap_paths()
|
|
|
|
|
|
class MainApp(ShowBase):
|
|
def __init__(self):
|
|
self.project_path = PROJECT_ROOT
|
|
self.gui_elements = []
|
|
self.chinese_font = None
|
|
self.script_manager = None
|
|
self.render_pipeline = None
|
|
self._movie_textures = []
|
|
self.camera_control_enabled = True
|
|
self.mouse_controller = None
|
|
self.cameraSpeed = 20.0
|
|
self.cameraRotateSpeed = 10.0
|
|
self.lastMouseX = 0
|
|
self.lastMouseY = 0
|
|
self.mouseRightPressed = False
|
|
self.use_ssbo_mouse_picking = True
|
|
self.use_ssbo_scene_import = True
|
|
self.ssbo_runtime_importer = None
|
|
self._ssbo_visible_scene = None
|
|
|
|
load_prc_file_data(
|
|
"",
|
|
f"""
|
|
win-size 1380 750
|
|
window-title {PROJECT_NAME}
|
|
load-display pandagl
|
|
aux-display p3tinydisplay
|
|
sync-video false
|
|
show-frame-rate-meter false
|
|
support-threads false
|
|
""",
|
|
)
|
|
|
|
from rpcore import RenderPipeline
|
|
|
|
self.render_pipeline = RenderPipeline()
|
|
self.render_pipeline.pre_showbase_init()
|
|
ShowBase.__init__(self)
|
|
self.render_pipeline.create(self)
|
|
self.render_pipeline._showbase.camera = self.render_pipeline._showbase.cam
|
|
self.disableMouse()
|
|
|
|
self._load_font()
|
|
self._init_script_manager()
|
|
self._init_ssbo_runtime_importer()
|
|
self._load_camera_state()
|
|
self._init_camera_controller()
|
|
self.load_full_scene()
|
|
self.load_gui_from_json()
|
|
|
|
def _init_script_manager(self):
|
|
script_module = importlib.import_module("core.script_system")
|
|
self.script_manager = script_module.ScriptManager(self)
|
|
self.script_manager.hot_reload_enabled = False
|
|
|
|
scripts_dir = self.get_resource_path("scripts")
|
|
if hasattr(self.script_manager, "set_scripts_directory"):
|
|
self.script_manager.set_scripts_directory(
|
|
scripts_dir,
|
|
create=False,
|
|
reload_scripts=False,
|
|
)
|
|
|
|
self.script_manager.start_system()
|
|
self.script_manager.set_hot_reload_enabled(False)
|
|
|
|
def _load_font(self):
|
|
font_candidates = [
|
|
"C:/Windows/Fonts/msyh.ttc",
|
|
"C:/Windows/Fonts/simhei.ttf",
|
|
]
|
|
for font_path in font_candidates:
|
|
if os.path.exists(font_path):
|
|
try:
|
|
self.chinese_font = self.loader.loadFont(font_path)
|
|
if self.chinese_font:
|
|
print(f"✓ 中文字体加载成功: {font_path}")
|
|
return
|
|
except Exception:
|
|
continue
|
|
print("⚠ 未找到可用中文字体,继续使用默认字体")
|
|
|
|
def _init_ssbo_runtime_importer(self):
|
|
if not (self.use_ssbo_mouse_picking and self.use_ssbo_scene_import):
|
|
return
|
|
try:
|
|
importer_module = importlib.import_module("ssbo_component.runtime_importer")
|
|
importer_cls = getattr(importer_module, "RuntimeSSBOSceneImporter", None)
|
|
if importer_cls:
|
|
self.ssbo_runtime_importer = importer_cls(self)
|
|
print("✓ 运行时 SSBO 场景导入器已启用")
|
|
except Exception as e:
|
|
print(f"⚠ 初始化运行时 SSBO 导入器失败: {e}")
|
|
|
|
def get_chinese_font(self):
|
|
return self.chinese_font
|
|
|
|
def get_resource_path(self, relative_path):
|
|
return os.path.normpath(os.path.join(PROJECT_ROOT, relative_path))
|
|
|
|
def _load_json_file(self, file_path, default_value):
|
|
if not os.path.exists(file_path):
|
|
return default_value
|
|
try:
|
|
with open(file_path, "r", encoding="utf-8-sig") as f:
|
|
return json.load(f)
|
|
except Exception as e:
|
|
print(f"⚠ 读取 JSON 失败 {file_path}: {e}")
|
|
return default_value
|
|
|
|
def _load_camera_state(self):
|
|
camera_state_path = self.get_resource_path("camera_state.json")
|
|
camera_state = self._load_json_file(camera_state_path, {})
|
|
if not isinstance(camera_state, dict):
|
|
camera_state = {}
|
|
|
|
position = camera_state.get("position") or [0, -20, 5]
|
|
rotation = camera_state.get("rotation") or [0, 0, 0]
|
|
self.camera_control_enabled = bool(camera_state.get("camera_control_enabled", True))
|
|
|
|
try:
|
|
if len(position) >= 3:
|
|
self.camera.setPos(position[0], position[1], position[2])
|
|
if len(rotation) >= 3:
|
|
self.camera.setHpr(rotation[0], rotation[1], rotation[2])
|
|
self.cam = self.camera
|
|
except Exception as e:
|
|
print(f"⚠ 恢复主相机状态失败: {e}")
|
|
|
|
def _init_camera_controller(self):
|
|
if not self.camera_control_enabled:
|
|
return
|
|
try:
|
|
controller_module = importlib.import_module("core.CustomMouseController")
|
|
controller_cls = getattr(controller_module, "CustomMouseController", None)
|
|
if not controller_cls:
|
|
return
|
|
|
|
self.mouse_controller = controller_cls(self)
|
|
self.mouse_controller.setUp()
|
|
self.accept("wheel_up", self.wheelForward)
|
|
self.accept("wheel_down", self.wheelBackward)
|
|
except Exception as e:
|
|
print(f"⚠ 初始化主相机控制失败: {e}")
|
|
|
|
def wheelForward(self, _data=None):
|
|
if not self.camera_control_enabled:
|
|
return
|
|
try:
|
|
forward = self.cam.getMat().getRow3(1)
|
|
distance = self.cameraSpeed * globalClock.getDt()
|
|
current_pos = self.cam.getPos()
|
|
self.cam.setPos(current_pos + forward * distance)
|
|
except Exception as e:
|
|
print(f"滚轮前进失败: {e}")
|
|
|
|
def wheelBackward(self, _data=None):
|
|
if not self.camera_control_enabled:
|
|
return
|
|
try:
|
|
forward = self.cam.getMat().getRow3(1)
|
|
distance = self.cameraSpeed * globalClock.getDt()
|
|
current_pos = self.cam.getPos()
|
|
self.cam.setPos(current_pos - forward * distance)
|
|
except Exception as e:
|
|
print(f"滚轮后退失败: {e}")
|
|
|
|
def _resolve_media_path(self, relative_path):
|
|
if not relative_path:
|
|
return ""
|
|
if str(relative_path).startswith(("http://", "https://")):
|
|
return relative_path
|
|
if os.path.isabs(relative_path):
|
|
return relative_path
|
|
return self.get_resource_path(relative_path.replace("/", os.sep))
|
|
|
|
def load_full_scene(self):
|
|
scene_file = self.get_resource_path("scene.bam")
|
|
if not os.path.exists(scene_file):
|
|
print(f"⚠ 未找到场景文件: {scene_file}")
|
|
return
|
|
|
|
metadata_scene = self.loader.loadModel(Filename.fromOsSpecific(scene_file))
|
|
if not metadata_scene:
|
|
print("⚠ 场景文件加载失败")
|
|
return
|
|
|
|
visible_scene = metadata_scene
|
|
use_ssbo_scene = bool(self.ssbo_runtime_importer and self.use_ssbo_scene_import)
|
|
interactive_root_names = self._collect_runtime_interactive_model_names(metadata_scene)
|
|
if use_ssbo_scene:
|
|
try:
|
|
visible_scene = self.ssbo_runtime_importer.load_scene(
|
|
scene_file,
|
|
interactive_root_names=interactive_root_names,
|
|
)
|
|
self._ssbo_visible_scene = visible_scene
|
|
except Exception as e:
|
|
print(f"⚠ 运行时 SSBO 场景导入失败,回退普通加载: {e}")
|
|
traceback.print_exc()
|
|
visible_scene = metadata_scene
|
|
use_ssbo_scene = False
|
|
|
|
if not use_ssbo_scene:
|
|
visible_scene.reparentTo(self.render)
|
|
|
|
self._prepare_scene_for_render_pipeline(visible_scene)
|
|
self.process_scene_elements(metadata_scene, skip_model_nodes=use_ssbo_scene)
|
|
print("✓ 场景加载完成")
|
|
|
|
def _collect_runtime_interactive_model_names(self, metadata_scene):
|
|
interactive_names = set()
|
|
try:
|
|
for node in metadata_scene.find_all_matches("**"):
|
|
if not node.hasTag("is_model_root"):
|
|
continue
|
|
node_name = node.getName()
|
|
if not node_name:
|
|
continue
|
|
|
|
has_runtime_tag = (
|
|
node.hasTag("runtime_interactive")
|
|
and node.getTag("runtime_interactive").lower() == "true"
|
|
)
|
|
has_scripts = (
|
|
node.hasTag("has_scripts")
|
|
and node.getTag("has_scripts").lower() == "true"
|
|
) or node.hasTag("scripts_info")
|
|
|
|
if has_runtime_tag or has_scripts:
|
|
interactive_names.add(node_name)
|
|
except Exception as e:
|
|
print(f"收集运行时交互模型失败: {e}")
|
|
if interactive_names:
|
|
print(f"✓ 运行时交互模型数量: {len(interactive_names)}")
|
|
return interactive_names
|
|
|
|
def _prepare_scene_for_render_pipeline(self, scene):
|
|
self._bake_effective_geom_materials(scene)
|
|
self._ensure_geom_materials(scene)
|
|
|
|
try:
|
|
self.render_pipeline.prepare_scene(scene)
|
|
except Exception as e:
|
|
print(f"⚠ RenderPipeline 场景预处理失败,继续使用原始场景: {e}")
|
|
traceback.print_exc()
|
|
|
|
def _build_default_material(self):
|
|
material = Material()
|
|
material.setAmbient(Vec4(0.2, 0.2, 0.2, 1.0))
|
|
material.setDiffuse(Vec4(0.8, 0.8, 0.8, 1.0))
|
|
material.setSpecular(Vec4(0.0, 0.0, 0.0, 1.0))
|
|
material.setEmission(Vec4(0.0, 0.0, 0.0, 1.0))
|
|
material.setShininess(0.0)
|
|
return material
|
|
|
|
def _ensure_geom_materials(self, scene):
|
|
for geom_np in scene.find_all_matches("**/+GeomNode"):
|
|
try:
|
|
geom_node = geom_np.node()
|
|
except Exception:
|
|
continue
|
|
|
|
fallback_material = None
|
|
try:
|
|
if geom_np.hasMaterial():
|
|
fallback_material = geom_np.getMaterial()
|
|
except Exception:
|
|
fallback_material = None
|
|
|
|
for geom_index in range(geom_node.getNumGeoms()):
|
|
try:
|
|
geom_state = geom_node.getGeomState(geom_index)
|
|
except Exception:
|
|
continue
|
|
|
|
material = None
|
|
try:
|
|
if geom_state.hasAttrib(MaterialAttrib):
|
|
material_attrib = geom_state.getAttrib(MaterialAttrib)
|
|
material = material_attrib.getMaterial() if material_attrib else None
|
|
except Exception:
|
|
material = None
|
|
|
|
if material is None:
|
|
material = fallback_material or self._build_default_material()
|
|
|
|
try:
|
|
geom_node.setGeomState(
|
|
geom_index,
|
|
geom_state.setAttrib(MaterialAttrib.make(material)),
|
|
)
|
|
except Exception:
|
|
continue
|
|
|
|
def _bake_effective_geom_materials(self, scene):
|
|
for geom_np in scene.find_all_matches("**/+GeomNode"):
|
|
try:
|
|
geom_node = geom_np.node()
|
|
net_state = geom_np.getNetState()
|
|
except Exception:
|
|
continue
|
|
|
|
for geom_index in range(geom_node.getNumGeoms()):
|
|
try:
|
|
geom_state = geom_node.getGeomState(geom_index)
|
|
except Exception:
|
|
continue
|
|
|
|
material = None
|
|
try:
|
|
if geom_state.hasAttrib(MaterialAttrib):
|
|
material_attrib = geom_state.getAttrib(MaterialAttrib)
|
|
material = material_attrib.getMaterial() if material_attrib else None
|
|
except Exception:
|
|
material = None
|
|
|
|
if material is None:
|
|
try:
|
|
if net_state.hasAttrib(MaterialAttrib):
|
|
material_attrib = net_state.getAttrib(MaterialAttrib)
|
|
material = material_attrib.getMaterial() if material_attrib else None
|
|
except Exception:
|
|
material = None
|
|
|
|
if material is None:
|
|
try:
|
|
if geom_np.hasMaterial():
|
|
material = geom_np.getMaterial()
|
|
except Exception:
|
|
material = None
|
|
|
|
if material is None:
|
|
continue
|
|
|
|
try:
|
|
geom_node.setGeomState(
|
|
geom_index,
|
|
geom_state.setAttrib(MaterialAttrib.make(material)),
|
|
)
|
|
except Exception:
|
|
continue
|
|
|
|
def process_scene_elements(self, root_node, skip_model_nodes=False):
|
|
processed_lights = set()
|
|
|
|
def walk(node_path):
|
|
self._apply_user_visibility(node_path)
|
|
|
|
is_model_root = node_path.hasTag("is_model_root")
|
|
if skip_model_nodes and is_model_root:
|
|
runtime_target = None
|
|
if self.ssbo_runtime_importer:
|
|
runtime_target = self.ssbo_runtime_importer.get_runtime_node_for_name(node_path.getName())
|
|
if runtime_target is None:
|
|
runtime_target = self._ssbo_visible_scene
|
|
|
|
if node_path.hasTag("scripts_info") and runtime_target:
|
|
try:
|
|
scripts_info = json.loads(node_path.getTag("scripts_info"))
|
|
self.process_scripts(runtime_target, scripts_info)
|
|
except Exception as e:
|
|
print(f"处理 SSBO 模型根脚本失败 {node_path.getName()}: {e}")
|
|
return
|
|
|
|
if node_path.hasTag("scripts_info"):
|
|
try:
|
|
scripts_info = json.loads(node_path.getTag("scripts_info"))
|
|
self.process_scripts(node_path, scripts_info)
|
|
except Exception as e:
|
|
print(f"处理节点脚本失败 {node_path.getName()}: {e}")
|
|
|
|
if node_path.hasTag("light_type") and node_path not in processed_lights:
|
|
if node_path.hasTag("is_auxiliary_light") and node_path.getTag("is_auxiliary_light").lower() == "true":
|
|
return
|
|
|
|
light_type = node_path.getTag("light_type")
|
|
if light_type == "spot_light":
|
|
self._recreate_spot_light(node_path)
|
|
elif light_type == "point_light":
|
|
self._recreate_point_light(node_path)
|
|
processed_lights.add(node_path)
|
|
|
|
for child in node_path.getChildren():
|
|
walk(child)
|
|
|
|
walk(root_node)
|
|
|
|
def _apply_user_visibility(self, node_path):
|
|
if not node_path.hasTag("user_visible"):
|
|
return
|
|
|
|
user_visible = node_path.getTag("user_visible").lower() == "true"
|
|
node_path.setPythonTag("user_visible", user_visible)
|
|
if user_visible:
|
|
node_path.show()
|
|
else:
|
|
node_path.hide()
|
|
|
|
def _recreate_spot_light(self, light_node):
|
|
try:
|
|
from rpcore import SpotLight
|
|
|
|
light = SpotLight()
|
|
light.direction = Vec3(0, 0, -1)
|
|
light.fov = float(light_node.getTag("light_fov")) if light_node.hasTag("light_fov") else 70.0
|
|
light.energy = float(light_node.getTag("light_energy")) if light_node.hasTag("light_energy") else 5000.0
|
|
light.radius = float(light_node.getTag("light_radius")) if light_node.hasTag("light_radius") else 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()}: {e}")
|
|
|
|
def _recreate_point_light(self, light_node):
|
|
try:
|
|
from rpcore import PointLight
|
|
|
|
light = PointLight()
|
|
light.energy = float(light_node.getTag("light_energy")) if light_node.hasTag("light_energy") else 5000.0
|
|
light.radius = float(light_node.getTag("light_radius")) if light_node.hasTag("light_radius") else 1000.0
|
|
light.inner_radius = 0.4
|
|
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()}: {e}")
|
|
|
|
def process_scripts(self, node_path, script_info_list):
|
|
if not self.script_manager:
|
|
return
|
|
|
|
for script_info in script_info_list or []:
|
|
script_name = str(script_info.get("name", "") or "").strip()
|
|
if not script_name:
|
|
continue
|
|
|
|
try:
|
|
if script_name not in self.script_manager.loader.script_classes:
|
|
script_path = ""
|
|
if hasattr(self.script_manager, "resolve_script_path"):
|
|
script_path = self.script_manager.resolve_script_path(script_info)
|
|
if script_path:
|
|
self.script_manager.load_script_from_file(script_path)
|
|
|
|
script_component = self.script_manager.add_script_to_object(node_path, script_name)
|
|
if script_component:
|
|
print(f"✓ 脚本 {script_name} 已挂载到 {node_path.getName()}")
|
|
else:
|
|
print(f"⚠ 脚本 {script_name} 挂载失败")
|
|
except Exception as e:
|
|
print(f"挂载脚本失败 {script_name}: {e}")
|
|
|
|
def load_gui_from_json(self):
|
|
gui_json_path = self.get_resource_path(os.path.join("gui", "gui_elements.json"))
|
|
if not os.path.exists(gui_json_path):
|
|
return
|
|
|
|
gui_data = self._load_json_file(gui_json_path, [])
|
|
if not gui_data:
|
|
return
|
|
self.create_gui_elements(gui_data)
|
|
|
|
def create_gui_elements(self, element_data):
|
|
processed_names = set()
|
|
created_elements = {}
|
|
pending_parent_links = []
|
|
|
|
for index, gui_info in enumerate(element_data or []):
|
|
try:
|
|
gui_type = gui_info.get("type", "unknown")
|
|
name = gui_info.get("name", f"gui_element_{index}")
|
|
if name in processed_names:
|
|
continue
|
|
processed_names.add(name)
|
|
|
|
position = list(gui_info.get("position", [0, 0, 0]))
|
|
scale = list(gui_info.get("scale", [1, 1, 1]))
|
|
parent_name = gui_info.get("parent_name")
|
|
|
|
text = gui_info.get("text", "")
|
|
raw_image_path = str(gui_info.get("image_path", "") or "")
|
|
raw_video_path = str(gui_info.get("video_path", "") or "")
|
|
image_path = self._resolve_media_path(raw_image_path)
|
|
video_path = self._resolve_media_path(raw_video_path)
|
|
|
|
new_element = None
|
|
if gui_type == "3d_text":
|
|
new_element = self.create_gui_3d_text(tuple(position), text, scale[0] if scale else 1.0)
|
|
elif gui_type == "3d_image":
|
|
new_element = self.create_gui_3d_image(tuple(position), image_path, scale)
|
|
elif gui_type == "button":
|
|
new_element = self.create_gui_button(tuple(position), text, scale[0] if scale else 1.0)
|
|
elif gui_type == "label":
|
|
new_element = self.create_gui_label(tuple(position), text, scale[0] if scale else 1.0)
|
|
elif gui_type == "entry":
|
|
new_element = self.create_gui_entry(tuple(position), text, scale[0] if scale else 1.0)
|
|
elif gui_type == "2d_image":
|
|
new_element = self.create_gui_2d_image(tuple(position), image_path, scale)
|
|
elif gui_type == "video_screen":
|
|
new_element = self.create_gui_video_screen(tuple(position), scale, video_path)
|
|
elif gui_type == "2d_video_screen":
|
|
new_element = self.create_gui_2d_video_screen(tuple(position), scale, video_path)
|
|
|
|
if not new_element:
|
|
continue
|
|
|
|
self._apply_gui_metadata(new_element, gui_info, gui_type, text, raw_image_path, raw_video_path)
|
|
self.gui_elements.append(new_element)
|
|
created_elements[name] = new_element
|
|
|
|
if parent_name:
|
|
pending_parent_links.append((new_element, parent_name))
|
|
|
|
if gui_info.get("scripts"):
|
|
self.process_scripts(new_element, gui_info["scripts"])
|
|
except Exception as e:
|
|
print(f"重建 GUI 元素失败: {e}")
|
|
traceback.print_exc()
|
|
|
|
for child, parent_name in pending_parent_links:
|
|
parent = created_elements.get(parent_name)
|
|
if not parent:
|
|
continue
|
|
try:
|
|
child.reparentTo(parent)
|
|
except Exception as e:
|
|
print(f"恢复 GUI 父子关系失败 {parent_name}: {e}")
|
|
|
|
def _apply_gui_metadata(self, node, gui_info, gui_type, text, image_path, video_path):
|
|
try:
|
|
name = gui_info.get("name")
|
|
if name and hasattr(node, "setName"):
|
|
node.setName(name)
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
position = gui_info.get("position", [])
|
|
if len(position) >= 3 and hasattr(node, "setPos"):
|
|
node.setPos(position[0], position[1], position[2])
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
rotation = gui_info.get("rotation", [])
|
|
if len(rotation) >= 3 and hasattr(node, "setHpr"):
|
|
node.setHpr(rotation[0], rotation[1], rotation[2])
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
scale = gui_info.get("scale", [])
|
|
if hasattr(node, "setScale"):
|
|
if isinstance(scale, (list, tuple)):
|
|
if len(scale) >= 3:
|
|
node.setScale(scale[0], scale[1], scale[2])
|
|
elif len(scale) == 1:
|
|
node.setScale(scale[0])
|
|
elif scale:
|
|
node.setScale(scale)
|
|
except Exception:
|
|
pass
|
|
|
|
if hasattr(node, "setTag"):
|
|
node.setTag("gui_type", gui_type)
|
|
node.setTag("saved_gui_type", gui_type)
|
|
node.setTag("is_gui_element", "true")
|
|
if text:
|
|
node.setTag("gui_text", str(text))
|
|
if image_path:
|
|
node.setTag("gui_image_path", str(image_path))
|
|
node.setTag("image_path", str(image_path))
|
|
if video_path:
|
|
node.setTag("video_path", str(video_path))
|
|
for key, value in (gui_info.get("tags") or {}).items():
|
|
try:
|
|
node.setTag(str(key), str(value))
|
|
except Exception:
|
|
continue
|
|
|
|
user_visible = bool(gui_info.get("user_visible", True))
|
|
if hasattr(node, "setPythonTag"):
|
|
node.setPythonTag("user_visible", user_visible)
|
|
if hasattr(node, "show") and hasattr(node, "hide"):
|
|
if user_visible:
|
|
node.show()
|
|
else:
|
|
node.hide()
|
|
|
|
def create_gui_button(self, pos=(0, 0, 0), text="按钮", size=0.1):
|
|
from direct.gui.DirectGui import DirectButton
|
|
|
|
return DirectButton(
|
|
text=text,
|
|
pos=(pos[0], pos[1], pos[2]),
|
|
scale=size,
|
|
frameColor=(0.2, 0.6, 0.8, 1),
|
|
text_font=self.get_chinese_font() or None,
|
|
rolloverSound=None,
|
|
clickSound=None,
|
|
parent=self.aspect2d,
|
|
command=None,
|
|
)
|
|
|
|
def create_gui_label(self, pos=(0, 0, 0), text="标签", size=0.08):
|
|
from direct.gui.DirectGui import DirectLabel
|
|
|
|
return 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.get_chinese_font() or None,
|
|
text_align=TextNode.ACenter,
|
|
text_mayChange=True,
|
|
parent=self.aspect2d,
|
|
)
|
|
|
|
def create_gui_entry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08):
|
|
from direct.gui.DirectGui import DirectEntry
|
|
|
|
return DirectEntry(
|
|
text="",
|
|
pos=(pos[0], pos[1], pos[2]),
|
|
scale=size,
|
|
command=self.on_gui_entry_submit,
|
|
initialText=placeholder,
|
|
numLines=1,
|
|
width=12,
|
|
focus=0,
|
|
frameColor=(0, 0, 0, 0),
|
|
text_fg=(1, 1, 1, 1),
|
|
text_font=self.get_chinese_font() or None,
|
|
text_align=TextNode.ACenter,
|
|
text_mayChange=True,
|
|
parent=self.aspect2d,
|
|
rolloverSound=None,
|
|
clickSound=None,
|
|
suppressKeys=True,
|
|
suppressMouse=True,
|
|
)
|
|
|
|
def on_gui_entry_submit(self, text, *_args):
|
|
print(f"GUI 输入框提交: {text}")
|
|
|
|
def create_gui_2d_image(self, pos=(0, 0, 0), image_path="", size=(1, 1, 1)):
|
|
if isinstance(size, (list, tuple)) and len(size) >= 3:
|
|
width_scale = float(size[0]) * 0.2
|
|
height_scale = float(size[2]) * 0.2
|
|
else:
|
|
scalar = float(size) if isinstance(size, (int, float)) else 0.2
|
|
width_scale = scalar * 0.1
|
|
height_scale = width_scale
|
|
|
|
cm = CardMaker("gui_2d_image")
|
|
cm.setFrame(-width_scale, width_scale, -height_scale, height_scale)
|
|
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.setTransparency(TransparencyAttrib.MAlpha)
|
|
if image_path:
|
|
texture = self.loader.loadTexture(image_path)
|
|
if texture:
|
|
image_node.setTexture(texture, 1)
|
|
return image_node
|
|
|
|
def create_gui_3d_text(self, pos=(0, 0, 0), text="3D文本", size=0.5):
|
|
text_node = TextNode("gui_3d_text")
|
|
text_node.setText(text)
|
|
text_node.setAlign(TextNode.ACenter)
|
|
if self.get_chinese_font():
|
|
text_node.setFont(self.get_chinese_font())
|
|
|
|
text_np = self.render.attachNewNode(text_node)
|
|
text_np.setPos(Vec3(pos[0], pos[1], pos[2]))
|
|
text_np.setScale(size)
|
|
text_np.setBin("fixed", 40)
|
|
text_np.setDepthWrite(False)
|
|
return text_np
|
|
|
|
def create_gui_3d_image(self, pos=(0, 0, 0), image_path="", size=(1, 1, 1)):
|
|
if isinstance(size, (list, tuple)) and len(size) >= 3:
|
|
width_scale = float(size[0])
|
|
height_scale = float(size[2])
|
|
else:
|
|
width_scale = float(size) if isinstance(size, (int, float)) else 1.0
|
|
height_scale = width_scale
|
|
|
|
cm = CardMaker("gui_3d_image")
|
|
cm.setFrame(-width_scale, width_scale, -height_scale, height_scale)
|
|
image_node = self.render.attachNewNode(cm.generate())
|
|
image_node.setPos(pos)
|
|
image_node.setTransparency(TransparencyAttrib.MAlpha)
|
|
if image_path:
|
|
texture = self.loader.loadTexture(image_path)
|
|
if texture:
|
|
image_node.setTexture(texture, 1)
|
|
return image_node
|
|
|
|
def _load_movie_texture(self, name, video_path):
|
|
if not video_path:
|
|
return None
|
|
if str(video_path).startswith(("http://", "https://")):
|
|
print(f"⚠ 当前运行时仅支持本地视频文件: {video_path}")
|
|
return None
|
|
movie_texture = MovieTexture(name)
|
|
if not movie_texture.read(Filename.fromOsSpecific(video_path)):
|
|
print(f"⚠ 无法加载视频: {video_path}")
|
|
return None
|
|
movie_texture.play()
|
|
return movie_texture
|
|
|
|
def create_gui_video_screen(self, pos=(0, 0, 0), size=(1, 1, 1), video_path=""):
|
|
if isinstance(size, (list, tuple)) and len(size) >= 3:
|
|
width_scale = float(size[0])
|
|
height_scale = float(size[2])
|
|
else:
|
|
width_scale = float(size) if isinstance(size, (int, float)) else 1.0
|
|
height_scale = width_scale
|
|
|
|
cm = CardMaker("gui_video_screen")
|
|
cm.setFrame(-width_scale, width_scale, -height_scale, height_scale)
|
|
video_node = self.render.attachNewNode(cm.generate())
|
|
video_node.setPos(pos)
|
|
video_node.setTransparency(TransparencyAttrib.MAlpha)
|
|
|
|
movie_texture = self._load_movie_texture("gui_video_texture_3d", video_path)
|
|
if movie_texture:
|
|
video_node.setTexture(movie_texture, 1)
|
|
video_node.setPythonTag("movie_texture", movie_texture)
|
|
self._movie_textures.append(movie_texture)
|
|
return video_node
|
|
|
|
def create_gui_2d_video_screen(self, pos=(0, 0, 0), size=(1, 1, 1), video_path=""):
|
|
if isinstance(size, (list, tuple)) and len(size) >= 3:
|
|
width_scale = float(size[0]) * 0.2
|
|
height_scale = float(size[2]) * 0.2
|
|
else:
|
|
scalar = float(size) if isinstance(size, (int, float)) else 0.2
|
|
width_scale = scalar * 0.1
|
|
height_scale = width_scale
|
|
|
|
cm = CardMaker("gui_2d_video_screen")
|
|
cm.setFrame(-width_scale, width_scale, -height_scale, height_scale)
|
|
video_node = self.aspect2d.attachNewNode(cm.generate())
|
|
video_node.setPos(pos)
|
|
video_node.setTransparency(TransparencyAttrib.MAlpha)
|
|
video_node.setDepthWrite(False)
|
|
video_node.setDepthTest(False)
|
|
|
|
movie_texture = self._load_movie_texture("gui_video_texture_2d", video_path)
|
|
if movie_texture:
|
|
video_node.setTexture(movie_texture, 1)
|
|
video_node.setPythonTag("movie_texture", movie_texture)
|
|
self._movie_textures.append(movie_texture)
|
|
return video_node
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
app = MainApp()
|
|
app.run()
|
|
except Exception as e:
|
|
print(f"应用程序启动失败: {e}")
|
|
traceback.print_exc()
|