法线外扩/视口夹角
Outline3DOutExternal
Shader "URP/Outline3DOutExternal"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _OutlineColor ("Color",Color) = (1,1,1,1)
        _OutlineWidth ("Outline Width",Range(0,0.1)) = 0.1
        _OutlinePowerIn ("Outline PowerIn",Range(0.01,10)) = 1
    }
    SubShader
    {
  


        Pass
        {
            Name "Outline3D_Inside"
  
            Tags
            {
                "RenderType"="Opaque"
                "Queue"="Transparent"
                "RenderPipeline"="UniversalPipeline"
                "LightMode"="UniversalForward"
            }
            LOD 100
            Blend SrcAlpha OneMinusSrcAlpha

            HLSLPROGRAM
  
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            #pragma vertex vert;
            #pragma fragment frag;
  
            TEXTURE2D_X(_MainTex);
            SAMPLER(sampler_linear_clamp_MainTex);

            CBUFFER_START(UnityPerMaterial)
                half4 _OutlineColor;
                half _OutlinePowerIn;
            CBUFFER_END
  
            struct Varyings
            {
                half4 position : SV_POSITION;
                half2 uv : TEXCOORD0;
                half3 normal_world : TEXCOORD1;
                half3 position_world : TEXCOORD2;
            };


            Varyings vert(half2 uv : TEXCOORD0,half4 position : POSITION,half4 normal : NORMAL)
            {
                Varyings OUT;
                OUT.position_world = mul(GetObjectToWorldMatrix(),position);
                OUT.normal_world = TransformObjectToWorldNormal(normal);
                OUT.position = TransformObjectToHClip(position);
                OUT.uv = uv;
                return OUT;
            }


            half4 frag(Varyings IN) : SV_Target
            {
                half3 direction_view = normalize(GetCameraPositionWS() - (IN.position_world));
                half v = dot(direction_view,IN.normal_world);
                v = 1 - saturate(v);
                v = pow(v,_OutlinePowerIn);
  
                half4 col = SAMPLE_TEXTURE2D_X(_MainTex,sampler_linear_clamp_MainTex,IN.uv);
                return lerp(col,_OutlineColor,v);
            }
  
            ENDHLSL
        }
  

        Pass
        {
            Name "Outline3D_Swell"
  
            Tags
            {
                "RenderType"="Opaque"
                "Queue"="Transparent"
                "RenderPipeline"="UniversalPipeline"
                "LightMode"="SRPDefaultUnlit"
            }
            LOD 100
            Blend SrcAlpha OneMinusSrcAlpha
            Cull Front

            HLSLPROGRAM
  
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            #pragma vertex vert;
            #pragma fragment frag;


            TEXTURE2D_X(_MainTex);
            SAMPLER(sampler_linear_clamp_MainTex);

            CBUFFER_START(UnityPerMaterial)
                half4 _OutlineColor;
                half _OutlineWidth;
            CBUFFER_END
  
            struct Varyings
            {
                half4 position : SV_POSITION;
                half2 uv : TEXCOORD0;
            };


            Varyings vert(half2 uv : TEXCOORD0,half4 position : POSITION,half4 normal : NORMAL)
            {
                Varyings OUT;
                position.xyz += normal * _OutlineWidth;
                OUT.position = TransformObjectToHClip(position);
                OUT.uv = uv;
                return OUT;
            }


            half4 frag(Varyings IN) : SV_Target
            {
                return _OutlineColor;
            }
  
            ENDHLSL
        }
    }

    Fallback "Hidden/Universal Render Pipeline/FallbackError"
}
UI描边(配合C#)
OutlineUI
Shader "URP/OutlineUI"
{
    Properties
    {
        [PerRendererData]_MainTex ("Main Texture", 2D) = "white" { }
        [HideInInspector]_Color ("Tint", Color) = (1, 1, 1, 1)

        [HideInInspector]_StencilComp ("Stencil Comparison", Float) = 8
        [HideInInspector]_Stencil ("Stencil ID", Float) = 0
        [HideInInspector]_StencilOp ("Stencil Operation", Float) = 0
        [HideInInspector]_StencilWriteMask ("Stencil Write Mask", Float) = 255
        [HideInInspector]_StencilReadMask ("Stencil Read Mask", Float) = 255

        [HideInInspector]_ColorMask ("Color Mask", Float) = 15

        [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
        _AlphaClipThreshold ("Alpha Clip Threshold",Range(0,1)) = 0.001
        // 注意:Softness属性这里定义只是为了Material Inspector显示,
        // 实际值将由RectMask2D组件通过MaterialPropertyBlock动态覆盖
        _Softness("Softness", Vector) = (0, 0, 0, 0)
    }

    SubShader
    {
        Tags
        {
            "Queue" = "Transparent"
            "IgnoreProjector" = "True"
            "RenderType" = "Transparent"
            "PreviewType" = "Plane"
            "CanUseSpriteAtlas" = "True"
            "RenderPipeline" = "UniversalPipeline"
        }
  

        Pass
        {
            Name "UIOutlineEx"
  
            Stencil
            {
                Ref [_Stencil]
                Comp [_StencilComp]
                Pass [_StencilOp]
                ReadMask [_StencilReadMask]
                WriteMask [_StencilWriteMask]
            }
  
            Cull Off
            Lighting Off
            ZWrite Off
            ZTest [unity_GUIZTestMode]
            Blend SrcAlpha OneMinusSrcAlpha
            ColorMask [_ColorMask]

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag


            #pragma multi_compile_local _ UNITY_UI_CLIP_RECT
            #pragma multi_compile_local _ UNITY_UI_ALPHACLIP
  
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);
            half4 _ClipRect;


            CBUFFER_START(UnityPerMaterial)
            half4 _Color;
            float4 _MainTex_TexelSize;
            half4 _Softness;
            half _AlphaClipThreshold;
            CBUFFER_END

            half4 _TextureSampleAdd;

            struct appdata
            {
                float4 vertex : POSITION;
                float4 texcoord : TEXCOORD0;
                float4 uv1 : TEXCOORD1;
                float4 uv2 : TEXCOORD2;
                half4 color : COLOR;
            };


            struct v2f
            {
                float4 vertex : SV_POSITION;
                float4 texcoord : TEXCOORD0;
                float4 uv1 : TEXCOORD1;
                float4 uv2 : TEXCOORD2;
                half4 color : COLOR;

                half4 objPosition : TEXCOORD3;
            };

            v2f vert(appdata IN)
            {
                v2f o;
  
                o.vertex = TransformObjectToHClip(IN.vertex.xyz);
                o.texcoord = IN.texcoord;
                o.color = IN.color * _Color;
                o.uv1 = IN.uv1;
                o.uv2 = IN.uv2;

                o.objPosition = IN.vertex;
                return o;
            }

            half IsInRect(float2 pPos, float2 pClipRectMin, float2 pClipRectMax)
            {
                pPos = step(pClipRectMin, pPos) * step(pPos, pClipRectMax);
                return pPos.x * pPos.y;
            }

            half SampleAlpha(int pIndex, v2f IN)
            {
                const half sinArray[12] =
                {
                    0, 0.5, 0.866, 1, 0.866, 0.5, 0, -0.5, -0.866, -1, -0.866, -0.5
                };
                const half cosArray[12] =
                {
                    1, 0.866, 0.5, 0, -0.5, -0.866, -1, -0.866, -0.5, 0, 0.5, 0.866
                };
                float2 pos = IN.texcoord.xy + _MainTex_TexelSize.xy * float2(cosArray[pIndex], sinArray[pIndex]) * IN.texcoord.z;
                half pos_uv_col_a = (SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, pos) + _TextureSampleAdd).w;
                return IsInRect(pos, IN.uv1.xy, IN.uv1.zw) * pos_uv_col_a * IN.uv2.w;
            }
  
            half4 frag(v2f IN) : SV_Target
            {
                half4 color = (SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, IN.texcoord.xy) + _TextureSampleAdd) * IN.color;

  
                if (all(IN.texcoord.z))
                {
                    half sign_a = IsInRect(IN.texcoord.xy, IN.uv1.xy, IN.uv1.zw);
                    color.a *= sign_a;
                    half4 val = half4(IN.uv2.rgb, 0);

                    val.w += SampleAlpha(0, IN);
                    val.w += SampleAlpha(1, IN);
                    val.w += SampleAlpha(2, IN);
                    val.w += SampleAlpha(3, IN);
                    val.w += SampleAlpha(4, IN);
                    val.w += SampleAlpha(5, IN);
                    val.w += SampleAlpha(6, IN);
                    val.w += SampleAlpha(7, IN);
                    val.w += SampleAlpha(8, IN);
                    val.w += SampleAlpha(9, IN);
                    val.w += SampleAlpha(10, IN);
                    val.w += SampleAlpha(11, IN);
                    val.w = saturate(val.w);

                    val.w *= IN.texcoord.w;
                    color = lerp(val, color, color.a) * val.w + color * (1 - val.w);
                }


               // 2. --- 核心:应用自定义的RectMask2D裁剪(带软边缘)---
                #if UNITY_UI_CLIP_RECT
                // 2.1 计算片元到裁剪矩形四条边的距离
                half2 minDist = (IN.objPosition.xy - _ClipRect.xy);
                half2 maxDist = (_ClipRect.zw - IN.objPosition.xy);
                half2 edgeDistance = min(minDist, maxDist); // 对于矩形内的点,这个值是正的

                // 2.2 计算软边缘因子
                // saturate(x / softness) :
                // - 当 x > softness:结果 >= 1,完全保留
                // - 当 0 < x < softness:结果在 0 到 1 之间,平滑过渡
                // - 当 x < 0:结果为 0,完全裁剪
                // 使用max(softness, 0.001)避免除以零
                half2 softnessFactor = saturate(edgeDistance / max(_Softness.xy, 0.001));

                // 2.3 综合两个方向的因子(取最小值,这样任何一个方向出界都会导致裁剪)
                half finalAlphaFactor = softnessFactor.x * softnessFactor.y;

                // 2.4 将裁剪因子应用到最终颜色的Alpha通道
                color.a *= finalAlphaFactor;
                #endif

  
                #if UNITY_UI_ALPHACLIP
                    clip(color.a - _AlphaClipThreshold);
                #endif


                return color;
            }

  
            ENDHLSL
        }
    }
}
UIOutlineEx
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;

