优化模型导入中英文输入
@ -166,8 +166,15 @@ void main() {
|
|||||||
color += ambient.diffuse;
|
color += ambient.diffuse;
|
||||||
color += ambient.specular;
|
color += ambient.specular;
|
||||||
color += get_sun_shading(m_out, view_dir);
|
color += get_sun_shading(m_out, view_dir);
|
||||||
|
color += get_forward_light_shading(m_out);
|
||||||
|
|
||||||
// XXX: Apply shading from lights too
|
// Transparent forward materials end up noticeably darker than the same
|
||||||
|
// object in the deferred path in this editor build, which makes them look
|
||||||
|
// "gone" when artists first switch the material type. Apply a small
|
||||||
|
// preview-space lift so the result stays visually comparable.
|
||||||
|
if (m_out.shading_model == SHADING_MODEL_TRANSPARENT) {
|
||||||
|
color *= 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
alpha = mix(alpha, 1.0, ambient.fresnel);
|
alpha = mix(alpha, 1.0, ambient.fresnel);
|
||||||
|
|
||||||
|
|||||||
BIN
__codex_merge_debug.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
__codex_opacity_high.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
__codex_opacity_low.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
__codex_overlay_direct_texture.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
__codex_surface_switch_after_forward_lights.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
__codex_surface_switch_after_forward_lights_crop.png
Normal file
|
After Width: | Height: | Size: 609 KiB |
BIN
__codex_surface_switch_final_preview.png
Normal file
|
After Width: | Height: | Size: 774 KiB |
BIN
__codex_surface_switch_opacity_06.png
Normal file
|
After Width: | Height: | Size: 778 KiB |
BIN
__codex_surface_switch_visible.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
__codex_surface_switch_visible_after_fix.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
__codex_surface_switch_visible_clamped.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
__codex_surface_switch_visible_reapply.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
__codex_surface_switch_visible_sync.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
__codex_transparent_dual_pass_test.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
30
imgui.ini
@ -24,26 +24,26 @@ Size=832,45
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][工具栏]
|
[Window][工具栏]
|
||||||
Pos=453,20
|
Pos=354,20
|
||||||
Size=1250,32
|
Size=1334,32
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x0000000D,0
|
DockId=0x0000000D,0
|
||||||
|
|
||||||
[Window][场景树]
|
[Window][场景树]
|
||||||
Pos=0,20
|
Pos=0,20
|
||||||
Size=451,1036
|
Size=352,748
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000007,0
|
DockId=0x00000007,0
|
||||||
|
|
||||||
[Window][属性面板]
|
[Window][属性面板]
|
||||||
Pos=1705,20
|
Pos=1690,20
|
||||||
Size=855,1036
|
Size=358,748
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000002,0
|
DockId=0x00000002,0
|
||||||
|
|
||||||
[Window][控制台]
|
[Window][控制台]
|
||||||
Pos=1705,20
|
Pos=1690,20
|
||||||
Size=855,1036
|
Size=358,748
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000002,1
|
DockId=0x00000002,1
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ Collapsed=0
|
|||||||
|
|
||||||
[Window][WindowOverViewport_11111111]
|
[Window][WindowOverViewport_11111111]
|
||||||
Pos=0,20
|
Pos=0,20
|
||||||
Size=2560,1372
|
Size=2048,1084
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][测试窗口1]
|
[Window][测试窗口1]
|
||||||
@ -99,8 +99,8 @@ Size=600,500
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][资源管理器]
|
[Window][资源管理器]
|
||||||
Pos=0,1058
|
Pos=0,770
|
||||||
Size=2560,334
|
Size=2048,334
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000006,0
|
DockId=0x00000006,0
|
||||||
|
|
||||||
@ -207,13 +207,13 @@ Collapsed=0
|
|||||||
DockId=0x00000002,2
|
DockId=0x00000002,2
|
||||||
|
|
||||||
[Docking][Data]
|
[Docking][Data]
|
||||||
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=2560,1372 Split=Y
|
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=2048,1084 Split=Y
|
||||||
DockNode ID=0x00000005 Parent=0x08BD597D SizeRef=2560,995 Split=X
|
DockNode ID=0x00000005 Parent=0x08BD597D SizeRef=2560,995 Split=X
|
||||||
DockNode ID=0x00000007 Parent=0x00000005 SizeRef=451,1084 Selected=0xE0015051
|
DockNode ID=0x00000007 Parent=0x00000005 SizeRef=352,1084 Selected=0xE0015051
|
||||||
DockNode ID=0x00000008 Parent=0x00000005 SizeRef=1595,1084 Split=X
|
DockNode ID=0x00000008 Parent=0x00000005 SizeRef=2206,1084 Split=X
|
||||||
DockNode ID=0x00000001 Parent=0x00000008 SizeRef=1250,989 Split=Y
|
DockNode ID=0x00000001 Parent=0x00000008 SizeRef=1846,989 Split=Y
|
||||||
DockNode ID=0x0000000D Parent=0x00000001 SizeRef=1318,32 HiddenTabBar=1 Selected=0x43A39006
|
DockNode ID=0x0000000D Parent=0x00000001 SizeRef=1318,32 HiddenTabBar=1 Selected=0x43A39006
|
||||||
DockNode ID=0x0000000E Parent=0x00000001 SizeRef=1318,714 CentralNode=1
|
DockNode ID=0x0000000E Parent=0x00000001 SizeRef=1318,714 CentralNode=1
|
||||||
DockNode ID=0x00000002 Parent=0x00000008 SizeRef=855,989 Selected=0x5DB6FF37
|
DockNode ID=0x00000002 Parent=0x00000008 SizeRef=358,989 Selected=0x5428E753
|
||||||
DockNode ID=0x00000006 Parent=0x08BD597D SizeRef=2560,334 Selected=0x3A2E05C3
|
DockNode ID=0x00000006 Parent=0x08BD597D SizeRef=2560,334 Selected=0x3A2E05C3
|
||||||
|
|
||||||
|
|||||||
3
main.py
@ -356,6 +356,9 @@ class MyWorld(PanelDelegates, CoreWorld):
|
|||||||
self._selected_template = 0
|
self._selected_template = 0
|
||||||
self._mount_script_index = 0
|
self._mount_script_index = 0
|
||||||
self.console_command_input = ""
|
self.console_command_input = ""
|
||||||
|
self._scene_tree_search_text = ""
|
||||||
|
self._resource_path_input = str(self.resource_manager.current_path)
|
||||||
|
self._resource_path_source = self._resource_path_input
|
||||||
|
|
||||||
# 变换监控相关
|
# 变换监控相关
|
||||||
self._transform_monitoring = False
|
self._transform_monitoring = False
|
||||||
|
|||||||
@ -264,22 +264,35 @@ class SceneManagerIOMixin:
|
|||||||
def expand_scene_package_wrappers(nodes):
|
def expand_scene_package_wrappers(nodes):
|
||||||
expanded_nodes = []
|
expanded_nodes = []
|
||||||
ssbo_editor = getattr(self.world, "ssbo_editor", None)
|
ssbo_editor = getattr(self.world, "ssbo_editor", None)
|
||||||
source_scene_model = getattr(ssbo_editor, "source_model", None) if ssbo_editor else None
|
source_scene_model = None
|
||||||
|
source_scene_root = None
|
||||||
|
if ssbo_editor:
|
||||||
|
source_scene_model = getattr(ssbo_editor, "source_model", None)
|
||||||
|
source_scene_root = getattr(ssbo_editor, "source_model_root", None)
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
if not node or node.isEmpty():
|
if not node or node.isEmpty():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
is_ssbo_runtime_root = (
|
||||||
|
ssbo_editor and
|
||||||
|
node == getattr(ssbo_editor, "model", None) and
|
||||||
|
source_scene_root and
|
||||||
|
not source_scene_root.isEmpty()
|
||||||
|
)
|
||||||
|
|
||||||
is_scene_wrapper = (
|
is_scene_wrapper = (
|
||||||
node.hasTag("scene_import_source") and
|
node.hasTag("scene_import_source") and
|
||||||
node.getTag("scene_import_source") == "project_scene_bam"
|
node.getTag("scene_import_source") == "project_scene_bam"
|
||||||
)
|
)
|
||||||
if not is_scene_wrapper:
|
|
||||||
|
if not is_scene_wrapper and not is_ssbo_runtime_root:
|
||||||
expanded_nodes.append(node)
|
expanded_nodes.append(node)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
source_children = []
|
source_children = []
|
||||||
if source_scene_model and not source_scene_model.isEmpty():
|
source_snapshot = source_scene_root if is_ssbo_runtime_root else source_scene_model
|
||||||
for child in source_scene_model.getChildren():
|
if source_snapshot and not source_snapshot.isEmpty():
|
||||||
|
for child in source_snapshot.getChildren():
|
||||||
try:
|
try:
|
||||||
if not child or child.isEmpty():
|
if not child or child.isEmpty():
|
||||||
continue
|
continue
|
||||||
@ -290,11 +303,11 @@ class SceneManagerIOMixin:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if source_children:
|
if source_children:
|
||||||
print(f"展开场景包节点 {node.getName()} -> 使用原始场景树的 {len(source_children)} 个顶层子节点")
|
print(f"展开SSBO场景节点 {node.getName()} -> 使用原始场景树的 {len(source_children)} 个顶层子节点")
|
||||||
expanded_nodes.extend(source_children)
|
expanded_nodes.extend(source_children)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
print(f"场景包节点 {node.getName()} 缺少原始场景树,回退为包装根节点保存")
|
print(f"SSBO场景节点 {node.getName()} 缺少原始场景树,回退为包装根节点保存")
|
||||||
expanded_nodes.append(node)
|
expanded_nodes.append(node)
|
||||||
return expanded_nodes
|
return expanded_nodes
|
||||||
|
|
||||||
|
|||||||
@ -217,23 +217,17 @@ class ObjectController:
|
|||||||
|
|
||||||
def should_hide_tree_node(self, key):
|
def should_hide_tree_node(self, key):
|
||||||
"""
|
"""
|
||||||
Hide a redundant wrapper node directly below the file root, e.g. ROOT.
|
Hide redundant wrapper nodes like ROOT so the UI shows the imported
|
||||||
This keeps `model.glb` as the visible root in the UI.
|
model hierarchy instead of source-format packaging nodes.
|
||||||
"""
|
"""
|
||||||
node = self.tree_nodes.get(key)
|
node = self.tree_nodes.get(key)
|
||||||
if not node:
|
if not node:
|
||||||
return False
|
return False
|
||||||
if node["parent"] != self.tree_root_key:
|
|
||||||
return False
|
|
||||||
|
|
||||||
name = (node["name"] or "").strip().lower()
|
name = (node["name"] or "").strip().lower()
|
||||||
if name != "root":
|
if name != "root":
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Keep node visible if it actually carries direct geoms.
|
|
||||||
if node["local_ids"]:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return len(node["children"]) > 0
|
return len(node["children"]) > 0
|
||||||
|
|
||||||
def _encode_id_color(self, vdata, object_id):
|
def _encode_id_color(self, vdata, object_id):
|
||||||
@ -435,23 +429,17 @@ class ObjectController:
|
|||||||
|
|
||||||
def should_hide_tree_node(self, key):
|
def should_hide_tree_node(self, key):
|
||||||
"""
|
"""
|
||||||
Hide a redundant wrapper node directly below the file root, e.g. ROOT.
|
Hide redundant wrapper nodes like ROOT so the UI shows the imported
|
||||||
This keeps `model.glb` as the visible root in the UI.
|
model hierarchy instead of source-format packaging nodes.
|
||||||
"""
|
"""
|
||||||
node = self.tree_nodes.get(key)
|
node = self.tree_nodes.get(key)
|
||||||
if not node:
|
if not node:
|
||||||
return False
|
return False
|
||||||
if node["parent"] != self.tree_root_key:
|
|
||||||
return False
|
|
||||||
|
|
||||||
name = (node["name"] or "").strip().lower()
|
name = (node["name"] or "").strip().lower()
|
||||||
if name != "root":
|
if name != "root":
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Keep node visible if it actually carries direct geoms.
|
|
||||||
if node["local_ids"]:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return len(node["children"]) > 0
|
return len(node["children"]) > 0
|
||||||
|
|
||||||
def _encode_id_color(self, vdata, object_id):
|
def _encode_id_color(self, vdata, object_id):
|
||||||
|
|||||||
@ -66,6 +66,8 @@ class SSBOEditor:
|
|||||||
self.model = None
|
self.model = None
|
||||||
self.source_model = None
|
self.source_model = None
|
||||||
self.source_model_root = None
|
self.source_model_root = None
|
||||||
|
self.last_import_tree_key = None
|
||||||
|
self.last_import_root_name = None
|
||||||
self.ssbo = None
|
self.ssbo = None
|
||||||
self.font_path = font_path
|
self.font_path = font_path
|
||||||
self._transform_gizmo = None
|
self._transform_gizmo = None
|
||||||
@ -277,11 +279,8 @@ class SSBOEditor:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'修复黑色模型材质时出错: {e}')
|
print(f'修复黑色模型材质时出错: {e}')
|
||||||
|
|
||||||
def load_model(self, model_path, keep_source_model=False):
|
def _load_source_model_from_path(self, model_path):
|
||||||
"""Load and process a model using hybrid static/dynamic chunks."""
|
"""Load a source model NodePath from disk without touching current runtime state."""
|
||||||
print(f"[SSBOEditor] Loading model: {model_path}")
|
|
||||||
self.source_model = None
|
|
||||||
self.source_model_root = None
|
|
||||||
source_model = None
|
source_model = None
|
||||||
last_error = None
|
last_error = None
|
||||||
for fn in self._build_filename_candidates(model_path):
|
for fn in self._build_filename_candidates(model_path):
|
||||||
@ -298,33 +297,245 @@ class SSBOEditor:
|
|||||||
raise RuntimeError(f"Failed to load model '{model_path}'")
|
raise RuntimeError(f"Failed to load model '{model_path}'")
|
||||||
self._fixBlackMaterials(source_model)
|
self._fixBlackMaterials(source_model)
|
||||||
self._repair_missing_textures(source_model, model_path)
|
self._repair_missing_textures(source_model, model_path)
|
||||||
model_name = os.path.basename(model_path)
|
return source_model
|
||||||
if model_name:
|
|
||||||
source_model.set_name(model_name)
|
|
||||||
|
|
||||||
if keep_source_model:
|
def _set_node_name(self, node, name):
|
||||||
self.source_model_root = NodePath(f"{model_name or 'scene'}__source_snapshot_root")
|
if not node:
|
||||||
self.source_model = source_model.copyTo(self.source_model_root)
|
return
|
||||||
|
try:
|
||||||
|
node.set_name(name)
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
node.setName(name)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _iter_children(self, node):
|
||||||
|
if not node:
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
return list(node.get_children())
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
return list(node.getChildren())
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _ensure_source_model_root(self):
|
||||||
|
root = self.source_model_root
|
||||||
|
if root:
|
||||||
|
try:
|
||||||
|
if not root.is_empty():
|
||||||
|
return root
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
if not root.isEmpty():
|
||||||
|
return root
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.source_model_root = NodePath("ssbo_source_scene_root")
|
||||||
|
return self.source_model_root
|
||||||
|
|
||||||
|
def _make_unique_source_child_name(self, desired_name):
|
||||||
|
root = self._ensure_source_model_root()
|
||||||
|
existing_names = set()
|
||||||
|
for child in self._iter_children(root):
|
||||||
|
try:
|
||||||
|
existing_names.add(child.get_name())
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
existing_names.add(child.getName())
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
base_name = desired_name or "imported_model"
|
||||||
|
if base_name not in existing_names:
|
||||||
|
return base_name
|
||||||
|
|
||||||
|
stem, ext = os.path.splitext(base_name)
|
||||||
|
index = 2
|
||||||
|
while True:
|
||||||
|
candidate = f"{stem}_{index}{ext}"
|
||||||
|
if candidate not in existing_names:
|
||||||
|
return candidate
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
def _get_top_level_group_keys(self):
|
||||||
|
if not self.controller or not getattr(self.controller, "tree_root_key", None):
|
||||||
|
return []
|
||||||
|
root_key = self.controller.tree_root_key
|
||||||
|
root_node = self.controller.tree_nodes.get(root_key)
|
||||||
|
if not root_node:
|
||||||
|
return []
|
||||||
|
return list(root_node.get("children", []))
|
||||||
|
|
||||||
|
def _snapshot_top_level_transforms_to_source_root(self):
|
||||||
|
"""Persist current top-level imported model transforms back into the source scene root."""
|
||||||
|
if not self.controller or not self.model or not self.source_model_root:
|
||||||
|
return
|
||||||
|
|
||||||
|
source_children = {}
|
||||||
|
for child in self._iter_children(self.source_model_root):
|
||||||
|
try:
|
||||||
|
source_children[child.get_name()] = child
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
source_children[child.getName()] = child
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for key in self._get_top_level_group_keys():
|
||||||
|
display_name = self.controller.display_names.get(key, key)
|
||||||
|
source_child = source_children.get(display_name)
|
||||||
|
if not source_child:
|
||||||
|
continue
|
||||||
|
|
||||||
|
group_ids = self.controller.name_to_ids.get(key, [])
|
||||||
|
if not group_ids:
|
||||||
|
continue
|
||||||
|
|
||||||
|
representative_id = None
|
||||||
|
for gid in group_ids:
|
||||||
|
obj_np = self.controller.id_to_object_np.get(gid)
|
||||||
|
if obj_np and not obj_np.is_empty():
|
||||||
|
representative_id = gid
|
||||||
|
break
|
||||||
|
if representative_id is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
current_mat = LMatrix4f(self.controller.id_to_object_np[representative_id].get_mat(self.model))
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
current_mat = LMatrix4f(self.controller.id_to_object_np[representative_id].getMat(self.model))
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if representative_id >= len(self.controller.global_transforms):
|
||||||
|
continue
|
||||||
|
original_mat = LMatrix4f(self.controller.global_transforms[representative_id])
|
||||||
|
inv_original = LMatrix4f(original_mat)
|
||||||
|
try:
|
||||||
|
inv_original.invertInPlace()
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
inv_original.invert_in_place()
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
delta_mat = current_mat * inv_original
|
||||||
|
try:
|
||||||
|
source_child.set_mat(delta_mat * source_child.get_mat())
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
source_child.setMat(delta_mat * source_child.getMat())
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
def _clear_runtime_state(self, preserve_source_models=False):
|
||||||
|
"""Remove runtime SSBO controller/model state while optionally keeping source snapshots."""
|
||||||
|
self.clear_selection()
|
||||||
|
self._cleanup_group_proxy()
|
||||||
|
self._reset_pick_sync_cache()
|
||||||
|
|
||||||
|
controller = self.controller
|
||||||
|
pick_model = getattr(controller, "pick_model", None) if controller else None
|
||||||
|
model = self.model
|
||||||
|
|
||||||
|
removable_nodes = [pick_model, model]
|
||||||
|
if not preserve_source_models:
|
||||||
|
removable_nodes.extend([self.source_model, self.source_model_root])
|
||||||
|
|
||||||
|
for node in removable_nodes:
|
||||||
|
if not node:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if not node.is_empty():
|
||||||
|
node.remove_node()
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
if not node.isEmpty():
|
||||||
|
node.removeNode()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.controller = None
|
||||||
|
self.model = None
|
||||||
|
self.selected_name = None
|
||||||
|
self.selected_ids = []
|
||||||
|
self.last_import_tree_key = None
|
||||||
|
self.last_import_root_name = None
|
||||||
|
if not preserve_source_models:
|
||||||
|
self.source_model = None
|
||||||
|
self.source_model_root = None
|
||||||
|
self._sync_pick_scene_binding()
|
||||||
|
|
||||||
|
def _rebuild_runtime_from_source_root(self, highlight_root_name=None):
|
||||||
|
root = self._ensure_source_model_root()
|
||||||
|
working_holder = NodePath("ssbo_source_scene_work")
|
||||||
|
working_root = root.copy_to(working_holder)
|
||||||
|
|
||||||
self.controller = ObjectController()
|
self.controller = ObjectController()
|
||||||
count = self.controller.bake_ids_and_collect(source_model)
|
count = self.controller.bake_ids_and_collect(working_root)
|
||||||
self.model = self.controller.model
|
self.model = self.controller.model
|
||||||
|
|
||||||
self.model.reparent_to(self.base.render)
|
self.model.reparent_to(self.base.render)
|
||||||
|
|
||||||
# Keep this off by default for better overall FPS/scaling with visibility.
|
|
||||||
self.set_realtime_shadow_updates(self.realtime_shadow_updates)
|
self.set_realtime_shadow_updates(self.realtime_shadow_updates)
|
||||||
|
|
||||||
# NO rp.set_effect() — use RP default rendering for max FPS
|
|
||||||
# NO SSBO creation — vertex positions are baked
|
|
||||||
|
|
||||||
# Setup GPU Picking (uses simple vertex-color shader)
|
|
||||||
self.setup_gpu_picking()
|
self.setup_gpu_picking()
|
||||||
# Keep pick clone aligned with source model transform.
|
|
||||||
self._sync_pick_model_transform()
|
self._sync_pick_model_transform()
|
||||||
|
|
||||||
|
self.last_import_tree_key = None
|
||||||
|
self.last_import_root_name = highlight_root_name
|
||||||
|
if highlight_root_name:
|
||||||
|
root_key = getattr(self.controller, "tree_root_key", None)
|
||||||
|
root_node = self.controller.tree_nodes.get(root_key, {}) if root_key else {}
|
||||||
|
for child_key in root_node.get("children", []):
|
||||||
|
if self.controller.display_names.get(child_key) == highlight_root_name:
|
||||||
|
self.last_import_tree_key = child_key
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not working_holder.is_empty():
|
||||||
|
working_holder.remove_node()
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
if not working_holder.isEmpty():
|
||||||
|
working_holder.removeNode()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
print(f"[SSBOEditor] Model loaded. Total objects: {count}")
|
print(f"[SSBOEditor] Model loaded. Total objects: {count}")
|
||||||
|
|
||||||
|
def load_model(self, model_path, keep_source_model=False, append=False):
|
||||||
|
"""Load and process one model into the aggregated SSBO scene."""
|
||||||
|
print(f"[SSBOEditor] Loading model: {model_path}")
|
||||||
|
source_model = self._load_source_model_from_path(model_path)
|
||||||
|
model_name = os.path.basename(model_path)
|
||||||
|
if model_name:
|
||||||
|
self._set_node_name(source_model, model_name)
|
||||||
|
|
||||||
|
if append and self.source_model_root:
|
||||||
|
if self.controller and self.model:
|
||||||
|
self._snapshot_top_level_transforms_to_source_root()
|
||||||
|
self._clear_runtime_state(preserve_source_models=True)
|
||||||
|
else:
|
||||||
|
self._clear_runtime_state(preserve_source_models=False)
|
||||||
|
|
||||||
|
source_root = self._ensure_source_model_root()
|
||||||
|
unique_root_name = self._make_unique_source_child_name(model_name or "imported_model")
|
||||||
|
self._set_node_name(source_model, unique_root_name)
|
||||||
|
imported_root = source_model.copyTo(source_root)
|
||||||
|
self._set_node_name(imported_root, unique_root_name)
|
||||||
|
|
||||||
|
if keep_source_model and not append:
|
||||||
|
self.source_model = imported_root
|
||||||
|
else:
|
||||||
|
self.source_model = source_root
|
||||||
|
|
||||||
|
self._rebuild_runtime_from_source_root(highlight_root_name=unique_root_name)
|
||||||
|
|
||||||
def _build_filename_candidates(self, path_text):
|
def _build_filename_candidates(self, path_text):
|
||||||
"""Build Filename candidates with wide-char first for Windows CJK paths."""
|
"""Build Filename candidates with wide-char first for Windows CJK paths."""
|
||||||
candidates = []
|
candidates = []
|
||||||
@ -1347,36 +1558,7 @@ class SSBOEditor:
|
|||||||
|
|
||||||
def reset_scene_state(self):
|
def reset_scene_state(self):
|
||||||
"""Remove the current SSBO model/controller state before loading another scene."""
|
"""Remove the current SSBO model/controller state before loading another scene."""
|
||||||
self.clear_selection()
|
self._clear_runtime_state(preserve_source_models=False)
|
||||||
self._cleanup_group_proxy()
|
|
||||||
self._reset_pick_sync_cache()
|
|
||||||
|
|
||||||
controller = self.controller
|
|
||||||
pick_model = getattr(controller, "pick_model", None) if controller else None
|
|
||||||
model = self.model
|
|
||||||
source_model = self.source_model
|
|
||||||
source_model_root = self.source_model_root
|
|
||||||
|
|
||||||
for node in (pick_model, model, source_model, source_model_root):
|
|
||||||
if not node:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
if not node.is_empty():
|
|
||||||
node.remove_node()
|
|
||||||
except Exception:
|
|
||||||
try:
|
|
||||||
if not node.isEmpty():
|
|
||||||
node.removeNode()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
self.controller = None
|
|
||||||
self.model = None
|
|
||||||
self.source_model = None
|
|
||||||
self.source_model_root = None
|
|
||||||
self.selected_name = None
|
|
||||||
self.selected_ids = []
|
|
||||||
self._sync_pick_scene_binding()
|
|
||||||
|
|
||||||
def _cleanup_group_proxy(self):
|
def _cleanup_group_proxy(self):
|
||||||
"""Reparent objects back to their chunk and remove the group proxy."""
|
"""Reparent objects back to their chunk and remove the group proxy."""
|
||||||
|
|||||||
6
third_party/p3dimgui/backend.py
vendored
@ -456,13 +456,17 @@ class ImGuiBackend(DirectObject):
|
|||||||
# Some Panda3D / Windows combinations do not emit a reliable "keystroke"
|
# Some Panda3D / Windows combinations do not emit a reliable "keystroke"
|
||||||
# event for ImGui text fields. Fall back to printable key-down events
|
# event for ImGui text fields. Fall back to printable key-down events
|
||||||
# until we observe real text events in this session.
|
# until we observe real text events in this session.
|
||||||
|
# On Windows, do not block this fallback just because IME is open:
|
||||||
|
# many Chinese input methods stay "open" even while typing raw
|
||||||
|
# English / digits. We only suppress ASCII synthesis while an IME
|
||||||
|
# composition is actively in progress.
|
||||||
if (
|
if (
|
||||||
down
|
down
|
||||||
and not self._keystroke_observed
|
and not self._keystroke_observed
|
||||||
and not self.io.key_ctrl
|
and not self.io.key_ctrl
|
||||||
and not self.io.key_alt
|
and not self.io.key_alt
|
||||||
and not self.io.key_super
|
and not self.io.key_super
|
||||||
and not (sys.platform == "win32" and self._ime_open and self.io.want_text_input)
|
and not (sys.platform == "win32" and self._ime_composing and self.io.want_text_input)
|
||||||
):
|
):
|
||||||
text = self.__resolve_text_input(keyName, button)
|
text = self.__resolve_text_input(keyName, button)
|
||||||
if had_shift:
|
if had_shift:
|
||||||
|
|||||||
@ -1018,28 +1018,10 @@ class AppActions:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Remove legacy scene-manager models to avoid duplicate rendering
|
|
||||||
if hasattr(self, 'scene_manager') and self.scene_manager and hasattr(self.scene_manager, 'models'):
|
|
||||||
for m in list(self.scene_manager.models):
|
|
||||||
try:
|
|
||||||
if m and not m.isEmpty():
|
|
||||||
m.removeNode()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
self.scene_manager.models = []
|
|
||||||
|
|
||||||
# Replace previous SSBO model
|
|
||||||
old_model = getattr(self.ssbo_editor, 'model', None)
|
|
||||||
if old_model is not None:
|
|
||||||
try:
|
|
||||||
if not old_model.isEmpty():
|
|
||||||
old_model.removeNode()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
self.ssbo_editor.load_model(
|
self.ssbo_editor.load_model(
|
||||||
file_path,
|
file_path,
|
||||||
keep_source_model=scene_package_import,
|
keep_source_model=scene_package_import,
|
||||||
|
append=not scene_package_import,
|
||||||
)
|
)
|
||||||
model_np = getattr(self.ssbo_editor, 'model', None)
|
model_np = getattr(self.ssbo_editor, 'model', None)
|
||||||
# Keep legacy ray-pick fallback usable by adding a collision body.
|
# Keep legacy ray-pick fallback usable by adding a collision body.
|
||||||
@ -1058,7 +1040,20 @@ class AppActions:
|
|||||||
model_np.setTag("is_model_root", "1")
|
model_np.setTag("is_model_root", "1")
|
||||||
model_np.setTag("is_scene_element", "1")
|
model_np.setTag("is_scene_element", "1")
|
||||||
model_np.setTag("file", os.path.basename(file_path))
|
model_np.setTag("file", os.path.basename(file_path))
|
||||||
model_np.setName(os.path.basename(file_path))
|
ssbo_source_root = getattr(self.ssbo_editor, "source_model_root", None)
|
||||||
|
source_children = []
|
||||||
|
if ssbo_source_root is not None:
|
||||||
|
try:
|
||||||
|
source_children = list(ssbo_source_root.getChildren())
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
source_children = list(ssbo_source_root.get_children())
|
||||||
|
except Exception:
|
||||||
|
source_children = []
|
||||||
|
if len(source_children) > 1 and not scene_package_import:
|
||||||
|
model_np.setName("导入模型")
|
||||||
|
else:
|
||||||
|
model_np.setName(os.path.basename(file_path))
|
||||||
|
|
||||||
if hasattr(self, 'scene_manager') and self.scene_manager:
|
if hasattr(self, 'scene_manager') and self.scene_manager:
|
||||||
try:
|
try:
|
||||||
@ -1071,6 +1066,9 @@ class AppActions:
|
|||||||
self.scene_manager._processModelAnimations(model_np)
|
self.scene_manager._processModelAnimations(model_np)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[SSBO] setup components failed: {e}")
|
print(f"[SSBO] setup components failed: {e}")
|
||||||
|
|
||||||
|
if hasattr(self, 'scene_manager') and self.scene_manager and hasattr(self.scene_manager, 'models'):
|
||||||
|
self.scene_manager.models = [model_np]
|
||||||
return model_np
|
return model_np
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[SSBO] load_model failed: {e}")
|
print(f"[SSBO] load_model failed: {e}")
|
||||||
@ -1143,14 +1141,24 @@ class AppActions:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.add_warning_message(f"材质处理警告: {e}")
|
self.add_warning_message(f"材质处理警告: {e}")
|
||||||
|
|
||||||
if set_origin:
|
if set_origin and not getattr(self, "use_ssbo_mouse_picking", False):
|
||||||
model_node.setPos(0, 0, 0)
|
model_node.setPos(0, 0, 0)
|
||||||
|
|
||||||
if hasattr(self.scene_manager, 'models'):
|
if hasattr(self.scene_manager, 'models'):
|
||||||
self.scene_manager.models.append(model_node)
|
if getattr(self, "use_ssbo_mouse_picking", False):
|
||||||
|
self.scene_manager.models = [model_node]
|
||||||
|
else:
|
||||||
|
self.scene_manager.models.append(model_node)
|
||||||
|
|
||||||
if select_model and hasattr(self, 'selection') and self.selection:
|
if select_model:
|
||||||
self.selection.updateSelection(model_node)
|
if (
|
||||||
|
getattr(self, "use_ssbo_mouse_picking", False)
|
||||||
|
and getattr(self, "ssbo_editor", None)
|
||||||
|
and getattr(self.ssbo_editor, "last_import_tree_key", None)
|
||||||
|
):
|
||||||
|
self.ssbo_editor.select_node(self.ssbo_editor.last_import_tree_key)
|
||||||
|
elif hasattr(self, 'selection') and self.selection:
|
||||||
|
self.selection.updateSelection(model_node)
|
||||||
|
|
||||||
if show_success_message:
|
if show_success_message:
|
||||||
self.add_success_message(f"模型导入成功: {file_name}")
|
self.add_success_message(f"模型导入成功: {file_name}")
|
||||||
|
|||||||
@ -23,60 +23,173 @@ class EditorPanelsLeftMixin:
|
|||||||
float(window_size.x),
|
float(window_size.x),
|
||||||
float(window_size.y),
|
float(window_size.y),
|
||||||
)
|
)
|
||||||
|
|
||||||
imgui.text("场景层级")
|
self._draw_scene_tree_header()
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
|
|
||||||
# 构建动态场景树
|
|
||||||
self._build_scene_tree()
|
self._build_scene_tree()
|
||||||
|
|
||||||
|
def _draw_scene_tree_header(self):
|
||||||
|
"""绘制场景树头部摘要与检索框。"""
|
||||||
|
model_count = self._get_scene_tree_model_display_count()
|
||||||
|
selected_node = self.app._get_selection_node()
|
||||||
|
selected_name = "未选择"
|
||||||
|
if selected_node and not selected_node.isEmpty():
|
||||||
|
selected_name = selected_node.getName() or "未命名对象"
|
||||||
|
|
||||||
|
imgui.text_disabled(f"模型 {model_count}")
|
||||||
|
imgui.same_line()
|
||||||
|
imgui.text_disabled(f"当前: {selected_name}")
|
||||||
|
|
||||||
|
imgui.set_next_item_width(-64)
|
||||||
|
changed, search_text = imgui.input_text("##scene_tree_search", self.app._scene_tree_search_text, 256)
|
||||||
|
if changed:
|
||||||
|
self.app._scene_tree_search_text = search_text
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.button("清空##scene_tree_search"):
|
||||||
|
self.app._scene_tree_search_text = ""
|
||||||
|
|
||||||
|
def _get_scene_tree_filter(self):
|
||||||
|
return getattr(self.app, "_scene_tree_search_text", "").strip().lower()
|
||||||
|
|
||||||
|
def _get_scene_tree_models(self):
|
||||||
|
models = []
|
||||||
|
if hasattr(self.app, "scene_manager") and self.app.scene_manager and hasattr(self.app.scene_manager, "models"):
|
||||||
|
models.extend([m for m in self.app.scene_manager.models if m and not m.isEmpty()])
|
||||||
|
|
||||||
|
ssbo_editor = getattr(self.app, "ssbo_editor", None)
|
||||||
|
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
|
||||||
|
if ssbo_model and not ssbo_model.isEmpty() and ssbo_model.hasParent() and ssbo_model not in models:
|
||||||
|
models.append(ssbo_model)
|
||||||
|
return models
|
||||||
|
|
||||||
|
def _get_ssbo_top_level_model_keys(self):
|
||||||
|
ssbo_editor = getattr(self.app, "ssbo_editor", None)
|
||||||
|
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
|
||||||
|
if not ssbo_editor or not controller or not getattr(controller, "tree_root_key", None):
|
||||||
|
return []
|
||||||
|
|
||||||
|
root_node = controller.tree_nodes.get(controller.tree_root_key)
|
||||||
|
if not root_node:
|
||||||
|
return []
|
||||||
|
|
||||||
|
keys = []
|
||||||
|
for child_key in root_node.get("children", []):
|
||||||
|
if not self._ssbo_key_matches_scene_filter(controller, child_key):
|
||||||
|
continue
|
||||||
|
keys.append(child_key)
|
||||||
|
return keys
|
||||||
|
|
||||||
|
def _get_scene_tree_model_display_count(self):
|
||||||
|
models = self._get_scene_tree_models()
|
||||||
|
ssbo_editor = getattr(self.app, "ssbo_editor", None)
|
||||||
|
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
|
||||||
|
count = 0
|
||||||
|
for model in models:
|
||||||
|
if ssbo_model and model == ssbo_model:
|
||||||
|
count += len(self._get_ssbo_top_level_model_keys())
|
||||||
|
else:
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
def _ssbo_key_matches_scene_filter(self, controller, key):
|
||||||
|
filter_text = self._get_scene_tree_filter()
|
||||||
|
if not filter_text:
|
||||||
|
return True
|
||||||
|
|
||||||
|
node_data = controller.tree_nodes.get(key)
|
||||||
|
if not node_data:
|
||||||
|
return False
|
||||||
|
|
||||||
|
display_name = controller.display_names.get(key, key)
|
||||||
|
if filter_text in display_name.lower() or filter_text in str(key).lower():
|
||||||
|
return True
|
||||||
|
|
||||||
|
return any(self._ssbo_key_matches_scene_filter(controller, child_key) for child_key in node_data["children"])
|
||||||
|
|
||||||
|
def _node_matches_scene_filter(self, node, name):
|
||||||
|
filter_text = self._get_scene_tree_filter()
|
||||||
|
if not filter_text:
|
||||||
|
return True
|
||||||
|
|
||||||
|
display_name = (name or "").lower()
|
||||||
|
node_name = ""
|
||||||
|
try:
|
||||||
|
node_name = (node.getName() or "").lower()
|
||||||
|
except Exception:
|
||||||
|
node_name = ""
|
||||||
|
|
||||||
|
if filter_text in display_name or filter_text in node_name:
|
||||||
|
return True
|
||||||
|
|
||||||
|
ssbo_editor = getattr(self.app, "ssbo_editor", None)
|
||||||
|
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
|
||||||
|
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
|
||||||
|
if controller and ssbo_model and node == ssbo_model:
|
||||||
|
root_key = getattr(controller, "tree_root_key", None)
|
||||||
|
if root_key and self._ssbo_key_matches_scene_filter(controller, root_key):
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
for child in node.getChildren():
|
||||||
|
if not child or child.isEmpty():
|
||||||
|
continue
|
||||||
|
child_name = child.getName() or ""
|
||||||
|
if child_name.startswith("modelCollision_"):
|
||||||
|
continue
|
||||||
|
if self._node_matches_scene_filter(child, child_name):
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def _build_scene_tree(self):
|
def _build_scene_tree(self):
|
||||||
"""构建动态场景树"""
|
"""构建动态场景树"""
|
||||||
# 渲染节点
|
render_entries = []
|
||||||
if imgui.tree_node("渲染"):
|
if hasattr(self.app, "ambient_light") and self.app.ambient_light:
|
||||||
# 环境光
|
render_entries.append((self.app.ambient_light, "环境光", "light"))
|
||||||
if hasattr(self.app, 'ambient_light') and self.app.ambient_light:
|
|
||||||
self._draw_scene_node(self.app.ambient_light, "环境光", "light")
|
|
||||||
|
|
||||||
# 聚光灯
|
|
||||||
if hasattr(self.app, 'scene_manager') and self.app.scene_manager:
|
|
||||||
if hasattr(self.app.scene_manager, 'Spotlight') and self.app.scene_manager.Spotlight:
|
|
||||||
for i, spotlight in enumerate(self.app.scene_manager.Spotlight):
|
|
||||||
self._draw_scene_node(spotlight, f"聚光灯_{i+1}", "light")
|
|
||||||
if hasattr(self.app.scene_manager, 'Pointlight') and self.app.scene_manager.Pointlight:
|
|
||||||
for i, pointlight in enumerate(self.app.scene_manager.Pointlight):
|
|
||||||
self._draw_scene_node(pointlight, f"点光源_{i+1}", "light")
|
|
||||||
|
|
||||||
# 地板
|
|
||||||
if hasattr(self.app, 'ground') and self.app.ground:
|
|
||||||
self._draw_scene_node(self.app.ground, "地板", "geometry")
|
|
||||||
|
|
||||||
imgui.tree_pop()
|
|
||||||
|
|
||||||
# 相机节点
|
|
||||||
if imgui.tree_node("相机"):
|
|
||||||
if hasattr(self.app, 'camera') and self.app.camera:
|
|
||||||
self._draw_scene_node(self.app.camera, "主相机", "camera")
|
|
||||||
imgui.tree_pop()
|
|
||||||
|
|
||||||
# 3D模型节点
|
|
||||||
if imgui.tree_node("模型"):
|
|
||||||
models = []
|
|
||||||
if hasattr(self.app, 'scene_manager') and self.app.scene_manager and hasattr(self.app.scene_manager, 'models'):
|
|
||||||
models.extend([m for m in self.app.scene_manager.models if m and not m.isEmpty()])
|
|
||||||
|
|
||||||
# SSBO模式下,模型可能不在 scene_manager.models 中,补充显示 ssbo_editor.model
|
if hasattr(self.app, "scene_manager") and self.app.scene_manager:
|
||||||
|
if hasattr(self.app.scene_manager, "Spotlight") and self.app.scene_manager.Spotlight:
|
||||||
|
for i, spotlight in enumerate(self.app.scene_manager.Spotlight):
|
||||||
|
render_entries.append((spotlight, f"聚光灯_{i + 1}", "light"))
|
||||||
|
if hasattr(self.app.scene_manager, "Pointlight") and self.app.scene_manager.Pointlight:
|
||||||
|
for i, pointlight in enumerate(self.app.scene_manager.Pointlight):
|
||||||
|
render_entries.append((pointlight, f"点光源_{i + 1}", "light"))
|
||||||
|
|
||||||
|
if hasattr(self.app, "ground") and self.app.ground:
|
||||||
|
render_entries.append((self.app.ground, "地板", "geometry"))
|
||||||
|
|
||||||
|
render_entries = [entry for entry in render_entries if self._node_matches_scene_filter(entry[0], entry[1])]
|
||||||
|
if render_entries and imgui.tree_node(f"渲染 ({len(render_entries)})"):
|
||||||
|
for node, name, node_type in render_entries:
|
||||||
|
self._draw_scene_node(node, name, node_type)
|
||||||
|
imgui.tree_pop()
|
||||||
|
|
||||||
|
camera_entries = []
|
||||||
|
if hasattr(self.app, "camera") and self.app.camera and self._node_matches_scene_filter(self.app.camera, "主相机"):
|
||||||
|
camera_entries.append((self.app.camera, "主相机", "camera"))
|
||||||
|
if camera_entries and imgui.tree_node(f"相机 ({len(camera_entries)})"):
|
||||||
|
for node, name, node_type in camera_entries:
|
||||||
|
self._draw_scene_node(node, name, node_type)
|
||||||
|
imgui.tree_pop()
|
||||||
|
|
||||||
|
models = [model for model in self._get_scene_tree_models() if self._node_matches_scene_filter(model, model.getName() or "模型")]
|
||||||
|
model_display_count = self._get_scene_tree_model_display_count()
|
||||||
|
if model_display_count and imgui.tree_node(f"模型 ({model_display_count})"):
|
||||||
ssbo_editor = getattr(self.app, "ssbo_editor", None)
|
ssbo_editor = getattr(self.app, "ssbo_editor", None)
|
||||||
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
|
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
|
||||||
if ssbo_model and not ssbo_model.isEmpty() and ssbo_model.hasParent() and ssbo_model not in models:
|
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
|
||||||
models.append(ssbo_model)
|
|
||||||
|
|
||||||
if models:
|
for i, model in enumerate(models):
|
||||||
for i, model in enumerate(models):
|
if ssbo_model and controller and model == ssbo_model:
|
||||||
self._draw_scene_node(model, model.getName() or f"模型_{i+1}", "model")
|
for child_key in self._get_ssbo_top_level_model_keys():
|
||||||
else:
|
self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key)
|
||||||
imgui.text("(空)")
|
continue
|
||||||
|
self._draw_scene_node(model, model.getName() or f"模型_{i + 1}", "model")
|
||||||
imgui.tree_pop()
|
imgui.tree_pop()
|
||||||
|
elif not render_entries and not camera_entries and not model_display_count and self._get_scene_tree_filter():
|
||||||
|
imgui.text_disabled("没有匹配的 3D 节点")
|
||||||
|
|
||||||
# if imgui.tree_node("GUI元素"):
|
# if imgui.tree_node("GUI元素"):
|
||||||
# if hasattr(self,'gui_manager') and self.app.gui_manager and hasattr(self.app.gui_manager,'gui_elements'):
|
# if hasattr(self,'gui_manager') and self.app.gui_manager and hasattr(self.app.gui_manager,'gui_elements'):
|
||||||
@ -121,6 +234,8 @@ class EditorPanelsLeftMixin:
|
|||||||
"""绘制单个场景节点"""
|
"""绘制单个场景节点"""
|
||||||
if not node or node.isEmpty():
|
if not node or node.isEmpty():
|
||||||
return
|
return
|
||||||
|
if not self._node_matches_scene_filter(node, name):
|
||||||
|
return
|
||||||
|
|
||||||
# 检查是否被选中
|
# 检查是否被选中
|
||||||
is_selected = (self.app._get_selection_node() == node)
|
is_selected = (self.app._get_selection_node() == node)
|
||||||
@ -234,6 +349,9 @@ class EditorPanelsLeftMixin:
|
|||||||
|
|
||||||
def _draw_ssbo_virtual_tree_node(self, ssbo_editor, controller, key):
|
def _draw_ssbo_virtual_tree_node(self, ssbo_editor, controller, key):
|
||||||
"""Recursively draw SSBO tree_nodes hierarchy in scene tree."""
|
"""Recursively draw SSBO tree_nodes hierarchy in scene tree."""
|
||||||
|
if not self._ssbo_key_matches_scene_filter(controller, key):
|
||||||
|
return
|
||||||
|
|
||||||
node_data = controller.tree_nodes.get(key)
|
node_data = controller.tree_nodes.get(key)
|
||||||
if not node_data:
|
if not node_data:
|
||||||
return
|
return
|
||||||
@ -291,16 +409,12 @@ class EditorPanelsLeftMixin:
|
|||||||
|
|
||||||
self._update_resource_manager_window_rect(rm)
|
self._update_resource_manager_window_rect(rm)
|
||||||
|
|
||||||
imgui.text("文件浏览器")
|
|
||||||
imgui.separator()
|
|
||||||
|
|
||||||
self._draw_resource_toolbar_and_filters(rm)
|
|
||||||
|
|
||||||
imgui.separator()
|
|
||||||
|
|
||||||
rm.refresh_if_needed()
|
rm.refresh_if_needed()
|
||||||
|
|
||||||
dirs, files = rm.get_directory_contents(rm.current_path)
|
dirs, files = rm.get_directory_contents(rm.current_path)
|
||||||
|
self._draw_resource_manager_header(rm, dirs, files)
|
||||||
|
imgui.separator()
|
||||||
|
self._draw_resource_toolbar_and_filters(rm)
|
||||||
|
imgui.separator()
|
||||||
self._draw_resource_directory_entries(rm, dirs)
|
self._draw_resource_directory_entries(rm, dirs)
|
||||||
self._draw_resource_file_entries(rm, files)
|
self._draw_resource_file_entries(rm, files)
|
||||||
self._draw_resource_context_menu(rm)
|
self._draw_resource_context_menu(rm)
|
||||||
@ -456,8 +570,42 @@ class EditorPanelsLeftMixin:
|
|||||||
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
|
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
|
||||||
self._open_resource_item_context_menu(rm, file_path)
|
self._open_resource_item_context_menu(rm, file_path)
|
||||||
|
|
||||||
|
def _draw_resource_manager_header(self, rm, dirs, files):
|
||||||
|
"""绘制资源管理器头部摘要。"""
|
||||||
|
total_items = len(dirs) + len(files)
|
||||||
|
try:
|
||||||
|
location = rm.current_path.relative_to(rm.project_root).as_posix()
|
||||||
|
except ValueError:
|
||||||
|
location = str(rm.current_path)
|
||||||
|
|
||||||
|
imgui.text_disabled(location or ".")
|
||||||
|
imgui.same_line()
|
||||||
|
imgui.text_disabled(f"{total_items} 项")
|
||||||
|
if rm.selected_files:
|
||||||
|
imgui.same_line()
|
||||||
|
imgui.text_colored((0.5, 0.8, 1.0, 1.0), f"已选 {len(rm.selected_files)}")
|
||||||
|
|
||||||
|
def _sync_resource_path_input(self, rm):
|
||||||
|
current_path_text = str(rm.current_path)
|
||||||
|
if getattr(self.app, "_resource_path_source", "") != current_path_text:
|
||||||
|
self.app._resource_path_input = current_path_text
|
||||||
|
self.app._resource_path_source = current_path_text
|
||||||
|
|
||||||
|
def _navigate_resource_path_from_input(self, rm):
|
||||||
|
raw_path = (getattr(self.app, "_resource_path_input", "") or "").strip().strip('"')
|
||||||
|
if not raw_path:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
rm.navigate_to(Path(raw_path))
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
self.app._resource_path_input = str(rm.current_path)
|
||||||
|
self.app._resource_path_source = self.app._resource_path_input
|
||||||
|
|
||||||
def _draw_resource_toolbar_and_filters(self, rm):
|
def _draw_resource_toolbar_and_filters(self, rm):
|
||||||
"""绘制资源管理器顶部工具条与筛选输入。保持原有按钮顺序与文案。"""
|
"""绘制资源管理器顶部工具条与筛选输入。"""
|
||||||
|
self._sync_resource_path_input(rm)
|
||||||
|
|
||||||
if imgui.button("◀"):
|
if imgui.button("◀"):
|
||||||
rm.navigate_back()
|
rm.navigate_back()
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
@ -479,20 +627,21 @@ class EditorPanelsLeftMixin:
|
|||||||
if changed:
|
if changed:
|
||||||
rm.set_auto_refresh(rm.auto_refresh_enabled)
|
rm.set_auto_refresh(rm.auto_refresh_enabled)
|
||||||
|
|
||||||
imgui.same_line()
|
imgui.text_disabled("路径")
|
||||||
imgui.text(" ")
|
imgui.set_next_item_width(-72)
|
||||||
imgui.same_line()
|
changed, new_path = imgui.input_text("##resource_path", self.app._resource_path_input, 512)
|
||||||
|
|
||||||
# 路径输入框
|
|
||||||
changed, new_path = imgui.input_text("路径", str(rm.current_path), 256)
|
|
||||||
if changed:
|
if changed:
|
||||||
try:
|
self.app._resource_path_input = new_path
|
||||||
rm.navigate_to(Path(new_path))
|
imgui.same_line()
|
||||||
except Exception:
|
if imgui.button("前往##resource_go"):
|
||||||
pass
|
self._navigate_resource_path_from_input(rm)
|
||||||
|
|
||||||
# 搜索框
|
imgui.text_disabled("搜索")
|
||||||
changed, rm.search_filter = imgui.input_text("搜索", rm.search_filter, 256)
|
imgui.set_next_item_width(-72)
|
||||||
|
changed, rm.search_filter = imgui.input_text("##resource_search", rm.search_filter, 256)
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.button("清除##resource_search"):
|
||||||
|
rm.search_filter = ""
|
||||||
|
|
||||||
def _load_resource_icon(self, icon_name: str):
|
def _load_resource_icon(self, icon_name: str):
|
||||||
"""加载资源图标;失败时返回 None。"""
|
"""加载资源图标;失败时返回 None。"""
|
||||||
|
|||||||
@ -45,35 +45,14 @@ class EditorPanelsRightMixin(
|
|||||||
if ssbo_summary:
|
if ssbo_summary:
|
||||||
self._draw_ssbo_selection_summary(ssbo_summary)
|
self._draw_ssbo_selection_summary(ssbo_summary)
|
||||||
return
|
return
|
||||||
# 无选中对象时显示提示(模仿Qt版本的空状态样式)
|
self._draw_empty_property_state()
|
||||||
imgui.spacing()
|
|
||||||
imgui.spacing()
|
def _draw_empty_property_state(self):
|
||||||
|
imgui.separator_text("属性")
|
||||||
# 居中显示提示信息
|
imgui.text_disabled("当前没有选中对象")
|
||||||
window_width = imgui.get_window_width()
|
imgui.spacing()
|
||||||
text_width = 200 # 估算文本宽度
|
imgui.bullet_text("从左侧场景树或场景视口中选择一个对象")
|
||||||
text_pos_x = (window_width - text_width) / 2
|
imgui.bullet_text("按 F 聚焦到对象,按 Delete 删除对象")
|
||||||
|
|
||||||
imgui.set_cursor_pos_x(text_pos_x)
|
|
||||||
imgui.text_colored((0.5, 0.5, 0.5, 1.0), "🔍 未选择任何对象")
|
|
||||||
|
|
||||||
imgui.set_cursor_pos_x(text_pos_x - 20)
|
|
||||||
imgui.text("请从场景树中选择一个对象")
|
|
||||||
|
|
||||||
imgui.set_cursor_pos_x(text_pos_x + 10)
|
|
||||||
imgui.text("以查看其属性")
|
|
||||||
|
|
||||||
imgui.spacing()
|
|
||||||
imgui.spacing()
|
|
||||||
|
|
||||||
# 添加一些分隔线和装饰
|
|
||||||
imgui.separator()
|
|
||||||
|
|
||||||
# 显示快速提示
|
|
||||||
imgui.text("💡 快速提示:")
|
|
||||||
imgui.bullet_text("单击场景树中的对象进行选择")
|
|
||||||
imgui.bullet_text("使用 F 键快速聚焦到选中对象")
|
|
||||||
imgui.bullet_text("使用 Delete 键删除选中对象")
|
|
||||||
|
|
||||||
def _draw_property_section(self, title, draw_callback, default_open=False):
|
def _draw_property_section(self, title, draw_callback, default_open=False):
|
||||||
flags = imgui.TreeNodeFlags_.span_avail_width.value
|
flags = imgui.TreeNodeFlags_.span_avail_width.value
|
||||||
@ -85,18 +64,15 @@ class EditorPanelsRightMixin(
|
|||||||
|
|
||||||
def _draw_ssbo_selection_summary(self, summary):
|
def _draw_ssbo_selection_summary(self, summary):
|
||||||
"""Render a safe summary for SSBO group selections without exposing wrong node properties."""
|
"""Render a safe summary for SSBO group selections without exposing wrong node properties."""
|
||||||
imgui.spacing()
|
imgui.separator_text("SSBO 选择")
|
||||||
imgui.text("SSBO 选择摘要")
|
|
||||||
imgui.separator()
|
|
||||||
imgui.text(f"名称: {summary.get('display_name') or '未命名'}")
|
imgui.text(f"名称: {summary.get('display_name') or '未命名'}")
|
||||||
imgui.text(f"对象数量: {summary.get('object_count', 0)}")
|
imgui.text(f"对象数量: {summary.get('object_count', 0)}")
|
||||||
if summary.get("is_root"):
|
if summary.get("is_root"):
|
||||||
imgui.text_colored((0.5, 0.8, 1.0, 1.0), "当前选中的是 SSBO 根模型")
|
imgui.text_colored((0.5, 0.8, 1.0, 1.0), "当前选中的是 SSBO 根模型")
|
||||||
elif summary.get("is_group"):
|
elif summary.get("is_group"):
|
||||||
imgui.text_colored((1.0, 0.8, 0.3, 1.0), "当前选中的是 SSBO 组合节点")
|
imgui.text_colored((1.0, 0.8, 0.3, 1.0), "当前选中的是 SSBO 组合节点")
|
||||||
imgui.text_wrapped("这个选择对应多个动态对象。为避免误改错误节点,这里先显示摘要信息。")
|
imgui.text_wrapped("这个选择对应多个动态对象。这里先显示摘要,避免误改到错误节点。")
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
imgui.text("建议:")
|
|
||||||
imgui.bullet_text("在左侧展开到叶子节点后查看单对象属性")
|
imgui.bullet_text("在左侧展开到叶子节点后查看单对象属性")
|
||||||
imgui.bullet_text("继续使用场景中的 Gizmo 做组合移动")
|
imgui.bullet_text("继续使用场景中的 Gizmo 做组合移动")
|
||||||
else:
|
else:
|
||||||
@ -166,78 +142,56 @@ class EditorPanelsRightMixin(
|
|||||||
except Exception:
|
except Exception:
|
||||||
parent = None
|
parent = None
|
||||||
|
|
||||||
imgui.text_disabled(f"{node_type} | Parent: {parent_name}")
|
imgui.text_disabled(f"{node_type} · 父级: {parent_name}")
|
||||||
|
self._draw_status_badges(node, node_type)
|
||||||
|
|
||||||
if hasattr(node, "getPythonTag") and node.getPythonTag("script"):
|
def _draw_status_badges(self, node, node_type=None):
|
||||||
imgui.same_line()
|
"""绘制精简后的对象状态徽章行。"""
|
||||||
imgui.text_colored((0.8, 0.4, 0.8, 1.0), "[Script]")
|
if node_type is None:
|
||||||
|
node_type = self.app._get_node_type_from_node(node)
|
||||||
|
|
||||||
if node_type == "模型" and node.hasTag("has_animations") and node.getTag("has_animations").lower() == "true":
|
badges = []
|
||||||
imgui.same_line()
|
|
||||||
imgui.text_colored((0.4, 0.8, 0.4, 1.0), "[Animation]")
|
|
||||||
|
|
||||||
def _draw_status_badges(self, node):
|
if node.is_hidden():
|
||||||
"""绘制对象状态徽章行。"""
|
badges.append(("已隐藏", (0.65, 0.65, 0.65, 1.0)))
|
||||||
is_visible = not node.is_hidden()
|
|
||||||
visibility_color = (0.176, 1.0, 0.769, 1.0) if is_visible else (0.953, 0.616, 0.471, 1.0)
|
|
||||||
visibility_text = "Visible" if is_visible else "Hidden"
|
|
||||||
|
|
||||||
badges = [(visibility_text, visibility_color)]
|
has_collision = hasattr(node, "getChild") and any(
|
||||||
|
"Collision" in child.getName() for child in node.getChildren() if child.getName()
|
||||||
|
)
|
||||||
|
if has_collision:
|
||||||
|
badges.append(("碰撞", (0.35, 0.65, 1.0, 1.0)))
|
||||||
|
|
||||||
node_type = self._get_node_type_from_node(node)
|
has_script = hasattr(node, "getPythonTag") and node.getPythonTag("script")
|
||||||
type_colors = {
|
if has_script:
|
||||||
"GUI元素": (0.188, 0.404, 0.753, 1.0),
|
badges.append(("脚本", (0.86, 0.48, 0.86, 1.0)))
|
||||||
"光源": (1.0, 0.8, 0.2, 1.0),
|
|
||||||
"模型": (0.6, 0.8, 1.0, 1.0),
|
|
||||||
"相机": (0.8, 0.8, 0.2, 1.0),
|
|
||||||
"几何体": (0.5, 0.5, 0.5, 1.0),
|
|
||||||
}
|
|
||||||
if node_type in type_colors:
|
|
||||||
badges.append((node_type, type_colors[node_type]))
|
|
||||||
|
|
||||||
has_collision = hasattr(node, "getChild") and any(
|
has_animation = False
|
||||||
"Collision" in child.getName() for child in node.getChildren() if child.getName()
|
if node_type == "模型":
|
||||||
)
|
if node.hasTag("has_animations"):
|
||||||
if has_collision:
|
has_animation = node.getTag("has_animations").lower() == "true"
|
||||||
badges.append(("Collision", (0.2, 0.4, 0.8, 1.0)))
|
if not has_animation:
|
||||||
|
|
||||||
has_script = hasattr(node, "getPythonTag") and node.getPythonTag("script")
|
|
||||||
if has_script:
|
|
||||||
badges.append(("Script", (0.8, 0.4, 0.8, 1.0)))
|
|
||||||
|
|
||||||
has_animation = False
|
|
||||||
if node_type == "模型":
|
|
||||||
if node.hasTag("has_animations"):
|
|
||||||
has_animation = node.getTag("has_animations").lower() == "true"
|
|
||||||
if not has_animation:
|
|
||||||
try:
|
|
||||||
has_character = node.findAllMatches("**/+Character").getNumPaths() > 0
|
|
||||||
has_bundle = node.findAllMatches("**/+AnimBundleNode").getNumPaths() > 0
|
|
||||||
has_animation = has_character or has_bundle
|
|
||||||
if has_animation:
|
|
||||||
node.setTag("has_animations", "true")
|
|
||||||
node.setTag("can_create_actor_from_memory", "true")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
cached_result = node.getPythonTag("animation")
|
cached_result = node.getPythonTag("animation")
|
||||||
if cached_result is True:
|
if cached_result is True:
|
||||||
has_animation = True
|
has_animation = True
|
||||||
elif cached_result is False:
|
elif cached_result is False:
|
||||||
has_animation = False
|
has_animation = False
|
||||||
else:
|
else:
|
||||||
has_animation = hasattr(node, "getPythonTag") and node.getPythonTag("animation")
|
has_animation = hasattr(node, "getPythonTag") and node.getPythonTag("animation")
|
||||||
|
|
||||||
if has_animation:
|
if has_animation:
|
||||||
badges.append(("Animation", (0.4, 0.8, 0.4, 1.0)))
|
badges.append(("动画", (0.45, 0.85, 0.55, 1.0)))
|
||||||
|
|
||||||
has_material = hasattr(node, "getMaterial") and node.getMaterial()
|
has_material = hasattr(node, "getMaterial") and node.getMaterial()
|
||||||
if has_material:
|
if has_material:
|
||||||
badges.append(("Material", (0.8, 0.6, 0.2, 1.0)))
|
badges.append(("材质", (0.9, 0.72, 0.35, 1.0)))
|
||||||
|
|
||||||
for index, (badge_text, badge_color) in enumerate(badges):
|
if not badges:
|
||||||
if index > 0:
|
return
|
||||||
imgui.same_line()
|
|
||||||
imgui.text_colored(badge_color, f"[{badge_text}]")
|
for index, (badge_text, badge_color) in enumerate(badges):
|
||||||
|
if index > 0:
|
||||||
|
imgui.same_line()
|
||||||
|
imgui.text_colored(badge_color, f"[{badge_text}]")
|
||||||
|
|
||||||
def _draw_gui_properties(self, node):
|
def _draw_gui_properties(self, node):
|
||||||
"""绘制GUI元素属性"""
|
"""绘制GUI元素属性"""
|
||||||
|
|||||||
@ -91,9 +91,10 @@ class EditorPanelsRightMaterialMixin:
|
|||||||
|
|
||||||
if self.app._get_material_surface_type(material) == 3:
|
if self.app._get_material_surface_type(material) == 3:
|
||||||
opacity = self.app._get_material_opacity(material)
|
opacity = self.app._get_material_opacity(material)
|
||||||
changed, new_opacity = imgui.slider_float("透明度", opacity, 0.0, 1.0)
|
transparency = 1.0 - opacity
|
||||||
|
changed, new_transparency = imgui.slider_float("透明度", transparency, 0.0, 1.0)
|
||||||
if changed:
|
if changed:
|
||||||
apply_opacity(new_opacity)
|
apply_opacity(1.0 - new_transparency)
|
||||||
|
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
|
|
||||||
@ -210,9 +211,15 @@ class EditorPanelsRightMaterialMixin:
|
|||||||
imgui.text("透明度设置")
|
imgui.text("透明度设置")
|
||||||
try:
|
try:
|
||||||
current_opacity = self.app._get_material_opacity(material)
|
current_opacity = self.app._get_material_opacity(material)
|
||||||
changed, new_opacity = imgui.slider_float(f"不透明度##opacity_{material_index}", current_opacity, 0.0, 1.0)
|
current_transparency = 1.0 - current_opacity
|
||||||
|
changed, new_transparency = imgui.slider_float(
|
||||||
|
f"透明度##opacity_{material_index}",
|
||||||
|
current_transparency,
|
||||||
|
0.0,
|
||||||
|
1.0,
|
||||||
|
)
|
||||||
if changed:
|
if changed:
|
||||||
self.app._set_material_opacity(node, material, new_opacity)
|
self.app._set_material_opacity(node, material, 1.0 - new_transparency)
|
||||||
except:
|
except:
|
||||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "透明度控制不可用")
|
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "透明度控制不可用")
|
||||||
|
|
||||||
|
|||||||
@ -751,13 +751,20 @@ class PropertyHelpers:
|
|||||||
if previous_surface_type == 3:
|
if previous_surface_type == 3:
|
||||||
opacity = self._get_material_opacity(material)
|
opacity = self._get_material_opacity(material)
|
||||||
else:
|
else:
|
||||||
base_alpha = float(self._get_material_base_color(material)[3])
|
# Entering transparent mode should keep the object fully
|
||||||
opacity = base_alpha if 0.0 < base_alpha <= 1.0 else 1.0
|
# visible by default. Reusing historical base-color alpha
|
||||||
|
# makes some assets appear to "disappear" immediately.
|
||||||
|
opacity = 0.92
|
||||||
|
material.set_emission(Vec4(float(surface_type), float(emission.y), float(emission.z), float(emission.w)))
|
||||||
|
self._set_material_opacity(node, material, opacity)
|
||||||
else:
|
else:
|
||||||
opacity = 1.0
|
opacity = 1.0
|
||||||
material.set_emission(Vec4(float(surface_type), float(emission.y), opacity, float(emission.w)))
|
material.set_emission(Vec4(float(surface_type), float(emission.y), opacity, float(emission.w)))
|
||||||
self._apply_material_to_geom_states(node, material)
|
base_color = list(self._get_material_base_color(material))
|
||||||
self._apply_material_surface_state(node, material)
|
base_color[3] = 1.0
|
||||||
|
self._set_material_base_color(material, tuple(base_color))
|
||||||
|
self._apply_material_to_geom_states(node, material)
|
||||||
|
self._apply_material_surface_state(node, material)
|
||||||
if refresh_pipeline:
|
if refresh_pipeline:
|
||||||
self._refresh_pipeline_material_mode(node, material)
|
self._refresh_pipeline_material_mode(node, material)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -789,9 +796,16 @@ class PropertyHelpers:
|
|||||||
if not render_pipeline or not node or node.isEmpty():
|
if not render_pipeline or not node or node.isEmpty():
|
||||||
return
|
return
|
||||||
if self._material_uses_transparent_pass(material) and hasattr(render_pipeline, "prepare_scene"):
|
if self._material_uses_transparent_pass(material) and hasattr(render_pipeline, "prepare_scene"):
|
||||||
|
current_opacity = self._get_material_opacity(material)
|
||||||
self._bake_effective_geom_materials(node)
|
self._bake_effective_geom_materials(node)
|
||||||
self._isolate_transparent_geoms(node)
|
split_changed = self._isolate_transparent_geoms(node)
|
||||||
|
if split_changed:
|
||||||
|
self._bake_effective_geom_materials(node)
|
||||||
|
self._apply_material_surface_state(node, material)
|
||||||
render_pipeline.prepare_scene(node)
|
render_pipeline.prepare_scene(node)
|
||||||
|
self._set_material_opacity(node, material, current_opacity)
|
||||||
|
if split_changed:
|
||||||
|
self._apply_material_surface_state(node, material)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"刷新RenderPipeline材质模式失败: {e}")
|
print(f"刷新RenderPipeline材质模式失败: {e}")
|
||||||
|
|
||||||
@ -1105,6 +1119,12 @@ class PropertyHelpers:
|
|||||||
surface_type = self._get_material_surface_type(material)
|
surface_type = self._get_material_surface_type(material)
|
||||||
if surface_type != 3:
|
if surface_type != 3:
|
||||||
surface_type = 3
|
surface_type = 3
|
||||||
|
else:
|
||||||
|
# The RP forward transparent path in this editor build becomes
|
||||||
|
# visually unusable at exact opacity 1.0. Treat transparent
|
||||||
|
# mode as a slightly blended surface; users can switch back to
|
||||||
|
# "不透明" when they want a fully opaque result.
|
||||||
|
opacity_value = min(opacity_value, 0.92)
|
||||||
material.set_emission(Vec4(float(surface_type), float(emission.y), opacity_value, float(emission.w)))
|
material.set_emission(Vec4(float(surface_type), float(emission.y), opacity_value, float(emission.w)))
|
||||||
|
|
||||||
base_color = list(self._get_material_base_color(material))
|
base_color = list(self._get_material_base_color(material))
|
||||||
|
|||||||