toon_refresh
This commit is contained in:
@@ -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"
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://dvoc5463koalp
|
||||
Reference in New Issue
Block a user