@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