Skip to content

Instantly share code, notes, and snippets.

@rittikornt
Forked from ScottJDaley/Outline.shader
Last active February 18, 2025 06:25
Show Gist options
  • Save rittikornt/54bf36c701df688fdc38b9f45c63d31f to your computer and use it in GitHub Desktop.
Save rittikornt/54bf36c701df688fdc38b9f45c63d31f to your computer and use it in GitHub Desktop.
Wide Outlines Renderer Feature for URP14

Wide Outlines Renderer Feature for URP14

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.

Compatibility

Test with:

  • Unity 2022.3.48f1
  • URP 14.0.11
// 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 JumpFloodOutlineFeature : ScriptableRendererFeature
{
public Settings outlineSettings;
private JumpFloodOutlinePass m_outlinePass;
private JumpFloodOutlineStencilPass m_stencilPass;
[Serializable]
public class Settings
{
[Header("Reference")]
public Shader outlineShader = null;
[Header("Visual")]
[ColorUsage(true, true)]
public Color color = new Color(0.2f, 0.4f, 1, 1f);
[Range(0.0f, 20.0f)]
public float width = 5f;
[Header("Rendering")]
public LayerMask layerMask = 0;
// 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;
}
/// <summary>
/// Called when render targets are allocated and ready to be used.
/// https://docs.unity3d.com/Packages/[email protected]/manual/upgrade-guide-2022-2.html
/// </summary>
public override void SetupRenderPasses(ScriptableRenderer renderer, in RenderingData renderingData)
{
if (m_outlinePass != null)
{
m_outlinePass.SetupRenderPasses(renderer);
}
}
public override void Create()
{
if (outlineSettings != null)
{
m_stencilPass = new JumpFloodOutlineStencilPass(outlineSettings);
m_outlinePass = new JumpFloodOutlinePass(outlineSettings);
}
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
if (m_stencilPass != null)
{
renderer.EnqueuePass(m_stencilPass);
}
if (m_outlinePass != null)
{
renderer.EnqueuePass(m_outlinePass);
}
}
}
// 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 JumpFloodOutlinePass : ScriptableRenderPass
{
private const string k_ProfilerTag = "JumpFlood Outline Pass";
private const string k_ShaderName = "Hidden/shader_jumpflood_outline";
public static class ShaderIDs
{
private static readonly ShaderTagId SRPDefaultUnlit = new ShaderTagId("SRPDefaultUnlit");
private static readonly ShaderTagId UniversalForward = new ShaderTagId("UniversalForward");
private static readonly ShaderTagId LightweightForward = new ShaderTagId("LightweightForward");
public static readonly List<ShaderTagId> ShaderTags = new List<ShaderTagId>
{
SRPDefaultUnlit, UniversalForward, LightweightForward,
};
public static readonly int _SilhouetteBuffer = Shader.PropertyToID("_SilhouetteBuffer");
public static readonly int _NearestPoint = Shader.PropertyToID("_NearestPoint");
public static readonly int _NearestPointPingPong = Shader.PropertyToID("_NearestPointPingPong");
public static readonly int _AxisWidth = Shader.PropertyToID("_AxisWidth");
public static readonly int _OutlineColor = Shader.PropertyToID("_OutlineColor");
public static readonly int _OutlineWidth = Shader.PropertyToID("_OutlineWidth");
public static readonly int _SourceTex = Shader.PropertyToID("_SourceTex");
public static readonly int _SourceTex_TexelSize = Shader.PropertyToID("_SourceTex_TexelSize");
}
private readonly Material m_outlineMaterial;
private readonly JumpFloodOutlineFeature.Settings m_settings;
private RenderTargetIdentifier m_cameraColor;
private FilteringSettings m_filteringSettings;
private RenderTextureDescriptor tempRTDesc = default;
private bool m_allocatedTempRTThisFrame = false;
// Ref to Camera RTHandle
private RTHandle m_camColor;
private RTHandle m_camDepth;
public bool IsValid => m_outlineMaterial != null
&& m_camColor != null && m_camDepth != null;
public JumpFloodOutlinePass(JumpFloodOutlineFeature.Settings settings)
{
profilingSampler = new ProfilingSampler(k_ProfilerTag);
m_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);
m_filteringSettings = new FilteringSettings(RenderQueueRange.all, settings.layerMask.value);
int flags = (int)ScriptableRenderPassInput.Depth; // require depth
this.ConfigureInput((ScriptableRenderPassInput)flags);
if (!m_outlineMaterial)
{
Shader shader = Shader.Find(k_ShaderName);
shader = shader == null ? settings.outlineShader : shader;
if (shader != null)
{
m_outlineMaterial = CoreUtils.CreateEngineMaterial(shader);
}
else
{
Debug.LogError($"Failed to Load shader: \"{k_ShaderName}\"");
}
}
if (m_outlineMaterial != null)
{
m_outlineMaterial.SetColor(ShaderIDs._OutlineColor, settings.color);
m_outlineMaterial.SetFloat(ShaderIDs._OutlineWidth, Mathf.Max(1f, settings.width));
}
}
public void SetupRenderPasses(ScriptableRenderer renderer)
{
m_camColor = renderer.cameraColorTargetHandle;
m_camDepth = renderer.cameraDepthTargetHandle;
}
public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
{
if (IsValid)
{
tempRTDesc = renderingData.cameraData.cameraTargetDescriptor;
//{
// tempRTDesc.width /= 2;
// tempRTDesc.height /= 2;
//}
tempRTDesc.graphicsFormat = GraphicsFormat.R8_UNorm;
tempRTDesc.msaaSamples = 1;
tempRTDesc.depthBufferBits = 0;
tempRTDesc.sRGB = false;
tempRTDesc.useMipMap = false;
tempRTDesc.autoGenerateMips = false;
cmd.GetTemporaryRT(ShaderIDs._SilhouetteBuffer, tempRTDesc, FilterMode.Point);
tempRTDesc.graphicsFormat = GraphicsFormat.R16G16_SNorm;
cmd.GetTemporaryRT(ShaderIDs._NearestPoint, tempRTDesc, FilterMode.Point);
cmd.GetTemporaryRT(ShaderIDs._NearestPointPingPong, tempRTDesc, FilterMode.Point);
m_allocatedTempRTThisFrame = true;
m_cameraColor = m_camColor; // renderingData.cameraData.renderer.cameraColorTargetHandle;
}
}
public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
{
if (IsValid)
{
ConfigureTarget(ShaderIDs._SilhouetteBuffer, m_camDepth);
ConfigureClear(ClearFlag.Color, Color.clear);
}
}
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
if (IsValid)
{
DrawingSettings drawingSettings = CreateDrawingSettings(
ShaderIDs.ShaderTags,
ref renderingData,
m_settings.sortingCriteria
);
drawingSettings.overrideMaterial = m_outlineMaterial;
drawingSettings.overrideMaterialPassIndex = 1;
int numMips = Mathf.CeilToInt(Mathf.Log(m_settings.width + 1.0f, 2f));
int jfaIterations = numMips - 1;
Vector4 rtTexelSize = new Vector4(1.0f / (float)tempRTDesc.width, 1.0f / (float)tempRTDesc.height, tempRTDesc.width, tempRTDesc.height);
CommandBuffer cmd = CommandBufferPool.Get();
using (new ProfilingScope(cmd, profilingSampler))
{
context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref m_filteringSettings);
// JumpFlood Init
//Blit(cmd, ShaderIDs._SilhouetteBuffer, ShaderIDs._NearestPoint, m_outlineMaterial, 2);
CoreUtils.SetRenderTarget(cmd, ShaderIDs._NearestPoint);
cmd.SetGlobalTexture(ShaderIDs._SourceTex, ShaderIDs._SilhouetteBuffer);
cmd.SetGlobalVector(ShaderIDs._SourceTex_TexelSize, rtTexelSize);
CoreUtils.DrawFullScreen(cmd, m_outlineMaterial, null, 6);
// Ping-pong JumpFlood Single Axis X then Y
for (int i = jfaIterations; i >= 0; i--)
{
float stepWidth = Mathf.Pow(2, i) + 0.5f;
CoreUtils.SetRenderTarget(cmd, ShaderIDs._NearestPointPingPong);
cmd.SetGlobalTexture(ShaderIDs._SourceTex, ShaderIDs._NearestPoint);
cmd.SetGlobalVector(ShaderIDs._SourceTex_TexelSize, rtTexelSize);
cmd.SetGlobalVector(ShaderIDs._AxisWidth, new Vector2(stepWidth, 0f));
//Blit(cmd, ShaderIDs._NearestPoint, ShaderIDs._NearestPointPingPong, m_outlineMaterial, 3);
CoreUtils.DrawFullScreen(cmd, m_outlineMaterial, null, 7);
CoreUtils.SetRenderTarget(cmd, ShaderIDs._NearestPoint);
cmd.SetGlobalTexture(ShaderIDs._SourceTex, ShaderIDs._NearestPointPingPong);
cmd.SetGlobalVector(ShaderIDs._SourceTex_TexelSize, rtTexelSize);
cmd.SetGlobalVector(ShaderIDs._AxisWidth, new Vector2(0f, stepWidth));
//Blit(cmd, ShaderIDs._NearestPointPingPong, ShaderIDs._NearestPoint, m_outlineMaterial, 3);
CoreUtils.DrawFullScreen(cmd, m_outlineMaterial, null, 7);
}
//cmd.Blit(_nearestPointID, _cameraColor, _outlineMaterial, 4);
CoreUtils.SetRenderTarget(cmd, m_cameraColor, m_camDepth);
cmd.SetGlobalTexture(ShaderIDs._SourceTex, ShaderIDs._NearestPoint);
CoreUtils.DrawFullScreen(cmd, m_outlineMaterial, null, 5);
}
context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);
}
}
public override void OnCameraCleanup(CommandBuffer cmd)
{
if (cmd == null)
{
throw new ArgumentNullException("cmd");
}
if (m_allocatedTempRTThisFrame)
{
cmd.ReleaseTemporaryRT(ShaderIDs._SilhouetteBuffer);
cmd.ReleaseTemporaryRT(ShaderIDs._NearestPoint);
cmd.ReleaseTemporaryRT(ShaderIDs._NearestPointPingPong);
m_allocatedTempRTThisFrame = false;
}
}
}
// 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 JumpFloodOutlineStencilPass : ScriptableRenderPass
{
private const string k_ProfilerTag = "JumpFlood Outline Stencil Pass";
private const string k_ShaderName = "Hidden/shader_jumpflood_outline";
public static class ShaderIDs
{
private static readonly ShaderTagId SRPDefaultUnlit = new ShaderTagId("SRPDefaultUnlit");
private static readonly ShaderTagId UniversalForward = new ShaderTagId("UniversalForward");
private static readonly ShaderTagId LightweightForward = new ShaderTagId("LightweightForward");
public static readonly List<ShaderTagId> ShaderTags = new List<ShaderTagId>
{
SRPDefaultUnlit, UniversalForward, LightweightForward,
};
}
private readonly JumpFloodOutlineFeature.Settings m_settings;
private readonly Material m_stencilMaterial;
private FilteringSettings m_filteringSettings;
public bool IsValid => m_stencilMaterial != null;
public JumpFloodOutlineStencilPass(JumpFloodOutlineFeature.Settings settings)
{
profilingSampler = new ProfilingSampler(k_ProfilerTag);
m_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);
m_filteringSettings = new FilteringSettings(RenderQueueRange.all, settings.layerMask.value);
int flags = (int)ScriptableRenderPassInput.Depth; // require depth
this.ConfigureInput((ScriptableRenderPassInput)flags);
if (!m_stencilMaterial)
{
Shader shader = Shader.Find(k_ShaderName);
shader = shader == null ? settings.outlineShader : shader;
if (shader != null)
{
m_stencilMaterial = CoreUtils.CreateEngineMaterial(shader);
}
else
{
Debug.LogError($"Failed to Load shader: \"{k_ShaderName}\"");
}
}
}
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
if (IsValid)
{
DrawingSettings drawingSettings = CreateDrawingSettings(
ShaderIDs.ShaderTags,
ref renderingData,
m_settings.sortingCriteria
);
drawingSettings.overrideMaterial = m_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 m_filteringSettings);
}
context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);
}
}
}
// 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/shader_jumpflood_outline"
{
Properties
{
[HideInInspector] _MainTex ("Texture", 2D) = "white" {}
_OutlineColor("Color", Color) = (1, 1, 1, 1)
_OutlineWidth ("Width", Range (0, 20)) = 5
}
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 Always
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 3.0
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 3.0
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 3.0
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;
half4 _OutlineColor;
float _OutlineWidth;
CBUFFER_END
v2f vert (appdata i)
{
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 3.0
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;
half4 _OutlineColor;
float _OutlineWidth;
CBUFFER_END
int2 _AxisWidth;
v2f vert (appdata i)
{
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 3.0
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;
half4 _OutlineColor;
float _OutlineWidth;
CBUFFER_END
v2f vert (appdata i)
{
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;
// profit!
return col;
}
ENDHLSL
}
Pass // 5
{
Name "OUTLINE FULLSCREEN TRIANGLE"
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.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#pragma target 3.0
struct Attributes
{
uint vertexID : SV_VertexID;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionCS : SV_Position;
UNITY_VERTEX_OUTPUT_STEREO
};
Varyings Vert(Attributes input)
{
Varyings output;
UNITY_SETUP_INSTANCE_ID(input);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
output.positionCS = GetFullScreenTriangleVertexPosition(input.vertexID);
return output;
}
TEXTURE2D_X(_SourceTex);
Texture2D _MainTex;
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_TexelSize;
half4 _OutlineColor;
float _OutlineWidth;
CBUFFER_END
float4 Frag(Varyings input) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
// integer pixel position
int2 uvInt = int2(input.positionCS.xy);
// load encoded position
float2 encodedPos = LOAD_TEXTURE2D_X(_SourceTex, uvInt).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 = input.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;
// profit!
return col;
}
ENDHLSL
}
Pass // 6
{
Name "JUMPFLOODINIT FULLSCREEN TRIANGLE"
HLSLPROGRAM
#pragma multi_compile_instancing
//#pragma multi_compile _ DOTS_INSTANCING_ON
#pragma vertex Vert
#pragma fragment Frag
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#pragma target 3.0
struct Attributes
{
uint vertexID : SV_VertexID;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionCS : SV_Position;
UNITY_VERTEX_OUTPUT_STEREO
};
Varyings Vert(Attributes input)
{
Varyings output;
UNITY_SETUP_INSTANCE_ID(input);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
output.positionCS = GetFullScreenTriangleVertexPosition(input.vertexID);
return output;
}
TEXTURE2D_X(_SourceTex);
SAMPLER(sampler_SourceTex);
CBUFFER_START(UnityPerMaterial)
float4 _SourceTex_TexelSize;
half4 _OutlineColor;
float _OutlineWidth;
CBUFFER_END
float4 Frag(Varyings input) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
// integer pixel position
int2 uvInt = int2(input.positionCS.xy);
float4 texelSize = _SourceTex_TexelSize;
// 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)texelSize.zw - 1);
//values[u][v] = _MainTex.Load(int3(sampleUV, 0)).r;
values[u][v] = LOAD_TEXTURE2D_X(_SourceTex, sampleUV).r;
}
}
// calculate output position for this pixel
float2 outPos = input.positionCS.xy * abs(texelSize.xy) * FLOOD_ENCODE_SCALE - FLOOD_ENCODE_OFFSET;
// interior, return position
if (values._m11 > 0.99)
return float4(outPos, 0.0f, 0.0f);
// exterior, return no position
if (values._m11 < 0.01)
return float4(FLOOD_NULL_POS_FLOAT2, 0.0f, 0.0f);
// 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 float4(outPos, 0.0f, 0.0f);
// normalize direction
dir = normalize(dir);
// sub pixel offset
float2 offset = dir * (1.0 - values._m11);
// output encoded offset position
return float4((input.positionCS.xy + offset) * abs(texelSize.xy) * FLOOD_ENCODE_SCALE - FLOOD_ENCODE_OFFSET, 0.0f, 0.0f);
}
ENDHLSL
}
Pass // 7
{
Name "JUMPFLOOD_SINGLEAXIS FULLSCREEN TRIANGLE"
HLSLPROGRAM
#pragma multi_compile_instancing
//#pragma multi_compile _ DOTS_INSTANCING_ON
#pragma vertex Vert
#pragma fragment Frag
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#pragma target 3.0
struct Attributes
{
uint vertexID : SV_VertexID;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionCS : SV_Position;
UNITY_VERTEX_OUTPUT_STEREO
};
Varyings Vert(Attributes input)
{
Varyings output;
UNITY_SETUP_INSTANCE_ID(input);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
output.positionCS = GetFullScreenTriangleVertexPosition(input.vertexID);
return output;
}
TEXTURE2D_X(_SourceTex);
SAMPLER(sampler_SourceTex);
CBUFFER_START(UnityPerMaterial)
float4 _SourceTex_TexelSize;
half4 _OutlineColor;
float _OutlineWidth;
CBUFFER_END
int2 _AxisWidth;
float2 Frag(Varyings input) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
// integer pixel position
int2 uvInt = int2(input.positionCS.xy);
float4 texelSize = _SourceTex_TexelSize;
// 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)texelSize.zw - 1);
// decode position from buffer
float2 offsetPos = (LOAD_TEXTURE2D_X(_SourceTex, offsetUV).rg + FLOOD_ENCODE_OFFSET) * texelSize.zw / FLOOD_ENCODE_SCALE;
// the offset from current position
float2 disp = input.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 * texelSize.xy * FLOOD_ENCODE_SCALE - FLOOD_ENCODE_OFFSET;
}
ENDHLSL
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment