diff --git a/addons/toon_refresh/plugin.cfg b/addons/toon_refresh/plugin.cfg new file mode 100644 index 0000000..230dd97 --- /dev/null +++ b/addons/toon_refresh/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Toon 材质自动刷新" +description="在编辑器里修改 shader/toon.tres(含内联的描边 next_pass 子材质)参数时,自动重刷当前编辑场景中所有 toon_apply 节点的材质,不必再手动勾 refresh_now。也可从 项目 > 工具 菜单手动触发。" +author="agame" +version="1.0" +script="plugin.gd" diff --git a/addons/toon_refresh/plugin.gd b/addons/toon_refresh/plugin.gd new file mode 100644 index 0000000..38ebcf9 --- /dev/null +++ b/addons/toon_refresh/plugin.gd @@ -0,0 +1,143 @@ +@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 diff --git a/addons/toon_refresh/plugin.gd.uid b/addons/toon_refresh/plugin.gd.uid new file mode 100644 index 0000000..0df7f11 --- /dev/null +++ b/addons/toon_refresh/plugin.gd.uid @@ -0,0 +1 @@ +uid://dvoc5463koalp diff --git a/project.godot b/project.godot index 8f2dd5f..4de98fb 100644 --- a/project.godot +++ b/project.godot @@ -36,7 +36,7 @@ project/assembly_name="agame" [editor_plugins] -enabled=PackedStringArray("res://addons/godot_ai/plugin.cfg", "res://addons/godotsteam/plugin.cfg", "res://addons/sky_3d/plugin.cfg") +enabled=PackedStringArray("res://addons/godot_ai/plugin.cfg", "res://addons/godotsteam/plugin.cfg", "res://addons/sky_3d/plugin.cfg", "res://addons/toon_refresh/plugin.cfg") [input]