EG/templates/main_template.py
2026-03-18 18:34:44 +08:00

905 lines
35 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,
)
from project.scene_description import normalize_runtime_scene
PROJECT_NAME = "__EG_PROJECT_NAME__"
def _bootstrap_paths():
if getattr(sys, "frozen", False):
bundle_root = getattr(sys, "_MEIPASS", "") or os.path.join(os.path.dirname(sys.executable), "_internal")
if not os.path.isdir(bundle_root):
bundle_root = os.path.dirname(sys.executable)
project_root = bundle_root
else:
source_root = os.path.dirname(os.path.abspath(__file__))
staged_root = os.path.normpath(os.path.join(source_root, ".."))
project_root = staged_root if os.path.isdir(os.path.join(staged_root, "data")) else source_root
data_root = os.path.join(project_root, "data")
os.chdir(data_root if os.path.isdir(data_root) else project_root)
search_paths = [
project_root,
data_root,
os.path.join(data_root, "pipeline"),
os.path.join(project_root, "third_party"),
]
for path in search_paths:
if os.path.isdir(path) and path not in sys.path:
sys.path.insert(0, path)
return project_root, data_root
PROJECT_ROOT, DATA_ROOT = _bootstrap_paths()
class MainApp(ShowBase):
def __init__(self):
self.project_path = DATA_ROOT
self.data_root = DATA_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
self.runtime_manifest = {}
self.runtime_scene = {}
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._load_manifest()
self._init_script_manager()
self._init_ssbo_runtime_importer()
self.load_runtime_scene()
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(DATA_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_manifest(self):
manifest_path = self.get_resource_path("manifest.json")
self.runtime_manifest = self._load_json_file(manifest_path, {})
if not isinstance(self.runtime_manifest, dict):
self.runtime_manifest = {}
def _load_camera_state(self, camera_state=None):
camera_state = dict(camera_state or {})
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 _resolve_startup_scene_runtime_path(self):
startup_scene_guid = self.runtime_manifest.get("startup_scene_guid", "")
for scene_entry in self.runtime_manifest.get("scenes", []) or []:
if scene_entry.get("guid") == startup_scene_guid:
return scene_entry.get("runtime_path", "")
scenes = self.runtime_manifest.get("scenes", []) or []
if scenes:
return scenes[0].get("runtime_path", "")
return ""
def load_runtime_scene(self):
runtime_scene_path = self._resolve_startup_scene_runtime_path()
if not runtime_scene_path:
print("⚠ 未找到启动场景")
return
runtime_scene_file = self.get_resource_path(runtime_scene_path)
self.runtime_scene = normalize_runtime_scene(self._load_json_file(runtime_scene_file, {}))
if not isinstance(self.runtime_scene, dict):
self.runtime_scene = {}
return
scene_components = dict(self.runtime_scene.get("scene_components", {}) or {})
camera_component = dict(scene_components.get("camera", {}) or self.runtime_scene.get("camera", {}) or {})
self._load_camera_state(camera_component)
self._init_camera_controller()
scene_file = self.get_resource_path(self.runtime_scene.get("cooked_scene", ""))
if not os.path.exists(scene_file):
print(f"⚠ 未找到 cooked 场景文件: {scene_file}")
return
metadata_scene = self.loader.loadModel(Filename.fromOsSpecific(scene_file))
if not metadata_scene:
print("⚠ cooked 场景加载失败")
return
cook_info = dict(self.runtime_scene.get("cook", {}) or {})
visible_scene = metadata_scene
use_ssbo_scene = bool(self.ssbo_runtime_importer and self.use_ssbo_scene_import)
interactive_root_names = list(self.runtime_scene.get("interactive_model_names", []) or [])
static_scene_rel = str(cook_info.get("static_scene", "") or "").strip()
interactive_scene_rel = str(cook_info.get("interactive_scene", "") or "").strip()
if use_ssbo_scene:
try:
if static_scene_rel or interactive_scene_rel:
visible_scene = self.ssbo_runtime_importer.load_split_scene(
static_scene_path=self.get_resource_path(static_scene_rel) if static_scene_rel else "",
interactive_scene_path=self.get_resource_path(interactive_scene_rel) if interactive_scene_rel else "",
interactive_root_names=interactive_root_names,
)
else:
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:
split_scene = self._load_split_runtime_scene(cook_info)
if split_scene is not None:
visible_scene = split_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,
runtime_root=None if use_ssbo_scene else visible_scene,
)
self.load_gui_from_runtime(self.runtime_scene)
print("✓ 场景加载完成")
def _load_split_runtime_scene(self, cook_info):
cook_info = dict(cook_info or {})
static_scene_rel = str(cook_info.get("static_scene", "") or "").strip()
interactive_scene_rel = str(cook_info.get("interactive_scene", "") or "").strip()
split_paths = [path for path in (static_scene_rel, interactive_scene_rel) if path]
if not split_paths:
return None
visible_root = self.render.attachNewNode("RuntimeCookedScene")
loaded_any = False
for relative_path in split_paths:
scene_path = self.get_resource_path(relative_path)
if not os.path.exists(scene_path):
continue
try:
split_scene = self.loader.loadModel(Filename.fromOsSpecific(scene_path))
if not split_scene or split_scene.isEmpty():
continue
split_scene.reparentTo(visible_root)
loaded_any = True
except Exception as e:
print(f"⚠ 读取分层 cooked 场景失败 {scene_path}: {e}")
if loaded_any:
print("✓ 已加载分层 cooked 场景")
return visible_root
if not visible_root.isEmpty():
visible_root.removeNode()
return None
def _find_runtime_node_by_name(self, runtime_root, node_name):
if runtime_root is None or runtime_root.isEmpty() or not node_name:
return None
try:
for candidate in runtime_root.find_all_matches("**"):
if candidate.getName() == node_name:
return candidate
except Exception:
return None
return None
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, runtime_root=None):
processed_lights = set()
def walk(node_path):
is_model_root = node_path.hasTag("is_model_root")
runtime_target = None
if is_model_root and runtime_root is not None:
runtime_target = self._find_runtime_node_by_name(runtime_root, node_path.getName())
self._apply_user_visibility(node_path)
if runtime_target is not None:
self._apply_user_visibility(runtime_target)
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(runtime_target or 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_runtime(self, runtime_scene):
gui_data = []
if isinstance(runtime_scene, dict):
scene_components = dict(runtime_scene.get("scene_components", {}) or {})
gui_component = dict(scene_components.get("gui", {}) or {})
gui_data = gui_component.get("elements", []) or runtime_scene.get("gui", []) or []
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()