using UnityEngine; namespace IndianOceanAssets.Engine2_5D { /// /// 描边管理器 —— 自动为场景中所有 SpriteRenderer(排除挂载 LightSource 的物体) /// 创建一个使用 SpriteEchoOutline shader 的子物体,用于回声描边效果。 /// /// 为什么需要子物体: /// - 精灵本体用原始材质(如 SpriteShader.shadergraph)渲染,在黑暗遮罩之下 → 黑暗中不可见 /// - 描边用 SpriteEchoOutline 材质渲染,队列在黑暗遮罩之上(Transparent+200)→ 黑暗中可见 /// - 同一个 SpriteRenderer 只能有一个材质,所以用子物体承载描边材质 /// /// 使用方式: /// 1. 场景中创建空物体,挂上此脚本 /// 2. 把使用 SpriteEchoOutline shader 的材质拖到 outlineMaterial 字段 /// 3. 运行后自动为每个非光源精灵生成描边子物体 /// 4. 场景中新增精灵后可右键组件 > Refresh Outlines 重新扫描 /// public class EchoOutlineManager : MonoBehaviour { [Header("描边材质(使用 SpriteEchoOutline shader)")] [SerializeField] private Material outlineMaterial; [Header("过滤")] [Tooltip("只处理这些层的精灵(默认 Everything)")] [SerializeField] private LayerMask includeLayers = ~0; [Tooltip("勾选后,挂载 LightSource 的物体(含父级)不生成描边")] [SerializeField] private bool skipLightSources = true; private static readonly int EchoIntensityID = Shader.PropertyToID("_EchoOutlineIntensity"); private static readonly int OutlineColorID = Shader.PropertyToID("_OutlineColor"); private static readonly int OutlineWidthID = Shader.PropertyToID("_OutlineWidth"); private static readonly int MainTexID = Shader.PropertyToID("_MainTex"); private readonly System.Collections.Generic.List _parentRenderers = new System.Collections.Generic.List(); private readonly System.Collections.Generic.List _outlineRenderers = new System.Collections.Generic.List(); private bool _renderersEnabled = false; private void Start() { RefreshList(); } /// /// 重新扫描场景,为符合条件的精灵创建描边子物体。 /// 场景中新增精灵后可调用(也有右键菜单)。 /// [ContextMenu("Refresh Outlines")] public void RefreshList() { ClearOutlines(); var all = FindObjectsByType(FindObjectsSortMode.None); foreach (var sr in all) { if (sr == null) continue; if ((includeLayers.value & (1 << sr.gameObject.layer)) == 0) continue; if (skipLightSources && sr.GetComponentInParent() != null) continue; // 跳过已经是描边子物体的(避免重复)。用 sharedMaterial 避免实例化父级材质 if (sr.sharedMaterial != null && sr.sharedMaterial.shader != null && sr.sharedMaterial.shader.name == "IndianOcean/SpriteEchoOutline") continue; // 读取自定义描边覆盖(颜色/宽度/禁用) var colorOverride = sr.GetComponentInParent(); if (colorOverride != null && colorOverride.disableOutline) continue; CreateOutlineChild(sr, colorOverride); } _renderersEnabled = false; UpdateRendererEnabled(); } private void CreateOutlineChild(SpriteRenderer parent, EchoOutlineColorOverride colorOverride) { var child = new GameObject($"EchoOutline_{parent.gameObject.name}"); child.transform.SetParent(parent.transform, false); child.transform.localPosition = Vector3.zero; child.transform.localRotation = Quaternion.identity; child.transform.localScale = Vector3.one; var cr = child.AddComponent(); cr.sprite = parent.sprite; cr.color = Color.white; cr.flipX = parent.flipX; cr.flipY = parent.flipY; // 有自定义颜色/宽度时创建材质实例;否则共享材质 if (colorOverride != null) { // 用材质实例而非 MPB:MPB 会干扰 SpriteRenderer 的 _MainTex 自动绑定, // 导致 shader 采样到白色贴图 → alpha=1 → 描边公式算出 0 → 描边消失 var mat = new Material(outlineMaterial); mat.SetColor(OutlineColorID, colorOverride.outlineColor); if (colorOverride.outlineWidth >= 0f) mat.SetFloat(OutlineWidthID, colorOverride.outlineWidth); if (parent.sprite != null) mat.SetTexture(MainTexID, parent.sprite.texture); cr.sharedMaterial = mat; } else { cr.sharedMaterial = outlineMaterial; } cr.sortingLayerID = parent.sortingLayerID; cr.sortingOrder = parent.sortingOrder + 1; cr.enabled = false; // 默认关闭,回声激活时再开 _parentRenderers.Add(parent); _outlineRenderers.Add(cr); } private void Update() { // 回声不活跃时关闭所有描边渲染器(省 draw call) UpdateRendererEnabled(); // 同步精灵帧(动画)、翻转、排序 if (!_renderersEnabled) return; for (int i = 0; i < _outlineRenderers.Count; i++) { var cr = _outlineRenderers[i]; var pr = _parentRenderers[i]; if (pr == null || cr == null) continue; if (cr.sprite != pr.sprite) { cr.sprite = pr.sprite; // 有自定义材质实例时同步纹理(共享材质由 SpriteRenderer 自动绑定) var mat = cr.sharedMaterial; if (mat != null && mat != outlineMaterial && pr.sprite != null) mat.SetTexture(MainTexID, pr.sprite.texture); } if (cr.flipX != pr.flipX) cr.flipX = pr.flipX; if (cr.flipY != pr.flipY) cr.flipY = pr.flipY; if (cr.sortingLayerID != pr.sortingLayerID) cr.sortingLayerID = pr.sortingLayerID; if (cr.sortingOrder != pr.sortingOrder + 1) cr.sortingOrder = pr.sortingOrder + 1; } } private void UpdateRendererEnabled() { float intensity = Shader.GetGlobalFloat(EchoIntensityID); bool shouldEnable = intensity > 0.01f; if (shouldEnable == _renderersEnabled) return; _renderersEnabled = shouldEnable; for (int i = 0; i < _outlineRenderers.Count; i++) { if (_outlineRenderers[i] != null) _outlineRenderers[i].enabled = shouldEnable; } } private void ClearOutlines() { for (int i = 0; i < _outlineRenderers.Count; i++) { if (_outlineRenderers[i] != null) { // 清理自定义材质实例(共享材质 outlineMaterial 不销毁) var mat = _outlineRenderers[i].sharedMaterial; if (mat != null && mat != outlineMaterial) Destroy(mat); Destroy(_outlineRenderers[i].gameObject); } } _parentRenderers.Clear(); _outlineRenderers.Clear(); } private void OnDestroy() { ClearOutlines(); } } }