Skip to content

Instantly share code, notes, and snippets.

@ScottJDaley
Last active October 11, 2025 15:48
Show Gist options
  • Save ScottJDaley/6cddf0c8995ed61cac7088e22c983de1 to your computer and use it in GitHub Desktop.
Save ScottJDaley/6cddf0c8995ed61cac7088e22c983de1 to your computer and use it in GitHub Desktop.

Revisions

  1. ScottJDaley revised this gist Mar 29, 2022. 3 changed files with 31 additions and 29 deletions.
    25 changes: 14 additions & 11 deletions Outline.shader
    Original file line number Diff line number Diff line change
    @@ -9,8 +9,9 @@ Shader "Hidden/Outline"
    {
    Properties
    {
    _MainTex ("Texture", 2D) = "white" {}
    _Transparency ("Transparency", Float) = 1
    [HideInInspector] _MainTex ("Texture", 2D) = "white" {}
    _OutlineColor("Color", Color) = (1, 1, 1, 1)
    _OutlineWidth ("Width", Range (0, 20)) = 5
    }

    SubShader
    @@ -36,7 +37,7 @@ Shader "Hidden/Outline"
    Ref 1
    ReadMask 1
    WriteMask 1
    Comp NotEqual
    Comp Always
    Pass Replace
    }

    @@ -149,9 +150,11 @@ Shader "Hidden/Outline"

    CBUFFER_START(UnityPerMaterial)
    float4 _MainTex_TexelSize;
    half4 _OutlineColor;
    float _OutlineWidth;
    CBUFFER_END

    v2f vert (appdata i) : SV_POSITION
    v2f vert (appdata i)
    {
    UNITY_SETUP_INSTANCE_ID(i);
    v2f o;
    @@ -245,10 +248,12 @@ Shader "Hidden/Outline"
    Texture2D _MainTex;
    CBUFFER_START(UnityPerMaterial)
    float4 _MainTex_TexelSize;
    half4 _OutlineColor;
    float _OutlineWidth;
    CBUFFER_END
    int2 _AxisWidth;

    v2f vert (appdata i) : SV_POSITION
    v2f vert (appdata i)
    {
    UNITY_SETUP_INSTANCE_ID(i);
    v2f o;
    @@ -343,15 +348,14 @@ Shader "Hidden/Outline"
    };

    Texture2D _MainTex;



    CBUFFER_START(UnityPerMaterial)
    float4 _MainTex_TexelSize;
    half4 _OutlineColor;
    float _OutlineWidth;
    CBUFFER_START(UnityPerMaterial)
    float _Transparency;
    CBUFFER_END

    v2f vert (appdata i) : SV_POSITION
    v2f vert (appdata i)
    {
    UNITY_SETUP_INSTANCE_ID(i);
    v2f o;
    @@ -389,7 +393,6 @@ Shader "Hidden/Outline"
    // apply outline to alpha
    half4 col = _OutlineColor;
    col.a *= outline;
    col.a *= _Transparency;

    // profit!
    return col;
    29 changes: 14 additions & 15 deletions OutlineFeature.cs
    Original file line number Diff line number Diff line change
    @@ -9,9 +9,9 @@

    public class OutlineFeature : ScriptableRendererFeature
    {
    public Settings[] OutlineSettings;
    private OutlinePass[] _outlinePasses;
    private StencilPass[] _stencilPasses;
    public Settings OutlineSettings;
    private OutlinePass _outlinePass;
    private StencilPass _stencilPass;

    [Serializable]
    public class Settings
    @@ -20,11 +20,11 @@ public class Settings
    [ColorUsage(true, true)]
    public Color Color = new Color(0.2f, 0.4f, 1, 1f);

    [Range(0.0f, 5.0f)]
    public float Width = 4f;
    [Range(0.0f, 20.0f)]
    public float Width = 5f;

    [Header("Rendering")]
    public LayerMask LayerMask = -1;
    public LayerMask LayerMask = 0;

    // TODO: Try this again when render layers are working with hybrid renderer.
    // [Range(0, 32)]
    @@ -37,22 +37,21 @@ public class Settings

    public override void Create()
    {
    _stencilPasses = new StencilPass[OutlineSettings.Length];
    _outlinePasses = new OutlinePass[OutlineSettings.Length];
    for (int i = 0; i < OutlineSettings.Length; i++)
    if (OutlineSettings == null)
    {
    Settings settings = OutlineSettings[i];
    _stencilPasses[i] = new StencilPass(settings);
    _outlinePasses[i] = new OutlinePass(settings);
    return;
    }
    _stencilPass = new StencilPass(OutlineSettings);
    _outlinePass = new OutlinePass(OutlineSettings);
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
    for (int i = 0; i < _outlinePasses.Length; i++)
    if (OutlineSettings == null)
    {
    renderer.EnqueuePass(_stencilPasses[i]);
    renderer.EnqueuePass(_outlinePasses[i]);
    return;
    }
    renderer.EnqueuePass(_stencilPass);
    renderer.EnqueuePass(_outlinePass);
    }
    }
    6 changes: 3 additions & 3 deletions info.md
    Original file line number Diff line number Diff line change
    @@ -4,8 +4,8 @@

    ## Usage
    1. Create a new Unity game object layer for the objects you wish to outline.
    2. In the ForwardRendererData asset of your URP asset, add the renderer feature named "Outline Feature"
    3. Add a new element to the "Outline Settings" of the new outline feature.
    2. In your URP asset, turn on MSAA or disable the Depth Texture.
    3. In the ForwardRendererData asset of your URP asset, add the renderer feature named "Outline Feature"
    4. Set the Layer Mask to the layer created in step 1.
    5. Add the objects you wish to outline to the outline layer (this can be done at runtime in ECS by setting the layer of the RenderMesh).
    6. Adjust the color and width of the outlines as desired in the renderer feature settings.
    @@ -16,7 +16,7 @@ When I first added support for the Hyrbid Renderer, render layers were not worki
    ## Credits
    - The technique for these wide outlines comes from [Ben Golus](https://bgolus.medium.com/) which is described and implemented in [this article](https://bgolus.medium.com/the-quest-for-very-wide-outlines-ba82ed442cd9).
    - [Alexander Ameye](https://alexanderameye.github.io/) created the renderer feature and shader modifications to make this work in URP, original shared [here](https://twitter.com/alexanderameye/status/1332286868222775298).
    - [Scott Daley](https://twitter.com/GravitonPunch) modified the shader to get it working with Unity ECS and the Hybrid Renderer as well as add support for multiple outline layers.
    - [Scott Daley](https://twitter.com/GravitonPunch) modified the shader to get it working with Unity ECS and the Hybrid Renderer.

    ## Compatibility

  2. ScottJDaley revised this gist Mar 27, 2022. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions info.md
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,5 @@
    # Wide Outlines Renderer Feature for URP and ECS/DOTS/Hybrid Renderer

    ![outline_shader](https://user-images.githubusercontent.com/2761011/160294277-7ce2243f-c40c-41dd-8a7f-19ed96d10f3d.gif)

    ## Usage
  3. ScottJDaley revised this gist Mar 27, 2022. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions info.md
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,5 @@
    ![outline_shader](https://user-images.githubusercontent.com/2761011/160294277-7ce2243f-c40c-41dd-8a7f-19ed96d10f3d.gif)

    ## Usage
    1. Create a new Unity game object layer for the objects you wish to outline.
    2. In the ForwardRendererData asset of your URP asset, add the renderer feature named "Outline Feature"
  4. ScottJDaley created this gist Mar 27, 2022.
    400 changes: 400 additions & 0 deletions Outline.shader
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,400 @@
    // Original shader by @bgolus, modified slightly by @alexanderameye for URP, modified slightly more
    // by @gravitonpunch for ECS/DOTS/HybridRenderer.
    // https://twitter.com/bgolus
    // https://medium.com/@bgolus/the-quest-for-very-wide-outlines-ba82ed442cd9
    // https://alexanderameye.github.io/
    // https://twitter.com/alexanderameye/status/1332286868222775298

    Shader "Hidden/Outline"
    {
    Properties
    {
    _MainTex ("Texture", 2D) = "white" {}
    _Transparency ("Transparency", Float) = 1
    }

    SubShader
    {
    Tags { "RenderType"="Opaque" "RenderPipeline" = "UniversalPipeline"}

    Cull Off ZWrite Off ZTest Always

    HLSLINCLUDE
    #define SNORM16_MAX_FLOAT_MINUS_EPSILON ((float)(32768-2) / (float)(32768-1))
    #define FLOOD_ENCODE_OFFSET float2(1.0, SNORM16_MAX_FLOAT_MINUS_EPSILON)
    #define FLOOD_ENCODE_SCALE float2(2.0, 1.0 + SNORM16_MAX_FLOAT_MINUS_EPSILON)

    #define FLOOD_NULL_POS -1.0
    #define FLOOD_NULL_POS_FLOAT2 float2(FLOOD_NULL_POS, FLOOD_NULL_POS)
    ENDHLSL

    Pass // 0
    {
    Name "STENCIL MASK"

    Stencil {
    Ref 1
    ReadMask 1
    WriteMask 1
    Comp NotEqual
    Pass Replace
    }

    ColorMask 0
    Blend Zero One

    HLSLPROGRAM
    #pragma multi_compile_instancing
    #pragma multi_compile _ DOTS_INSTANCING_ON
    #pragma vertex vert
    #pragma fragment frag

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

    #pragma target 4.5

    struct appdata
    {
    float4 positionOS : POSITION;
    #if UNITY_ANY_INSTANCING_ENABLED
    uint instanceID : INSTANCEID_SEMANTIC;
    #endif
    };

    float4 vert (appdata i) : SV_POSITION
    {
    UNITY_SETUP_INSTANCE_ID(i);
    return TransformObjectToHClip(i.positionOS.xyz);
    }

    void frag () {}
    ENDHLSL
    }

    Pass // 1
    {
    Name "BUFFERFILL"

    HLSLPROGRAM
    #pragma multi_compile_instancing
    #pragma multi_compile _ DOTS_INSTANCING_ON
    #pragma vertex vert
    #pragma fragment frag

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

    #pragma target 4.5

    struct appdata
    {
    float4 positionOS : POSITION;
    #if UNITY_ANY_INSTANCING_ENABLED
    uint instanceID : INSTANCEID_SEMANTIC;
    #endif
    };

    float4 vert (appdata i) : SV_POSITION
    {
    UNITY_SETUP_INSTANCE_ID(i);
    float4 pos = TransformObjectToHClip(i.positionOS.xyz);

    // flip the rendering "upside down" in non OpenGL to make things easier later
    // you'll notice none of the later passes need to pass UVs
    #ifdef UNITY_UV_STARTS_AT_TOP
    // pos.y = -pos.y;
    #endif

    return pos;
    }

    half frag () : SV_TARGET
    {
    return 1.0;
    }
    ENDHLSL
    }

    Pass // 2
    {
    Name "JUMPFLOODINIT"

    HLSLPROGRAM
    #pragma multi_compile_instancing
    #pragma multi_compile _ DOTS_INSTANCING_ON
    #pragma vertex vert
    #pragma fragment frag

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

    #pragma target 4.5

    struct appdata
    {
    float4 positionOS : POSITION;
    float2 uv : TEXCOORD0;
    #if UNITY_ANY_INSTANCING_ENABLED
    uint instanceID : INSTANCEID_SEMANTIC;
    #endif
    };

    struct v2f
    {
    float4 positionCS : SV_POSITION;
    #if UNITY_ANY_INSTANCING_ENABLED
    uint instanceID : CUSTOM_INSTANCE_ID;
    #endif
    };

    Texture2D _MainTex;

    CBUFFER_START(UnityPerMaterial)
    float4 _MainTex_TexelSize;
    CBUFFER_END

    v2f vert (appdata i) : SV_POSITION
    {
    UNITY_SETUP_INSTANCE_ID(i);
    v2f o;
    o.positionCS = TransformObjectToHClip(i.positionOS.xyz);

    return o;
    }

    float2 frag (v2f i) : SV_TARGET
    {
    // integer pixel position
    int2 uvInt = i.positionCS.xy;

    // sample silhouette texture for sobel
    half3x3 values;
    UNITY_UNROLL
    for(int u=0; u<3; u++)
    {
    UNITY_UNROLL
    for(int v=0; v<3; v++)
    {
    uint2 sampleUV = clamp(uvInt + int2(u-1, v-1), int2(0,0), (int2)_MainTex_TexelSize.zw - 1);
    values[u][v] = _MainTex.Load(int3(sampleUV, 0)).r;
    }
    }

    // calculate output position for this pixel
    float2 outPos = i.positionCS.xy * abs(_MainTex_TexelSize.xy) * FLOOD_ENCODE_SCALE - FLOOD_ENCODE_OFFSET;

    // interior, return position
    if (values._m11 > 0.99)
    return outPos;

    // exterior, return no position
    if (values._m11 < 0.01)
    return FLOOD_NULL_POS_FLOAT2;

    // sobel to estimate edge direction
    float2 dir = -float2(
    values[0][0] + values[0][1] * 2.0 + values[0][2] - values[2][0] - values[2][1] * 2.0 - values[2][2],
    values[0][0] + values[1][0] * 2.0 + values[2][0] - values[0][2] - values[1][2] * 2.0 - values[2][2]
    );

    // if dir length is small, this is either a sub pixel dot or line
    // no way to estimate sub pixel edge, so output position
    if (abs(dir.x) <= 0.005 && abs(dir.y) <= 0.005)
    return outPos;

    // normalize direction
    dir = normalize(dir);

    // sub pixel offset
    float2 offset = dir * (1.0 - values._m11);

    // output encoded offset position
    return (i.positionCS.xy + offset) * abs(_MainTex_TexelSize.xy) * FLOOD_ENCODE_SCALE - FLOOD_ENCODE_OFFSET;
    }
    ENDHLSL
    }

    Pass // 3
    {
    Name "JUMPFLOOD_SINGLEAXIS"

    HLSLPROGRAM
    #pragma multi_compile_instancing
    #pragma multi_compile _ DOTS_INSTANCING_ON
    #pragma vertex vert
    #pragma fragment frag

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

    #pragma target 4.5

    struct appdata
    {
    float4 positionOS : POSITION;
    #if UNITY_ANY_INSTANCING_ENABLED
    uint instanceID : INSTANCEID_SEMANTIC;
    #endif
    };

    struct v2f
    {
    float4 positionCS : SV_POSITION;
    #if UNITY_ANY_INSTANCING_ENABLED
    uint instanceID : CUSTOM_INSTANCE_ID;
    #endif
    };

    Texture2D _MainTex;
    CBUFFER_START(UnityPerMaterial)
    float4 _MainTex_TexelSize;
    CBUFFER_END
    int2 _AxisWidth;

    v2f vert (appdata i) : SV_POSITION
    {
    UNITY_SETUP_INSTANCE_ID(i);
    v2f o;
    o.positionCS = TransformObjectToHClip(i.positionOS.xyz);

    return o;
    }

    half2 frag (v2f i) : SV_TARGET {
    // integer pixel position
    int2 uvInt = int2(i.positionCS.xy);

    // initialize best distance at infinity
    float bestDist = 100000000;
    float2 bestCoord;

    // jump samples
    // only one loop
    UNITY_UNROLL
    for(int u=-1; u<=1; u++)
    {
    // calculate offset sample position
    int2 offsetUV = uvInt + _AxisWidth * u;

    // .Load() acts funny when sampling outside of bounds, so don't
    offsetUV = clamp(offsetUV, int2(0,0), (int2)_MainTex_TexelSize.zw - 1);

    // decode position from buffer
    float2 offsetPos = (_MainTex.Load(int3(offsetUV, 0)).rg + FLOOD_ENCODE_OFFSET) * _MainTex_TexelSize.zw / FLOOD_ENCODE_SCALE;

    // the offset from current position
    float2 disp = i.positionCS.xy - offsetPos;

    // square distance
    float dist = dot(disp, disp);

    // if offset position isn't a null position or is closer than the best
    // set as the new best and store the position
    if (offsetPos.x != -1.0 && dist < bestDist)
    {
    bestDist = dist;
    bestCoord = offsetPos;
    }
    }

    // if not valid best distance output null position, otherwise output encoded position
    return isinf(bestDist) ? FLOOD_NULL_POS_FLOAT2 : bestCoord * _MainTex_TexelSize.xy * FLOOD_ENCODE_SCALE - FLOOD_ENCODE_OFFSET;
    }
    ENDHLSL
    }

    Pass // 4
    {
    Name "OUTLINE"


    Stencil {
    Ref 1
    ReadMask 1
    WriteMask 1
    Comp NotEqual
    Pass Zero
    Fail Zero
    }

    Blend SrcAlpha OneMinusSrcAlpha

    HLSLPROGRAM
    #pragma multi_compile_instancing
    #pragma multi_compile _ DOTS_INSTANCING_ON
    #pragma vertex vert
    #pragma fragment frag

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

    #pragma target 4.5

    struct appdata
    {
    float4 positionOS : POSITION;
    #if UNITY_ANY_INSTANCING_ENABLED
    uint instanceID : INSTANCEID_SEMANTIC;
    #endif
    };

    struct v2f
    {
    float4 positionCS : SV_POSITION;
    #if UNITY_ANY_INSTANCING_ENABLED
    uint instanceID : CUSTOM_INSTANCE_ID;
    #endif
    };

    Texture2D _MainTex;


    half4 _OutlineColor;
    float _OutlineWidth;
    CBUFFER_START(UnityPerMaterial)
    float _Transparency;
    CBUFFER_END

    v2f vert (appdata i) : SV_POSITION
    {
    UNITY_SETUP_INSTANCE_ID(i);
    v2f o;
    o.positionCS = TransformObjectToHClip(i.positionOS.xyz);

    return o;
    }

    half4 frag (v2f i) : SV_Target {
    // integer pixel position
    int2 uvInt = int2(i.positionCS.xy);

    // load encoded position
    float2 encodedPos = _MainTex.Load(int3(uvInt, 0)).rg;

    // early out if null position
    if (encodedPos.y == -1)
    return half4(0,0,0,0);

    // decode closest position
    float2 nearestPos = (encodedPos + FLOOD_ENCODE_OFFSET) * abs(_ScreenParams.xy) / FLOOD_ENCODE_SCALE;

    // current pixel position
    float2 currentPos = i.positionCS.xy;

    // distance in pixels to closest position
    half dist = length(nearestPos - currentPos);

    // calculate outline
    // + 1.0 is because encoded nearest position is half a pixel inset
    // not + 0.5 because we want the anti-aliased edge to be aligned between pixels
    // distance is already in pixels, so this is already perfectly anti-aliased!
    half outline = saturate(_OutlineWidth - dist + 1.0);

    // apply outline to alpha
    half4 col = _OutlineColor;
    col.a *= outline;
    col.a *= _Transparency;

    // profit!
    return col;
    }
    ENDHLSL
    }
    }
    }
    58 changes: 58 additions & 0 deletions OutlineFeature.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,58 @@
    // Modified version of outline renderer feature by Alexander Ameye.
    // https://alexanderameye.github.io/
    // https://twitter.com/alexanderameye/status/1332286868222775298

    using System;
    using UnityEngine;
    using UnityEngine.Rendering;
    using UnityEngine.Rendering.Universal;

    public class OutlineFeature : ScriptableRendererFeature
    {
    public Settings[] OutlineSettings;
    private OutlinePass[] _outlinePasses;
    private StencilPass[] _stencilPasses;

    [Serializable]
    public class Settings
    {
    [Header("Visual")]
    [ColorUsage(true, true)]
    public Color Color = new Color(0.2f, 0.4f, 1, 1f);

    [Range(0.0f, 5.0f)]
    public float Width = 4f;

    [Header("Rendering")]
    public LayerMask LayerMask = -1;

    // TODO: Try this again when render layers are working with hybrid renderer.
    // [Range(0, 32)]
    // public int RenderLayer = 1;

    public RenderPassEvent RenderPassEvent = RenderPassEvent.AfterRenderingTransparents;

    public SortingCriteria SortingCriteria = SortingCriteria.CommonOpaque;
    }

    public override void Create()
    {
    _stencilPasses = new StencilPass[OutlineSettings.Length];
    _outlinePasses = new OutlinePass[OutlineSettings.Length];
    for (int i = 0; i < OutlineSettings.Length; i++)
    {
    Settings settings = OutlineSettings[i];
    _stencilPasses[i] = new StencilPass(settings);
    _outlinePasses[i] = new OutlinePass(settings);
    }
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
    for (int i = 0; i < _outlinePasses.Length; i++)
    {
    renderer.EnqueuePass(_stencilPasses[i]);
    renderer.EnqueuePass(_outlinePasses[i]);
    }
    }
    }
    131 changes: 131 additions & 0 deletions OutlinePass.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,131 @@
    // Modified version of outline render pass by Alexander Ameye.
    // https://alexanderameye.github.io/
    // https://twitter.com/alexanderameye/status/1332286868222775298

    using System;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.Experimental.Rendering;
    using UnityEngine.Rendering;
    using UnityEngine.Rendering.Universal;

    public class OutlinePass : ScriptableRenderPass
    {
    private const string ProfilerTag = "Outline Pass";
    private const string ShaderName = "Hidden/Outline";

    private static readonly ShaderTagId _srpDefaultUnlit = new ShaderTagId("SRPDefaultUnlit");
    private static readonly ShaderTagId _universalForward = new ShaderTagId("UniversalForward");
    private static readonly ShaderTagId _lightweightForward = new ShaderTagId("LightweightForward");

    private static readonly List<ShaderTagId> _shaderTags = new List<ShaderTagId>
    {
    _srpDefaultUnlit, _universalForward, _lightweightForward,
    };

    private static readonly int _silhouetteBufferID = Shader.PropertyToID("_SilhouetteBuffer");
    private static readonly int _nearestPointID = Shader.PropertyToID("_NearestPoint");
    private static readonly int _nearestPointPingPongID = Shader.PropertyToID("_NearestPointPingPong");
    private static readonly int _axisWidthID = Shader.PropertyToID("_AxisWidth");
    private static readonly int _outlineColorID = Shader.PropertyToID("_OutlineColor");
    private static readonly int _outlineWidthID = Shader.PropertyToID("_OutlineWidth");
    private readonly Material _bufferFillMaterial;
    private readonly Material _outlineMaterial;
    private readonly OutlineFeature.Settings _settings;
    private RenderTargetIdentifier _cameraColor;
    private FilteringSettings _filteringSettings;

    public OutlinePass(OutlineFeature.Settings settings)
    {
    profilingSampler = new ProfilingSampler(ProfilerTag);
    _settings = settings;
    renderPassEvent = settings.RenderPassEvent;

    // TODO: Try this again when render layers are working with hybrid renderer.
    // uint renderingLayerMask = 1u << settings.RenderLayer - 1;
    // _filteringSettings = new FilteringSettings(RenderQueueRange.all, settings.LayerMask.value, renderingLayerMask);
    _filteringSettings = new FilteringSettings(RenderQueueRange.all, settings.LayerMask.value);

    if (!_outlineMaterial)
    {
    _outlineMaterial = CoreUtils.CreateEngineMaterial(ShaderName);
    }
    _outlineMaterial.SetColor(_outlineColorID, settings.Color);
    _outlineMaterial.SetFloat(_outlineWidthID, Mathf.Max(1f, settings.Width));
    }

    public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
    {
    RenderTextureDescriptor descriptor = renderingData.cameraData.cameraTargetDescriptor;

    descriptor.graphicsFormat = GraphicsFormat.R8_UNorm;
    descriptor.msaaSamples = 1;
    descriptor.depthBufferBits = 0;
    descriptor.sRGB = false;
    descriptor.useMipMap = false;
    descriptor.autoGenerateMips = false;
    cmd.GetTemporaryRT(_silhouetteBufferID, descriptor, FilterMode.Point);

    descriptor.graphicsFormat = GraphicsFormat.R16G16_SNorm;
    cmd.GetTemporaryRT(_nearestPointID, descriptor, FilterMode.Point);
    cmd.GetTemporaryRT(_nearestPointPingPongID, descriptor, FilterMode.Point);

    ConfigureTarget(_silhouetteBufferID);
    ConfigureClear(ClearFlag.Color, Color.clear);

    _cameraColor = renderingData.cameraData.renderer.cameraColorTarget;
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
    DrawingSettings drawingSettings = CreateDrawingSettings(
    _shaderTags,
    ref renderingData,
    _settings.SortingCriteria
    );
    drawingSettings.overrideMaterial = _outlineMaterial;
    drawingSettings.overrideMaterialPassIndex = 1;

    int numMips = Mathf.CeilToInt(Mathf.Log(_settings.Width + 1.0f, 2f));
    int jfaIterations = numMips - 1;

    // TODO: Switch to this once mismatched markers bug is fixed.
    // CommandBuffer cmd = CommandBufferPool.Get(ProfilerTag);
    CommandBuffer cmd = CommandBufferPool.Get();
    using (new ProfilingScope(cmd, profilingSampler))
    {
    context.ExecuteCommandBuffer(cmd);
    cmd.Clear();

    context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref _filteringSettings);
    Blit(cmd, _silhouetteBufferID, _nearestPointID, _outlineMaterial, 2);

    for (int i = jfaIterations; i >= 0; i--)
    {
    float stepWidth = Mathf.Pow(2, i) + 0.5f;

    cmd.SetGlobalVector(_axisWidthID, new Vector2(stepWidth, 0f));
    Blit(cmd, _nearestPointID, _nearestPointPingPongID, _outlineMaterial, 3);
    cmd.SetGlobalVector(_axisWidthID, new Vector2(0f, stepWidth));
    Blit(cmd, _nearestPointPingPongID, _nearestPointID, _outlineMaterial, 3);
    }

    cmd.Blit(_nearestPointID, _cameraColor, _outlineMaterial, 4);
    }

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

    public override void OnCameraCleanup(CommandBuffer cmd)
    {
    if (cmd == null)
    {
    throw new ArgumentNullException("cmd");
    }

    cmd.ReleaseTemporaryRT(_silhouetteBufferID);
    cmd.ReleaseTemporaryRT(_nearestPointID);
    cmd.ReleaseTemporaryRT(_nearestPointPingPongID);
    }
    }
    69 changes: 69 additions & 0 deletions StencilPass.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,69 @@
    // Modified version of stencil render pass by Alexander Ameye.
    // https://alexanderameye.github.io/
    // https://twitter.com/alexanderameye/status/1332286868222775298

    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.Rendering;
    using UnityEngine.Rendering.Universal;

    public class StencilPass : ScriptableRenderPass
    {
    private const string ProfilerTag = "Stencil Pass";
    private const string ShaderName = "Hidden/Outline";

    private static readonly ShaderTagId _srpDefaultUnlit = new ShaderTagId("SRPDefaultUnlit");
    private static readonly ShaderTagId _universalForward = new ShaderTagId("UniversalForward");
    private static readonly ShaderTagId _lightweightForward = new ShaderTagId("LightweightForward");

    private static readonly List<ShaderTagId> _shaderTags = new List<ShaderTagId>
    {
    _srpDefaultUnlit, _universalForward, _lightweightForward,
    };

    private readonly OutlineFeature.Settings _settings;

    private readonly Material _stencilMaterial;
    private FilteringSettings _filteringSettings;

    public StencilPass(OutlineFeature.Settings settings)
    {
    profilingSampler = new ProfilingSampler(ProfilerTag);
    _settings = settings;
    renderPassEvent = settings.RenderPassEvent;

    // TODO: Try this again when render layers are working with hybrid renderer.
    // uint renderingLayerMask = 1u << settings.RenderLayer - 1;
    // _filteringSettings = new FilteringSettings(RenderQueueRange.all, settings.LayerMask.value, renderingLayerMask);
    _filteringSettings = new FilteringSettings(RenderQueueRange.all, settings.LayerMask.value);

    if (!_stencilMaterial)
    {
    _stencilMaterial = CoreUtils.CreateEngineMaterial(ShaderName);
    }
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
    DrawingSettings drawingSettings = CreateDrawingSettings(
    _shaderTags,
    ref renderingData,
    _settings.SortingCriteria
    );
    drawingSettings.overrideMaterial = _stencilMaterial;
    drawingSettings.overrideMaterialPassIndex = 0;

    // TODO: Switch to this once mismatched markers bug is fixed.
    // CommandBuffer cmd = CommandBufferPool.Get(ProfilerTag);
    CommandBuffer cmd = CommandBufferPool.Get();
    using (new ProfilingScope(cmd, profilingSampler))
    {
    context.ExecuteCommandBuffer(cmd);
    cmd.Clear();
    context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref _filteringSettings);
    }

    context.ExecuteCommandBuffer(cmd);
    CommandBufferPool.Release(cmd);
    }
    }
    23 changes: 23 additions & 0 deletions info.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,23 @@
    ## Usage
    1. Create a new Unity game object layer for the objects you wish to outline.
    2. In the ForwardRendererData asset of your URP asset, add the renderer feature named "Outline Feature"
    3. Add a new element to the "Outline Settings" of the new outline feature.
    4. Set the Layer Mask to the layer created in step 1.
    5. Add the objects you wish to outline to the outline layer (this can be done at runtime in ECS by setting the layer of the RenderMesh).
    6. Adjust the color and width of the outlines as desired in the renderer feature settings.

    ## Notes
    When I first added support for the Hyrbid Renderer, render layers were not working correctly. This required the use of game object layers to filter out the outlined objects. This might be fixed now, but I have not tested it yet.

    ## Credits
    - The technique for these wide outlines comes from [Ben Golus](https://bgolus.medium.com/) which is described and implemented in [this article](https://bgolus.medium.com/the-quest-for-very-wide-outlines-ba82ed442cd9).
    - [Alexander Ameye](https://alexanderameye.github.io/) created the renderer feature and shader modifications to make this work in URP, original shared [here](https://twitter.com/alexanderameye/status/1332286868222775298).
    - [Scott Daley](https://twitter.com/GravitonPunch) modified the shader to get it working with Unity ECS and the Hybrid Renderer as well as add support for multiple outline layers.

    ## Compatibility

    Test with:
    - Unity 2020.3.30f1
    - URP 10.8.1
    - Entities 0.50.0-preview.24
    - Hybrid Renderer 0.50.0-preview.24