Skip to content

Instantly share code, notes, and snippets.

@ScottJDaley
Last active April 2, 2024 03:09
Show Gist options
  • Star 44 You must be signed in to star a gist
  • Fork 9 You must be signed in to fork a gist
  • Save ScottJDaley/6cddf0c8995ed61cac7088e22c983de1 to your computer and use it in GitHub Desktop.
Save ScottJDaley/6cddf0c8995ed61cac7088e22c983de1 to your computer and use it in GitHub Desktop.
Wide Outlines Renderer Feature for URP and ECS/DOTS/Hybrid Renderer

Wide Outlines Renderer Feature for URP and ECS/DOTS/Hybrid Renderer

outline_shader

Usage

  1. Create a new Unity game object layer for the objects you wish to outline.
  2. In your URP asset, turn on MSAA or disable the Depth Texture.
  3. In the ForwardRendererData asset of your URP asset, add the renderer feature named "Outline Feature"
  4. Set the Layer Mask to the layer created in step 1.
  5. Add the objects you wish to outline to the outline layer (this can be done at runtime in ECS by setting the layer of the RenderMesh).
  6. Adjust the color and width of the outlines as desired in the renderer feature settings.

Notes

When I first added support for the Hyrbid Renderer, render layers were not working correctly. This required the use of game object layers to filter out the outlined objects. This might be fixed now, but I have not tested it yet.

Credits

  • The technique for these wide outlines comes from Ben Golus which is described and implemented in this article.
  • Alexander Ameye created the renderer feature and shader modifications to make this work in URP, original shared here.
  • Scott Daley modified the shader to get it working with Unity ECS and the Hybrid Renderer.

Compatibility

Test with:

  • Unity 2020.3.30f1
  • URP 10.8.1
  • Entities 0.50.0-preview.24
  • Hybrid Renderer 0.50.0-preview.24
