Skip to content

Instantly share code, notes, and snippets.

@CianNoonan
Forked from bgolus/HiddenJumpFloodOutline.shader
Last active November 7, 2023 14:27
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save CianNoonan/c56256433801991038c9c40a48fe3002 to your computer and use it in GitHub Desktop.
Save CianNoonan/c56256433801991038c9c40a48fe3002 to your computer and use it in GitHub Desktop.
Shader "Hidden/JumpFloodOutline"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "PreviewType" = "Plane" }
Cull Off ZWrite Off ZTest Always
CGINCLUDE
// 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)
ENDCG
Pass // 0
{
Name "INNERSTENCIL"
Stencil {
Ref 1
ReadMask 1
WriteMask 1
Comp NotEqual
Pass Replace
}
ColorMask 0
Blend Zero One
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#pragma target 4.5
float4 vert (float4 vertex : POSITION) : SV_POSITION
{
return UnityObjectToClipPos(vertex);
}
// null frag
void frag () {}
ENDCG
}
Pass // 1
{
Name "BUFFERFILL"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#pragma target 4.5
struct appdata
{
float4 vertex : POSITION;
};
float4 vert (appdata v) : SV_POSITION
{
float4 pos = UnityObjectToClipPos(v.vertex);
// 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;
}
ENDCG
}
Pass // 2
{
Name "JUMPFLOODINIT"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#pragma target 4.5
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
};
Texture2D _MainTex;
float4 _MainTex_TexelSize;
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(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 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.pos.xy + offset) * abs(_MainTex_TexelSize.xy) * FLOOD_ENCODE_SCALE - FLOOD_ENCODE_OFFSET;
}
ENDCG
}
Pass // 3
{
Name "JUMPFLOOD"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#pragma target 4.5
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 pos : SV_POSITION;
};
Texture2D _MainTex;
float4 _MainTex_TexelSize;
int _StepWidth;
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
return o;
}
float2 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
UNITY_UNROLL
for(int u=-1; u<=1; u++)
{
UNITY_UNROLL
for(int v=-1; v<=1; v++)
{
// calculate offset sample position
int2 offsetUV = uvInt + int2(u, v) * _StepWidth;
// .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.y != FLOOD_NULL_POS && 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;
}
ENDCG
}
Pass // 4
{
Name "JUMPFLOOD_SINGLEAXIS"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#pragma target 4.5
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 pos : SV_POSITION;
};
Texture2D _MainTex;
float4 _MainTex_TexelSize;
int2 _AxisWidth;
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(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;
}
ENDCG
}
Pass // 5
{
Name "JUMPFLOODOUTLINE"
Stencil {
Ref 1
ReadMask 1
WriteMask 1
Comp NotEqual
Pass Zero
Fail Zero
}
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#pragma target 4.5
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 pos : SV_POSITION;
};
Texture2D _MainTex;
half4 _OutlineColor;
float _OutlineWidth;
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(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;
// profit!
return col;
}
ENDCG
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
//Highlighter tool is just an external static class which stores collections for consumption by the scriptable renderer passes.
//It is not included due to it being a bad implementation, you should implement some decent way of deciding what is rendered to the highlight
public class JumpFloodOutlineRenderFeature : ScriptableRendererFeature
{
[ColorUsage(true, true)] public Color OutlineColor = Color.white;
[Range(0.0f, 1000.0f)] public float OutlinePixelWidth = 4f;
public bool UseSeparableAxisMethod = true;
public RenderPassEvent PassEvent = RenderPassEvent.AfterRenderingTransparents;
public string[] SpecialShaderNames;
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
HighlighterTool.SpecialShaderNames = SpecialShaderNames;
if (HighlighterTool.DefaultPPKey.Color != OutlineColor) HighlighterTool.UpdateDefaultColor(OutlineColor);
var values = HighlighterTool.GetSortedPairs();
foreach (var pp in values)
{
if (pp.Renderers.Count == 0) continue;
pp.OutlinePass.OutlinePixelWidth = OutlinePixelWidth;
pp.OutlinePass.UseSeparableAxisMethod = UseSeparableAxisMethod;
pp.FillPass.renderPassEvent = pp.OutlinePass.renderPassEvent = PassEvent;
renderer.EnqueuePass(pp.FillPass);
renderer.EnqueuePass(pp.OutlinePass);
}
}
public override void Create()
{
HighlighterTool.Create(OutlineColor);
}
}
public class JFOBufferFillPass : ScriptableRenderPass
{
const int SHADER_PASS_SILHOUETTE_BUFFER_FILL = 1;
public HighlighterState PassKey;
readonly int _meshOcculsionID = Shader.PropertyToID("_MeshOcculsion");
public override void Configure(CommandBuffer cb, RenderTextureDescriptor cameraTextureDescriptor)
{
base.Configure(cb, cameraTextureDescriptor);
// match current quality settings' MSAA settings
// doesn't check if current camera has MSAA enabled
// also could just always do MSAA if you so pleased
var msaa = Mathf.Max(1, QualitySettings.antiAliasing);
var rtd = new RenderTextureDescriptor()
{
dimension = TextureDimension.Tex2D,
colorFormat = RenderTextureFormat.R8,
width = cameraTextureDescriptor.width,
height = cameraTextureDescriptor.height,
msaaSamples = msaa,
depthBufferBits = 0,
sRGB = false,
useMipMap = false,
autoGenerateMips = false
};
cb.GetTemporaryRT(_meshOcculsionID, rtd, FilterMode.Point);
ConfigureTarget(_meshOcculsionID);
ConfigureClear(ClearFlag.All, Color.clear);
}
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
//if (!Application.isFocused) return;
var cb = CommandBufferPool.Get("JFOutline FillPass");
var cullingMask = renderingData.cameraData.camera.cullingMask;
//HighlighterTool.DrawRenderers(cb, SHADER_PASS_SILHOUETTE_BUFFER_FILL, PassKey, cullingMask);
//cb.DrawRenderer over all renderers you want outlined
cb.SetGlobalTexture(_meshOcculsionID, _meshOcculsionID);
context.ExecuteCommandBuffer(cb);
CommandBufferPool.Release(cb);
}
public override void FrameCleanup(CommandBuffer cb)
{
cb.ReleaseTemporaryRT(_meshOcculsionID);
base.FrameCleanup(cb);
}
}
public class JumpFloodOutlinePass : ScriptableRenderPass
{
const int SHADER_PASS_SILHOUETTE_BUFFER_FILL = 1;
const int SHADER_PASS_JFA_INIT = 2;
const int SHADER_PASS_JFA_FLOOD = 3;
const int SHADER_PASS_JFA_FLOOD_SINGLE_AXIS = 4;
const int SHADER_PASS_JFA_OUTLINE = 5;
const int SHADER_PASS_JFA_OUTLINEKEEPINNER = 6;
const int SHADER_PASS_BLIT = 7;
public float OutlinePixelWidth;
public bool UseSeparableAxisMethod;
public HighlighterState PassKey;
RenderTargetIdentifier _target;
readonly int _meshOcculsionID = Shader.PropertyToID("_MeshOcculsion");
readonly int _silhouetteBufferID = Shader.PropertyToID("_SilhouetteBuffer");
readonly int _nearestPointID = Shader.PropertyToID("_NearestPoint");
readonly int _nearestPointPingPongID = Shader.PropertyToID("_NearestPointPingPong");
readonly int _outlineColorID = Shader.PropertyToID("_OutlineColor");
readonly int _mousePositionID = Shader.PropertyToID("_MousePosition");
readonly int _outlineWidthID = Shader.PropertyToID("_OutlineWidth");
readonly int _stepWidthID = Shader.PropertyToID("_StepWidth");
readonly int _axisWidthID = Shader.PropertyToID("_AxisWidth");
public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
{
base.OnCameraSetup(cmd, ref renderingData);
_target = renderingData.cameraData.renderer.cameraColorTarget;
}
public override void Configure(CommandBuffer cb, RenderTextureDescriptor cameraTextureDescriptor)
{
base.Configure(cb, cameraTextureDescriptor);
// match current quality settings' MSAA settings
// doesn't check if current camera has MSAA enabled
// also could just always do MSAA if you so pleased
var msaa = Mathf.Max(1, QualitySettings.antiAliasing);
var silhouetteRTD = new RenderTextureDescriptor()
{
dimension = TextureDimension.Tex2D,
graphicsFormat = GraphicsFormat.R8_UNorm,
width = cameraTextureDescriptor.width,
height = cameraTextureDescriptor.height,
msaaSamples = msaa,
depthBufferBits = 0,
sRGB = false,
useMipMap = false,
autoGenerateMips = false
};
cb.GetTemporaryRT(_silhouetteBufferID, silhouetteRTD, FilterMode.Point);
// setup descriptor for jump flood render textures
var jfaRTD = silhouetteRTD;
jfaRTD.msaaSamples = 1;
jfaRTD.graphicsFormat = GraphicsFormat.R16G16_SNorm;
// create jump flood buffers to ping pong between
cb.GetTemporaryRT(_nearestPointID, jfaRTD, FilterMode.Point);
cb.GetTemporaryRT(_nearestPointPingPongID, jfaRTD, FilterMode.Point);
ConfigureTarget(_silhouetteBufferID);
ConfigureClear(ClearFlag.All, Color.clear);
}
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
//if (!Application.isFocused) return;
var mouseViewPortPos = renderingData.cameraData.camera.ScreenToViewportPoint(Input.mousePosition);
var cb = CommandBufferPool.Get("JumpFloorOutlinePass");
CreateCommandBuffer(cb, mouseViewPortPos);
context.ExecuteCommandBuffer(cb);
CommandBufferPool.Release(cb);
}
private void CreateCommandBuffer(CommandBuffer cb, Vector4 mouseViewPortPos)
{
//Mesh occlusion buffer contains silhouette already so we dont want to rerender them
cb.Blit(_meshOcculsionID, _silhouetteBufferID, HighlighterTool.OutlineMat, SHADER_PASS_BLIT);
// Humus3D wire trick, keep line 1 pixel wide and fade alpha instead of making line smaller
// slightly nicer looking and no more expensive
var adjustedOutlineColor = PassKey.Color;
adjustedOutlineColor.a *= Mathf.Clamp01(OutlinePixelWidth);
cb.SetGlobalColor(_outlineColorID, adjustedOutlineColor.linear);
cb.SetGlobalFloat(_outlineWidthID, Mathf.Max(1f, OutlinePixelWidth));
cb.SetGlobalVector(_mousePositionID, mouseViewPortPos);
// calculate the number of jump flood passes needed for the current outline width
// + 1.0f to handle half pixel inset of the init pass and antialiasing
var numMips = Mathf.CeilToInt(Mathf.Log(OutlinePixelWidth + 1.0f, 2f));
var jfaIter = numMips - 1;
// Alan Wolfe's separable axis JFA - https://www.shadertoy.com/view/Mdy3D3
if (UseSeparableAxisMethod)
{
// jfa init
cb.Blit(_silhouetteBufferID, _nearestPointID, HighlighterTool.OutlineMat, SHADER_PASS_JFA_INIT);
// jfa flood passes
for (var i = jfaIter; 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
var stepWidth = Mathf.Pow(2, i) + 0.5f;
// the two separable passes, one axis at a time
cb.SetGlobalVector(_axisWidthID, new Vector2(stepWidth, 0f));
cb.Blit(_nearestPointID, _nearestPointPingPongID, HighlighterTool.OutlineMat, SHADER_PASS_JFA_FLOOD_SINGLE_AXIS);
cb.SetGlobalVector(_axisWidthID, new Vector2(0f, stepWidth));
cb.Blit(_nearestPointPingPongID, _nearestPointID, HighlighterTool.OutlineMat, SHADER_PASS_JFA_FLOOD_SINGLE_AXIS);
}
}
else // traditional JFA
{
// choose a starting buffer so we always finish on the same buffer
var startBufferID = ( jfaIter % 2 == 0 ) ? _nearestPointPingPongID : _nearestPointID;
// jfa init
cb.Blit(_silhouetteBufferID, startBufferID, HighlighterTool.OutlineMat, SHADER_PASS_JFA_INIT);
// jfa flood passes
for (var i = jfaIter; 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
cb.SetGlobalFloat(_stepWidthID, Mathf.Pow(2, i) + 0.5f);
// ping pong between buffers
if (i % 2 == 1)
cb.Blit(_nearestPointID, _nearestPointPingPongID, HighlighterTool.OutlineMat, SHADER_PASS_JFA_FLOOD);
else
cb.Blit(_nearestPointPingPongID, _nearestPointID, HighlighterTool.OutlineMat, SHADER_PASS_JFA_FLOOD);
}
}
// jfa decode & outline render
cb.Blit(_nearestPointID, _target, HighlighterTool.OutlineMat, PassKey.KeepInnerArea ? SHADER_PASS_JFA_OUTLINEKEEPINNER : SHADER_PASS_JFA_OUTLINE);
}
public override void FrameCleanup(CommandBuffer cb)
{
cb.ReleaseTemporaryRT(_silhouetteBufferID);
cb.ReleaseTemporaryRT(_nearestPointID);
cb.ReleaseTemporaryRT(_nearestPointPingPongID);
base.FrameCleanup(cb);
}
}
@CianNoonan
Copy link
Author

