#!/usr/bin/env python3 # -*- coding: utf-8 -*- """MetaCore 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, ModelRoot, MovieTexture, NodePath, 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 = {} self._asset_index = { str(asset.get("guid", "") or ""): dict(asset or {}) for asset in (self.runtime_manifest.get("assets", []) or []) if isinstance(asset, dict) and asset.get("guid") } 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() visible_scene = self._build_runtime_scene_from_description(self.runtime_scene) if visible_scene is None or visible_scene.isEmpty(): print("⚠ 运行时场景重建失败") return visible_scene.reparentTo(self.render) self._prepare_scene_for_render_pipeline(visible_scene) self.process_scene_elements( visible_scene, skip_model_nodes=False, runtime_root=visible_scene, ) self.load_gui_from_runtime(self.runtime_scene) print("✓ 场景加载完成") def _build_runtime_scene_from_description(self, runtime_scene): scene_root = NodePath(ModelRoot("RuntimeSceneRoot")) nodes = list(runtime_scene.get("nodes", []) or []) node_lookup = { str(node.get("node_id", "") or ""): dict(node) for node in nodes if node.get("node_id") is not None } keep_nodes = {} for node_id, node in node_lookup.items(): if self._should_keep_runtime_node(node_id, node, node_lookup): keep_nodes[node_id] = node built_nodes = {} for node_id, node in keep_nodes.items(): parent_id = self._resolve_runtime_parent(node, keep_nodes, node_lookup) parent_np = built_nodes.get(parent_id, scene_root) built_np = self._instantiate_runtime_node(node, parent_np) if built_np is not None and not built_np.isEmpty(): built_nodes[node_id] = built_np return scene_root def _should_keep_runtime_node(self, node_id, node, node_lookup): if not node_id: return False if node.get("asset_guid"): return True components = node.get("components", {}) or {} if components.get("light"): return True if node.get("scripts"): return True tags = node.get("tags", {}) or {} if any(key in tags for key in ("is_scene_element", "runtime_interactive", "element_type", "has_scripts")): return True parent_id = node.get("parent_id") while parent_id: parent_node = node_lookup.get(parent_id) if not parent_node: break if parent_node.get("asset_guid"): return False parent_id = parent_node.get("parent_id") return True def _resolve_runtime_parent(self, node, keep_nodes, node_lookup): parent_id = node.get("parent_id") while parent_id: if parent_id in keep_nodes: return parent_id parent_node = node_lookup.get(parent_id) parent_id = parent_node.get("parent_id") if parent_node else None return None def _instantiate_runtime_node(self, node, parent_np): node_name = str(node.get("name", "") or "Node") components = dict(node.get("components", {}) or {}) model_component = dict(components.get("model", {}) or {}) metadata_component = dict(components.get("metadata", {}) or {}) asset_guid = str(model_component.get("asset_guid", "") or node.get("asset_guid", "") or "") imported_node_key = str(model_component.get("imported_node_key", "") or node.get("imported_node_key", "") or "") if asset_guid: loaded_np = self._load_runtime_asset_node(asset_guid, imported_node_key, node_name, node) rebuilt_np = loaded_np if loaded_np and not loaded_np.isEmpty() else parent_np.attachNewNode(node_name) if rebuilt_np.getParent() != parent_np: rebuilt_np.reparentTo(parent_np) rebuilt_np.setName(node_name) else: rebuilt_np = parent_np.attachNewNode(node_name) transform = node.get("transform", {}) or {} position = list(transform.get("position", [0, 0, 0]) or [0, 0, 0]) rotation = list(transform.get("rotation", [0, 0, 0]) or [0, 0, 0]) scale = list(transform.get("scale", [1, 1, 1]) or [1, 1, 1]) if len(position) >= 3: rebuilt_np.setPos(float(position[0]), float(position[1]), float(position[2])) if len(rotation) >= 3: rebuilt_np.setHpr(float(rotation[0]), float(rotation[1]), float(rotation[2])) if len(scale) >= 3: rebuilt_np.setScale(float(scale[0]), float(scale[1]), float(scale[2])) for tag_name, tag_value in (node.get("tags", {}) or {}).items(): if tag_value is None: continue rebuilt_np.setTag(str(tag_name), str(tag_value)) if bool(metadata_component.get("runtime_interactive", node.get("runtime_interactive", False))): rebuilt_np.setTag("runtime_interactive", "true") scripts = list((components.get("scripts", {}) or {}).get("entries", []) or node.get("scripts", []) or []) if scripts: rebuilt_np.setTag("has_scripts", "true") rebuilt_np.setTag("scripts_info", json.dumps(scripts, ensure_ascii=False)) if asset_guid: rebuilt_np.setTag("asset_guid", asset_guid) if imported_node_key: rebuilt_np.setTag("imported_node_key", imported_node_key) return rebuilt_np def _load_runtime_asset_node(self, asset_guid, imported_node_key="", node_name="", node_data=None): asset_record = dict(self._asset_index.get(str(asset_guid or ""), {}) or {}) if not asset_record: return None candidate_paths = [] node_data = dict(node_data or {}) components = dict(node_data.get("components", {}) or {}) metadata_component = dict(components.get("metadata", {}) or {}) node_tags = dict(node_data.get("tags", {}) or {}) has_animations = str( metadata_component.get( "has_animations", metadata_component.get( "saved_has_animations", node_tags.get("has_animations", node_tags.get("saved_has_animations", "")), ), ) or "" ).lower() == "true" can_create_actor = str( metadata_component.get( "can_create_actor_from_memory", metadata_component.get( "saved_can_create_actor_from_memory", node_tags.get( "can_create_actor_from_memory", node_tags.get("saved_can_create_actor_from_memory", ""), ), ), ) or "" ).lower() == "true" asset_dir = os.path.join(DATA_ROOT, "assets", asset_guid) asset_path = str(asset_record.get("asset_path", "") or "").replace("\\", "/").strip() if asset_path: candidate_paths.append(os.path.join(DATA_ROOT, asset_path.replace("/", os.sep))) candidate_paths.append(os.path.join(asset_dir, os.path.basename(asset_path))) imported_cache = dict(asset_record.get("imported_cache", {}) or {}) imported_model_rel = str(imported_cache.get("model_bam", "") or "").replace("\\", "/").strip() if imported_model_rel and not (has_animations or can_create_actor): candidate_paths.append(os.path.join(DATA_ROOT, imported_model_rel.replace("/", os.sep))) imported_model_path = os.path.join(asset_dir, "imported", "model.bam") if os.path.exists(imported_model_path) and not (has_animations or can_create_actor): candidate_paths.append(imported_model_path) for candidate_path in candidate_paths: if not candidate_path or not os.path.exists(candidate_path): continue try: loaded_np = self.loader.loadModel(Filename.fromOsSpecific(candidate_path)) if not loaded_np or loaded_np.isEmpty(): continue if imported_node_key: return self._clone_runtime_subnode(loaded_np, imported_node_key, node_name) return loaded_np except Exception as e: print(f"⚠ 运行时加载资产失败 {candidate_path}: {e}") return None def _clone_runtime_subnode(self, loaded_np, imported_node_key, node_name): imported_node_key = str(imported_node_key or "").strip().strip("/") if not imported_node_key: return loaded_np target_np = loaded_np try: for part in imported_node_key.split("/"): if part == "": continue child_index = int(part) children = list(target_np.getChildren()) if child_index < 0 or child_index >= len(children): return loaded_np target_np = children[child_index] except Exception: return loaded_np try: clone_root = NodePath(ModelRoot(node_name or target_np.getName() or "ImportedNode")) cloned_child = target_np.copyTo(clone_root) cloned_child.wrtReparentTo(clone_root) cloned_child.setName(node_name or target_np.getName()) return cloned_child except Exception: return target_np 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()