/// <summary>
/// UGUI描边
/// </summary>
public class UIOutlineEx : BaseMeshEffect
{
    private static List<UIVertex> m_VetexList = new List<UIVertex>();

    // 公开变量
    public Color OutlineColor = Color.black;
    [Range(0.01f, 6)] public float OutlineWidth = 1;
    [SerializeField] private Material m_outlineMaterial;
    [SerializeField] private bool m_DrawOutline;

    // 私有变量
    private float m_uivertex_alpha_max = 0;
    private CanvasGroup[] m_canvasGroups;
    private float m_canvasGroups_alphamul = 1;


    public void SetDrawOutline(bool draw)
    {
        m_DrawOutline = draw;
    }
  
  
  
    protected override void Awake()
    {
        base.Awake();

        if (base.graphic)
        {
            if (base.graphic.canvas)
            {
                var v1 = base.graphic.canvas.additionalShaderChannels;
                var v2 = AdditionalCanvasShaderChannels.TexCoord1;
                if ((v1 & v2) != v2)
                {
                    base.graphic.canvas.additionalShaderChannels |= v2;
                }
                v2 = AdditionalCanvasShaderChannels.TexCoord2;
                if ((v1 & v2) != v2)
                {
                    base.graphic.canvas.additionalShaderChannels |= v2;
                }
            }
            this._Refresh();
        }
    }

