Shader "IndianOcean/SpriteEchoOutline" { // 回声描边 Shader —— 配合 EchoSystem 使用 // 渲染精灵的"外描边",颜色默认白色(可自定义),由全局回声参数控制显隐。 // 渲染队列在黑暗遮罩(Transparent+100)之上(Transparent+200), // 所以即使在黑暗中(黑暗遮罩把场景乘成纯黑),描边依然可见。 // // 工作原理: // - 顶点着色器:以精灵中心为基准向外扩展顶点,为外描边腾出空间 // - 片元着色器:8 方向膨胀采样精灵 alpha,邻居有 alpha 但中心没有 → 描边像素 // - 回声控制:精灵世界坐标距 _EchoCenter 小于 _EchoRadius 才显示描边 Properties { _MainTex ("Sprite Texture", 2D) = "white" {} _OutlineColor ("描边颜色(乘以全局回声颜色,默认白)", Color) = (1,1,1,1) _OutlineWidth ("描边宽度(像素/纹素)", Range(0, 8)) = 2.0 } SubShader { Tags { "RenderType" = "Transparent" "Queue" = "Transparent+200" "RenderPipeline" = "UniversalPipeline" } // 普通半透明叠加(在黑暗遮罩之后渲染,所以不被黑暗乘掉) Blend SrcAlpha OneMinusSrcAlpha ZWrite Off Cull Off Pass { Name "EchoOutline" HLSLPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; float3 worldPos : TEXCOORD1; UNITY_VERTEX_INPUT_INSTANCE_ID }; CBUFFER_START(UnityPerMaterial) float4 _OutlineColor; float _OutlineWidth; float4 _MainTex_ST; CBUFFER_END TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); float4 _MainTex_TexelSize; // 全局回声参数(由 EchoSystem 通过 Shader.SetGlobalX 设置,不是材质属性) float4 _EchoCenter; // xy = 回声中心世界坐标 XZ float _EchoRadius; // 当前波纹前沿半径 float _EchoWidth; // 波纹前沿柔和宽度 float _EchoOutlineIntensity; // 整体描边强度(维持=1, 消散=1→0, 关闭=0) float4 _EchoOutlineColor; // 全局描边颜色(默认白色) // 8 方向采样偏移(对角线用 0.7071 保持等距) static const float2 _Offsets[8] = { float2( 1.0, 0.0), float2(-1.0, 0.0), float2( 0.0, 1.0), float2( 0.0, -1.0), float2( 0.7071, 0.7071), float2(-0.7071, 0.7071), float2( 0.7071, -0.7071), float2(-0.7071, -0.7071) }; // 带边界检查的 alpha 采样:UV 超出 [0,1] 视为空(0),避免边缘错误填充 float sampleAlphaBounded(float2 uv) { float a = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv).a; float inBounds = step(0.0, uv.x) * step(uv.x, 1.0) * step(0.0, uv.y) * step(uv.y, 1.0); return a * inBounds; } Varyings vert(Attributes input) { Varyings output; UNITY_SETUP_INSTANCE_ID(input); UNITY_TRANSFER_INSTANCE_ID(input, output); float3 posOS = input.positionOS.xyz; output.positionCS = TransformObjectToHClip(posOS); output.uv = TRANSFORM_TEX(input.uv, _MainTex); output.worldPos = TransformObjectToWorld(posOS); // 以精灵中心为基准向外扩展顶点,为外描边腾出屏幕空间 float4 centerCS = TransformObjectToHClip(float3(0,0,0)); float2 csDir = output.positionCS.xy - centerCS.xy; float len = length(csDir); csDir = (len > 0.0001) ? csDir / len : float2(1.0, 0.0); float2 ndcPerPixel = 2.0 / _ScreenParams.xy; output.positionCS.xy += csDir * ndcPerPixel * _OutlineWidth; return output; } half4 frag(Varyings input) : SV_Target { // ===== 回声距离检查 ===== // 波纹前沿在 _EchoRadius;精灵距中心小于半径 → 已被扫描 → 显示描边 float2 wp = input.worldPos.xz; float ed = distance(wp, _EchoCenter.xy); // smoothstep: 距离从 (radius-width) 到 radius 时从1降到0;取反 = 半径内为1 float reached = 1.0 - smoothstep(_EchoRadius - _EchoWidth, _EchoRadius, ed); float intensity = reached * _EchoOutlineIntensity; // 回声未到达或已关闭 → 直接丢弃,零开销 if (intensity < 0.001) discard; // ===== 膨胀采样得到描边 ===== float2 uv = input.uv; float origAlpha = sampleAlphaBounded(uv); float2 texel = _MainTex_TexelSize.xy; float w = max(1.0, _OutlineWidth); float dilatedAlpha = 0.0; [unroll] for (int i = 0; i < 8; i++) { float2 ouv = uv + _Offsets[i] * texel * w; dilatedAlpha = max(dilatedAlpha, sampleAlphaBounded(ouv)); } // 描边 = 邻居有 alpha 但中心没有(外描边) float outline = step(0.5, dilatedAlpha) * (1.0 - step(0.5, origAlpha)); // ===== 输出 ===== float3 col = _EchoOutlineColor.rgb * _OutlineColor.rgb; float alpha = outline * intensity * _EchoOutlineColor.a * _OutlineColor.a; return half4(col, alpha); } ENDHLSL } } FallBack Off }