-
-
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); | |
} | |
} | |
} |
hello, how to use this?
Hey, this is great!
For those wondering how to use this:
- Add a rendering pass to your URP renderer asset, using the included script. Make sure "Default" is one of the layers it is targeting.
- Create a material that uses this shader.
- Assign the material to an object in your scene. Make sure a texture is assigned to the shader - one with transparencies.
@SudoCat - could you possibly release under some kind of license? I'm nervous to include this in my work with it being unlicensed.
sure thing, it's still kind of buggy tho! I need to iron out some issues with small outline widths when I get time. I wouldn't recommend using this code as-is.
I'm honestly not sure what I'd need to do to license it? Do I need to add a file with a license, or will me commenting stating "Released under WTFPL" do the trick?
Because, yeah, it's definitely WTFPL
EDIT: should note tho, that I've based all of this code on BGolus' work, so I can't strictly claim it as my own. maybe muddies the licensing waters? I'm really not accustomed to licensing things, sorry.
sure thing, it's still kind of buggy tho! I need to iron out some issues with small outline widths when I get time. I wouldn't recommend using this code as-is.
I'm honestly not sure what I'd need to do to license it? Do I need to add a file with a license, or will me commenting stating "Released under WTFPL" do the trick?
Because, yeah, it's definitely WTFPL
EDIT: should note tho, that I've based all of this code on BGolus' work, so I can't strictly claim it as my own. maybe muddies the licensing waters? I'm really not accustomed to licensing things, sorry.
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).
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)
This takes BGolus' original approach and updates it to function in URP. This is heavily tailored towards 2D multi-part sprites, and likely leaves a whole lot of room for improvement - however I have never used any of Unity's Graphics APIs before, so I'm far from an expert.
It also only implements Alan Wolfe's JFA, rather than the trad JFA as well.
Please feel free to extend, or if you have any suggestions of how it could be better, please let me know! ♥