    protected override void OnDestroy()
    {
        base.graphic.material = null;
    }


    protected override void OnEnable()
    {
        base.graphic.material = m_outlineMaterial;
        this._Refresh();
    }

    protected override void OnDisable()
    {
        base.graphic.material = null;
        this._Refresh();
    }

    protected override void Start()
    {
        base.graphic.material = m_outlineMaterial;
    }


    protected override void OnCanvasGroupChanged()
    {
        base.OnCanvasGroupChanged();
        if (m_canvasGroups == null) m_canvasGroups = GetComponentsInParent<CanvasGroup>();

        m_canvasGroups_alphamul = 1;
        for (int i = 0; i < m_canvasGroups.Length; i++)
        {
            m_canvasGroups_alphamul *= m_canvasGroups[i].alpha;
        }
        this._Refresh();
    }
  

    private void _Refresh()
    {
        base.graphic.SetVerticesDirty();
    }


    public override void ModifyMesh(VertexHelper vh)
    {
        if(!m_DrawOutline) return;
  
        vh.GetUIVertexStream(m_VetexList);

        m_uivertex_alpha_max = 0;
        for (int i = 0; i < m_VetexList.Count; i++)
        {
            var v = m_VetexList[i];
            if (v.color.a / 255f > m_uivertex_alpha_max) m_uivertex_alpha_max = v.color.a / 255f;
        }
        m_uivertex_alpha_max *= m_canvasGroups_alphamul;

        this._ProcessVertices();

        vh.Clear();
        vh.AddUIVertexTriangleStream(m_VetexList);
    }


    private void _ProcessVertices()
    {
        for (int i = 0, count = m_VetexList.Count - 3; i <= count; i += 3)
        {
            var v1 = m_VetexList[i];
            var v2 = m_VetexList[i + 1];
            var v3 = m_VetexList[i + 2];
  
  
  
            // 计算原顶点坐标中心点
            //
            var minX = _Min(v1.position.x, v2.position.x, v3.position.x);
            var minY = _Min(v1.position.y, v2.position.y, v3.position.y);
            var maxX = _Max(v1.position.x, v2.position.x, v3.position.x);
            var maxY = _Max(v1.position.y, v2.position.y, v3.position.y);
            var posCenter = new Vector2(minX + maxX, minY + maxY) * 0.5f;
  
  
  
            // 计算原始顶点坐标和UV的方向
            //
            Vector2 triX, triY, uvX, uvY;
            Vector2 pos1 = v1.position;
            Vector2 pos2 = v2.position;
            Vector2 pos3 = v3.position;
            if (Mathf.Abs(Vector2.Dot((pos2 - pos1).normalized, Vector2.right))
                > Mathf.Abs(Vector2.Dot((pos3 - pos2).normalized, Vector2.right)))
            {
                triX = pos2 - pos1;
                triY = pos3 - pos2;
                uvX = v2.uv0 - v1.uv0;
                uvY = v3.uv0 - v2.uv0;
            }
            else
            {
                triX = pos3 - pos2;
                triY = pos2 - pos1;
                uvX = v3.uv0 - v2.uv0;
                uvY = v2.uv0 - v1.uv0;
            }
  
  
  
            // 计算原始UV框
            var uvMin = _Min(v1.uv0, v2.uv0, v3.uv0);
            var uvMax = _Max(v1.uv0, v2.uv0, v3.uv0);

            // 为每个顶点设置新的Position和UV,并传入原始UV框
            v1 = _SetNewPosAndUV(v1, this.OutlineWidth, posCenter, triX, triY, uvX, uvY, uvMin, uvMax);
            v2 = _SetNewPosAndUV(v2, this.OutlineWidth, posCenter, triX, triY, uvX, uvY, uvMin, uvMax);
            v3 = _SetNewPosAndUV(v3, this.OutlineWidth, posCenter, triX, triY, uvX, uvY, uvMin, uvMax);


            // 应用设置后的UIVertex
            //
            m_VetexList[i] = v1;
            m_VetexList[i + 1] = v2;
            m_VetexList[i + 2] = v3;
        }
    }


    private UIVertex _SetNewPosAndUV(
        UIVertex pVertex, float pOutLineWidth,
        Vector2 pPosCenter,
        Vector2 pTriangleX, Vector2 pTriangleY,
        Vector2 pUVX, Vector2 pUVY,
        Vector2 pUVOriginMin, Vector2 pUVOriginMax)
    {
        // Position
        var pos = pVertex.position;
        var posXOffset = pos.x > pPosCenter.x ? pOutLineWidth : -pOutLineWidth;
        var posYOffset = pos.y > pPosCenter.y ? pOutLineWidth : -pOutLineWidth;
        pos.x += posXOffset;
        pos.y += posYOffset;
        pVertex.position = pos;
  
        // UV
        var uv = (Vector2)pVertex.uv0;
        var uv_additional_x = pUVX / pTriangleX.magnitude * posXOffset * (Vector2.Dot(pTriangleX, Vector2.right) > 0 ? 1 : -1);
        var uv_additional_y = pUVY / pTriangleY.magnitude * posYOffset * (Vector2.Dot(pTriangleY, Vector2.up) > 0 ? 1 : -1);
        uv += uv_additional_x;
        uv += uv_additional_y;
        pVertex.uv0 = uv;
  
  
        pVertex.uv0.z = pOutLineWidth; // z:定位为宽度
        pVertex.uv0.w = m_uivertex_alpha_max; // w:所有顶点中最大的透明度值

  
        // uv1 uv2 可用
        pVertex.uv1 = pUVOriginMin;     // uv1:定位为最小最大值数据
        pVertex.uv1.z = pUVOriginMax.x;
        pVertex.uv1.w = pUVOriginMax.y;

        pVertex.uv2 = this.OutlineColor; // uv2:定位为描边颜色

        return pVertex;
    }


    private static float _Min(float pA, float pB, float pC)
    {
        return Mathf.Min(Mathf.Min(pA, pB), pC);
    }


    private static float _Max(float pA, float pB, float pC)
    {
        return Mathf.Max(Mathf.Max(pA, pB), pC);
    }


    private static Vector2 _Min(Vector2 pA, Vector2 pB, Vector2 pC)
    {
        return new Vector2(_Min(pA.x, pB.x, pC.x), _Min(pA.y, pB.y, pC.y));
    }


    private static Vector2 _Max(Vector2 pA, Vector2 pB, Vector2 pC)
    {
        return new Vector2(_Max(pA.x, pB.x, pC.x), _Max(pA.y, pB.y, pC.y));
    }
}
后处理Feature
URP_Feature_Outline3D
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class URP_Feature_Outline3D : ScriptableRendererFeature
{
    class Outline3DRenderPass : ScriptableRenderPass
    {
        private static readonly List<ShaderTagId> s_shaderTagIds = new List<ShaderTagId>()
        {
            new ShaderTagId("UniversalForward"),
            new ShaderTagId("UniversalForwardOnly"),
            new ShaderTagId("SRPDefaultUnlit")
        };
        private static readonly int s_shaderProperty_outline3d = Shader.PropertyToID("_Outline3D");



        private readonly Material m_outline_material;
        private readonly FilteringSettings m_filteringSettings;
        private readonly MaterialPropertyBlock m_materialPropertyBlock;
        private RTHandle m_outline_rtHandle;


        public Outline3DRenderPass(Material material)
        {
            // Configures where the render pass should be injected.
            renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing;
        
            m_outline_material = material;
            m_filteringSettings = new FilteringSettings(RenderQueueRange.all, renderingLayerMask: 0b10);
            m_materialPropertyBlock = new MaterialPropertyBlock();
        }


        public void Dispose()
        {
            m_outline_rtHandle?.Release();
            m_outline_rtHandle = null;
        
        }





        // This method is called before executing the render pass.
        // It can be used to configure render targets and their clear state. Also to create temporary render target textures.
        // When empty this render pass will render to the active camera render target.
        // You should never call CommandBuffer.SetRenderTarget. Instead call <c>ConfigureTarget</c> and <c>ConfigureClear</c>.
        // The render pipeline will ensure target setup and clearing happens in a performant manner.
        public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
        {

            var desc_camera = renderingData.cameraData.cameraTargetDescriptor;
            desc_camera.msaaSamples = 1;
            desc_camera.depthBufferBits = 0;
            desc_camera.colorFormat = RenderTextureFormat.ARGB32;
            RenderingUtils.ReAllocateIfNeeded(ref m_outline_rtHandle, desc_camera, name: "_Outline3D");
        }

        // Here you can implement the rendering logic.
        // Use <c>ScriptableRenderContext</c> to issue drawing commands or execute command buffers
        // https://docs.unity3d.com/ScriptReference/Rendering.ScriptableRenderContext.html
        // You don't have to call ScriptableRenderContext.submit, the render pipeline will call it at specific points in the pipeline.
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            var cmd = CommandBufferPool.Get("Outline3D CMD");

            // CMD
            cmd.SetRenderTarget(m_outline_rtHandle);
            cmd.ClearRenderTarget(true, true, Color.clear);


            var draw_settings = CreateDrawingSettings(s_shaderTagIds, ref renderingData, SortingCriteria.None);
            RendererListParams rendererListParams = new RendererListParams(renderingData.cullResults, draw_settings, m_filteringSettings);
            var list_draw = context.CreateRendererList(ref rendererListParams);
            cmd.DrawRendererList(list_draw);
        
        
            // cmd.SetRenderTarget(renderingData.cameraData.renderer.cameraColorTargetHandle);
            // m_materialPropertyBlock.SetTexture(s_shaderProperty_outline3d, m_outline_rtHandle);
            // cmd.DrawProcedural(Matrix4x4.identity, m_outline_material, 0, MeshTopology.Triangles, 3, 1, m_materialPropertyBlock);
            m_outline_material.SetTexture(s_shaderProperty_outline3d, m_outline_rtHandle);
            Blitter.BlitCameraTexture(cmd, m_outline_rtHandle,renderingData.cameraData.renderer.cameraColorTargetHandle,m_outline_material,0);



            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();
            CommandBufferPool.Release(cmd);
        }

        // Cleanup any allocated resources that were created during the execution of this render pass.
        //public override void OnCameraCleanup(CommandBuffer cmd)
        //{
        //}
    }






    [SerializeField] private Material m_OutlineMaterial;
    private Outline3DRenderPass m_Outline3DScriptablePass;

    private bool IsMaterialValid => m_OutlineMaterial && m_OutlineMaterial.shader && m_OutlineMaterial.shader.isSupported;


    /// <inheritdoc/>
    public override void Create()
    {
        if (!IsMaterialValid)
        {
            return;
        }

        m_Outline3DScriptablePass = new Outline3DRenderPass(m_OutlineMaterial);
    }

    // Here you can inject one or multiple render passes in the renderer.
    // This method is called when setting up the renderer once per-camera.
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        if (m_Outline3DScriptablePass == null)
        {
            return;
        }


        renderer.EnqueuePass(m_Outline3DScriptablePass);
    }


    protected override void Dispose(bool disposing)
    {
        m_Outline3DScriptablePass?.Dispose();
    }

}
Outline3D
Shader "URP/Outline3D"
{
    Properties
    {
        _OutlineColor ("Outline Color", Color) = (1, 1, 1, 1)
        _OutlineWidth ("Outline Width", Range(0, 0.01)) = 0.001
    }

    SubShader
    {
        Tags
        {
            "RenderType" = "Opaque"
            "RenderPipeline" = "UniversalPipeline"
            "Queue" = "Geometry"
        }
        LOD 100
        
        ZWrite Off
        Cull Off
        Blend SrcAlpha OneMinusSrcAlpha

        Pass
        {
            HLSLPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"


            struct Attributes
            {
                uint vertex_id : SV_VERTEXID;
            };

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                half2 uv : TEXCOORD;
                half2 offsets[8] : TEXCOORD1;
            };


            TEXTURE2D_X(_Outline3D);
            SAMPLER(sampler_linear_clamp_Outline3D);

            CBUFFER_START(UnityPerMaterial)
            half4 _OutlineColor;
            float _OutlineWidth;
            CBUFFER_END


            Varyings vert(Attributes IN)
            {
                Varyings Output;
                Output.positionCS = GetFullScreenTriangleVertexPosition(IN.vertex_id);
                Output.uv = GetFullScreenTriangleTexCoord(IN.vertex_id);

                const half ratio_w_h = _ScreenParams.x / _ScreenParams.y;
                const float multipty_num_2 = 0.707; // sprt(2) / 2

                Output.offsets[0] = half2(-1, ratio_w_h) * _OutlineWidth * multipty_num_2;
                Output.offsets[1] = half2(0, ratio_w_h) * _OutlineWidth;
                Output.offsets[2] = half2(1, ratio_w_h) * _OutlineWidth * multipty_num_2;
                Output.offsets[3] = half2(-1, 0) * _OutlineWidth;
                
                Output.offsets[4] = half2(1, 0) * _OutlineWidth;
                Output.offsets[5] = half2(-1, -ratio_w_h) * _OutlineWidth * multipty_num_2;
                Output.offsets[6] = half2(0, -ratio_w_h) * _OutlineWidth;
                Output.offsets[7] = half2(1, -ratio_w_h) * _OutlineWidth * multipty_num_2;

                return Output;
            }



            half4 frag(Varyings IN) : SV_Target
            {
                const half kernelX[8] =
                {
                    - 1, 0, 1,
                    - 2, 2,
                    - 1, 0, 1
                };

                const half kernelY[8] =
                {
                    - 1, -2, -1,
                    0, 0,
                    1, 2, 1
                };


                half gx = 0;
                half gy = 0;
                half multiply_num = 0;

                for (int i = 0; i < 8; i++)
                {
                    multiply_num = SAMPLE_TEXTURE2D_X(_Outline3D, sampler_linear_clamp_Outline3D, IN.uv + IN.offsets[i]).a;
                    gx += multiply_num * kernelX[i];
                    gy += multiply_num * kernelY[i];
                }

                const half _a = SAMPLE_TEXTURE2D_X(_Outline3D, sampler_linear_clamp_Outline3D, IN.uv).a;
                half4 col = _OutlineColor;
                col.a = saturate(abs(gx) + abs(gy)) * (1 - _a);

                
                return col;
            }

            ENDHLSL
        }
    }
}
后处理Feature2
URP_Feature_Outline3D_2
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RendererUtils;
using UnityEngine.Rendering.Universal;
using UnityEngine.Serialization;

