// 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 } } }