Skip to content

Instantly share code, notes, and snippets.

@Nesh108
Forked from SudoCat/CharacterShader.shader
Created January 3, 2023 11:54
Show Gist options
  • Save Nesh108/f9bf43d2df62ca9cc2a9f52a7cdf4fef to your computer and use it in GitHub Desktop.
Save Nesh108/f9bf43d2df62ca9cc2a9f52a7cdf4fef 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);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment