Forked from bgolus/HiddenJumpFloodOutline.shader
Last active
August 18, 2024 12:35
-
-
Save CianNoonan/c56256433801991038c9c40a48fe3002 to your computer and use it in GitHub Desktop.
Jump flood based outline effect for Unity https://medium.com/@bgolus/the-quest-for-very-wide-outlines-ba82ed442cd9
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
Could you remove it to avoid confusing people? This is not the complete solution, even if you put static properties into it.
It's not provided as a full implementation, only as a rough draft to be
fully implemented.
The basic idea is all there. If you have revisions you'd like made I'll
accept a PR or further fork
…On Mon, 12 Sep 2022, 11:36 Maxim Karpov, ***@***.***> wrote:
***@***.**** commented on this gist.
------------------------------
I cannot say thanks for sharing such an ugly code. Please remove it to
avoid confusing people.
—
Reply to this email directly, view it on GitHub
<https://gist.github.com/c56256433801991038c9c40a48fe3002#gistcomment-4298057>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ACDZ73HRL2MXMRSBO7O25HLV54BSPANCNFSM42ZKENNQ>
.
You are receiving this because you authored the thread.Message ID:
***@***.***>
What are those SpecialShaderNames? What HighlighterTool does with it?
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
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:
I'm getting this error:
Invalid pass number (7) for Graphics.Blit (Material "(Unknown material)" with 6 passes)
Thanks!