-
-
Save SudoCat/05b954083f4cd706b8af738b9cf37860 to your computer and use it in GitHub Desktop.
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); | |
} | |
} | |
} |
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?
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 ;
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)
Ideally with a full repo there's first class support for a LICENSE file. I think the preferred thing to do with a gist is to just put the license in a comment at the top of every file–or at least I feel more comfortable with that approach.
My guess is that adding a free license at least frees the code from your own claims on it (though yes, there might be other claims higher up the chain). I'm not a lawyer, this is not legal advice.
Anyways, I ended up writing the whole thing from scratch anyways since my use case is a bit different and I needed to actually understand what was happening in the shader (it's the first postprocessing effect i've ever written).