public class URP_Feature_Outline3D_2 : ScriptableRendererFeature
{
    private class Outline3DRenderPass : ScriptableRenderPass
    {
        private static readonly List<ShaderTagId> s_shaderTagIds = new List<ShaderTagId>()
        {
            new ShaderTagId("UniversalForward"),
            new ShaderTagId("UniversalForwardOnly"),
            new ShaderTagId("SRPDefaultUnlit")
        };
        
        
        private readonly Material m_outline_material;
        private readonly Color m_outlineColor = Color.yellow;
        private readonly float m_downSampleScale = 0.5f; 
        private readonly int m_blurIterations = 1; 
        private readonly float m_blurWidth = 1.0f;
        
        
        
        private readonly FilteringSettings m_filteringSettings;
        private readonly MaterialPropertyBlock m_materialPropertyBlock;
        
        private RTHandle m_outline_rtHandle;
        private RTHandle m_temp_downsample_rtHandle;
        private RTHandle m_temp_blurRt;

        public Outline3DRenderPass(Material material,Color outlineColor,float downSampleScale,int blurIterations,float blurWidth)
        {
            // Configures where the render pass should be injected.
            renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing;

            m_outline_material = material;
            m_outlineColor = outlineColor;
            m_downSampleScale = downSampleScale;
            m_blurIterations = blurIterations;
            m_blurWidth = blurWidth;
            
            m_filteringSettings = new FilteringSettings(RenderQueueRange.all, renderingLayerMask: 0b10);
            m_materialPropertyBlock = new MaterialPropertyBlock();
        }


        public void Dispose()
        {
            m_outline_rtHandle?.Release();
            m_outline_rtHandle = null;
            m_temp_blurRt?.Release();
            m_temp_blurRt = null;
            m_temp_downsample_rtHandle?.Release();
            m_temp_downsample_rtHandle = null;
        }




        public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
        {

            var desc_camera = renderingData.cameraData.cameraTargetDescriptor;
            desc_camera.msaaSamples = 1;
            desc_camera.depthBufferBits = 0;
            desc_camera.colorFormat = RenderTextureFormat.ARGB32;
            RenderingUtils.ReAllocateIfNeeded(ref m_outline_rtHandle, desc_camera, name: "_Outline3D");
            
            Vector2 scaleFactor = new Vector2(m_downSampleScale, m_downSampleScale);
            RenderingUtils.ReAllocateIfNeeded(ref m_temp_downsample_rtHandle,scaleFactor,desc_camera ,name: "_TempDownsample",filterMode:FilterMode.Bilinear);
            RenderingUtils.ReAllocateIfNeeded(ref m_temp_blurRt,scaleFactor,desc_camera ,name: "_TempBlur",filterMode:FilterMode.Bilinear);
        }
        
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            var cameraData = renderingData.cameraData;
            if (cameraData.camera.cameraType != CameraType.Game)
                return;

            if (m_outline_material == null)
                return;
            
            
            
            
            var cmd = CommandBufferPool.Get("Outline3D CMD");

            // CMD
            cmd.SetRenderTarget(m_temp_downsample_rtHandle);
            cmd.ClearRenderTarget(true, true, Color.clear);
            
            cmd.SetRenderTarget(m_temp_blurRt);
            cmd.ClearRenderTarget(true, true, Color.clear);
            
            cmd.SetRenderTarget(m_outline_rtHandle);
            cmd.ClearRenderTarget(true, true, Color.clear);


