Files
gold_dolphin/unity/Assets/Light/scripts/EchoOutlineManager.cs
2026-06-27 03:36:22 +08:00

186 lines
7.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using UnityEngine;
namespace IndianOceanAssets.Engine2_5D
{
/// <summary>
/// 描边管理器 —— 自动为场景中所有 SpriteRenderer排除挂载 LightSource 的物体)
/// 创建一个使用 SpriteEchoOutline shader 的子物体,用于回声描边效果。
///
/// 为什么需要子物体:
/// - 精灵本体用原始材质(如 SpriteShader.shadergraph渲染在黑暗遮罩之下 → 黑暗中不可见
/// - 描边用 SpriteEchoOutline 材质渲染队列在黑暗遮罩之上Transparent+200→ 黑暗中可见
/// - 同一个 SpriteRenderer 只能有一个材质,所以用子物体承载描边材质
///
/// 使用方式:
/// 1. 场景中创建空物体,挂上此脚本
/// 2. 把使用 SpriteEchoOutline shader 的材质拖到 outlineMaterial 字段
/// 3. 运行后自动为每个非光源精灵生成描边子物体
/// 4. 场景中新增精灵后可右键组件 > Refresh Outlines 重新扫描
/// </summary>
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<SpriteRenderer> _parentRenderers
= new System.Collections.Generic.List<SpriteRenderer>();
private readonly System.Collections.Generic.List<SpriteRenderer> _outlineRenderers
= new System.Collections.Generic.List<SpriteRenderer>();
private bool _renderersEnabled = false;
private void Start()
{
RefreshList();
}
/// <summary>
/// 重新扫描场景,为符合条件的精灵创建描边子物体。
/// 场景中新增精灵后可调用(也有右键菜单)。
/// </summary>
[ContextMenu("Refresh Outlines")]
public void RefreshList()
{
ClearOutlines();
var all = FindObjectsByType<SpriteRenderer>(FindObjectsSortMode.None);
foreach (var sr in all)
{
if (sr == null) continue;
if ((includeLayers.value & (1 << sr.gameObject.layer)) == 0) continue;
if (skipLightSources && sr.GetComponentInParent<LightSource>() != null) continue;
// 跳过已经是描边子物体的(避免重复)。用 sharedMaterial 避免实例化父级材质
if (sr.sharedMaterial != null && sr.sharedMaterial.shader != null
&& sr.sharedMaterial.shader.name == "IndianOcean/SpriteEchoOutline")
continue;
// 读取自定义描边覆盖(颜色/宽度/禁用)
var colorOverride = sr.GetComponentInParent<EchoOutlineColorOverride>();
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<SpriteRenderer>();
cr.sprite = parent.sprite;
cr.color = Color.white;
cr.flipX = parent.flipX;
cr.flipY = parent.flipY;
// 有自定义颜色/宽度时创建材质实例;否则共享材质
if (colorOverride != null)
{
// 用材质实例而非 MPBMPB 会干扰 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();
}
}
}