144 lines
4.2 KiB
GDScript
144 lines
4.2 KiB
GDScript
@tool
|
||
extends EditorPlugin
|
||
|
||
## 让 toon_apply 的材质在编辑器里"改模板即时生效"。
|
||
##
|
||
## 背景:toon_apply.gd 给每个 MeshInstance3D 都 duplicate() 了一份独立材质
|
||
## (因为每个模型有自己的贴图,必须独立),所以之后再改 shader/toon.tres 上的
|
||
## 材质级参数(color / specular / fresnel / uv_scale / uv_offset / normal_scale …)
|
||
## 不会传到那些副本上,必须重新跑一次 _apply_materials()。
|
||
##
|
||
## ShaderMaterial 的 Resource.changed 信号在改 shader 参数时不会发出(官方出于性能
|
||
## 考虑只在 resource_name 变化时发),所以没法在 toon_apply.gd 内部自己监听。
|
||
## 这里改从编辑器侧挂:监听 Inspector 的 property_edited,命中 toon.tres 时重刷。
|
||
##
|
||
## 注意:本插件只影响"当前正在编辑的场景"里的 toon_apply 节点。
|
||
|
||
const TARGET_MATERIAL := "res://shader/toon.tres"
|
||
const TARGET_SCRIPT_FILE := "toon_apply.gd"
|
||
|
||
## 防抖间隔:拖颜色滑块时每一步都会触发 property_edited,等停手再重刷一次。
|
||
const DEBOUNCE_SEC := 0.2
|
||
|
||
const MENU_ITEM := "刷新 toon 材质(当前场景)"
|
||
|
||
var _inspector: EditorInspector
|
||
var _dirty := false
|
||
var _elapsed := 0.0
|
||
|
||
|
||
func _enter_tree() -> void:
|
||
_inspector = EditorInterface.get_inspector()
|
||
if _inspector != null and not _inspector.property_edited.is_connected(_on_property_edited):
|
||
_inspector.property_edited.connect(_on_property_edited)
|
||
|
||
add_tool_menu_item(MENU_ITEM, refresh_edited_scene)
|
||
set_process(false)
|
||
print("[toon_refresh] 已启用:改 %s 会自动重刷当前场景的 toon_apply 材质" % TARGET_MATERIAL)
|
||
|
||
|
||
func _exit_tree() -> void:
|
||
remove_tool_menu_item(MENU_ITEM)
|
||
|
||
if _inspector != null and _inspector.property_edited.is_connected(_on_property_edited):
|
||
_inspector.property_edited.disconnect(_on_property_edited)
|
||
_inspector = null
|
||
|
||
|
||
func _process(delta: float) -> void:
|
||
# 即便 set_process(false) 被引擎重开也安全:这里第一件事就是挡掉
|
||
if not _dirty:
|
||
return
|
||
|
||
_elapsed += delta
|
||
if _elapsed < DEBOUNCE_SEC:
|
||
return
|
||
|
||
_dirty = false
|
||
_elapsed = 0.0
|
||
set_process(false)
|
||
refresh_edited_scene()
|
||
|
||
|
||
# --- 触发源 ---------------------------------------------------------------
|
||
|
||
func _on_property_edited(_property: String) -> void:
|
||
if _inspector == null:
|
||
return
|
||
if not _is_target_material(_inspector.get_edited_object()):
|
||
return
|
||
|
||
_dirty = true
|
||
_elapsed = 0.0
|
||
set_process(true)
|
||
|
||
|
||
## 判断被编辑的资源是不是 toon.tres,或者是它内联的 next_pass 子材质(描边)。
|
||
func _is_target_material(obj: Object) -> bool:
|
||
if not (obj is ShaderMaterial):
|
||
return false
|
||
|
||
var mat: ShaderMaterial = obj
|
||
|
||
if mat.resource_path == TARGET_MATERIAL:
|
||
return true
|
||
|
||
# 内联子资源(描边材质)的路径形如
|
||
# res://shader/toon.tres::ShaderMaterial_m5gih
|
||
if mat.resource_path.begins_with(TARGET_MATERIAL + "::"):
|
||
return true
|
||
|
||
# 兜底:按实例比对 next_pass 链
|
||
var template := load(TARGET_MATERIAL) as ShaderMaterial
|
||
if template == null:
|
||
return false
|
||
|
||
var pass_mat: Material = template.next_pass
|
||
while pass_mat != null:
|
||
if pass_mat == mat:
|
||
return true
|
||
pass_mat = pass_mat.next_pass
|
||
|
||
return false
|
||
|
||
|
||
# --- 刷新 -----------------------------------------------------------------
|
||
|
||
## 重刷当前编辑场景里所有挂着 toon_apply.gd 的节点。返回刷新的节点数。
|
||
func refresh_edited_scene() -> int:
|
||
var root := EditorInterface.get_edited_scene_root()
|
||
if root == null:
|
||
return 0
|
||
|
||
var count := _refresh_branch(root)
|
||
if count > 0:
|
||
print("[toon_refresh] 已重刷 %d 个 toon_apply 节点的材质" % count)
|
||
return count
|
||
|
||
|
||
func _refresh_branch(root: Node) -> int:
|
||
var count := 0
|
||
var stack: Array[Node] = [root]
|
||
|
||
while not stack.is_empty():
|
||
var node: Node = stack.pop_back()
|
||
if _is_toon_apply(node):
|
||
node.call("_apply_materials")
|
||
count += 1
|
||
for child in node.get_children():
|
||
stack.append(child)
|
||
|
||
return count
|
||
|
||
|
||
## 按脚本文件名匹配,避免依赖 class_name;同时要求确实有 _apply_materials,
|
||
## 防止脚本编译失败时调用出错。
|
||
func _is_toon_apply(node: Node) -> bool:
|
||
if not node.has_method("_apply_materials"):
|
||
return false
|
||
|
||
var scr := node.get_script()
|
||
if not (scr is Script):
|
||
return false
|
||
return (scr as Script).resource_path.get_file() == TARGET_SCRIPT_FILE
|