@kugimasa
Copy link

kugimasa commented Aug 23, 2021

Hi thanks for your great sample!
I wanted to try out your sample shader in my URP project but I couldn't figure out how to implement the HighlighterTool.
If I just want to try out the traditional JFA, should I just use only the JumpFloodOutlinePass??

@CianNoonan
Copy link
Author

You need the Feature and the Pass. You can see that the pass draws by using commandBuffer.DrawRenderer, so all the highlighter tool stuff is just a static class that returns an object that holds a reference to both the passes required and the list of renderers the passes are going to draw. I can't really explain further how to implement the handling of this as it's entirely dependant on your project's code and what you require from the highlight. I'll answer any questions you have though!

This comment at the top explains this:
//Highlighter tool is just an external static class that stores collections for consumption by the scriptable renderer passes.
//It is not included due to it being a bad implementation, you should implement some decent way of deciding what is rendered to the highlight

JFOBufferFillPass just draws pixels in a buffer that are going to be used in JumpFloodOutlinePass, this is separated as it's required to have a buffer that contains the initial silloutette so it can be clipped etc.

@kugimasa
Copy link

Thank you so much!!
I'll give it a try! 💪

@avrahamy
Copy link

avrahamy commented Oct 28, 2021

Hi! Thanks for this translation to URP!

I've almost got it working. Is it possible that the shader is missing some of the passes?
There are only 6 passes (0 to 5) defined in the shader, but the code is using pass 6 and 7:

    const int SHADER_PASS_JFA_OUTLINEKEEPINNER = 6;
    const int SHADER_PASS_BLIT = 7;

I'm getting this error: Invalid pass number (7) for Graphics.Blit (Material "(Unknown material)" with 6 passes)

Thanks!

@makamekm
Copy link

makamekm commented Sep 12, 2022

Could you remove it to avoid confusing people? This is not the complete solution, even if you put static properties into it.

@CianNoonan
Copy link
Author

CianNoonan commented Sep 12, 2022 via email

@YvesAlbuquerque
Copy link

What are those SpecialShaderNames? What HighlighterTool does with it?

@tremuska
Copy link

I don't care about your HighlighterTool. Code needs to be plug and play. What is your purpose of wasting everybodies time. Why do you promote your code on orginal thread

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