            var draw_settings = CreateDrawingSettings(s_shaderTagIds, ref renderingData, SortingCriteria.None);
            RendererListParams rendererListParams = new RendererListParams(renderingData.cullResults, draw_settings, m_filteringSettings);
            var list_draw = context.CreateRendererList(ref rendererListParams);
            cmd.DrawRendererList(list_draw);

            
            
            
            // Pass0
            m_outline_material.SetColor("_OutlineColor", m_outlineColor);
            Blitter.BlitCameraTexture(cmd, m_outline_rtHandle,m_outline_rtHandle,m_outline_material,0);
            m_outline_material.SetTexture("_OutlineColorTex", m_outline_rtHandle);

            
            
            // Pass1
            m_outline_material.SetFloat("_BlurWidth", m_blurWidth);
            for (int i = 0; i < m_blurIterations; ++i)
            {
                if (i == 0)
                {
                    Blitter.BlitCameraTexture(cmd, m_outline_rtHandle,m_temp_downsample_rtHandle,m_outline_material,1);
                    Blitter.BlitCameraTexture(cmd, m_temp_downsample_rtHandle,m_temp_blurRt,m_outline_material,1);
                }
                else
                {
                    Blitter.BlitCameraTexture(cmd, m_temp_blurRt,m_temp_downsample_rtHandle,m_outline_material,1);
                    Blitter.BlitCameraTexture(cmd, m_temp_downsample_rtHandle,m_temp_blurRt,m_outline_material,1);
                }
            }
            
            

            // Pass2
            m_outline_material.SetTexture("_BlurTex", m_temp_blurRt);
            
            
            RTHandle targetRT = cameraData.renderer.cameraColorTargetHandle;
            Blitter.BlitCameraTexture(cmd, targetRT,targetRT,m_outline_material,2);
            
            
            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();
            CommandBufferPool.Release(cmd);
        }

        
        public override void OnCameraCleanup(CommandBuffer cmd)
        {
            
        }
    }






    [SerializeField] private Material m_OutlineMaterial;
    public Color m_outlineColor = Color.yellow;

    [Range(0.1f, 1)]
    public float m_downSampleScale = 0.5f;                 // 降采样比例
    [Range(0, 4)]
    public int m_blurIterations = 1;                  // 均值糊迭代次数
    [FormerlySerializedAs("m_blurRadius")] [Range (0.2f, 10.0f)]
    public float m_blurWidth = 1.0f;
    
    private bool IsMaterialValid => m_OutlineMaterial && m_OutlineMaterial.shader && m_OutlineMaterial.shader.isSupported;

    
    
    
    private Outline3DRenderPass m_Outline3DScriptablePass;
    

    /// <inheritdoc/>
    public override void Create()
    {
        if (!IsMaterialValid)
        {
            return;
        }

        m_Outline3DScriptablePass = new Outline3DRenderPass(m_OutlineMaterial,m_outlineColor,m_downSampleScale,m_blurIterations,m_blurWidth);
    }
    
    
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        if (m_Outline3DScriptablePass == null)
        {
            return;
        }

        if (renderingData.cameraData.cameraType == CameraType.Game)
        {
            renderer.EnqueuePass(m_Outline3DScriptablePass);
        }
    }


    protected override void Dispose(bool disposing)
    {
        m_Outline3DScriptablePass?.Dispose();
    }

}
PostEffectOutline3D
Shader "URP/PostEffectOutline3D"
{
    Properties
    {
    	[PerRendererData]_OutlineColor ("Outline Color",Color) = (1,1,1,1)
    	[PerRendererData]_BlurRadius("Blur Width",Float) = 1
    }
    SubShader
    {
        Tags
        {
            "RenderType"="Opaque"
            "RenderPipeline"="UniversalPipeline"
            "IgnoreProjector"="true"
        	"Queue" = "Geometry"
        }
        LOD 100
		Cull Off 
		ZWrite Off
	    Blend SrcAlpha OneMinusSrcAlpha
        
        HLSLINCLUDE
        
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
        #include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"
        
        TEXTURE2D_X(_CameraOpaqueTexture);
        TEXTURE2D_X(_OutlineColorTex);
        TEXTURE2D_X(_BlurTex);
        SAMPLER(sampler_linear_clamp_CameraOpaqueTexture);

        half4 _BlitTexture_TexelSize;
        CBUFFER_START(UnityPerMaterial)
            half4 _OutlineColor;
		    float _BlurWidth;
        CBUFFER_END


        struct appdata_img
        {
	        half4 vertex : POSITION;
        	half2 texcoord : TEXCOORD0;
        };

        
        struct v2f
		{
			float4 pos : SV_POSITION;
			half2 uv_ : TEXCOORD0;
			half4 uv1 : TEXCOORD3;
			half4 uv2 : TEXCOORD4;
			half4 uv3 : TEXCOORD5;
			half4 uv4 : TEXCOORD6;
		};
        
        

        // 均值模糊
        // ---------------------------【顶点着色器】---------------------------
        v2f vert_average(appdata_img v,uint vid : SV_VertexID)  
        {  
            v2f o;  
            o.pos = GetFullScreenTriangleVertexPosition(vid); //TransformObjectToHClip(v.vertex);  
            //uv坐标  
            o.uv_ = GetFullScreenTriangleTexCoord(vid); //v.texcoord.xy;  
            //计算周围的8个uv坐标
            o.uv1.xy = o.uv_ + _BlitTexture_TexelSize.xy * float2(1, 0) * _BlurWidth;  
            o.uv1.zw = o.uv_ + _BlitTexture_TexelSize.xy * float2(-1, 0) * _BlurWidth;
            
            o.uv2.xy = o.uv_ + _BlitTexture_TexelSize.xy * float2(0, 1) * _BlurWidth;
            o.uv2.zw = o.uv_ + _BlitTexture_TexelSize.xy * float2(0, -1) * _BlurWidth;
            
            o.uv3.xy = o.uv_ + _BlitTexture_TexelSize.xy * float2(1, 1) * _BlurWidth;
            o.uv3.zw = o.uv_ + _BlitTexture_TexelSize.xy * float2(-1, 1) * _BlurWidth;
            
            o.uv4.xy = o.uv_ + _BlitTexture_TexelSize.xy * float2(1, -1) * _BlurWidth;
            o.uv4.zw = o.uv_ + _BlitTexture_TexelSize.xy * float2(-1, -1) * _BlurWidth;
            return o;  
        }  
        
        // ---------------------------【片元着色器】---------------------------
        half4 frag_average(v2f i) : SV_Target  
        {  
            half4 color = half4(0,0,0,0);  
            color += SAMPLE_TEXTURE2D_X(_BlitTexture,sampler_linear_clamp_CameraOpaqueTexture, i.uv_.xy);
            color += SAMPLE_TEXTURE2D_X(_BlitTexture,sampler_linear_clamp_CameraOpaqueTexture, i.uv1.xy);
            color += SAMPLE_TEXTURE2D_X(_BlitTexture,sampler_linear_clamp_CameraOpaqueTexture, i.uv1.zw);
            color += SAMPLE_TEXTURE2D_X(_BlitTexture,sampler_linear_clamp_CameraOpaqueTexture, i.uv2.xy);
            color += SAMPLE_TEXTURE2D_X(_BlitTexture,sampler_linear_clamp_CameraOpaqueTexture, i.uv2.zw);
            color += SAMPLE_TEXTURE2D_X(_BlitTexture,sampler_linear_clamp_CameraOpaqueTexture, i.uv3.xy);
            color += SAMPLE_TEXTURE2D_X(_BlitTexture,sampler_linear_clamp_CameraOpaqueTexture, i.uv3.zw);
            color += SAMPLE_TEXTURE2D_X(_BlitTexture,sampler_linear_clamp_CameraOpaqueTexture, i.uv4.xy);
            color += SAMPLE_TEXTURE2D_X(_BlitTexture,sampler_linear_clamp_CameraOpaqueTexture, i.uv4.zw);
            // 取平均值
            return color / 9;
        }
        
        
        ENDHLSL
        
        Pass
        {
            HLSLPROGRAM

            #pragma vertex vert
			#pragma fragment frag
            
            v2f vert (appdata_img v,uint vid : SV_VertexID)
            {
                v2f o;
				o.uv_ = GetFullScreenTriangleTexCoord(vid); //v.texcoord;
        		o.pos = GetFullScreenTriangleVertexPosition(vid); //TransformObjectToHClip(v.vertex);
                return o;
            }

            half4 frag (v2f i) : SV_Target
            {
            	half4 col = SAMPLE_TEXTURE2D_X(_BlitTexture,sampler_linear_clamp_CameraOpaqueTexture, i.uv_);
            	clip(col.r - 0.1f);
            	return _OutlineColor;
            }
            
           ENDHLSL
        }


        Pass
        {
            HLSLPROGRAM

            #pragma vertex vert_average
			#pragma fragment frag_average
            
            ENDHLSL
        }




        Pass
        {
            HLSLPROGRAM

            #pragma vertex vert;
            #pragma fragment frag;

            v2f vert(appdata_img v,uint vid : SV_VertexID)
            {
                v2f o;
                o.pos = GetFullScreenTriangleVertexPosition(vid); //TransformObjectToHClip(v.vertex);
                o.uv_ = GetFullScreenTriangleTexCoord(vid); //v.texcoord;
                return o;
            }
            

			half4 frag(v2f i) : SV_Target
			{
				half4 scene = SAMPLE_TEXTURE2D_X(_BlitTexture,sampler_linear_clamp_CameraOpaqueTexture, i.uv_);
				half4 blur = SAMPLE_TEXTURE2D_X(_BlurTex, sampler_linear_clamp_CameraOpaqueTexture,i.uv_);
				half4 outlieColor = SAMPLE_TEXTURE2D_X(_OutlineColorTex,sampler_linear_clamp_CameraOpaqueTexture, i.uv_);
				half4 o = outlieColor - blur;

				half sign = step(0.01h,o.a);
				half4 colMix = lerp(scene,_OutlineColor,o.a);
				return scene * (1-sign) + sign * colMix;
			}
            
            ENDHLSL
        }
        

    }
}