// Original shader by @bgolus, modified slightly by @alexanderameye for URP, modified slightly more
// by @gravitonpunch for ECS/DOTS/HybridRenderer.
// https://twitter.com/bgolus
// https://medium.com/@bgolus/the-quest-for-very-wide-outlines-ba82ed442cd9
// https://alexanderameye.github.io/
// https://twitter.com/alexanderameye/status/1332286868222775298
Shader "Hidden/Outline"
{
Properties
{
[HideInInspector] _MainTex ("Texture", 2D) = "white" {}
_OutlineColor("Color", Color) = (1, 1, 1, 1)
_OutlineWidth ("Width", Range (0, 20)) = 5
}
SubShader
{
Tags { "RenderType"="Opaque" "RenderPipeline" = "UniversalPipeline"}
Cull Off ZWrite Off ZTest Always
HLSLINCLUDE
#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
Pass // 0
{
Name "STENCIL MASK"
Stencil {
Ref 1
ReadMask 1
WriteMask 1
Comp Always
Pass Replace
}
ColorMask 0
Blend Zero One
HLSLPROGRAM
#pragma multi_compile_instancing
#pragma multi_compile _ DOTS_INSTANCING_ON
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#pragma target 4.5
struct appdata
{
float4 positionOS : POSITION;
#if UNITY_ANY_INSTANCING_ENABLED
uint instanceID : INSTANCEID_SEMANTIC;
#endif
};
float4 vert (appdata i) : SV_POSITION
{
UNITY_SETUP_INSTANCE_ID(i);
return TransformObjectToHClip(i.positionOS.xyz);
}
void frag () {}
ENDHLSL
}
Pass // 1
{
Name "BUFFERFILL"
HLSLPROGRAM
#pragma multi_compile_instancing
#pragma multi_compile _ DOTS_INSTANCING_ON
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#pragma target 4.5
struct appdata
{
float4 positionOS : POSITION;
#if UNITY_ANY_INSTANCING_ENABLED
uint instanceID : INSTANCEID_SEMANTIC;
#endif
};
float4 vert (appdata i) : SV_POSITION
{
UNITY_SETUP_INSTANCE_ID(i);
float4 pos = TransformObjectToHClip(i.positionOS.xyz);
// 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;
}
ENDHLSL
}
Pass // 2
{
Name "JUMPFLOODINIT"
HLSLPROGRAM
#pragma multi_compile_instancing
#pragma multi_compile _ DOTS_INSTANCING_ON
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#pragma target 4.5
struct appdata
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
#if UNITY_ANY_INSTANCING_ENABLED
uint instanceID : INSTANCEID_SEMANTIC;
#endif
};
struct v2f
{
float4 positionCS : SV_POSITION;
#if UNITY_ANY_INSTANCING_ENABLED
uint instanceID : CUSTOM_INSTANCE_ID;
#endif
};
Texture2D _MainTex;
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_TexelSize;
half4 _OutlineColor;
float _OutlineWidth;
CBUFFER_END
v2f vert (appdata i)
{
UNITY_SETUP_INSTANCE_ID(i);
v2f o;
o.positionCS = TransformObjectToHClip(i.positionOS.xyz);
return o;
}
float2 frag (v2f i) : SV_TARGET
{
// integer pixel position
int2 uvInt = i.positionCS.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.positionCS.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.positionCS.xy + offset) * abs(_MainTex_TexelSize.xy) * FLOOD_ENCODE_SCALE - FLOOD_ENCODE_OFFSET;
}
ENDHLSL
}
Pass // 3
{
Name "JUMPFLOOD_SINGLEAXIS"
HLSLPROGRAM
#pragma multi_compile_instancing
#pragma multi_compile _ DOTS_INSTANCING_ON
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#pragma target 4.5
struct appdata
{
float4 positionOS : POSITION;
#if UNITY_ANY_INSTANCING_ENABLED
uint instanceID : INSTANCEID_SEMANTIC;
#endif
};
struct v2f
{
float4 positionCS : SV_POSITION;
#if UNITY_ANY_INSTANCING_ENABLED
uint instanceID : CUSTOM_INSTANCE_ID;
#endif
};
Texture2D _MainTex;
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_TexelSize;
half4 _OutlineColor;
float _OutlineWidth;
CBUFFER_END
int2 _AxisWidth;
v2f vert (appdata i)
{
UNITY_SETUP_INSTANCE_ID(i);
v2f o;
o.positionCS = TransformObjectToHClip(i.positionOS.xyz);
return o;
}
half2 frag (v2f i) : SV_TARGET {
// integer pixel position
int2 uvInt = int2(i.positionCS.xy);
// initialize best distance at infinity
float bestDist = 100000000;
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.positionCS.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 "OUTLINE"
Stencil {
Ref 1
ReadMask 1
WriteMask 1
Comp NotEqual
Pass Zero
Fail Zero
}
Blend SrcAlpha OneMinusSrcAlpha
HLSLPROGRAM
#pragma multi_compile_instancing
#pragma multi_compile _ DOTS_INSTANCING_ON
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#pragma target 4.5
struct appdata
{
float4 positionOS : POSITION;
#if UNITY_ANY_INSTANCING_ENABLED
uint instanceID : INSTANCEID_SEMANTIC;
#endif
};
struct v2f
{
float4 positionCS : SV_POSITION;
#if UNITY_ANY_INSTANCING_ENABLED
uint instanceID : CUSTOM_INSTANCE_ID;
#endif
};
Texture2D _MainTex;
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_TexelSize;
half4 _OutlineColor;
float _OutlineWidth;
CBUFFER_END
v2f vert (appdata i)
{
UNITY_SETUP_INSTANCE_ID(i);
v2f o;
o.positionCS = TransformObjectToHClip(i.positionOS.xyz);
return o;
}
half4 frag (v2f i) : SV_Target {
// integer pixel position
int2 uvInt = int2(i.positionCS.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.positionCS.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;
}
ENDHLSL
}
}
}
// Modified version of outline renderer feature by Alexander Ameye.
// https://alexanderameye.github.io/
// https://twitter.com/alexanderameye/status/1332286868222775298
using System;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
public class OutlineFeature : ScriptableRendererFeature
{
public Settings OutlineSettings;
private OutlinePass _outlinePass;
private StencilPass _stencilPass;
[Serializable]
public class Settings
{
[Header("Visual")]
[ColorUsage(true, true)]
public Color Color = new Color(0.2f, 0.4f, 1, 1f);
[Range(0.0f, 20.0f)]
public float Width = 5f;
[Header("Rendering")]
public LayerMask LayerMask = 0;
// TODO: Try this again when render layers are working with hybrid renderer.
// [Range(0, 32)]
// public int RenderLayer = 1;
public RenderPassEvent RenderPassEvent = RenderPassEvent.AfterRenderingTransparents;
public SortingCriteria SortingCriteria = SortingCriteria.CommonOpaque;
}
public override void Create()
{
if (OutlineSettings == null)
{
return;
}
_stencilPass = new StencilPass(OutlineSettings);
_outlinePass = new OutlinePass(OutlineSettings);
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
if (OutlineSettings == null)
{
return;
}
renderer.EnqueuePass(_stencilPass);
renderer.EnqueuePass(_outlinePass);
}
}
// Modified version of outline render pass by Alexander Ameye.
// https://alexanderameye.github.io/
// https://twitter.com/alexanderameye/status/1332286868222775298
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
public class OutlinePass : ScriptableRenderPass
{
private const string ProfilerTag = "Outline Pass";
private const string ShaderName = "Hidden/Outline";
private static readonly ShaderTagId _srpDefaultUnlit = new ShaderTagId("SRPDefaultUnlit");
private static readonly ShaderTagId _universalForward = new ShaderTagId("UniversalForward");
private static readonly ShaderTagId _lightweightForward = new ShaderTagId("LightweightForward");
private static readonly List<ShaderTagId> _shaderTags = new List<ShaderTagId>
{
_srpDefaultUnlit, _universalForward, _lightweightForward,
};
private static readonly int _silhouetteBufferID = Shader.PropertyToID("_SilhouetteBuffer");
private static readonly int _nearestPointID = Shader.PropertyToID("_NearestPoint");
private static readonly int _nearestPointPingPongID = Shader.PropertyToID("_NearestPointPingPong");
private static readonly int _axisWidthID = Shader.PropertyToID("_AxisWidth");
private static readonly int _outlineColorID = Shader.PropertyToID("_OutlineColor");
private static readonly int _outlineWidthID = Shader.PropertyToID("_OutlineWidth");
private readonly Material _bufferFillMaterial;
private readonly Material _outlineMaterial;
private readonly OutlineFeature.Settings _settings;
private RenderTargetIdentifier _cameraColor;
private FilteringSettings _filteringSettings;
public OutlinePass(OutlineFeature.Settings settings)
{
profilingSampler = new ProfilingSampler(ProfilerTag);
_settings = settings;
renderPassEvent = settings.RenderPassEvent;
// TODO: Try this again when render layers are working with hybrid renderer.
// uint renderingLayerMask = 1u << settings.RenderLayer - 1;
// _filteringSettings = new FilteringSettings(RenderQueueRange.all, settings.LayerMask.value, renderingLayerMask);
_filteringSettings = new FilteringSettings(RenderQueueRange.all, settings.LayerMask.value);
if (!_outlineMaterial)
{
_outlineMaterial = CoreUtils.CreateEngineMaterial(ShaderName);
}
_outlineMaterial.SetColor(_outlineColorID, settings.Color);
_outlineMaterial.SetFloat(_outlineWidthID, Mathf.Max(1f, settings.Width));
}
public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
{
RenderTextureDescriptor descriptor = renderingData.cameraData.cameraTargetDescriptor;
descriptor.graphicsFormat = GraphicsFormat.R8_UNorm;
descriptor.msaaSamples = 1;
descriptor.depthBufferBits = 0;
descriptor.sRGB = false;
descriptor.useMipMap = false;
descriptor.autoGenerateMips = false;
cmd.GetTemporaryRT(_silhouetteBufferID, descriptor, FilterMode.Point);
descriptor.graphicsFormat = GraphicsFormat.R16G16_SNorm;
cmd.GetTemporaryRT(_nearestPointID, descriptor, FilterMode.Point);
cmd.GetTemporaryRT(_nearestPointPingPongID, descriptor, FilterMode.Point);
ConfigureTarget(_silhouetteBufferID);
ConfigureClear(ClearFlag.Color, Color.clear);
_cameraColor = renderingData.cameraData.renderer.cameraColorTarget;
}
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
DrawingSettings drawingSettings = CreateDrawingSettings(
_shaderTags,
ref renderingData,
_settings.SortingCriteria
);
drawingSettings.overrideMaterial = _outlineMaterial;
drawingSettings.overrideMaterialPassIndex = 1;
int numMips = Mathf.CeilToInt(Mathf.Log(_settings.Width + 1.0f, 2f));
int jfaIterations = numMips - 1;
// TODO: Switch to this once mismatched markers bug is fixed.
// CommandBuffer cmd = CommandBufferPool.Get(ProfilerTag);
CommandBuffer cmd = CommandBufferPool.Get();
using (new ProfilingScope(cmd, profilingSampler))
{
context.ExecuteCommandBuffer(cmd);
cmd.Clear();
context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref _filteringSettings);
Blit(cmd, _silhouetteBufferID, _nearestPointID, _outlineMaterial, 2);
for (int i = jfaIterations; i >= 0; i--)
{
float stepWidth = Mathf.Pow(2, i) + 0.5f;
cmd.SetGlobalVector(_axisWidthID, new Vector2(stepWidth, 0f));
Blit(cmd, _nearestPointID, _nearestPointPingPongID, _outlineMaterial, 3);
cmd.SetGlobalVector(_axisWidthID, new Vector2(0f, stepWidth));
Blit(cmd, _nearestPointPingPongID, _nearestPointID, _outlineMaterial, 3);
}
cmd.Blit(_nearestPointID, _cameraColor, _outlineMaterial, 4);
}
context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);
}
public override void OnCameraCleanup(CommandBuffer cmd)
{
if (cmd == null)
{
throw new ArgumentNullException("cmd");
}
cmd.ReleaseTemporaryRT(_silhouetteBufferID);
cmd.ReleaseTemporaryRT(_nearestPointID);
cmd.ReleaseTemporaryRT(_nearestPointPingPongID);
}
}
// Modified version of stencil render pass by Alexander Ameye.
// https://alexanderameye.github.io/
// https://twitter.com/alexanderameye/status/1332286868222775298
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
public class StencilPass : ScriptableRenderPass
{
private const string ProfilerTag = "Stencil Pass";
private const string ShaderName = "Hidden/Outline";
private static readonly ShaderTagId _srpDefaultUnlit = new ShaderTagId("SRPDefaultUnlit");
private static readonly ShaderTagId _universalForward = new ShaderTagId("UniversalForward");
private static readonly ShaderTagId _lightweightForward = new ShaderTagId("LightweightForward");
private static readonly List<ShaderTagId> _shaderTags = new List<ShaderTagId>
{
_srpDefaultUnlit, _universalForward, _lightweightForward,
};
private readonly OutlineFeature.Settings _settings;
private readonly Material _stencilMaterial;
private FilteringSettings _filteringSettings;
public StencilPass(OutlineFeature.Settings settings)
{
profilingSampler = new ProfilingSampler(ProfilerTag);
_settings = settings;
renderPassEvent = settings.RenderPassEvent;
// TODO: Try this again when render layers are working with hybrid renderer.
// uint renderingLayerMask = 1u << settings.RenderLayer - 1;
// _filteringSettings = new FilteringSettings(RenderQueueRange.all, settings.LayerMask.value, renderingLayerMask);
_filteringSettings = new FilteringSettings(RenderQueueRange.all, settings.LayerMask.value);
if (!_stencilMaterial)
{
_stencilMaterial = CoreUtils.CreateEngineMaterial(ShaderName);
}
}
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
DrawingSettings drawingSettings = CreateDrawingSettings(
_shaderTags,
ref renderingData,
_settings.SortingCriteria
);
drawingSettings.overrideMaterial = _stencilMaterial;
drawingSettings.overrideMaterialPassIndex = 0;
// TODO: Switch to this once mismatched markers bug is fixed.
// CommandBuffer cmd = CommandBufferPool.Get(ProfilerTag);
CommandBuffer cmd = CommandBufferPool.Get();
using (new ProfilingScope(cmd, profilingSampler))
{
context.ExecuteCommandBuffer(cmd);
cmd.Clear();
context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref _filteringSettings);
}
context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);
}
}
@JokerDen
Copy link

Your asset is awesome! I hope it performance is not width related.

Is there any solution how to outline only visible part of objects? Render not only Layer Mask but check it overlapping.
image

@ScottJDaley
Copy link
Author

Hi @JokerDen!

Firstly, the performance of the shader IS dependent on the width. The thicker the outline, the more work the shader has to do every frame. Ben Golus explains this well in his article here: https://bgolus.medium.com/the-quest-for-very-wide-outlines-ba82ed442cd9

Regarding only rendering the visible parts of the outlines, this is quite a bit more complicated, but it is possible. (I actually added this to my project recently: https://twitter.com/GravitonPunch/status/1512085776271503360). Unfortunately, I don't have this code ready to share at this time and I'm still deciding on the best way to share it.

If you want to try to tackle this yourself, I would first read and try to understand the algorithm in the article above. To support occlusion, you'll need to know the nearest object edge corresponding to a given pixel in the outline. Once you have the position of the object edge, you can sample the depth texture using that position and compare that to the depth at the outline pixel position. Compare the two depths to see if the outline should be occluded or not.

Also, is your game 2D or 3D? If 2D, this might not be the best approach in general. I think there are much more performant and flexible options if you have a 2D game.

@jwindie
Copy link

jwindie commented May 13, 2022

image
image

The outline is solid when in my game view and builds but renders properly within the scene view. Any hints as to why this is/ remedies to fix it?

@ScottJDaley
Copy link
Author

Hey @jwindie, this is an issue that a few people have mentioned. I don't fully understand the problem, but it has something to do with the stencil getting cleared with certain configurations. There are a few different settings you can try. Firstly, if you don't need it, you can disable Depth Texture in the renderer settings. Alternatively, you can try enabling MSAA in the renderer settings. Finally, you can mess with the Render Pass Event in the outline settings. You might need to try a combination of these.

I've actually reworked how the outline stencils are being done in my own project that avoids this issue. If I have some time, I'll try to update this post with those changes.

@ScottJDaley
Copy link
Author

In case anyone is having trouble with these outlines in a build, you need to make sure the outline shader is being loaded. There are two ways that I know how to do this.

A) Add the "Outline" shader to the list of "Always Included Shaders" under Project Settings > Graphics > Build-in Shader Settings:
image

B) Create a Shader Variant Collection and include the "Outline" shader inside of it:
image

@makamekm
Copy link

Hi. Could you share how you fix that issue? It would be really great.

@ScottJDaley
Copy link
Author

Hi. Could you share how you fix that issue? It would be really great.

I'm not currently using this shader in my own project so its hard to test again, but based on my last reply, you could try messing with the depth texture and MSAA settings:

image

If I recall correctly, enabling MSAA or disabling the Depth Texture fixed the issue. Alternatively, you could try using a different render pass event in the outline settings.

I believe the issue is that the stencil values are getting cleared between the two render passes. In the first pass, the objects that are going to be outlined are rendered. Each rendered pixel writes a value to the stencil buffer. Then the second pass renders the objects again and generates the outline silhouette. At the end of the second pass, it cuts out all the pixels that had a stencil value set in the first pass. If the stencil buffer is cleared, it will fail to do so like in those screenshots.

The generation of the Depth Texture and MSAA both rely on the depth texture which is coupled with the stencil buffer. That's why toggling those settings can sometimes fix the issue.

It is possible to rewrite the shader in a way that does all of this in a single RenderPass (technically still multiple gpu passes, but guaranteed to run right after each other when packaged up in a single RenderPass). This ensures that the stencil values won't be cleared by something out of your control. Unfortunately, I haven't had time to update this shader to do this. It is also surprisingly difficult to make this work due to how the unity render pipelines handle the camera depth texture. Many things just don't work how they should.

@makamekm
Copy link

makamekm commented Sep 14, 2022

I figured it out.

  1. Remove descriptor.depthBufferBits = 0;
  2. Set ConfigureClear(ClearFlag.All, Color.clear);
  3. Then render mask to maskBuffer and set cmd.SetGlobalTexture(_maskTextureID, _maskBufferID);
  4. Change Outline shader to clip it for example:
half4 color = _MainMaskTex.Sample(sampler_MainMaskTex, i.uv);
if (color.r > 0.5) {
    return half4(0,0,0,0);
}

@vyach-vasilev
Copy link

@makamekm сould you share your solution?

@knordb
Copy link

knordb commented Mar 9, 2023

It is possible to rewrite the shader in a way that does all of this in a single RenderPass (technically still multiple gpu passes, but guaranteed to run right after each other when packaged up in a single RenderPass).

Would that be useful for performance as well?

@ScottJDaley
Copy link
Author

ScottJDaley commented Mar 9, 2023

It is possible to rewrite the shader in a way that does all of this in a single RenderPass (technically still multiple gpu passes, but guaranteed to run right after each other when packaged up in a single RenderPass).

Would that be useful for performance as well?

Not necessarily. The GPU would still be doing the same exact draw calls, but they would be set up and organized slightly differently. The terminology used by unity's render pipelines makes this quite confusing to talk about. A ScriptableRenderPass is not one-to-one with an draw call on the GPU. A fullscreen draw call is sometimes referred to as a render pass, but that is not the same thing as a ScriptableRenderPass. A single ScriptableRenderPass can execute many draw calls, and often does.

When I said you could do this all in a single ScriptableRenderPass, I just mean executing both the stencil and outline draw calls from the same place.

Technically, there might be some subtle differences in the CPU performance by reducing the number of ScriptableRenderPass in your render pipeline, but I think it would have negligible impact.

With that said, I'm not a graphics performance expert, so I might be totally wrong about all of this.

@PauPlayGo
Copy link

If I recall correctly, enabling MSAA or disabling the Depth Texture fixed the issue.

You recalled correctly, when I disabled the Depth Texture in my Universal Render Pipeline Asset the outline started to work correctly.

I can confirm also that the outline is working like a charm in Unity 2021.3.16f with URP 12.1.8 in builds of PC (Windows and Mac), Android and Iphone.

Really thanks @ScottJDaley for sharing your work, I'm learning a lot. I'll stay tuned in case you upgrade/improve your project. I'll give you my best feedback.

@keitzer
Copy link

keitzer commented Apr 24, 2023

@makamekm / @ScottJDaley -- curious to hear from you since I can't quite figure out what I'm doing wrong.
Using URP version 14.0.6
Unity Version 2022.2.10

Here is the image of 2 enemies (1 as a cube for testing, another as a Skeleton with animations etc)
image

Renderer Feature settings:
image

Added to Always Include shader list:
image

URP Settings with Depth Texture enabled and MSAA set to 8x:
image

Here you can see I try enabling the Renderer Feature but the entire object is being covered up (first screenshot shows a semi-transparent color -- 2nd screenshot shows a 1.0 alpha color):

Semi-Transparent alpha on Color Full Alpha on Color
image image

Interestingly, if i change the Render Pass Event to be After Transparents, the Canvas slider / image gets obfuscated too:
image

But no matter which setting i enable / disable, it doesn't seem to affect the Overlaying of the color over the entire object:

Depth On, 8x MSAA Depth Off, 8x MSAA
image image
Depth On, MSAA Disabled Depth Off, MSAA Disabled
image image

And lastly, even in Game mode (not just Scene view), the outline shader / feature seems to be overlaying color over the entire object:
image

@keitzer
Copy link

keitzer commented Apr 24, 2023

Also fwiw, i'm getting a decent amount of "deprecated" warnings in the OutlinePass.cs script:
image

@ArieLeo
Copy link

ArieLeo commented Jun 8, 2023

@keitzer I think because it's still using the old render feature API, not the new RTHandle API.

@keitzer
Copy link

keitzer commented Jun 14, 2023

@ArieLeo
image

possibly -- but even with the deprecated warnings i was able to figure out a workaround / possible solution to this

i have a Renderer Feature for each Player that would have their own highlight -- followed by a Renderer Feature which renders the same object as Opaque.

It even works with the object being obstructed which is nice:
image

@keitzer
Copy link

keitzer commented Jun 14, 2023

Basically using this as a targeting system to show which "enemy" or object you're aiming at (or in the case of multiple players aiming at the same target -- very similar to what Minecraft Dungeons does)

@keitzer
Copy link

keitzer commented Jun 14, 2023

image

There's still the issue of overlapping draw calls on these objects which actually removes the "outline" since all outlines happen behind these objects -- otherwise i think it looks great and is easy to change the Layer of the objects and the highlight simply applies with basically no change in Framerate

@lolisbest
Copy link

유니티 URP 2021.3.19f1 에서
외곽선이 오브젝트를 뒤덮는 현상을 해결했습니다.

저의 경우, Renderer Asset에서 Depth Priming Mode를 Disabled로 변경하니 올바르게 동작했습니다.

image

@lanmac03
Copy link

lanmac03 commented Nov 6, 2023

unfortunately, doesn't work. Is @keitzer's solution the recommended one? I get the same results, silhouette won't render below the model. I think I'll try and write an updated one when I have more time for a deep dive.

@lolisbest
Copy link

unfortunately, doesn't work. Is @keitzer's solution the recommended one? I get the same results, silhouette won't render below the model. I think I'll try and write an updated one when I have more time for a deep dive.

Render Type이 Overlay인 카메라를 추가한 이후로, 저도 다시 동일한 문제가 발생했습니다.

@keitzer
Copy link

keitzer commented Nov 6, 2023

Yeah if there's a better way that might increase FPS i'm all for it

But as of right now i got pretty high FPS on a macbook air using this method so i'm okay with it for now.

@babon
Copy link

babon commented Mar 8, 2024

Okay so I think i figured it out, for anyone following in my footsteps of trying to make a Renderer Feature read stencil and it bugging out:
So, sometimes, renderingData.cameraData.renderer.cameraColorTarget has both the depth (depthStencilFormat = D32_SFloat_S8_UInt) and color (graphicsFormat = B10G11R11_UFloatPack32) in one. Presumably cameraDepthTarget is unused.

But then sometimes URP goes and splits them into two textures, cameraColorTarget has color but (depthStencilFormat = None), and renderingData.cameraData.renderer.cameraDepthTarget has (depthStencilFormat = D32_SFloat_S8_UInt)
Screenshot 2024-03-08 at 04 01 55
example of this split mode being active

And here's the kicker: When this split is active (and god knows what can activate it, look into urp source code), calling CommandBuffer.SetRenderTarget(destination) / ConfigureTarget() / Blit() with the overload of only one RenderTargetIdentifier will leave the depth texture unbound, as you're binding only the color.

So solution: Store renderingData.cameraData.renderer.cameraDepthTarget into variable, then every time you call SetRenderTarget() and such call the overload with depth: cmd.SetRenderTarget(destination, rememberedDepthStencilVar);

@keitzer
Copy link

keitzer commented Mar 8, 2024

Interesting find @babon -- do you have an example of the modified shader code from OP that can make this work?

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