411 lines
16 KiB
Python
411 lines
16 KiB
Python
import os
|
|
|
|
from panda3d.core import Filename, GeomNode, Material, MaterialAttrib, NodePath
|
|
|
|
from .ssbo_controller import ObjectController
|
|
|
|
|
|
class RuntimeSSBOSceneImporter:
|
|
"""Minimal runtime-only SSBO scene importer without editor UI dependencies."""
|
|
|
|
def __init__(self, base_app):
|
|
self.base = base_app
|
|
self.controller = None
|
|
self.model = None
|
|
self.source_model = None
|
|
self.source_model_root = None
|
|
self.static_scene_root = None
|
|
self.interactive_scene_root = None
|
|
self.interactive_nodes = {}
|
|
|
|
def load_scene(self, scene_path, interactive_root_names=None):
|
|
source_model = self._load_source_model_from_path(scene_path)
|
|
self._clear_runtime_state()
|
|
|
|
source_root = self._ensure_source_model_root()
|
|
imported_root = source_model.copyTo(source_root)
|
|
self._set_node_name(imported_root, os.path.basename(scene_path) or "scene.bam")
|
|
self.source_model = imported_root
|
|
interactive_root_names = {str(name).strip() for name in (interactive_root_names or []) if str(name).strip()}
|
|
|
|
top_level_children = self._get_scene_top_level_children(imported_root)
|
|
if interactive_root_names and top_level_children:
|
|
self._build_release_split_scene(top_level_children, interactive_root_names)
|
|
return self.interactive_scene_root or self.static_scene_root
|
|
|
|
working_holder = NodePath("ssbo_source_scene_work")
|
|
working_root = source_root.copyTo(working_holder)
|
|
|
|
self.controller = ObjectController()
|
|
self.controller.bake_ids_and_collect(working_root)
|
|
self.model = self.controller.model
|
|
self.model.reparentTo(self.base.render)
|
|
|
|
try:
|
|
if not working_holder.isEmpty():
|
|
working_holder.removeNode()
|
|
except Exception:
|
|
pass
|
|
|
|
return self.model
|
|
|
|
def load_split_scene(self, *, static_scene_path="", interactive_scene_path="", interactive_root_names=None):
|
|
self._clear_runtime_state()
|
|
interactive_root_names = {str(name).strip() for name in (interactive_root_names or []) if str(name).strip()}
|
|
|
|
runtime_root = NodePath("runtime_split_scene")
|
|
loaded_any = False
|
|
|
|
if static_scene_path and os.path.exists(static_scene_path):
|
|
static_source = self._load_source_model_from_path(static_scene_path)
|
|
static_holder = NodePath("ssbo_static_scene_work")
|
|
static_root = static_source.copyTo(static_holder)
|
|
|
|
self.controller = ObjectController()
|
|
self.controller.bake_ids_and_collect(static_root)
|
|
self.model = self.controller.model
|
|
if self.model and not self.model.isEmpty():
|
|
self.model.reparentTo(runtime_root)
|
|
self.static_scene_root = self.model
|
|
loaded_any = True
|
|
|
|
try:
|
|
if not static_holder.isEmpty():
|
|
static_holder.removeNode()
|
|
except Exception:
|
|
pass
|
|
|
|
if interactive_scene_path and os.path.exists(interactive_scene_path):
|
|
interactive_source = self._load_source_model_from_path(interactive_scene_path)
|
|
interactive_root = interactive_source.copyTo(runtime_root)
|
|
self.interactive_scene_root = interactive_root
|
|
loaded_any = True
|
|
for child in self._get_scene_top_level_children(interactive_root):
|
|
child_name = self._get_node_name(child, "")
|
|
if not child_name:
|
|
continue
|
|
if interactive_root_names and child_name not in interactive_root_names:
|
|
continue
|
|
self.interactive_nodes[child_name] = child
|
|
|
|
if not loaded_any:
|
|
try:
|
|
if not runtime_root.isEmpty():
|
|
runtime_root.removeNode()
|
|
except Exception:
|
|
pass
|
|
raise RuntimeError("Failed to load split runtime scene")
|
|
|
|
runtime_root.reparentTo(self.base.render)
|
|
print(
|
|
f"[RuntimeSSBO] 已加载分层 cooked 场景: 静态={'yes' if self.static_scene_root else 'no'}, "
|
|
f"交互对象={len(self.interactive_nodes)}"
|
|
)
|
|
return runtime_root
|
|
|
|
def _clear_runtime_state(self):
|
|
for node in (self.model, self.static_scene_root, self.interactive_scene_root, self.source_model_root):
|
|
try:
|
|
if node and not node.isEmpty():
|
|
node.removeNode()
|
|
except Exception:
|
|
continue
|
|
self.controller = None
|
|
self.model = None
|
|
self.source_model = None
|
|
self.source_model_root = None
|
|
self.static_scene_root = None
|
|
self.interactive_scene_root = None
|
|
self.interactive_nodes = {}
|
|
|
|
def get_runtime_node_for_name(self, node_name):
|
|
return self.interactive_nodes.get(str(node_name or "").strip())
|
|
|
|
def _get_scene_top_level_children(self, imported_root):
|
|
children = []
|
|
try:
|
|
for child in imported_root.getChildren():
|
|
if child and not child.isEmpty():
|
|
children.append(child)
|
|
except Exception:
|
|
children = []
|
|
|
|
if children:
|
|
return children
|
|
return [imported_root] if imported_root and not imported_root.isEmpty() else []
|
|
|
|
def _build_release_split_scene(self, top_level_children, interactive_root_names):
|
|
static_holder = NodePath("runtime_static_scene")
|
|
interactive_holder = NodePath("runtime_interactive_scene")
|
|
interactive_count = 0
|
|
static_count = 0
|
|
|
|
for child in top_level_children:
|
|
child_name = self._get_node_name(child, "")
|
|
if child_name in interactive_root_names:
|
|
runtime_child = child.copyTo(interactive_holder)
|
|
self._set_node_name(runtime_child, child_name)
|
|
self.interactive_nodes[child_name] = runtime_child
|
|
interactive_count += 1
|
|
else:
|
|
child.copyTo(static_holder)
|
|
static_count += 1
|
|
|
|
if static_count:
|
|
self.static_scene_root = static_holder
|
|
self.static_scene_root.reparentTo(self.base.render)
|
|
try:
|
|
self.static_scene_root.flattenStrong()
|
|
except Exception as e:
|
|
print(f"静态场景合批失败,继续使用未合批结果: {e}")
|
|
else:
|
|
try:
|
|
if not static_holder.isEmpty():
|
|
static_holder.removeNode()
|
|
except Exception:
|
|
pass
|
|
|
|
if interactive_count:
|
|
self.interactive_scene_root = interactive_holder
|
|
self.interactive_scene_root.reparentTo(self.base.render)
|
|
else:
|
|
try:
|
|
if not interactive_holder.isEmpty():
|
|
interactive_holder.removeNode()
|
|
except Exception:
|
|
pass
|
|
|
|
print(f"[RuntimeSSBO] 运行时分层导入完成: 交互对象 {interactive_count} 个, 静态合批对象 {static_count} 个")
|
|
|
|
def _load_source_model_from_path(self, model_path):
|
|
source_model = None
|
|
last_error = None
|
|
for fn in self._build_filename_candidates(model_path):
|
|
try:
|
|
source_model = self.base.loader.loadModel(fn)
|
|
if source_model and not source_model.isEmpty():
|
|
break
|
|
except Exception as e:
|
|
last_error = e
|
|
source_model = None
|
|
|
|
if not source_model or source_model.isEmpty():
|
|
if last_error:
|
|
raise RuntimeError(f"Failed to load model '{model_path}': {last_error}")
|
|
raise RuntimeError(f"Failed to load model '{model_path}'")
|
|
|
|
self._fix_black_materials(source_model)
|
|
self._repair_missing_textures(source_model, model_path)
|
|
return source_model
|
|
|
|
def _build_filename_candidates(self, path_text):
|
|
candidates = []
|
|
seen = set()
|
|
for ctor_name in ("fromOsSpecificW", "from_os_specific_w", "fromOsSpecific", "from_os_specific"):
|
|
ctor = getattr(Filename, ctor_name, None)
|
|
if not ctor:
|
|
continue
|
|
try:
|
|
fn = ctor(path_text)
|
|
key = fn.getFullpath() if hasattr(fn, "getFullpath") else fn.get_fullpath()
|
|
if key in seen:
|
|
continue
|
|
seen.add(key)
|
|
candidates.append(fn)
|
|
except Exception:
|
|
continue
|
|
if not candidates:
|
|
try:
|
|
candidates.append(Filename(path_text))
|
|
except Exception:
|
|
pass
|
|
return candidates
|
|
|
|
def _ensure_source_model_root(self):
|
|
root = self.source_model_root
|
|
if root and not root.isEmpty():
|
|
return root
|
|
self.source_model_root = NodePath("ssbo_source_scene_root")
|
|
return self.source_model_root
|
|
|
|
def _set_node_name(self, node, name):
|
|
if not node:
|
|
return
|
|
try:
|
|
node.set_name(name)
|
|
return
|
|
except Exception:
|
|
pass
|
|
try:
|
|
node.setName(name)
|
|
except Exception:
|
|
pass
|
|
|
|
def _get_node_name(self, node, default_name=None):
|
|
if not node:
|
|
return default_name
|
|
try:
|
|
return node.get_name()
|
|
except Exception:
|
|
pass
|
|
try:
|
|
return node.getName()
|
|
except Exception:
|
|
return default_name
|
|
|
|
def _fix_black_materials(self, model):
|
|
try:
|
|
for geom_path in model.findAllMatches("**/+GeomNode"):
|
|
geom_node = geom_path.node()
|
|
node_state = geom_path.getState()
|
|
if node_state.hasAttrib(MaterialAttrib.getClassType()):
|
|
mat_attrib = node_state.getAttrib(MaterialAttrib.getClassType())
|
|
mat = mat_attrib.getMaterial()
|
|
if mat and self._is_dark_material(mat):
|
|
new_mat = Material(mat)
|
|
new_mat.setBaseColor((0.8, 0.8, 0.8, 1.0))
|
|
new_mat.setDiffuse((0.8, 0.8, 0.8, 1.0))
|
|
geom_path.setState(node_state.setAttrib(MaterialAttrib.make(new_mat)))
|
|
|
|
for i in range(geom_node.getNumGeoms()):
|
|
geom_state = geom_node.getGeomState(i)
|
|
if geom_state.hasAttrib(MaterialAttrib.getClassType()):
|
|
mat_attrib = geom_state.getAttrib(MaterialAttrib.getClassType())
|
|
mat = mat_attrib.getMaterial()
|
|
if mat and self._is_dark_material(mat):
|
|
new_mat = Material(mat)
|
|
new_mat.setBaseColor((0.8, 0.8, 0.8, 1.0))
|
|
new_mat.setDiffuse((0.8, 0.8, 0.8, 1.0))
|
|
geom_node.setGeomState(i, geom_state.setAttrib(MaterialAttrib.make(new_mat)))
|
|
else:
|
|
new_mat = Material()
|
|
new_mat.setBaseColor((0.8, 0.8, 0.8, 1.0))
|
|
new_mat.setDiffuse((0.8, 0.8, 0.8, 1.0))
|
|
new_mat.setSpecular((0.2, 0.2, 0.2, 1.0))
|
|
new_mat.setRoughness(0.8)
|
|
geom_node.setGeomState(i, geom_state.addAttrib(MaterialAttrib.make(new_mat)))
|
|
model.clearColor()
|
|
except Exception as e:
|
|
print(f"修复黑色模型材质时出错: {e}")
|
|
|
|
def _is_dark_material(self, material):
|
|
try:
|
|
if material.hasBaseColor():
|
|
c = material.getBaseColor()
|
|
elif material.hasDiffuse():
|
|
c = material.getDiffuse()
|
|
else:
|
|
return True
|
|
return c.x <= 0.05 and c.y <= 0.05 and c.z <= 0.05
|
|
except Exception:
|
|
return False
|
|
|
|
def _repair_missing_textures(self, model_np, model_path):
|
|
if not model_np or model_np.isEmpty():
|
|
return
|
|
|
|
search_dirs = self._build_texture_search_dirs(model_path)
|
|
texture_index = self._index_texture_files(search_dirs)
|
|
|
|
for node in [model_np] + list(model_np.findAllMatches("**")):
|
|
if not node or node.isEmpty():
|
|
continue
|
|
try:
|
|
stages = node.findAllTextureStages()
|
|
stage_count = stages.getNumTextureStages()
|
|
stage_at = stages.getTextureStage
|
|
except Exception:
|
|
continue
|
|
|
|
for i in range(stage_count):
|
|
stage = stage_at(i)
|
|
if not stage:
|
|
continue
|
|
try:
|
|
tex = node.getTexture(stage)
|
|
except Exception:
|
|
tex = None
|
|
if not tex or self._texture_is_valid(tex):
|
|
continue
|
|
|
|
basename = self._extract_texture_basename(tex)
|
|
if not basename:
|
|
continue
|
|
|
|
replacement = texture_index.get(basename.lower())
|
|
if replacement:
|
|
new_tex = self._load_texture_from_path(replacement)
|
|
if new_tex:
|
|
try:
|
|
node.setTexture(stage, new_tex, 1)
|
|
continue
|
|
except Exception:
|
|
pass
|
|
try:
|
|
node.clearTexture(stage)
|
|
except Exception:
|
|
pass
|
|
|
|
def _build_texture_search_dirs(self, model_path):
|
|
dirs = []
|
|
model_dir = os.path.dirname(os.path.abspath(model_path))
|
|
project_root = getattr(self.base, "project_path", "") or os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
def add_dir(path):
|
|
if path and os.path.isdir(path):
|
|
normalized = os.path.normpath(path)
|
|
if normalized not in dirs:
|
|
dirs.append(normalized)
|
|
|
|
add_dir(model_dir)
|
|
for sub in ("textures", "texture", "tex", "assets", "materials"):
|
|
add_dir(os.path.join(model_dir, sub))
|
|
add_dir(os.path.join(project_root, "resources", sub))
|
|
add_dir(os.path.join(project_root, "resources"))
|
|
return dirs
|
|
|
|
def _index_texture_files(self, dirs, limit=30000):
|
|
texture_exts = {".png", ".jpg", ".jpeg", ".tga", ".bmp", ".dds", ".ktx", ".ktx2", ".webp"}
|
|
index = {}
|
|
scanned = 0
|
|
for root_dir in dirs:
|
|
try:
|
|
for root, _, files in os.walk(root_dir):
|
|
for filename in files:
|
|
if os.path.splitext(filename)[1].lower() not in texture_exts:
|
|
continue
|
|
index.setdefault(filename.lower(), os.path.join(root, filename))
|
|
scanned += 1
|
|
if scanned >= limit:
|
|
return index
|
|
except Exception:
|
|
continue
|
|
return index
|
|
|
|
def _load_texture_from_path(self, texture_path):
|
|
for fn in self._build_filename_candidates(texture_path):
|
|
try:
|
|
tex = self.base.loader.loadTexture(fn)
|
|
if tex:
|
|
return tex
|
|
except Exception:
|
|
continue
|
|
return None
|
|
|
|
def _texture_is_valid(self, texture):
|
|
try:
|
|
x_size = texture.getXSize()
|
|
y_size = texture.getYSize()
|
|
return bool(x_size and y_size)
|
|
except Exception:
|
|
return False
|
|
|
|
def _extract_texture_basename(self, texture):
|
|
try:
|
|
path = texture.getFilename().toOsSpecific()
|
|
except Exception:
|
|
path = ""
|
|
if not path:
|
|
return ""
|
|
return os.path.basename(path)
|