模型导入成黑色问题修复。打开项目使用SSBO导入模型逻辑流畅运行。
This commit is contained in:
parent
036b68ef41
commit
37b3cc30dc
1
EG
1
EG
@ -1 +0,0 @@
|
||||
Subproject commit 69e2bda47e9713705ad5c45a08b6fc643a2b51f6
|
||||
File diff suppressed because it is too large
Load Diff
@ -389,6 +389,12 @@ class ResourceManager:
|
||||
shutil.copytree(src, dst)
|
||||
else:
|
||||
shutil.copy2(src, dst)
|
||||
if src.suffix.lower() == '.fbx':
|
||||
fbm_src = src.with_name(src.stem + '.fbm')
|
||||
if fbm_src.exists() and fbm_src.is_dir():
|
||||
fbm_dst = destination_root / fbm_src.name
|
||||
if not fbm_dst.exists():
|
||||
shutil.copytree(fbm_src, fbm_dst)
|
||||
imported.append(dst)
|
||||
except Exception as e:
|
||||
errors.append(f"导入失败 {src}: {e}")
|
||||
|
||||
@ -2198,6 +2198,40 @@ class SelectionSystem:
|
||||
"""获取当前选中的节点"""
|
||||
return self.selectedNode
|
||||
|
||||
def deleteSelectedNode(self):
|
||||
"""兼容旧接口:删除当前选中节点。"""
|
||||
node = self.selectedNode
|
||||
if not node or node.isEmpty():
|
||||
return False
|
||||
|
||||
try:
|
||||
if node.getName() == "render":
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
self._deleting_node = True
|
||||
try:
|
||||
# 优先走应用层统一删除链路(命令系统/SSBO清理/消息等)。
|
||||
if hasattr(self.world, "_delete_node") and callable(self.world._delete_node):
|
||||
deleted = bool(self.world._delete_node(node))
|
||||
else:
|
||||
deleted = False
|
||||
scene_manager = getattr(self.world, "scene_manager", None)
|
||||
if scene_manager and hasattr(scene_manager, "models") and node in scene_manager.models:
|
||||
scene_manager.models.remove(node)
|
||||
try:
|
||||
node.removeNode()
|
||||
deleted = True
|
||||
except Exception:
|
||||
deleted = False
|
||||
|
||||
if deleted:
|
||||
self.clearSelection()
|
||||
return deleted
|
||||
finally:
|
||||
self._deleting_node = False
|
||||
|
||||
def hasSelection(self):
|
||||
"""检查是否有选中的节点"""
|
||||
return self.selectedNode is not None
|
||||
|
||||
10
imgui.ini
10
imgui.ini
@ -48,8 +48,8 @@ Collapsed=0
|
||||
DockId=0x00000008,0
|
||||
|
||||
[Window][脚本管理]
|
||||
Pos=1540,20
|
||||
Size=380,390
|
||||
Pos=1653,20
|
||||
Size=267,390
|
||||
Collapsed=0
|
||||
DockId=0x00000003,1
|
||||
|
||||
@ -84,7 +84,7 @@ Size=400,300
|
||||
Collapsed=0
|
||||
|
||||
[Window][选择路径]
|
||||
Pos=660,245
|
||||
Pos=660,254
|
||||
Size=600,500
|
||||
Collapsed=0
|
||||
|
||||
@ -150,8 +150,8 @@ Size=101,226
|
||||
Collapsed=0
|
||||
|
||||
[Window][LUI编辑器]
|
||||
Pos=1628,412
|
||||
Size=292,597
|
||||
Pos=1653,412
|
||||
Size=267,597
|
||||
Collapsed=0
|
||||
DockId=0x00000004,0
|
||||
|
||||
|
||||
4
main.py
4
main.py
@ -26,7 +26,6 @@ from core.Command_System import CommandManager
|
||||
from core.terrain_manager import TerrainManager
|
||||
from scene.scene_manager import SceneManager
|
||||
from project.project_manager import ProjectManager
|
||||
from core.InfoPanelManager import InfoPanelManager
|
||||
from core.collision_manager import CollisionManager
|
||||
from core.CustomMouseController import CustomMouseController
|
||||
from core.resource_manager import ResourceManager
|
||||
@ -78,6 +77,7 @@ class MyWorld(PanelDelegates, CoreWorld):
|
||||
self.accept("F", self.onFocusKeyPressed) # 大写F
|
||||
|
||||
self.use_ssbo_mouse_picking = True
|
||||
self.use_ssbo_scene_import = True
|
||||
if not self.use_ssbo_mouse_picking:
|
||||
self.accept("mouse1", self.onMouseClick)
|
||||
# Keep release/move bindings even in SSBO mode so gizmo drag can work.
|
||||
@ -117,8 +117,6 @@ class MyWorld(PanelDelegates, CoreWorld):
|
||||
self.project_manager = ProjectManager(self)
|
||||
# print(f"[DEBUG] 项目管理系统初始化完成")
|
||||
|
||||
self.info_panel_manager = InfoPanelManager(self)
|
||||
|
||||
self.command_manager = CommandManager()
|
||||
|
||||
# 初始化碰撞管理器
|
||||
|
||||
@ -478,7 +478,6 @@ class ProjectManager:
|
||||
def _copyScriptSystemToBuild(self,build_dir):
|
||||
core_files = [
|
||||
"script_system.py",
|
||||
"InfoPanelManager.py",
|
||||
"CustomMouseController.py"
|
||||
]
|
||||
|
||||
|
||||
@ -74,8 +74,8 @@ class SceneManagerIOMixin:
|
||||
gui_info["parent_name"] = parent_name
|
||||
|
||||
# 收集所有标签(仅对NodePath类型的对象)
|
||||
if hasattr(gui_node, 'getTagNames'):
|
||||
for tag in gui_node.getTagNames():
|
||||
if hasattr(gui_node, 'getTagKeys'):
|
||||
for tag in gui_node.getTagKeys():
|
||||
gui_info["tags"][tag] = gui_node.getTag(tag)
|
||||
elif hasattr(gui_node, 'getTags'): # 对于DirectGUI对象
|
||||
# DirectGUI对象使用不同的方法存储标签
|
||||
@ -219,22 +219,6 @@ class SceneManagerIOMixin:
|
||||
all_nodes.extend(self.Spotlight)
|
||||
all_nodes.extend(self.Pointlight)
|
||||
|
||||
# 添加GUI元素节点
|
||||
gui_elements = []
|
||||
if hasattr(self.world, 'gui_elements'):
|
||||
# 过滤掉空的或重复的GUI元素
|
||||
unique_gui_elements = []
|
||||
seen_names = set()
|
||||
|
||||
for elem in self.world.gui_elements:
|
||||
if elem and not elem.isEmpty():
|
||||
if not elem.isEmpty() and elem.getName() not in seen_names:
|
||||
unique_gui_elements.append(elem)
|
||||
seen_names.add(elem.getName())
|
||||
gui_elements = unique_gui_elements
|
||||
|
||||
print(f"保存时GUI元素列表=>>>>>>>>>>>>{self.world.gui_elements}")
|
||||
all_nodes.extend(gui_elements)
|
||||
|
||||
# 创建用于保存GUI信息的JSON文件路径
|
||||
gui_info_file = filename.replace('.bam', '_gui.json')
|
||||
@ -321,15 +305,6 @@ class SceneManagerIOMixin:
|
||||
node.setTag(f"python_tag_{tag_name}", str(tag_value))
|
||||
except:
|
||||
pass
|
||||
elif node.hasTag("element_type") and node.getTag("element_type") == "info_panel":
|
||||
# 保存信息面板特定信息
|
||||
print(f"保存信息面板信息: {node.getName()}")
|
||||
panel_id = node.getTag("panel_id") if node.hasTag("panel_id") else node.getName()
|
||||
if hasattr(self.world, 'info_panel_manager'):
|
||||
panel_data = self.world.info_panel_manager.serializePanelData(panel_id)
|
||||
if panel_data:
|
||||
import json
|
||||
node.setTag("info_panel_data", json.dumps(panel_data, ensure_ascii=False))
|
||||
|
||||
# 保存模型动画信息
|
||||
elif node.hasTag("is_model_root"):
|
||||
@ -448,11 +423,7 @@ class SceneManagerIOMixin:
|
||||
max_retries = 3
|
||||
try:
|
||||
print(f"\n=== 开始加载场景: {filename} (尝试 {retry_count + 1}/{max_retries + 1}) ===")
|
||||
# print(f"[DEBUG] loadScene 被调用,参数: {filename}")
|
||||
# print(f"[DEBUG] 当前场景中的模型数量: {len(self.models)}")
|
||||
# print(f"[DEBUG] 当前场景中的灯光数量: {len(self.Spotlight) + len(self.Pointlight)}")
|
||||
# print(f"[DEBUG] 当前GUI元素数量: {len(self.world.gui_elements) if hasattr(self.world, 'gui_elements') else 0}")
|
||||
|
||||
|
||||
# 确保文件路径是规范化的
|
||||
filename = os.path.normpath(filename)
|
||||
# print(f"[DEBUG] 规范化后的文件路径: {filename}")
|
||||
@ -553,15 +524,6 @@ class SceneManagerIOMixin:
|
||||
if not light.isEmpty():
|
||||
light.removeNode()
|
||||
self.Pointlight.clear()
|
||||
|
||||
for gui in self.world.gui_elements:
|
||||
if not gui.isEmpty():
|
||||
gui.removeNode()
|
||||
self.world.gui_elements.clear()
|
||||
|
||||
if hasattr(self.world,'info_panel_manager'):
|
||||
self.world.info_panel_manager.removeAllPanels()
|
||||
|
||||
# 清理可能存在的辅助节点
|
||||
self._cleanupAuxiliaryNodes()
|
||||
|
||||
@ -587,18 +549,55 @@ class SceneManagerIOMixin:
|
||||
|
||||
# 4. 使用更安全的加载方式
|
||||
# print(f"[DEBUG] 尝试加载BAM文件...")
|
||||
panda_filename = Filename.fromOsSpecific(filename)
|
||||
# print(f"[DEBUG] Panda3D文件名: {panda_filename}")
|
||||
|
||||
# 设置加载选项
|
||||
from panda3d.core import LoaderOptions
|
||||
loader_options = LoaderOptions()
|
||||
loader_options.setFlags(loader_options.LFNoCache) # 禁用缓存
|
||||
|
||||
scene = self.world.loader.loadModel(panda_filename, loader_options)
|
||||
|
||||
if not scene:
|
||||
print("场景加载失败")
|
||||
|
||||
# 兼容中文路径:优先使用宽字符接口,再回退常规接口。
|
||||
candidate_files = []
|
||||
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(filename)
|
||||
key = fn.get_fullpath()
|
||||
if key not in [c.get_fullpath() for c in candidate_files]:
|
||||
candidate_files.append(fn)
|
||||
except Exception:
|
||||
continue
|
||||
if not candidate_files:
|
||||
candidate_files = [Filename(filename)]
|
||||
|
||||
scene = None
|
||||
last_error = None
|
||||
for panda_filename in candidate_files:
|
||||
try:
|
||||
scene = self.world.loader.loadModel(panda_filename, loader_options)
|
||||
if scene and not scene.isEmpty():
|
||||
break
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
scene = None
|
||||
|
||||
# 极端回退:把场景复制到ASCII路径再加载(规避少数编码问题)
|
||||
if (not scene or scene.isEmpty()) and os.path.exists(filename):
|
||||
try:
|
||||
fallback_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_scene_load_cache")
|
||||
os.makedirs(fallback_dir, exist_ok=True)
|
||||
fallback_file = os.path.join(fallback_dir, "scene_fallback.bam")
|
||||
shutil.copy2(filename, fallback_file)
|
||||
scene = self.world.loader.loadModel(Filename.fromOsSpecific(fallback_file), loader_options)
|
||||
if scene and not scene.isEmpty():
|
||||
print("[SceneLoad] Fallback loaded from ASCII cache path.")
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
|
||||
if not scene or scene.isEmpty():
|
||||
print(f"场景加载失败: {filename}")
|
||||
if last_error:
|
||||
print(f"[SceneLoad] 最后错误: {last_error}")
|
||||
return False
|
||||
|
||||
# print(f"[DEBUG] BAM文件加载成功")
|
||||
@ -640,6 +639,26 @@ class SceneManagerIOMixin:
|
||||
|
||||
#存储所有加载的节点,用于后续处理父子关系
|
||||
loaded_nodes = {} #name->nodePath映射
|
||||
use_ssbo_scene_import = bool(
|
||||
getattr(self.world, "use_ssbo_mouse_picking", False) and
|
||||
getattr(self.world, "use_ssbo_scene_import", False) and
|
||||
getattr(self.world, "ssbo_editor", None) and
|
||||
callable(getattr(self.world, "_import_model_for_runtime", None))
|
||||
)
|
||||
ssbo_scene_model = None
|
||||
if use_ssbo_scene_import:
|
||||
try:
|
||||
print(f"[SSBO] 打开项目使用统一导入链路: {filename}")
|
||||
ssbo_scene_model = self.world._import_model_for_runtime(filename)
|
||||
if ssbo_scene_model and not ssbo_scene_model.isEmpty():
|
||||
if ssbo_scene_model not in self.models:
|
||||
self.models.append(ssbo_scene_model)
|
||||
else:
|
||||
print("[SSBO] 统一导入未返回有效模型,回退旧流程。")
|
||||
use_ssbo_scene_import = False
|
||||
except Exception as e:
|
||||
print(f"[SSBO] 统一导入失败,回退旧流程: {e}")
|
||||
use_ssbo_scene_import = False
|
||||
|
||||
# 遍历场景中的所有节点
|
||||
def processNode(nodePath, depth=0):
|
||||
@ -700,6 +719,7 @@ class SceneManagerIOMixin:
|
||||
|
||||
if is_scene_element or is_potential_gui:
|
||||
print(f"{indent}找到场景元素节点: {nodePath.getName()}")
|
||||
is_model_root = nodePath.hasTag("is_model_root")
|
||||
|
||||
# 如果是潜在的GUI元素但没有标签,添加基本标签
|
||||
if is_potential_gui and not (nodePath.hasTag("gui_type") or nodePath.hasTag("is_gui_element")):
|
||||
@ -721,9 +741,7 @@ class SceneManagerIOMixin:
|
||||
else:
|
||||
nodePath.setTag("gui_type", "unknown")
|
||||
|
||||
# 清除现有材质状态
|
||||
nodePath.clearMaterial()
|
||||
nodePath.clearColor()
|
||||
# 保留原始材质/颜色状态,避免在场景恢复阶段造成黑模。
|
||||
|
||||
# 恢复变换信息
|
||||
def parseVec3(vec_str):
|
||||
@ -769,7 +787,11 @@ class SceneManagerIOMixin:
|
||||
else:
|
||||
nodePath.hide()
|
||||
|
||||
if nodePath.hasTag("has_scripts") and nodePath.getTag("has_scripts") == "true":
|
||||
if (
|
||||
nodePath.hasTag("has_scripts")
|
||||
and nodePath.getTag("has_scripts") == "true"
|
||||
and not (use_ssbo_scene_import and is_model_root)
|
||||
):
|
||||
if hasattr(self.world,'script_manager') and self.world.script_manager:
|
||||
try:
|
||||
import json
|
||||
@ -824,40 +846,41 @@ class SceneManagerIOMixin:
|
||||
except:
|
||||
return Vec4(1, 1, 1, 1)
|
||||
|
||||
# 创建并恢复材质
|
||||
material = Material()
|
||||
material_changed = False
|
||||
if not is_model_root:
|
||||
# 创建并恢复材质
|
||||
material = Material()
|
||||
material_changed = False
|
||||
|
||||
if nodePath.hasTag("material_ambient"):
|
||||
material.setAmbient(parseColor(nodePath.getTag("material_ambient")))
|
||||
material_changed = True
|
||||
if nodePath.hasTag("material_ambient"):
|
||||
material.setAmbient(parseColor(nodePath.getTag("material_ambient")))
|
||||
material_changed = True
|
||||
|
||||
if nodePath.hasTag("material_diffuse"):
|
||||
material.setDiffuse(parseColor(nodePath.getTag("material_diffuse")))
|
||||
material_changed = True
|
||||
if nodePath.hasTag("material_diffuse"):
|
||||
material.setDiffuse(parseColor(nodePath.getTag("material_diffuse")))
|
||||
material_changed = True
|
||||
|
||||
if nodePath.hasTag("material_specular"):
|
||||
material.setSpecular(parseColor(nodePath.getTag("material_specular")))
|
||||
material_changed = True
|
||||
if nodePath.hasTag("material_specular"):
|
||||
material.setSpecular(parseColor(nodePath.getTag("material_specular")))
|
||||
material_changed = True
|
||||
|
||||
if nodePath.hasTag("material_emission"):
|
||||
material.setEmission(parseColor(nodePath.getTag("material_emission")))
|
||||
material_changed = True
|
||||
if nodePath.hasTag("material_emission"):
|
||||
material.setEmission(parseColor(nodePath.getTag("material_emission")))
|
||||
material_changed = True
|
||||
|
||||
if nodePath.hasTag("material_shininess"):
|
||||
material.setShininess(float(nodePath.getTag("material_shininess")))
|
||||
material_changed = True
|
||||
if nodePath.hasTag("material_shininess"):
|
||||
material.setShininess(float(nodePath.getTag("material_shininess")))
|
||||
material_changed = True
|
||||
|
||||
if nodePath.hasTag("material_basecolor"):
|
||||
material.setBaseColor(parseColor(nodePath.getTag("material_basecolor")))
|
||||
material_changed = True
|
||||
if nodePath.hasTag("material_basecolor"):
|
||||
material.setBaseColor(parseColor(nodePath.getTag("material_basecolor")))
|
||||
material_changed = True
|
||||
|
||||
if material_changed:
|
||||
nodePath.setMaterial(material)
|
||||
if material_changed:
|
||||
nodePath.setMaterial(material)
|
||||
|
||||
# 恢复颜色属性
|
||||
if nodePath.hasTag("color"):
|
||||
nodePath.setColor(parseColor(nodePath.getTag("color")))
|
||||
# 恢复颜色属性
|
||||
if nodePath.hasTag("color"):
|
||||
nodePath.setColor(parseColor(nodePath.getTag("color")))
|
||||
|
||||
# 处理特定类型的节点
|
||||
if nodePath.hasTag("light_type"):
|
||||
@ -893,32 +916,47 @@ class SceneManagerIOMixin:
|
||||
# 这里我们先保持原挂载关系
|
||||
pass
|
||||
else:
|
||||
if use_ssbo_scene_import and is_model_root:
|
||||
# SSBO 模式下模型节点由新导入链路接管,这里不直接挂载。
|
||||
pass
|
||||
# 其他节点确保挂载到render下
|
||||
if nodePath.getParent() != self.world.render and not nodePath.getName() in ["render",
|
||||
"aspect2d",
|
||||
"render2d"]:
|
||||
elif nodePath.getParent() != self.world.render and not nodePath.getName() in ["render",
|
||||
"aspect2d",
|
||||
"render2d"]:
|
||||
nodePath.wrtReparentTo(self.world.render)
|
||||
|
||||
# 为模型节点设置碰撞检测
|
||||
if nodePath.hasTag("is_model_root"):
|
||||
if is_model_root:
|
||||
print(f"J{indent}处理模型节点{nodePath.getName()}")
|
||||
|
||||
#self._validateAndFixAllTransforms(nodePath)
|
||||
|
||||
self._fixModelStructure(nodePath)
|
||||
|
||||
# 恢复模型动画信息
|
||||
self._restoreModelAnimationInfo(nodePath)
|
||||
|
||||
# 检测并处理模型动画(类似property_panel.py中的逻辑)
|
||||
self._processModelAnimations(nodePath)
|
||||
|
||||
# if self.world.property_panel._hasCollision(nodePath):
|
||||
# print(f"{indent}模型{nodePath.getName()}已有碰撞体,跳过碰撞体设置")
|
||||
# else:
|
||||
# print(f"{indent}为模型{nodePath.getName()}设置碰撞检测")
|
||||
# self.setupCollision(nodePath)
|
||||
self.models.append(nodePath)
|
||||
if use_ssbo_scene_import:
|
||||
# SSBO 模式下整个 scene.bam 已通过统一导入链路载入,
|
||||
# 这里跳过逐模型旧导入逻辑,避免与菜单导入路径不一致。
|
||||
pass
|
||||
else:
|
||||
#self._validateAndFixAllTransforms(nodePath)
|
||||
self._fixModelStructure(nodePath)
|
||||
self._restoreModelAnimationInfo(nodePath)
|
||||
self._processModelAnimations(nodePath)
|
||||
try:
|
||||
ssbo_editor = getattr(self.world, "ssbo_editor", None)
|
||||
repair_fn = getattr(ssbo_editor, "_repair_missing_textures", None) if ssbo_editor else None
|
||||
if callable(repair_fn):
|
||||
model_path = ""
|
||||
for tag_name in ("model_path", "saved_model_path", "original_path", "file"):
|
||||
if nodePath.hasTag(tag_name):
|
||||
candidate = nodePath.getTag(tag_name).strip()
|
||||
if candidate:
|
||||
model_path = candidate
|
||||
break
|
||||
repair_fn(nodePath, model_path or filename)
|
||||
except Exception as e:
|
||||
print(f"[SceneLoad] 贴图修复失败: {e}")
|
||||
# if self.world.property_panel._hasCollision(nodePath):
|
||||
# print(f"{indent}模型{nodePath.getName()}已有碰撞体,跳过碰撞体设置")
|
||||
# else:
|
||||
# print(f"{indent}为模型{nodePath.getName()}设置碰撞检测")
|
||||
# self.setupCollision(nodePath)
|
||||
self.models.append(nodePath)
|
||||
|
||||
# 递归处理子节点
|
||||
for child in nodePath.getChildren():
|
||||
@ -927,6 +965,8 @@ class SceneManagerIOMixin:
|
||||
print("\n开始处理场景节点...")
|
||||
processNode(scene)
|
||||
|
||||
# SSBO 模式下模型已在前面统一导入;若失败已自动回退旧流程。
|
||||
|
||||
#处理父子关系 - 在所有节点加载完成后设置正确的父子关系
|
||||
print("\n开始重建父子关系...")
|
||||
self._rebuildParentChildRelationships(loaded_nodes)
|
||||
@ -964,13 +1004,6 @@ class SceneManagerIOMixin:
|
||||
#self.updateSceneTree()
|
||||
#self._get_tree_widget().create_model_items(scene)
|
||||
|
||||
print(f"加载完成,GUI元素数量: {len(self.world.gui_elements)}")
|
||||
if len(self.world.gui_elements) > 0:
|
||||
print("GUI元素列表:")
|
||||
for i, elem in enumerate(self.world.gui_elements):
|
||||
print(
|
||||
f" {i + 1}. {elem.getName()} (类型: {elem.getTag('gui_type') if elem.hasTag('gui_type') else 'unknown'})")
|
||||
|
||||
print("=== 场景加载完成 ===\n")
|
||||
return True
|
||||
|
||||
|
||||
@ -61,11 +61,8 @@ class SceneManagerModelMixin:
|
||||
model_name = "imported_model"
|
||||
model.setName(model_name)
|
||||
|
||||
# 设置默认颜色以避免get_color警告
|
||||
try:
|
||||
model.setColor(0.8, 0.8, 0.8, 1.0) # 设置为中性灰
|
||||
except:
|
||||
pass # 如果设置颜色失败,继续执行
|
||||
# 移除统一设置颜色的代码,因为它可能覆盖PBR纹理并导致模型在RenderPipeline中渲染异常纯黑
|
||||
# # model.setColor(0.8, 0.8, 0.8, 1.0) # 移除以防覆盖PBR纹理
|
||||
|
||||
# 将模型添加到场景
|
||||
model.reparentTo(self.world.render)
|
||||
@ -140,7 +137,7 @@ class SceneManagerModelMixin:
|
||||
|
||||
# 创建并设置基础材质
|
||||
#print("\n=== 开始设置材质 ===")
|
||||
#self._applyMaterialsToModel(model)
|
||||
self._fixBlackMaterials(model)
|
||||
|
||||
# 设置碰撞检测(重要!用于选择功能)
|
||||
print("\n=== 设置碰撞检测 ===")
|
||||
@ -467,6 +464,71 @@ class SceneManagerModelMixin:
|
||||
print(f"应用材质时出错: {e}")
|
||||
print("=== 材质设置完成 ===\n")
|
||||
|
||||
|
||||
def _fixBlackMaterials(self, model):
|
||||
# 修复模型中全黑的材质问题,同时为缺失材质的几何体添加默认材质,保留原有纹理
|
||||
try:
|
||||
from panda3d.core import MaterialAttrib, Material, GeomNode
|
||||
|
||||
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:
|
||||
is_black = False
|
||||
if mat.hasBaseColor():
|
||||
c = mat.getBaseColor()
|
||||
if c.x <= 0.05 and c.y <= 0.05 and c.z <= 0.05:
|
||||
is_black = True
|
||||
elif mat.hasDiffuse():
|
||||
c = mat.getDiffuse()
|
||||
if c.x <= 0.05 and c.y <= 0.05 and c.z <= 0.05:
|
||||
is_black = True
|
||||
|
||||
if is_black or not (mat.hasBaseColor() or mat.hasDiffuse()):
|
||||
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:
|
||||
is_black = False
|
||||
if mat.hasBaseColor():
|
||||
c = mat.getBaseColor()
|
||||
if c.x <= 0.05 and c.y <= 0.05 and c.z <= 0.05:
|
||||
is_black = True
|
||||
elif mat.hasDiffuse():
|
||||
c = mat.getDiffuse()
|
||||
if c.x <= 0.05 and c.y <= 0.05 and c.z <= 0.05:
|
||||
is_black = True
|
||||
|
||||
if is_black or not (mat.hasBaseColor() or mat.hasDiffuse()):
|
||||
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 _adjustModelToGround(self, model):
|
||||
"""智能调整模型到地面,但保持原有缩放结构"""
|
||||
try:
|
||||
@ -917,15 +979,27 @@ class SceneManagerModelMixin:
|
||||
from direct.actor.Actor import Actor
|
||||
from panda3d.core import Filename
|
||||
|
||||
candidate_paths = [model_path]
|
||||
candidate_paths.append(Filename.from_os_specific(model_path).get_fullpath())
|
||||
candidate_paths = []
|
||||
# Always prefer normalized Panda paths; avoid raw Windows path fallback,
|
||||
# which triggers noisy loader(error) logs for some absolute/Unicode paths.
|
||||
try:
|
||||
normalized = util.normalize_model_path(model_path)
|
||||
if normalized:
|
||||
if normalized and normalized != model_path:
|
||||
candidate_paths.append(normalized)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for ctor_name in ("fromOsSpecificW", "from_os_specific_w", "fromOsSpecific", "from_os_specific"):
|
||||
ctor = getattr(Filename, ctor_name, None)
|
||||
if not ctor:
|
||||
continue
|
||||
try:
|
||||
panda_path = ctor(model_path).get_fullpath()
|
||||
if panda_path:
|
||||
candidate_paths.append(panda_path)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
seen = set()
|
||||
unique_paths = []
|
||||
for p in candidate_paths:
|
||||
|
||||
@ -56,16 +56,29 @@ class CrossPlatformPathHandler:
|
||||
exists = os.path.exists(filepath)
|
||||
return exists
|
||||
|
||||
def _panda3d_normalize(self, filepath):
|
||||
"""使用Panda3D标准化路径"""
|
||||
try:
|
||||
panda_filename = Filename.from_os_specific(filepath)
|
||||
normalized_path = panda_filename.get_fullpath()
|
||||
print(f"✓ Panda3D标准化: {normalized_path}")
|
||||
return normalized_path
|
||||
except Exception as e:
|
||||
print(f"⚠️ Panda3D标准化失败: {e}")
|
||||
return filepath
|
||||
def _panda3d_normalize(self, filepath):
|
||||
"""使用Panda3D标准化路径"""
|
||||
for ctor_name in ("fromOsSpecificW", "from_os_specific_w", "fromOsSpecific", "from_os_specific"):
|
||||
ctor = getattr(Filename, ctor_name, None)
|
||||
if not ctor:
|
||||
continue
|
||||
try:
|
||||
panda_filename = ctor(filepath)
|
||||
normalized_path = panda_filename.get_fullpath()
|
||||
if normalized_path:
|
||||
print(f"✓ Panda3D标准化: {normalized_path}")
|
||||
return normalized_path
|
||||
except Exception:
|
||||
continue
|
||||
try:
|
||||
panda_filename = Filename(filepath)
|
||||
normalized_path = panda_filename.get_fullpath()
|
||||
if normalized_path:
|
||||
print(f"✓ Panda3D标准化: {normalized_path}")
|
||||
return normalized_path
|
||||
except Exception as e:
|
||||
print(f"⚠️ Panda3D标准化失败: {e}")
|
||||
return filepath
|
||||
|
||||
def _attempt_path_fixes(self, filepath):
|
||||
"""尝试各种路径修复方法"""
|
||||
@ -293,4 +306,4 @@ def normalize_model_path(filepath):
|
||||
|
||||
def suggest_file_placement(filename):
|
||||
"""便捷函数:建议文件放置位置"""
|
||||
return path_handler.suggest_file_placement(filename)
|
||||
return path_handler.suggest_file_placement(filename)
|
||||
|
||||
@ -11,7 +11,7 @@ from panda3d.core import (
|
||||
TransparencyAttrib, BoundingSphere, NodePath,
|
||||
GraphicsEngine, WindowProperties, FrameBufferProperties,
|
||||
GraphicsPipe, GraphicsOutput, Camera, DisplayRegion, OrthographicLens,
|
||||
BoundingBox, BitMask32
|
||||
BoundingBox, BitMask32, Material, MaterialAttrib, ColorAttrib, TextureAttrib, PNMImage
|
||||
)
|
||||
|
||||
# p3dimgui.backend first tries `from shaders import *`, which can be shadowed by
|
||||
@ -213,11 +213,87 @@ class SSBOEditor:
|
||||
io.fonts.clear()
|
||||
io.fonts.add_font_default()
|
||||
|
||||
def _fixBlackMaterials(self, model):
|
||||
try:
|
||||
from panda3d.core import MaterialAttrib, Material, GeomNode
|
||||
|
||||
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:
|
||||
is_black = False
|
||||
if mat.hasBaseColor():
|
||||
c = mat.getBaseColor()
|
||||
if c.x <= 0.05 and c.y <= 0.05 and c.z <= 0.05:
|
||||
is_black = True
|
||||
elif mat.hasDiffuse():
|
||||
c = mat.getDiffuse()
|
||||
if c.x <= 0.05 and c.y <= 0.05 and c.z <= 0.05:
|
||||
is_black = True
|
||||
|
||||
if is_black or not (mat.hasBaseColor() or mat.hasDiffuse()):
|
||||
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:
|
||||
is_black = False
|
||||
if mat.hasBaseColor():
|
||||
c = mat.getBaseColor()
|
||||
if c.x <= 0.05 and c.y <= 0.05 and c.z <= 0.05:
|
||||
is_black = True
|
||||
elif mat.hasDiffuse():
|
||||
c = mat.getDiffuse()
|
||||
if c.x <= 0.05 and c.y <= 0.05 and c.z <= 0.05:
|
||||
is_black = True
|
||||
|
||||
if is_black or not (mat.hasBaseColor() or mat.hasDiffuse()):
|
||||
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 load_model(self, model_path):
|
||||
"""Load and process a model using hybrid static/dynamic chunks."""
|
||||
print(f"[SSBOEditor] Loading model: {model_path}")
|
||||
fn = Filename.fromOsSpecific(model_path)
|
||||
source_model = self.base.loader.loadModel(fn)
|
||||
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.is_empty():
|
||||
break
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
source_model = None
|
||||
if not source_model or source_model.is_empty():
|
||||
if last_error:
|
||||
raise RuntimeError(f"Failed to load model '{model_path}': {last_error}")
|
||||
raise RuntimeError(f"Failed to load model '{model_path}'")
|
||||
self._fixBlackMaterials(source_model)
|
||||
self._repair_missing_textures(source_model, model_path)
|
||||
model_name = os.path.basename(model_path)
|
||||
if model_name:
|
||||
source_model.set_name(model_name)
|
||||
@ -241,6 +317,545 @@ class SSBOEditor:
|
||||
|
||||
print(f"[SSBOEditor] Model loaded. Total objects: {count}")
|
||||
|
||||
def _build_filename_candidates(self, path_text):
|
||||
"""Build Filename candidates with wide-char first for Windows CJK paths."""
|
||||
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.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 _load_texture_from_path(self, texture_path):
|
||||
"""Load texture with robust path constructors."""
|
||||
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 _build_texture_search_dirs(self, model_path):
|
||||
"""Build candidate directories for missing texture recovery."""
|
||||
dirs = []
|
||||
model_dir = os.path.dirname(os.path.abspath(model_path))
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
def add_dir(path):
|
||||
if not path:
|
||||
return
|
||||
path = os.path.normpath(path)
|
||||
if path in dirs:
|
||||
return
|
||||
if os.path.isdir(path):
|
||||
dirs.append(path)
|
||||
|
||||
add_dir(model_dir)
|
||||
|
||||
try:
|
||||
if os.path.isdir(model_dir):
|
||||
for item in os.listdir(model_dir):
|
||||
if item.lower().endswith('.fbm'):
|
||||
fbm_dir = os.path.join(model_dir, item)
|
||||
if os.path.isdir(fbm_dir):
|
||||
add_dir(fbm_dir)
|
||||
for sub in ("textures", "texture", "tex", "assets", "materials"):
|
||||
add_dir(os.path.join(fbm_dir, sub))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for sub in ("textures", "texture", "tex", "assets", "materials"):
|
||||
add_dir(os.path.join(model_dir, sub))
|
||||
|
||||
parent = model_dir
|
||||
for _ in range(2):
|
||||
parent = os.path.dirname(parent)
|
||||
if not parent:
|
||||
break
|
||||
for sub in ("textures", "texture", "tex", "assets", "materials"):
|
||||
add_dir(os.path.join(parent, sub))
|
||||
|
||||
add_dir(os.path.join(project_root, "Resources"))
|
||||
add_dir(os.path.join(project_root, "Resources", "textures"))
|
||||
add_dir(os.path.join(project_root, "Resources", "materials"))
|
||||
add_dir(os.path.join(project_root, "Resources", "models"))
|
||||
return dirs
|
||||
|
||||
def _index_texture_files(self, dirs, limit=30000):
|
||||
"""Index texture files by basename for fast lookup."""
|
||||
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:
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
if ext not in texture_exts:
|
||||
continue
|
||||
key = filename.lower()
|
||||
if key not in index:
|
||||
index[key] = os.path.join(root, filename)
|
||||
scanned += 1
|
||||
if scanned >= limit:
|
||||
return index
|
||||
except Exception:
|
||||
continue
|
||||
return index
|
||||
|
||||
def _repair_missing_textures(self, model_np, model_path):
|
||||
"""
|
||||
Repair broken texture paths by basename search; if unresolved, clear that
|
||||
missing texture binding to avoid black PBR sampling.
|
||||
"""
|
||||
if not model_np or model_np.is_empty():
|
||||
return
|
||||
|
||||
search_dirs = self._build_texture_search_dirs(model_path)
|
||||
texture_index = self._index_texture_files(search_dirs)
|
||||
|
||||
fixed = 0
|
||||
cleared = 0
|
||||
white_tex = self._get_white_fallback_texture()
|
||||
nodes = [model_np] + list(model_np.find_all_matches("**"))
|
||||
for node in nodes:
|
||||
if not node or node.is_empty():
|
||||
continue
|
||||
|
||||
try:
|
||||
stages = node.find_all_texture_stages()
|
||||
except Exception:
|
||||
try:
|
||||
stages = node.findAllTextureStages()
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
try:
|
||||
stage_count = stages.get_num_texture_stages()
|
||||
stage_at = stages.get_texture_stage
|
||||
except Exception:
|
||||
try:
|
||||
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.get_texture(stage)
|
||||
except Exception:
|
||||
tex = node.getTexture(stage)
|
||||
if not tex:
|
||||
continue
|
||||
|
||||
if self._texture_is_valid(tex):
|
||||
continue
|
||||
|
||||
basename = self._extract_texture_basename(tex)
|
||||
if not basename:
|
||||
continue
|
||||
|
||||
replacement = texture_index.get(basename.lower())
|
||||
if replacement:
|
||||
try:
|
||||
new_tex = self._load_texture_from_path(replacement)
|
||||
if new_tex:
|
||||
try:
|
||||
node.set_texture(stage, new_tex, 1)
|
||||
except Exception:
|
||||
node.setTexture(stage, new_tex, 1)
|
||||
fixed += 1
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Missing texture with no replacement: CLEAR the texture binding.
|
||||
# Do NOT use a white fallback texture, because if this is a Metallic or Roughness
|
||||
# map, pure white will force Metallic=1.0 and Roughness=1.0, which turns models black!
|
||||
try:
|
||||
try:
|
||||
node.clear_texture(stage)
|
||||
except Exception:
|
||||
node.clearTexture(stage)
|
||||
cleared += 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# GeomState-level texture bindings (common in imported FBX/GLTF):
|
||||
# inspect and apply the same fallback on this GeomNode path.
|
||||
try:
|
||||
gnode = node.node()
|
||||
get_num_geoms = getattr(gnode, "get_num_geoms", None) or getattr(gnode, "getNumGeoms", None)
|
||||
get_geom_state = getattr(gnode, "get_geom_state", None) or getattr(gnode, "getGeomState", None)
|
||||
geom_count = int(get_num_geoms()) if callable(get_num_geoms) else 0
|
||||
except Exception:
|
||||
geom_count = 0
|
||||
|
||||
for gi in range(geom_count):
|
||||
try:
|
||||
state = get_geom_state(gi)
|
||||
except Exception:
|
||||
continue
|
||||
if state is None:
|
||||
continue
|
||||
|
||||
tattr = None
|
||||
try:
|
||||
if state.has_attrib(TextureAttrib.get_class_type()):
|
||||
tattr = state.get_attrib(TextureAttrib.get_class_type())
|
||||
except Exception:
|
||||
try:
|
||||
if state.hasAttrib(TextureAttrib.getClassType()):
|
||||
tattr = state.getAttrib(TextureAttrib.getClassType())
|
||||
except Exception:
|
||||
tattr = None
|
||||
if not tattr:
|
||||
continue
|
||||
|
||||
try:
|
||||
num_on = tattr.get_num_on_stages()
|
||||
get_stage = tattr.get_on_stage
|
||||
get_tex = tattr.get_on_texture
|
||||
except Exception:
|
||||
try:
|
||||
num_on = tattr.getNumOnStages()
|
||||
get_stage = tattr.getOnStage
|
||||
get_tex = tattr.getOnTexture
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
for si in range(int(num_on)):
|
||||
try:
|
||||
stage = get_stage(si)
|
||||
tex = get_tex(stage)
|
||||
except Exception:
|
||||
continue
|
||||
if not stage or not tex:
|
||||
continue
|
||||
if self._texture_is_valid(tex):
|
||||
continue
|
||||
|
||||
basename = self._extract_texture_basename(tex)
|
||||
replacement = texture_index.get(basename.lower()) if basename else None
|
||||
if replacement:
|
||||
try:
|
||||
new_tex = self._load_texture_from_path(replacement)
|
||||
if new_tex:
|
||||
try:
|
||||
node.set_texture(stage, new_tex, 100000)
|
||||
except Exception:
|
||||
node.setTexture(stage, new_tex, 100000)
|
||||
fixed += 1
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
try:
|
||||
node.clear_texture(stage)
|
||||
except Exception:
|
||||
node.clearTexture(stage)
|
||||
cleared += 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if fixed or cleared:
|
||||
print(f"[SSBOEditor] Texture repair: fixed={fixed}, cleared_missing={cleared}")
|
||||
self._apply_nonblack_material_fallback(model_np)
|
||||
|
||||
def _texture_is_valid(self, tex):
|
||||
if not tex:
|
||||
return False
|
||||
tex_path = self._extract_texture_os_path(tex)
|
||||
if tex_path and os.path.exists(tex_path):
|
||||
return True
|
||||
if tex_path:
|
||||
# Some valid textures use Panda VFS virtual paths; keep them RAM-checkable.
|
||||
path_norm = tex_path.replace("\\", "/")
|
||||
if path_norm.startswith("/$$") or path_norm.startswith("$$"):
|
||||
pass
|
||||
else:
|
||||
# File-backed texture with missing source path should be considered invalid,
|
||||
# even when Panda keeps a tiny fallback image in RAM.
|
||||
return False
|
||||
try:
|
||||
if tex.has_ram_image():
|
||||
return tex.get_ram_image_size() > 0
|
||||
except Exception:
|
||||
try:
|
||||
if tex.hasRamImage():
|
||||
return tex.getRamImageSize() > 0
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
def _extract_texture_os_path(self, tex):
|
||||
tex_path = ""
|
||||
try:
|
||||
if tex.has_fullpath():
|
||||
fullpath = tex.get_fullpath()
|
||||
try:
|
||||
tex_path = fullpath.to_os_specific()
|
||||
except Exception:
|
||||
try:
|
||||
tex_path = fullpath.toOsSpecific()
|
||||
except Exception:
|
||||
tex_path = str(fullpath)
|
||||
except Exception:
|
||||
try:
|
||||
if tex.hasFullpath():
|
||||
fullpath = tex.getFullpath()
|
||||
try:
|
||||
tex_path = fullpath.toOsSpecific()
|
||||
except Exception:
|
||||
tex_path = str(fullpath)
|
||||
except Exception:
|
||||
tex_path = ""
|
||||
|
||||
tex_path = str(tex_path or "").strip()
|
||||
if not tex_path:
|
||||
return ""
|
||||
|
||||
tex_path = os.path.normpath(tex_path)
|
||||
if os.path.exists(tex_path):
|
||||
return tex_path
|
||||
|
||||
# Convert Panda internal drive path (/d/foo/bar) to Windows path if needed.
|
||||
if len(tex_path) >= 3 and tex_path[0] in ("/", "\\") and tex_path[1].isalpha() and tex_path[2] in ("/", "\\"):
|
||||
drive_path = f"{tex_path[1]}:{tex_path[2:]}"
|
||||
drive_path = os.path.normpath(drive_path)
|
||||
if os.path.exists(drive_path):
|
||||
return drive_path
|
||||
return tex_path
|
||||
|
||||
def _extract_texture_basename(self, tex):
|
||||
tex_path = self._extract_texture_os_path(tex)
|
||||
if tex_path:
|
||||
return os.path.basename(tex_path.replace("\\", "/"))
|
||||
try:
|
||||
name = tex.get_name()
|
||||
except Exception:
|
||||
try:
|
||||
name = tex.getName()
|
||||
except Exception:
|
||||
name = ""
|
||||
return os.path.basename(str(name).replace("\\", "/"))
|
||||
|
||||
def _get_white_fallback_texture(self):
|
||||
tex = getattr(self, "_ssbo_white_fallback_tex", None)
|
||||
if tex:
|
||||
return tex
|
||||
try:
|
||||
img = PNMImage(2, 2, 4)
|
||||
img.fill(1.0, 1.0, 1.0)
|
||||
img.alpha_fill(1.0)
|
||||
tex = Texture("ssbo_white_fallback")
|
||||
tex.load(img)
|
||||
tex.set_minfilter(Texture.FT_nearest)
|
||||
tex.set_magfilter(Texture.FT_nearest)
|
||||
self._ssbo_white_fallback_tex = tex
|
||||
return tex
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _node_has_valid_texture(self, node):
|
||||
if not node or node.is_empty():
|
||||
return False
|
||||
try:
|
||||
stages = node.find_all_texture_stages()
|
||||
stage_count = stages.get_num_texture_stages()
|
||||
stage_at = stages.get_texture_stage
|
||||
except Exception:
|
||||
try:
|
||||
stages = node.findAllTextureStages()
|
||||
stage_count = stages.getNumTextureStages()
|
||||
stage_at = stages.getTextureStage
|
||||
except Exception:
|
||||
return False
|
||||
for i in range(stage_count):
|
||||
stage = stage_at(i)
|
||||
if not stage:
|
||||
continue
|
||||
try:
|
||||
tex = node.get_texture(stage)
|
||||
except Exception:
|
||||
tex = node.getTexture(stage)
|
||||
if self._texture_is_valid(tex):
|
||||
return True
|
||||
|
||||
# GeomState TextureAttrib bindings can hold the effective textures.
|
||||
try:
|
||||
gnode = node.node()
|
||||
get_num_geoms = getattr(gnode, "get_num_geoms", None) or getattr(gnode, "getNumGeoms", None)
|
||||
get_geom_state = getattr(gnode, "get_geom_state", None) or getattr(gnode, "getGeomState", None)
|
||||
geom_count = int(get_num_geoms()) if callable(get_num_geoms) else 0
|
||||
except Exception:
|
||||
geom_count = 0
|
||||
|
||||
for gi in range(geom_count):
|
||||
try:
|
||||
state = get_geom_state(gi)
|
||||
except Exception:
|
||||
continue
|
||||
if state is None:
|
||||
continue
|
||||
|
||||
tattr = None
|
||||
try:
|
||||
if state.has_attrib(TextureAttrib.get_class_type()):
|
||||
tattr = state.get_attrib(TextureAttrib.get_class_type())
|
||||
except Exception:
|
||||
try:
|
||||
if state.hasAttrib(TextureAttrib.getClassType()):
|
||||
tattr = state.getAttrib(TextureAttrib.getClassType())
|
||||
except Exception:
|
||||
tattr = None
|
||||
if not tattr:
|
||||
continue
|
||||
|
||||
try:
|
||||
num_on = tattr.get_num_on_stages()
|
||||
get_stage = tattr.get_on_stage
|
||||
get_tex = tattr.get_on_texture
|
||||
except Exception:
|
||||
try:
|
||||
num_on = tattr.getNumOnStages()
|
||||
get_stage = tattr.getOnStage
|
||||
get_tex = tattr.getOnTexture
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
for si in range(int(num_on)):
|
||||
try:
|
||||
stage = get_stage(si)
|
||||
tex = get_tex(stage)
|
||||
except Exception:
|
||||
continue
|
||||
if self._texture_is_valid(tex):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _is_node_material_dark(self, node):
|
||||
"""Heuristic: detect near-black material/color state."""
|
||||
if not node or node.is_empty():
|
||||
return False
|
||||
try:
|
||||
state = node.get_state()
|
||||
except Exception:
|
||||
try:
|
||||
state = node.getState()
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
def _vec_dark(v):
|
||||
try:
|
||||
return max(float(v[0]), float(v[1]), float(v[2])) < 0.08
|
||||
except Exception:
|
||||
try:
|
||||
return max(float(v.x), float(v.y), float(v.z)) < 0.08
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
# Material color
|
||||
mat = None
|
||||
try:
|
||||
if state.has_attrib(MaterialAttrib.get_class_type()):
|
||||
mat_attr = state.get_attrib(MaterialAttrib.get_class_type())
|
||||
mat = mat_attr.get_material()
|
||||
except Exception:
|
||||
try:
|
||||
if state.hasAttrib(MaterialAttrib.getClassType()):
|
||||
mat_attr = state.getAttrib(MaterialAttrib.getClassType())
|
||||
mat = mat_attr.getMaterial()
|
||||
except Exception:
|
||||
mat = None
|
||||
|
||||
if mat:
|
||||
for has_name, get_name in (
|
||||
("has_base_color", "get_base_color"),
|
||||
("has_diffuse", "get_diffuse"),
|
||||
("hasBaseColor", "getBaseColor"),
|
||||
("hasDiffuse", "getDiffuse"),
|
||||
):
|
||||
has_fn = getattr(mat, has_name, None)
|
||||
get_fn = getattr(mat, get_name, None)
|
||||
if callable(has_fn) and callable(get_fn):
|
||||
try:
|
||||
if has_fn():
|
||||
return _vec_dark(get_fn())
|
||||
except Exception:
|
||||
continue
|
||||
return False
|
||||
|
||||
# ColorAttrib fallback
|
||||
try:
|
||||
if state.has_attrib(ColorAttrib.get_class_type()):
|
||||
color_attr = state.get_attrib(ColorAttrib.get_class_type())
|
||||
if not color_attr.is_off():
|
||||
return _vec_dark(color_attr.get_color())
|
||||
except Exception:
|
||||
try:
|
||||
if state.hasAttrib(ColorAttrib.getClassType()):
|
||||
color_attr = state.getAttrib(ColorAttrib.getClassType())
|
||||
if not color_attr.isOff():
|
||||
return _vec_dark(color_attr.getColor())
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
def _apply_nonblack_material_fallback(self, model_np):
|
||||
"""If a geom has no texture and is effectively black, apply a neutral material."""
|
||||
if not model_np or model_np.is_empty():
|
||||
return
|
||||
neutral = Material()
|
||||
neutral.set_base_color((0.75, 0.75, 0.75, 1.0)) if hasattr(neutral, "set_base_color") else neutral.setBaseColor((0.75, 0.75, 0.75, 1.0))
|
||||
neutral.set_diffuse((0.75, 0.75, 0.75, 1.0)) if hasattr(neutral, "set_diffuse") else neutral.setDiffuse((0.75, 0.75, 0.75, 1.0))
|
||||
neutral.set_ambient((0.22, 0.22, 0.22, 1.0)) if hasattr(neutral, "set_ambient") else neutral.setAmbient((0.22, 0.22, 0.22, 1.0))
|
||||
neutral.set_specular((0.1, 0.1, 0.1, 1.0)) if hasattr(neutral, "set_specular") else neutral.setSpecular((0.1, 0.1, 0.1, 1.0))
|
||||
neutral.set_shininess(8.0) if hasattr(neutral, "set_shininess") else neutral.setShininess(8.0)
|
||||
|
||||
patched = 0
|
||||
for geom_np in model_np.find_all_matches("**/+GeomNode"):
|
||||
if self._node_has_valid_texture(geom_np):
|
||||
continue
|
||||
if not self._is_node_material_dark(geom_np):
|
||||
continue
|
||||
try:
|
||||
try:
|
||||
geom_np.set_material(neutral, 1)
|
||||
except Exception:
|
||||
geom_np.setMaterial(neutral, 1)
|
||||
try:
|
||||
geom_np.set_color_scale(1.0, 1.0, 1.0, 1.0)
|
||||
except Exception:
|
||||
geom_np.setColorScale(1.0, 1.0, 1.0, 1.0)
|
||||
patched += 1
|
||||
except Exception:
|
||||
continue
|
||||
if patched:
|
||||
print(f"[SSBOEditor] Applied non-black fallback material to {patched} geom nodes.")
|
||||
|
||||
# No custom effect needed — RP default rendering for maximum FPS
|
||||
|
||||
def _inject_ssbo_into_shadow_state(self, effect_path):
|
||||
|
||||
@ -13,7 +13,6 @@ from direct.actor.Actor import Actor
|
||||
from direct.showbase.ShowBaseGlobal import globalClock
|
||||
from panda3d.core import TextNode, CardMaker, TextureStage, NodePath, Texture, TransparencyAttrib, CollisionTraverser, \
|
||||
Point3
|
||||
from core.InfoPanelManager import InfoPanelManager
|
||||
# 获取渲染管线路径
|
||||
# 在文件开头添加sys导入(如果还没有的话)
|
||||
import sys
|
||||
@ -97,8 +96,6 @@ class MainApp(ShowBase):
|
||||
# 加载所有脚本e
|
||||
self.script_manager.load_all_scripts_from_directory()
|
||||
|
||||
self.info_panel_manager = InfoPanelManager(self)
|
||||
|
||||
try:
|
||||
# 再导入controller模块
|
||||
from rpcore.util.movement_controller import MovementController
|
||||
@ -538,8 +535,6 @@ class MainApp(ShowBase):
|
||||
video_path=video_path,
|
||||
size=absolute_scale
|
||||
)
|
||||
elif gui_type == "info_panel":
|
||||
new_element = self.info_panel_manager.onCreateSampleInfoPanel()
|
||||
|
||||
if "scripts" in gui_info and new_element:
|
||||
self.processScripts(new_element,gui_info["scripts"])
|
||||
|
||||
@ -707,7 +707,18 @@ class AnimationTools:
|
||||
resolved_source = source
|
||||
if isinstance(source, (str, os.PathLike)):
|
||||
src_text = os.fspath(source)
|
||||
resolved_source = Filename.from_os_specific(src_text).get_fullpath()
|
||||
resolved_source = src_text
|
||||
for ctor_name in ("fromOsSpecificW", "from_os_specific_w", "fromOsSpecific", "from_os_specific"):
|
||||
ctor = getattr(Filename, ctor_name, None)
|
||||
if not ctor:
|
||||
continue
|
||||
try:
|
||||
candidate = ctor(src_text).get_fullpath()
|
||||
if candidate:
|
||||
resolved_source = candidate
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
actor = Actor(resolved_source)
|
||||
# 无论是否已检测到动画名,都显式绑定一次,避免“有名字但无可播放控制”
|
||||
try:
|
||||
|
||||
@ -930,6 +930,8 @@ class AppActions:
|
||||
# 检查场景文件
|
||||
scene_file = os.path.join(project_path, "scenes", "scene.bam")
|
||||
if os.path.exists(scene_file):
|
||||
if getattr(self, "use_ssbo_mouse_picking", False) and callable(getattr(self, "_import_model_for_runtime", None)):
|
||||
self.use_ssbo_scene_import = True
|
||||
# 加载场景
|
||||
try:
|
||||
if self.scene_manager.loadScene(scene_file):
|
||||
@ -1074,9 +1076,15 @@ class AppActions:
|
||||
model_np = getattr(self.ssbo_editor, 'model', None)
|
||||
# Keep legacy ray-pick fallback usable by adding a collision body.
|
||||
if model_np:
|
||||
try:
|
||||
from scene import util as scene_util
|
||||
normalized_model_path = scene_util.normalize_model_path(file_path)
|
||||
except Exception:
|
||||
normalized_model_path = file_path
|
||||
# Apply vital tags manually since SSBO overrides SceneManager loader
|
||||
model_np.setTag("model_path", file_path)
|
||||
model_np.setTag("model_path", normalized_model_path)
|
||||
model_np.setTag("original_path", file_path)
|
||||
model_np.setTag("saved_model_path", normalized_model_path)
|
||||
model_np.setTag("is_model_root", "1")
|
||||
model_np.setTag("is_scene_element", "1")
|
||||
model_np.setTag("file", os.path.basename(file_path))
|
||||
@ -1135,28 +1143,30 @@ class AppActions:
|
||||
self.add_error_message("模型导入失败")
|
||||
return None
|
||||
|
||||
if hasattr(self.scene_manager, 'processMaterials'):
|
||||
self.scene_manager.processMaterials(model_node)
|
||||
if show_info_message:
|
||||
self.add_info_message("已应用默认材质")
|
||||
|
||||
try:
|
||||
model_node.clearMaterial()
|
||||
model_node.clearTexture()
|
||||
|
||||
# SSBO 模式下保留原始材质/纹理,避免模型发黑。
|
||||
if not getattr(self, "use_ssbo_mouse_picking", False):
|
||||
if hasattr(self.scene_manager, 'processMaterials'):
|
||||
self.scene_manager.processMaterials(model_node)
|
||||
if show_info_message:
|
||||
self.add_info_message("已应用默认材质")
|
||||
|
||||
try:
|
||||
color = model_node.getColor()
|
||||
if color and len(color) >= 4 and color == (1, 1, 1, 1):
|
||||
model_node.clearMaterial()
|
||||
model_node.clearTexture()
|
||||
|
||||
if hasattr(self.scene_manager, 'processMaterials'):
|
||||
self.scene_manager.processMaterials(model_node)
|
||||
|
||||
try:
|
||||
color = model_node.getColor()
|
||||
if color and len(color) >= 4 and color == (1, 1, 1, 1):
|
||||
model_node.setColor(0.8, 0.8, 0.8, 1.0)
|
||||
elif not color:
|
||||
model_node.setColor(0.8, 0.8, 0.8, 1.0)
|
||||
except Exception:
|
||||
model_node.setColor(0.8, 0.8, 0.8, 1.0)
|
||||
elif not color:
|
||||
model_node.setColor(0.8, 0.8, 0.8, 1.0)
|
||||
except Exception:
|
||||
model_node.setColor(0.8, 0.8, 0.8, 1.0)
|
||||
except Exception as e:
|
||||
self.add_warning_message(f"材质处理警告: {e}")
|
||||
except Exception as e:
|
||||
self.add_warning_message(f"材质处理警告: {e}")
|
||||
|
||||
if set_origin:
|
||||
model_node.setPos(0, 0, 0)
|
||||
|
||||
@ -2229,7 +2229,10 @@ class EditorPanels:
|
||||
# 删除对象
|
||||
imgui.same_line()
|
||||
if imgui.button("删除"):
|
||||
if hasattr(self, 'selection') and self.selection:
|
||||
if hasattr(self.app, "_on_delete") and callable(self.app._on_delete):
|
||||
self.app._on_delete()
|
||||
elif hasattr(self, 'selection') and self.selection and hasattr(self.selection, "deleteSelectedNode"):
|
||||
# 兼容旧选择系统接口
|
||||
self.selection.deleteSelectedNode()
|
||||
|
||||
|
||||
|
||||
@ -339,51 +339,6 @@ class ObjectFactory:
|
||||
except Exception as e:
|
||||
print(f"✗ 创建平面失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def create2DSamplePanel(self):
|
||||
"""创建2D示例面板"""
|
||||
try:
|
||||
from core.InfoPanelManager import createSampleInfoPanel
|
||||
result = createSampleInfoPanel(self.render)
|
||||
return result
|
||||
except Exception as e:
|
||||
print(f"创建2D示例面板失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def create3DSamplePanel(self):
|
||||
"""创建3D实例面板"""
|
||||
try:
|
||||
if hasattr(self, 'info_panel_manager') and self.info_panel_manager:
|
||||
# 创建3D信息面板
|
||||
panel_id = f"3d_sample_{int(time.time())}"
|
||||
result = self.info_panel_manager.create3DInfoPanel(
|
||||
panel_id=panel_id,
|
||||
position=(0, 0, 2),
|
||||
size=(1.0, 0.6),
|
||||
bg_color=(0.15, 0.25, 0.35, 0.95),
|
||||
border_color=(0.3, 0.5, 0.7, 1.0),
|
||||
title_color=(0.7, 0.9, 1.0, 1.0),
|
||||
content_color=(0.95, 0.95, 0.95, 1.0)
|
||||
)
|
||||
|
||||
# 添加示例内容
|
||||
if result:
|
||||
sample_data = {
|
||||
"标题": "3D信息面板",
|
||||
"状态": "运行中",
|
||||
"创建时间": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"位置": f"X:0, Y:0, Z:2"
|
||||
}
|
||||
self.info_panel_manager.updatePanelContent(panel_id, content=sample_data)
|
||||
|
||||
return result
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"创建3D示例面板失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def createWebPanel(self, url="https://www.example.com"):
|
||||
"""创建Web面板"""
|
||||
|
||||
4319
ui/widgets.py
4319
ui/widgets.py
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user