1038 lines
41 KiB
Python
1038 lines
41 KiB
Python
#!/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()
|