Skip to content

Instantly share code, notes, and snippets.

@mandarinx
Forked from ScottJDaley/Outline.shader
Created March 28, 2022 07:48
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mandarinx/5826e24f701141aaa0587d1a58cb3c6c to your computer and use it in GitHub Desktop.
Save mandarinx/5826e24f701141aaa0587d1a58cb3c6c to your computer and use it in GitHub Desktop.
Wide Outlines Renderer Feature for URP and ECS/DOTS/Hybrid Renderer

Wide Outlines Renderer Feature for URP and ECS/DOTS/Hybrid Renderer

outline_shader

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 which is described and implemented in this article.
  • Alexander Ameye created the renderer feature and shader modifications to make this work in URP, original shared here.
  • Scott Daley 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
// 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
}
}
}
// 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]);
}
}
}
// 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);
}
}
// 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);
}
}
@Fenikkel
Copy link

Works perfectly on Editor, but when I create a build for Windows, the build goes black screen and the Player.log it's constatly throwing me this error: Cannot create required material because shader Hidden/Outline could not be found

I'm using Unity 2012.3.16 and URP 12.1.8

Any idea of what it is?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment