Skip to content

Instantly share code, notes, and snippets.

@SudoCat
Forked from bgolus/HiddenJumpFloodOutline.shader
Last active August 16, 2023 22:25
Show Gist options
  • Save SudoCat/05b954083f4cd706b8af738b9cf37860 to your computer and use it in GitHub Desktop.
Save SudoCat/05b954083f4cd706b8af738b9cf37860 to your computer and use it in GitHub Desktop.
Jump flood based outline effect for Unity, updated to URP https://medium.com/@bgolus/the-quest-for-very-wide-outlines-ba82ed442cd9
Shader "Custom/Warlocracy/CharacterShader"
{
Properties
{
_MainTex ("Sprite Texture", 2D) = "white" {}
_OutlineColor ("Outline Color", Color) = (1,1,1,1)
_OutlineWidth ("Outline Width", float) = 2
[HideInInspector] _Color ("Tint", Color) = (1,1,1,1)
[HideInInspector] _RendererColor ("RendererColor", Color) = (1,1,1,1)
}
SubShader
{
Tags { "Queue" = "Transparent" "RenderType" = "Transparent" "RenderPipeline" = "UniversalPipeline" }
Cull Off
ZWrite Off
ZTest Always
HLSLINCLUDE
// just inside the precision of a R16G16_SNorm to keep encoded range 1.0 >= and > -1.0
#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
// This pass is copied straight from Sprite-Unlit-Default
Pass
{
Name "SPRITE RENDER"
Blend SrcAlpha OneMinusSrcAlpha
Tags { "LightMode" = "Universal2D" }
HLSLPROGRAM
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#if defined(DEBUG_DISPLAY)
#include "Packages/com.unity.render-pipelines.universal/Shaders/2D/Include/InputData2D.hlsl"
#include "Packages/com.unity.render-pipelines.universal/Shaders/2D/Include/SurfaceData2D.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Debug/Debugging2D.hlsl"
#endif
#pragma vertex UnlitVertex
#pragma fragment UnlitFragment
#pragma multi_compile _ DEBUG_DISPLAY
struct attributes
{
float3 positionOS : POSITION;
float4 color : COLOR;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionCS : SV_POSITION;
half4 color : COLOR;
float2 uv : TEXCOORD0;
#if defined(DEBUG_DISPLAY)
float3 positionWS : TEXCOORD2;
#endif
UNITY_VERTEX_OUTPUT_STEREO
};
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
half4 _MainTex_ST;
float4 _Color;
half4 _RendererColor;
Varyings UnlitVertex(attributes v)
{
Varyings o = (Varyings)0;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
o.positionCS = TransformObjectToHClip(v.positionOS);
#if defined(DEBUG_DISPLAY)
o.positionWS = TransformObjectToWorld(v.positionOS);
#endif
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.color = v.color * _Color * _RendererColor;
return o;
}
half4 UnlitFragment(Varyings i) : SV_Target
{
float4 mainTex = i.color * SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
#if defined(DEBUG_DISPLAY)
SurfaceData2D surfaceData;
InputData2D inputData;
half4 debugColor = 0;
InitializeSurfaceData(mainTex.rgb, mainTex.a, surfaceData);
InitializeInputData(i.uv, inputData);
SETUP_DEBUG_DATA_2D(inputData, i.positionWS);
if(CanDebugOverrideOutputColor(surfaceData, inputData, debugColor))
{
return debugColor;
}
#endif
return mainTex;
}
ENDHLSL
}
// Pass 1 - Create alpha mask
Pass
{
Name "BUFFERFILL"
Tags { "LightMode" = "PlayerCharacter" }
Blend SrcAlpha OneMinusSrcAlpha
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
// #pragma target 4.5
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct attributes
{
float3 positionOS : POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_OUTPUT_STEREO
};
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
half4 _MainTex_ST;
Varyings vert(attributes v)
{
Varyings o;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
o.positionCS = TransformObjectToHClip(v.positionOS);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
half4 frag(Varyings i) : SV_Target
{
float4 mainTex = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
return mainTex.a;
}
ENDHLSL
}
Pass // 2
{
Name "JUMPFLOODINIT"
Tags { "LightMode" = "PlayerCharacter" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#pragma target 4.5
struct appdata
{
float3 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
};
Texture2D _MainTex;
float4 _MainTex_TexelSize;
v2f vert (appdata v)
{
v2f o;
o.pos = TransformObjectToHClip(v.vertex);
return o;
}
float2 frag (v2f i) : SV_Target {
// integer pixel position
int2 uvInt = i.pos.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.pos.xy * abs(_MainTex_TexelSize.xy) * FLOOD_ENCODE_SCALE - FLOOD_ENCODE_OFFSET;
// interior, return position
if (values._m11 > 0.99)
return FLOOD_NULL_POS_FLOAT2;
// 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.pos.xy + offset) * abs(_MainTex_TexelSize.xy) * FLOOD_ENCODE_SCALE - FLOOD_ENCODE_OFFSET;
}
ENDHLSL
}
Pass // 3
{
Name "JUMPFLOOD_SINGLEAXIS"
Tags { "LightMode" = "PlayerCharacter" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#pragma target 4.5
struct appdata
{
float3 vertex : POSITION;
};
struct v2f
{
float4 pos : SV_POSITION;
};
Texture2D _MainTex;
float4 _MainTex_TexelSize;
int2 _AxisWidth;
v2f vert (appdata v)
{
v2f o;
o.pos = TransformObjectToHClip(v.vertex);
return o;
}
half2 frag (v2f i) : SV_Target {
// integer pixel position
int2 uvInt = int2(i.pos.xy);
// initialize best distance at infinity
float bestDist = 1.#INF;
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.pos.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 "JUMPFLOODOUTLINE"
Tags { "LightMode" = "PlayerCharacter" }
// Stencil {
// Ref 1
// ReadMask 1
// WriteMask 1
// Comp NotEqual
// Pass Zero
// Fail Zero
// }
Blend SrcAlpha OneMinusSrcAlpha
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#pragma target 4.5
struct appdata
{
float3 vertex : POSITION;
};
struct v2f
{
float4 pos : SV_POSITION;
};
Texture2D _MainTex;
Texture2D _OutlineMaskBuffer;
half4 _OutlineColor;
float _OutlineWidth;
v2f vert (appdata v)
{
v2f o;
o.pos = TransformObjectToHClip(v.vertex);
return o;
}
half4 frag (v2f i) : SV_Target {
// integer pixel position
int2 uvInt = int2(i.pos.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.pos.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 *= 1-_OutlineMaskBuffer.Load(int3(uvInt, 0)).r;
// profit!
return col;
}
ENDHLSL
}
}
}
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
namespace Engine.Rendering
{
public enum JFAOutlinePass
{
SpriteRender = 0,
AlphaMask = 1,
Init = 2,
Flood = 3,
Outline = 4
}
public class OutlineRendererFeature : ScriptableRendererFeature
{
private BufferPopulatePass _populatePass;
[SerializeField] private Material outlineMaterial;
[SerializeField] private LayerMask targetLayers;
[SerializeField] private float outlinePixelWidth = 2f;
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
renderer.EnqueuePass(_populatePass);
}
public override void Create()
{
_populatePass = new BufferPopulatePass(outlineMaterial, targetLayers, outlinePixelWidth);
_populatePass.renderPassEvent = RenderPassEvent.BeforeRenderingTransparents;
}
}
public class BufferPopulatePass : ScriptableRenderPass
{
private static readonly int MaskBufferID = Shader.PropertyToID("_OutlineMaskBuffer");
private static readonly int JumpFloodBufferPingID = Shader.PropertyToID("_JFBPing");
private static readonly int JumpFloodBufferPongID = Shader.PropertyToID("_JFBPong");
private static readonly int StepWidthID = Shader.PropertyToID("_StepWidth");
private static readonly int AxisWidthID = Shader.PropertyToID("_AxisWidth");
private readonly Material _outlineMaterial;
private readonly float _outlinePixelWidth;
private FilteringSettings _filteringSettings;
private readonly List<ShaderTagId> _tags;
private readonly LayerMask _layerMask;
private readonly ProfilingSampler _profilingSampler;
public BufferPopulatePass(Material outlineMaterial, LayerMask targetLayers, float outlinePixelWidth)
{
_outlineMaterial = outlineMaterial;
_filteringSettings = new FilteringSettings(RenderQueueRange.transparent, targetLayers);
_layerMask = targetLayers;
_tags = new List<ShaderTagId>();
_tags.Add(new ShaderTagId("PlayerCharacter"));
_outlinePixelWidth = outlinePixelWidth;
_profilingSampler = new ProfilingSampler("Buffer Populate Pass");
}
public override void Configure(CommandBuffer cb, RenderTextureDescriptor cameraTextureDescriptor)
{
var rtd = new RenderTextureDescriptor(
cameraTextureDescriptor.width,
cameraTextureDescriptor.height,
GraphicsFormat.R8_UNorm,
0,
0
);
cb.GetTemporaryRT(MaskBufferID, rtd, FilterMode.Point);
var rtd2 = new RenderTextureDescriptor(
cameraTextureDescriptor.width,
cameraTextureDescriptor.height,
GraphicsFormat.R16G16_SNorm,
0,
0
);
cb.GetTemporaryRT(JumpFloodBufferPingID, rtd2, FilterMode.Point);
cb.GetTemporaryRT(JumpFloodBufferPongID, rtd2, FilterMode.Point);
ConfigureTarget(MaskBufferID);
ConfigureClear(ClearFlag.Color, Color.clear);
}
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
var drawingSettings = CreateDrawingSettings(_tags, ref renderingData, SortingCriteria.CommonTransparent);
drawingSettings.enableDynamicBatching = true;
drawingSettings.SetShaderPassName((int) JFAOutlinePass.AlphaMask, new ShaderTagId("BUFFERFILL"));
context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref _filteringSettings);
// Alan Wolfe's separable axis JFA - https://www.shadertoy.com/view/Mdy3D3
var numMips = Mathf.CeilToInt(Mathf.Log(_outlinePixelWidth + 1.0f, 2f));
var cmd = CommandBufferPool.Get();
using (new ProfilingScope(cmd, _profilingSampler))
{
cmd.Blit(MaskBufferID, JumpFloodBufferPingID, _outlineMaterial, 2);
for (int i = numMips - 1; i >= 0; i--)
{
// calculate appropriate jump width for each iteration
// + 0.5 is just me being cautious to avoid any floating point math rounding errors
float stepWidth = Mathf.Pow(2, i) + 0.5f;
// the two separable passes, one axis at a time
cmd.SetGlobalVector(AxisWidthID, new Vector2(stepWidth, 0f));
cmd.Blit(JumpFloodBufferPingID, JumpFloodBufferPongID, _outlineMaterial, (int)JFAOutlinePass.Flood);
cmd.SetGlobalVector(AxisWidthID, new Vector2(0f, stepWidth));
cmd.Blit(JumpFloodBufferPongID, JumpFloodBufferPingID, _outlineMaterial, (int)JFAOutlinePass.Flood);
}
cmd.SetGlobalTexture(MaskBufferID, MaskBufferID);
cmd.Blit(JumpFloodBufferPingID, renderingData.cameraData.renderer.cameraColorTarget, _outlineMaterial,
(int)JFAOutlinePass.Outline);
}
context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);
}
public override void OnCameraCleanup(CommandBuffer cmd)
{
cmd.ReleaseTemporaryRT(MaskBufferID);
cmd.ReleaseTemporaryRT(JumpFloodBufferPingID);
cmd.ReleaseTemporaryRT(JumpFloodBufferPongID);
}
}
}
@mistyskye
Copy link

Hi, this is really promising. You mention your fork is tailored to 2D sprites, but I'm interested in using this URP version for 3D object outlines.
Do you know how I could go about making this work?

@Renardjojo
Copy link

Thanks for sharing!
There is however a critical error in this shader line 223.
You should return "return outPos;", not return FLOOD_NULL_POS_FLOAT2 ;

@SudoCat
Copy link
Author

SudoCat commented Apr 25, 2023

Hi, this is really promising. You mention your fork is tailored to 2D sprites, but I'm interested in using this URP version for 3D object outlines. Do you know how I could go about making this work?

Not a clue, I'm afraid! I'm not the most experienced myself. Probably somewhere between BGolus' and what I've done.

Thanks for sharing! There is however a critical error in this shader line 223. You should return "return outPos;", not return FLOOD_NULL_POS_FLOAT2 ;

Thanks! That probably explains some of the odd behaviour I've seen 😅 (edit: apparently i fixed that locally at some point and never updated the gist)

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