Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Join us on Thursday, June 8, for a Q&A with Unity's Content Pipeline group here on the forum, and on the Unity Discord, and discuss topics around Content Build, Import Workflows, Asset Database, and Addressables!
    Dismiss Notice

Official Outline Effect

Discussion in 'Open Projects' started by MrFlyingChip, Sep 30, 2020.

  1. cirocontinisio


    Unity Technologies

    Jun 20, 2016
    This is not a choice for some though. You can do this if you're on a retina or 4K screen. We should always make sure that the outline works also (and mainly, actually) on 1080p screens.

    In fact, the whole point with the task (at least the wish I have) is that the outline looks the same width at both 1080p and 4k resolutions. Not the same effect, of course the 4k is always going to be more crisp, but the same relative width (as if its size was measured in world space units).
  2. Neonage


    May 22, 2020
    Is it possible to support MSAA with this post-process outline approach?
    Or maybe apply some local AA based off normals?
  3. daneobyrd


    Mar 29, 2018
    Ah okay, I mentioned it because anecdotally I've had situations in other projects where the game view (in editor) is more pixelated than how the build looks on a 1080p screen (I likely just didn't understand some settings back then).

    I just set my game view to 1920x1080 and it looks good!
    Last edited: Dec 12, 2020
    cirocontinisio likes this.
  4. daneobyrd


    Mar 29, 2018
    I pulled this into my local project and saw that the outline will remain visible as long as Outline Thickness is non-zero, regardless of whatever value any of the other properties have.
  5. treivize


    Jan 27, 2016
    @daneobyrd, actually only the border outline is always drawn after my changes, because they are now always correct thanks to the outlined object masking. My understanding of these sensitivity parameters are only here to find a good compromise to make the outlines looking good enough for each object not to be able to remove it completely. So now they control only the inner outlines.

    For the reduction of the outline thickness based on the distance from the camera, I think we can inject the distance in the algo at different places to force the outline to become thinner and thinner.
    daneobyrd likes this.
  6. cirocontinisio


    Unity Technologies

    Jun 20, 2016
    I've done some tests on a 1080p monitor.
    I really like the ideas in this rework, but I think that in situations of lower resolution it ends up creating more aliasing - actually I should say dither.

    This is the current solution:



    In the second screenshot I zoomed in 5x to make the issues very evident. You can see how things are aliased and all the problems we talked about, but it doesn't look that bad.

    This is the PR with the new proposed solution:



    You can see how the algorighm to detect the outlines produces this x-shaped dither, which, when going against very thin lines (see headband) create a lot of noise.
    This is zoomed 4x, but even when zoomed out I'd argue is quite visible.

    So personally I think the old one still looks better?
    The only big disadvantage is that is resolution-dependent, and that I'd really like to fix.
    daneobyrd likes this.
  7. daneobyrd


    Mar 29, 2018
    Ah okay, I thought that the post-thickness reduction outline would then be affected by these parameters.
  8. treivize


    Jan 27, 2016
    Let me have a look at this problem, because in this new version of the current algo, the inner outline detection is almost the same as the existing one (except the part related to the thresholding based on the view direction), maybe there is an error somewhere.
  9. daneobyrd


    Mar 29, 2018
    Here's a couple samples of what I see at 1920x1080 at 1x scale:

    Amount of aliasing at closest possible camera position during gameplay:

    Close-up (I paused and manually moved the camera)

    and at 4x scale:
    Amount of aliasing when using the game view scaling slider:

    I think the new approach can be tweaked to avoid this dithering. It looks like this is happening because of the Robert’s Cross sampling being used. I think the new approach is better but the sampling method needs iteration.

    1. Combine the Depth and Normals textures into a DepthNormals texture
    2. In place of a robert's cross, use the sobel sampling method, using two convolution matrices, one for x and one for y.
    3. Using the sampled data and Harry H's method, determine which pixels can receive an outline, and set the initial thickness for inter-object outlines and inside lines.
    4. Modify these values using our current material parameters

    Here are a few references on thick and thin lines in an edge detection shader.

    Harry Alisavakis uses the algorithm listed below from William Chyr's blog (Manifold Garden), initially based on an edge detection method Lucas Pope wrote about in a DevLog for Return of the Obra Dinn:
    1. Harry Alisaviks – My take on shaders: Edge Detection
    2. William Chyr – Edge Detection Shader Deep Dive! Part 1 – Even or Thinner Edges?
      – Older DevLog William links to on edge detection
    3. Lucas Pope Edge Detection DevLog
    The new approach in @treivize's PR is close to the second algorithm in William's blog:

    – sample surrounding pixels
    – combine depth and normal values to form a color
    – compare the values of the new combined color in surrounding pixels (if values are close, it’s not an edge, else it is)

    I suggest we use this algorithm to set a variable outline (as the current PR does). However, in place of a Robert's Cross or the single sampling method Harry Alisavakis uses, we use two sobel convolution matrices as NedMakesGames does (and was previously mentioned in the main ToonShader thread).

    Using two matrices, one for the x component and one for the y component, should eliminate this dithering. We use the sampled information to decide which pixels receive an outline and set the initial outline thickness for each pixel.

    This utilizes Harry H's method I mentioned earlier in this thread:
    • Use R channel to store a float for each object, determine inter-object lines (outlines).
    • Use G channel for inside lines.
    • Use B to control transparency/harshness
      • He input noise into this channel to create some animation/flipbook effect
    Comparing each channel with the DepthNormals texture will result in the best edge detection. This initial thickness will then be affected by the parameters we have in the inspector/shader graph blackboard.
    Last edited: Dec 12, 2020
  10. treivize


    Jan 27, 2016
    I was thinking the same, and on the road to move for Sobel in my code :) Let's see if it brings nicer outlining
    daneobyrd likes this.
  11. daneobyrd


    Mar 29, 2018
    Sorry that I keep editing the post to add more info! Haha
  12. daneobyrd


    Mar 29, 2018
    In reference to anti-aliasing, I did see a unique approach to this when helping someone in the Unity Discord on their topographic-map contour line shader. I rebuilt and modified this shader forge shader. It was fairly easy to implement here since the only input was World Position Y. I'm not sure if this could be used for the outline shader but I thought I would share it just in case.

    Use a fraction node for both sides of the outline. Smoothstep with DDXY and the line's position. Multiply both results together.

    Anti-aliasing method:
    Last edited: Dec 12, 2020
  13. treivize


    Jan 27, 2016
    @cirocontinisio and @daneobyrd, I have just pushed the algo bases on Sobel edge detection instead of Roberts cross.
    If you can have a look again and let me know if it looks better now than before.

    For antialiasing like you are proposing Dane, I have to digest it, I am not understanding 100% of it yet and it is too late for me now :) let's see that tomorrow!
    cirocontinisio likes this.
  14. daneobyrd


    Mar 29, 2018
    Haha no worries! I thought I would share the method to get the conversation started
  15. daneobyrd


    Mar 29, 2018
    I've done some testing and overall I would say this is a step forward. Using the sobel algorithm has decreased dithering and also has resulted in a nicer looking outline in my opinion.

    The sharp edges where on the border outline ends also needs to be addressed. I’m actually confused as to why these sharp border outlines are not subject to the properties in the inspector. This is more prominent as you increase the outline thickness (see video thumbnail).

    I think the next step is to tweak and adjust how we outline different objects as well as treating inter-object lines (outlines) differently than inside lines.

    Currently the Lantern and the headband both interrupt the outline drawing.

    I'm pretty sure the first issue is related to what layer the lantern and it's child objects are set to. The second issue is related to how characters have been modeled. The headband on pig chef and the townfolk are currently part of the character models. The same is true for Townsfolk_M's necklace. They are separate meshes but have been exported as a single model.

    To me it makes sense to update the characters by removing the headband and other accessories from the main model, and place them in an accessory slot? Maybe I haven't grasped the project's layer setup yet.

    @cirocontinisio What do you think?

    I increased outline thickness right before I took this video. At around 0:20 I set the thickness closer to it's original value.

    Last edited: Dec 14, 2020
  16. treivize


    Jan 27, 2016
    An update on this topic to explain what has been added to the PR.

    First I addressed also the other problems raised about outine:
    1. Resolution dependency: A simple proposition is to bias the outline thickness based on _ScreenParams.y as folloing in HSLS code:
    Code (CSharp):
    1.   float screenBiasedTickness = OutlineThickness * _ScreenParams.y / 1080;
    So the thickness is defined for FullHD resolution and it will scale depending on the actual screen resolution.
    2. Distance outline fading: again the Thickness is biased based on the distance between the object and the camera.

    The idea is that the thickness width is reduced by one every 10 distance unit from the camera.

    Finally regarding the outline algo itself, an update from the previous one, depth data has been added in the outline mask render pass, here is the outline mask shader.
    The idea is to draw only border outline in the object which is the closest to the camera in the algo instead of drawing it on both object with half of the thickness, first the algo is quicker (based on the first cross sampling pass) and it removes the small outline displacement when object overlaps.

    Everything is in the PR. Just only Pig Chef material has been tuned properly, so other outlined object materials have to be readjusted to display nice outline based on their own constrains.

    Another thing that seems to improve the outline rendering is render scale, instead of changing MSAA, it seems that increasing scale from 1 to 1.25 is improving significantly the quality of the outline for an equivalent framerate cost
    Last edited: Dec 16, 2020
  17. daneobyrd


    Mar 29, 2018
    For anti-aliasing, why don't we try out the technique Freya Holmér uses for Shapes, her Unity Vector Graphics Library?


    Last edited: Dec 20, 2020
  18. cirocontinisio


    Unity Technologies

    Jun 20, 2016
    Yeah... but that has an explosive effect on the cost of rendering, 1.25 doesn't just mean
    Width x Height x 1.25
    , but
    Width x 1.25 x Height x 1.25
    the pixels! :confused:
    So I'd like to avoid that.
  19. treivize


    Jan 27, 2016
    @cirocontinisio, definitely it is quite costly. I tried to find a way to apply a render scale only for render pass to improve the quality of the depth normal texture before applying the outline algo, but I did not find how to do that in ScriptableRenderFeature / CommandBuffer

    @daneobyrd, again an interesting technique, I am not sure in which part we should use it, I do not know how to use it to improve globally the outline when become aliased /dither because the algo is not really drawing any line but black dot one by one and magically draw lines. Next it could be used to draw outline during the fading effect when the object start to be far, to be tested, I have the feeling that the thinner line effect based on light grey works in contrast of dark line, but if all the lines are light grey I am not sure it will work well. Again maybe to be tested.
    daneobyrd likes this.
  20. cirocontinisio


    Unity Technologies

    Jun 20, 2016
    I'm still not convinced about the improvements...

    I did some quick test with the latest of @treivize 's commits (the merge, 15a9d55) and I can still see some decent "wandering ants" effect like Freya calls it in the gif above.

    Although I don't think it's worse, it's also not improved much...
    Here's a comparison of the before solution, and the after (the PR):

    Also, the outline keeps being doubled when the geometry overlaps with itself. You can see it here in the headband, hand and overall sleeve to the right:

  21. daneobyrd


    Mar 29, 2018
    I've been busy and not able to contribute lately, so to get back into things I thought I'd try and explain my understanding of the current outline improvements. @treivize please correct any misunderstandings I have below:

    Edit: I thought this was the case after looking at the outline's behavior with the model's hands. However I noticed this outline doubling on the ear and the jaw, with both depth and normal outlines.

    As long as Outline Thickness is non-zero, the new DepthNormal outline remains visible. Even if the new Depth Normal properties, Depth Sensitivity, and Normal Sensitivity are all set to zero.​

    @cirocontinisio ̶t̶h̶e̶ ̶c̶u̶r̶r̶e̶n̶t̶ ̶o̶u̶t̶l̶i̶n̶e̶ ̶d̶o̶u̶b̶l̶i̶n̶g̶ ̶y̶o̶u̶ ̶s̶e̶e̶ ̶n̶o̶w̶ ̶i̶s̶ ̶a̶ ̶r̶e̶s̶u̶l̶t̶ ̶o̶f̶ ̶t̶w̶o̶ ̶p̶a̶s̶s̶e̶s̶,̶ ̶e̶a̶c̶h̶ ̶d̶r̶a̶w̶i̶n̶g̶ ̶a̶n̶ ̶o̶u̶t̶l̶i̶n̶e̶ ̶o̶n̶c̶e̶ ̶r̶a̶t̶h̶e̶r̶ ̶t̶h̶a̶n̶ ̶a̶ ̶s̶i̶n̶g̶l̶e̶ ̶p̶a̶s̶s̶ ̶d̶r̶a̶w̶i̶n̶g̶ ̶t̶h̶e̶ ̶o̶u̶t̶l̶i̶n̶e̶ ̶t̶w̶i̶c̶e̶.̶

    As far as I know, the depth-based outline has been updated to use the sobel algorithm as well (replacing the robert's cross sampling algorithm).

    The original issue we aimed to solve was that the depth-based outline was drawn twice when the geometry overlapped itself as the algorithm did not determine whether the background geometry was a part of the same object. N̶o̶w̶ ̶b̶y̶ ̶s̶t̶o̶r̶i̶n̶g̶ ̶a̶ ̶f̶l̶o̶a̶t̶ ̶f̶o̶r̶ ̶e̶a̶c̶h̶ ̶o̶b̶j̶e̶c̶t̶,̶ ̶t̶h̶e̶ ̶d̶e̶p̶t̶h̶-̶b̶a̶s̶e̶d̶ ̶o̶u̶t̶l̶i̶n̶e̶ ̶i̶s̶ ̶o̶n̶l̶y̶ ̶d̶r̶a̶w̶n̶ ̶o̶n̶c̶e̶.̶​

    N̶o̶w̶ ̶w̶h̶e̶n̶ ̶t̶h̶e̶ ̶g̶e̶o̶m̶e̶t̶r̶y̶ ̶o̶v̶e̶r̶l̶a̶p̶s̶ ̶w̶i̶t̶h̶ ̶i̶t̶s̶e̶l̶f̶,̶ ̶t̶h̶e̶ ̶d̶o̶u̶b̶l̶i̶n̶g̶ ̶w̶e̶ ̶s̶e̶e̶ ̶i̶s̶ ̶t̶h̶e̶ ̶o̶v̶e̶r̶l̶a̶p̶ ̶o̶f̶ ̶t̶h̶e̶ ̶s̶o̶b̶e̶l̶ ̶o̶u̶t̶l̶i̶n̶e̶ ̶a̶n̶d̶ ̶o̶u̶r̶ ̶o̶r̶i̶g̶i̶n̶a̶l̶ ̶a̶p̶p̶r̶o̶a̶c̶h̶.̶
    Edit: This is the cause of some outline doubling at the edges of the geometry but I'm not sure if this is happening when geometry overlap. I haven't yet looked at turning off the DepthNormal outline to test this.

    Lastly, the new outline used at the geometry's edge terminates sharply. This results in some abrupt changes in outline thickness.

    I had a few ideas on possible next steps:
    A. Consolidate the new DepthNormal outline with the original depth-based outline to avoid doubling and simplify the shader
    B. Make the DepthNormal outline continuous
    C. Remove doubling from the normal-based outline
    D. Add a color or alpha transition as lines get smaller, to eliminate or mitigate any "wandering ants."
    Last edited: Jan 25, 2021
  22. treivize


    Jan 27, 2016
    I have been silent for a while, because I was trying to figure out how to improve the outline because I am also not satisfied by the result of my last proposal...

    And I am reaching a point where I would challenge the usage of the Depth/Normal approach, because it seems really a nightmare to make it works at different scale of details in a complex model, at different distance from the camera, at different screen resolution, etc...

    I had a look at this paper from Pixar ( which was mentioned in Roystan's article conclusion. The approach seems to provide more control over the outline process in exchange of manual work for each model we want to outline (outline thickness texture and vertex color assignment to define regions)

    How I see the implementation:
    - Keep the computation of the contour based on a render pass encoding the object distance from the camera in the Red channel then apply a Roberts' cross/Sobel edge detection to draw the contour
    - Encode in Vertex Color blue channel the regions of the model that should generate outline between each other (ie. head, ears, torso, arms, legs, bandana, inner arm shirt).
    - Draw a greyscale texture that will control the outline thickness (ie fading outline between ears and head where surface is smooth)
    - Read the vertex blue channel value in outline process, apply again a Roberts' cross/Sobel algo to determine frontiers and modulate the thickness based on the value of the thickness texture at this pixel.

    I will work on a showcase of this approach (if I manage to set the vertex color of the pig without breaking the animations in the FBX files...)

    What is your feeling about this approach?

    NOTE: I give a try (breaking animation so with Pig in "Rest Pose") and the result looks like that:
    Last edited: Jan 13, 2021
    Smurjo and daneobyrd like this.
  23. daneobyrd


    Mar 29, 2018
    I agree. As I experimented with your last proposal I found it difficult to get consistent results for detailed areas of the geometry like the chef's hands. I definitely prefer more control over the outline process in exchange for more manual work. Having greater control over line thickness will bring us closer to the line-work shown in the concept art.

    I had come across that paper mentioned in Roy's article before but I hadn't looked into it much. The approach reminds me of some of what Harry Heath's approach that I shared in previous forum post.

    This new approach will help with outlining props and accessories as well. Previously there has been odd outline behavior between the chef and props like the lantern on his back.
  24. cirocontinisio


    Unity Technologies

    Jun 20, 2016
    It looks ok, but the lines now are not super dynamic. For instance, along the line where the apron connects to the shirt, there's always a black line regardless of how he turns. I think this way it loses some nuance. I like that some black lines form when surfaces fold and bend (because the normal difference becomes more accentuated).

    I tried your latest PR, and I thought the AA looks good, except for the broken lines and other issues noted by @daneobyrd.

    I was thinking: personally, I don't care too much about the double lines. What if we just port the AA to the original solution, and keep the outline like that?
  25. treivize


    Jan 27, 2016
    Yeah, I was also thinking the same actually :) I would keep AA, screen size adjustment, and distance outline fading the part in shader graph actually. I can push that into a new PR, you are fine with the listed features included.
    cirocontinisio likes this.
  26. treivize


    Jan 27, 2016
    Here is the PR without touching at the inner outline algo:

    A pure shader graph update:

    Distance fading:
    Outline Thickness is reduced based on the distance of an outlined object from the camera. Reduced by 1 every 10 world dimension unit

    Screen Resolution adjustment:

    Outline thickness is scaled up or down based on the screen height size. The material Outline Thickness property will be the one for a HD resolution (1080px height).

    Antialiasing algorithm:

    Based on Harry solution shared in Discord
    The main idea is to run the outline function twice with different thickness value and to lerp both outputs.

    NOTE: The materials of outlined object might have to be adjusted following this implementation if they were not defined with the targeted HD resolution.
    daneobyrd likes this.
  27. treivize


    Jan 27, 2016
    cirocontinisio likes this.
  28. cirocontinisio


    Unity Technologies

    Jun 20, 2016
    No, you're right, we can probably mark it as done, finally! :D
    vx4 and treivize like this.
  29. TimeStrafer


    Oct 12, 2012
    How well does Harry's solution to anti-aliasing work?

    What about some kind of fast median filter or Scale2x algorithm? Game emulators use various pixel art upscaling algorithms that might be useful in this case?

    I've seen Scale2x for Unity on the forums somewhere.
  30. daneobyrd


    Mar 29, 2018
    I have been working on custom color buffer outlines for a little bit (Harry's method) and there are many many ways to implement anti-aliasing. Once I have something up on github I will post here about it.
    TimeStrafer likes this.
  31. dexhort


    Dec 18, 2021
    Hi, sorry to pop on this thread out of no where, I have read all previous message and I'm looking for answers to my problems.

    I have, like you, made an outline render that works well (I don't search for a realy precise work, it's just for a selection outline) based on articles you previously posted here :

    But I have a problem, as you can see here, my outline is applied on every single mesh on my scene, but I tried to make it applied only to object in a outlineLayer.

    Someone have a solution ? I have add a layermask parameter in the DepthNormalsRenderPass, cause the only option I have seen for adding it was into the DrawRenderers.

    Also it seems like my Blit() is applied to all my screen and not only a part of it.

    here is the code for the Depth Normals :
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Rendering.Universal;
    3. using UnityEngine.Rendering;
    4. using System.Collections;
    5. using System.Collections.Generic;
    7. public class DepthNormalsFeature : ScriptableRendererFeature
    8. {
    9.     class DepthNormalsPass : ScriptableRenderPass
    10.     {
    11.         int kDepthBufferBits = 32;
    12.         private RenderTargetHandle depthAttachmentHandle {get;set;}
    13.         internal RenderTextureDescriptor descriptor {get; private set;} //
    15.         private Material depthNormalsMaterial = null;
    16.         private FilteringSettings filteringSettings; //Structure décrivant comment filtrer les objet lors d'un DrawRenderers.
    17.         string profilerTag = "DepthNormals Prepass";
    18.         private List<ShaderTagId> shaderTagId = new List<ShaderTagId> {
    19.             new ShaderTagId("UniversalForward"),
    20.             new ShaderTagId("UniversalForwardOnly"),
    21.             new ShaderTagId("LightweightForward"),
    22.             new ShaderTagId("SRPDefaultUnlit"),
    23.             new ShaderTagId("DepthOnly")
    24.         };
    27.         //Constructeur, initialise le filteringSettings et le matériaux pour override texture
    28.         public DepthNormalsPass(RenderQueueRange renderQueueRange, LayerMask layerMask,Material material)
    29.         {
    30.             int mask = 1 << layerMask.value;
    31.             filteringSettings = new FilteringSettings(renderQueueRange, mask);
    32.             depthNormalsMaterial = material;
    33.         }
    35.         public void Setup(RenderTextureDescriptor baseDescriptor, RenderTargetHandle depthAttachmentHandle)
    36.         {
    37.             this.depthAttachmentHandle = depthAttachmentHandle;
    38.             baseDescriptor.colorFormat = RenderTextureFormat.ARGB32;
    39.             baseDescriptor.depthBufferBits = kDepthBufferBits;
    40.             descriptor = baseDescriptor;
    41.         }
    44.         //Appeler avant l'exectution de la renderpass
    45.         //Jamais utiliser CommandBuffer.SetRenderTarget, plutôt utiliser ConfigureTarget et ConfigureClear
    46.         //permet d'assurer un fonctionnement opti performance
    47.         public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
    48.         {
    49.             cmd.GetTemporaryRT(,descriptor,FilterMode.Point);
    50.             ConfigureTarget(depthAttachmentHandle.Identifier());
    51.             ConfigureClear(ClearFlag.All,; //Configure comment le pipeline clear les partit de l'image non retenu par le filtre
    52.         }
    54.         //Ici s'inscript la logique de rendu
    55.         //ScriptableRenderContext permet de réaliser le rendu ou executer les commandes du buffer
    56.         //
    57.         //pas besoin de faire ScriptableRenderContext.submit(), il se fait automatiquement durant le pipeline
    58.         public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    59.         {
    60.             CommandBuffer cmd = CommandBufferPool.Get(profilerTag);
    62.             using (new ProfilingScope(cmd, new ProfilingSampler(profilerTag))) //
    63.             {
    64.                 context.ExecuteCommandBuffer(cmd);
    65.                 cmd.Clear();
    67.                 var sortFlags = renderingData.cameraData.defaultOpaqueSortFlags;
    68.                 var drawSettings = CreateDrawingSettings(shaderTagId, ref renderingData, sortFlags); //
    69.                 drawSettings.perObjectData = PerObjectData.None;
    71.                 ref CameraData cameraData = ref renderingData.cameraData;
    72.                 Camera camera =;
    73.                 if(XRGraphics.enabled)  //CameraData.isStereoEnabled est obsolete
    74.                     context.StartMultiEye(camera);
    76.                 drawSettings.overrideMaterial = depthNormalsMaterial;
    78.                 context.DrawRenderers(renderingData.cullResults, ref drawSettings, ref filteringSettings);
    80.                 cmd.SetGlobalTexture("_CameraDepthNormalsTexture",;
    81.             }
    83.             context.ExecuteCommandBuffer(cmd);
    84.             CommandBufferPool.Release(cmd);
    85.         }
    87.         // Permet de netoyer les ressources allouées pendant la création de la renderpass
    88.         public override void FrameCleanup(CommandBuffer cmd)
    89.         {
    90.             if(depthAttachmentHandle != RenderTargetHandle.CameraTarget)
    91.             {
    92.                 cmd.ReleaseTemporaryRT(;
    93.                 depthAttachmentHandle = RenderTargetHandle.CameraTarget;
    94.             }
    95.         }
    96.     }
    98.     DepthNormalsPass depthNormalsPass;
    99.     RenderTargetHandle depthNormalsTexture;
    100.     Material depthNormalsMaterial;
    101.     [SerializeField] private LayerMask layerMask;
    103.     public override void Create()
    104.     {
    105.         depthNormalsMaterial = CoreUtils.CreateEngineMaterial("Hidden/Internal-DepthNormalsTexture");
    106.         depthNormalsPass = new DepthNormalsPass(RenderQueueRange.opaque, layerMask, depthNormalsMaterial);
    107.         depthNormalsPass.renderPassEvent = RenderPassEvent.AfterRenderingPrePasses;
    108.         depthNormalsTexture.Init("_CameraDepthNormalsTexture");
    109.     }
    111.     //Permet d'injecter des renderpass dans le pipeling
    112.     //Cette méthode est appeler une fois par camera au moment du setup du renderer
    113.     public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    114.     {
    115.         depthNormalsPass.Setup(renderingData.cameraData.cameraTargetDescriptor, depthNormalsTexture);
    116.         renderer.EnqueuePass(depthNormalsPass);
    117.     }
    118. }
    and here for the Outline Features :
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Rendering;
    3. using UnityEngine.Rendering.Universal;
    5. public class OutlineFeatures : ScriptableRendererFeature
    6. {
    7.     class OutlinePass : ScriptableRenderPass
    8.     {
    9.         private RenderTargetIdentifier source {get; set;}
    10.         private RenderTargetHandle destination {get; set;}
    11.         public Material outlineMaterial = null;
    12.         RenderTargetHandle temporaryColorTexture;
    14.         public void Setup(RenderTargetIdentifier source, RenderTargetHandle destination)
    15.         {
    16.             this.source = source;
    17.             this.destination = destination;
    18.         }
    20.         public OutlinePass(Material outlineMaterial)
    21.         {
    22.             this.outlineMaterial = outlineMaterial;
    23.         }
    25.         public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
    26.         {
    27.             cmd.GetTemporaryRT(,cameraTextureDescriptor,FilterMode.Point);
    28.             ConfigureTarget(destination.Identifier());
    29.             ConfigureClear(ClearFlag.All,;
    30.         }
    32.         public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    33.         {
    34.             CommandBuffer cmd = CommandBufferPool.Get("_OutlinePass");
    36.             RenderTextureDescriptor opaqueDescriptor = renderingData.cameraData.cameraTargetDescriptor;
    37.             opaqueDescriptor.depthBufferBits = 0;
    39.             using (new ProfilingScope(cmd, new ProfilingSampler("_OutlinePass")))
    40.             {
    41.                 if (destination == RenderTargetHandle.CameraTarget)
    42.                 {
    43.                     cmd.GetTemporaryRT(, opaqueDescriptor, FilterMode.Point);
    44.                     Blit(cmd,source,temporaryColorTexture.Identifier(),outlineMaterial,0);
    45.                     Blit(cmd,temporaryColorTexture.Identifier(),source);
    46.                 }
    47.                 else {
    48.                     Blit(cmd,source,destination.Identifier(),outlineMaterial,0);
    49.                 }
    50.             }
    52.             context.ExecuteCommandBuffer(cmd);
    53.             CommandBufferPool.Release(cmd);
    54.         }
    56.         public override void FrameCleanup(CommandBuffer cmd)
    57.         {
    58.             if(destination == RenderTargetHandle.CameraTarget)
    59.                 cmd.ReleaseTemporaryRT(;
    60.         }  
    61.     }
    63.     [System.Serializable]
    64.     public class OutlineSettings
    65.     {
    66.         public Material outlineMaterial = null;
    67.     }
    68.     public OutlineSettings settings = new OutlineSettings();
    69.     OutlinePass outlinePass;
    70.     RenderTargetHandle outlineTexture;
    72.     public override void Create()
    73.     {
    74.         outlinePass = new OutlinePass(settings.outlineMaterial);
    75.         outlinePass.renderPassEvent = RenderPassEvent.AfterRenderingTransparents;
    76.         outlineTexture.Init("_OutlineTexture");
    77.     }
    79.     public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    80.     {
    81.         if(settings.outlineMaterial == null)
    82.         {
    83.             Debug.LogWarningFormat("Missing Outline Material");
    84.             return;
    85.         }
    86.         outlinePass.Setup(renderer.cameraColorTarget, RenderTargetHandle.CameraTarget);
    87.         renderer.EnqueuePass(outlinePass);
    88.     }
    89. }
    my Outline.hlsl is a pure copy of the alexander project.

    Thank you if you have some kind of ideas (I'm new in this type of things), I have tried some things but it didn't work either.
  32. daneobyrd


    Mar 29, 2018
    I assume the reason your layerMask isn’t working as expected is because you are using a bit shift when you can simply cast LayerMask to int. Therefore, the int value of mask used by your filteringSettings is incorrect.

    For even greater control over the filtering you can enable light layers and use them for FilteringSettings.renderingLayerMask. You can use LightLayerEnum in your settings class just like LayerMask, and don’t forget to cast it to int.

    Additional resource, you should read Alexander’s subsequent article 5 ways to draw an outline.

    For an example of the custom discontinuity source Alexander mentions in the section on edge detection, see these pastebin links of Harry Heath’s render passes and renderer feature.

    I would go into more detail but I am currently away from my computer so I will stop here.
    Last edited: Jun 23, 2022
  33. dexhort


    Dec 18, 2021
    Thank you ! I go read this right now.
    daneobyrd likes this.
  34. daneobyrd


    Mar 29, 2018
    I said this in October and since then I’ve gotten way more ambitious with this outline repo.

    I will likely expand it to provide examples of all the methods listed in Alexander Ameye’s “5 ways to create an outline” as well as include a compute shader and fragment shader version.

    Sorry for the silence on this!
  35. dexhort


    Dec 18, 2021
    Hello, I don't know if it's the right place to ask but :

    In this article linked in "5 ways to create an outline" for the JFA method, I understand that HDRP can ask a custom Renderpass from a monobehavior.

    Is it possible to do the same in URP or it's only possible through a scriptableRendererFeature ?

    I have tried to transform the code from HDRP to URP but I have only negativ results (no image or completely white meshes).

    I have tried to replace the rendererDraw phase by a DrawRenderers with a layermask that will restrict the gameobjects drawn.

    But when I try to aply the JFA on this black and white renderpass, I have negativ results.

    Sorry if it's hard to read, I haven't put comment yet and don't have time right now.

    Code (CSharp):
    1. using System.Collections.Generic;
    2. using System.Linq;
    3. using System;
    4. using UnityEngine;
    5. using UnityEngine.Rendering;
    6. using UnityEngine.Rendering.Universal;
    7. using UnityEngine.Experimental.Rendering;
    9. public class JumpFloodRenderFeatures : ScriptableRendererFeature
    10. {
    12.     [Serializable]class RenderSettings
    13.     {
    14.         const int SHADER_PASS_INTERIOR_STENCIL = 0;
    15.         const int SHADER_PASS_SILHOUETTE_BUFFER_FILL = 1;
    16.         const string shaderName = "Hidden/URPJumpFloodShader";
    17.         readonly int _meshOcculsionID = Shader.PropertyToID("_MeshOcculsion");
    18.         const bool useSeparableAxisMethod = true;
    20.         [ColorUsageAttribute(true,true)] public Color outlineColor = Color.white; // Outline Color, setup in the rendererFeature
    21.         [Range(0.0f, 1000.0f)] public float outlinePixelWidth = 4f; // Outline Width, setup in the rendererFeature
    22.         [HideInInspector, SerializeField] public Shader outlineShader; // Outline Shader, setup in the renderFeature
    23.         [SerializeField] public LayerMask layerMask;
    25.         public Material outlineMaterial;
    26.         public bool doRender;
    28.         public RenderSettings()
    29.         {
    30.             if(outlineColor.a <= (1f/255f) || outlinePixelWidth <= 0f)
    31.             {
    32.                 doRender = false;
    33.             }
    34.             else
    35.                 doRender = true;
    37.         }
    39.         public bool UseSeparableAxis()
    40.         {
    41.             return useSeparableAxisMethod;
    42.         }
    44.         public string GetShaderName()
    45.         {
    46.             return shaderName;
    47.         }
    48.     }
    51.     class AlphaPass : ScriptableRenderPass
    52.     {
    53.         const int SHADER_PASS_INTERIOR_STENCIL = 0;
    54.         const int SHADER_PASS_SILHOUETTE_BUFFER_FILL = 1;
    56.         readonly int _meshOcculsionID = Shader.PropertyToID("_MeshOcculsion");
    59.         private RenderTextureDescriptor silhouetteDescriptor;
    61.         private FilteringSettings filteringSettings;
    62.         private RenderSettings settings;
    63.         private List<ShaderTagId> shaderTagIds = new List<ShaderTagId>{
    64.             new ShaderTagId("UniversalForward"),
    65.             new ShaderTagId("UniversalForwardOnly"),
    66.             new ShaderTagId("LightweightForward"),
    67.             new ShaderTagId("SRPDefaultUnlit")
    68.         };
    70.         public AlphaPass(RenderQueueRange renderQueueRange, RenderSettings settings)
    71.         {
    72.             this.settings = settings;
    73.             filteringSettings = new FilteringSettings(renderQueueRange, (int) settings.layerMask);
    74.         }
    76.         public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
    77.         {
    78.             int msaa = Mathf.Max(1,QualitySettings.antiAliasing);
    80.             RenderTextureDescriptor silhouetteDescriptor = new RenderTextureDescriptor(){
    81.                 dimension = TextureDimension.Tex2D,
    82.                 graphicsFormat = GraphicsFormat.R8_UNorm,
    84.                 width = cameraTextureDescriptor.width,
    85.                 height = cameraTextureDescriptor.height,
    87.                 msaaSamples = msaa,
    88.                 depthBufferBits = 0,
    90.                 sRGB = false,
    91.                 useMipMap = false,
    92.                 autoGenerateMips = false
    93.             };
    94.             cmd.GetTemporaryRT(_meshOcculsionID,silhouetteDescriptor,FilterMode.Point);
    95.             ConfigureTarget(_meshOcculsionID);
    96.             ConfigureClear(ClearFlag.All,;
    97.         }
    99.         public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    100.         {
    101.             CommandBuffer cmd = CommandBufferPool.Get("SilhouetteBufferPass");
    102.             using (new ProfilingScope(cmd, new ProfilingSampler("SilhouetteBufferPass")))
    103.             {
    104.                 context.ExecuteCommandBuffer(cmd);
    105.                 cmd.Clear();
    106.                 DrawingSettings drawSetting =  CreateDrawingSettings(shaderTagIds,ref renderingData, renderingData.cameraData.defaultOpaqueSortFlags);
    107.                 drawSetting.overrideMaterial = settings.outlineMaterial;
    108.                 context.DrawRenderers(renderingData.cullResults, ref drawSetting, ref filteringSettings);              
    109.                 cmd.SetGlobalTexture(_meshOcculsionID, _meshOcculsionID);
    110.             }
    111.             context.ExecuteCommandBuffer(cmd);
    112.             CommandBufferPool.Release(cmd);
    113.         }
    115.         public override void FrameCleanup(CommandBuffer cmd)
    116.         {
    117.             cmd.ReleaseTemporaryRT(_meshOcculsionID);
    118.         }
    121.     }
    124.     class OutlinePass : ScriptableRenderPass
    125.     {
    126.         const int SHADER_PASS_INTERIOR_STENCIL = 0;
    127.         const int SHADER_PASS_SILHOUETTE_BUFFER_FILL = 1;
    128.         const int SHADER_PASS_JFA_INIT = 2;
    129.         const int SHADER_PASS_JFA_FLOOD = 3;
    130.         const int SHADER_PASS_JFA_FLOOD_SINGLE_AXIS = 4;
    131.         const int SHADER_PASS_JFA_OUTLINE = 5;
    133.         private RenderTargetIdentifier _target;
    135.         private RenderSettings settings;
    137.         readonly int _meshOcculsionID = Shader.PropertyToID("_MeshOcculsion");
    139.         readonly int _silhouetteBufferID = Shader.PropertyToID("_SilhouetteBuffer");
    140.         readonly int _nearestPointID = Shader.PropertyToID("_NearestPoint");
    141.         readonly int _nearestPointPingPongID = Shader.PropertyToID("_NearestPointPingPong");
    143.         readonly int _outlineColorID = Shader.PropertyToID("_OutlineColor");
    144.         readonly int _mousePositionID = Shader.PropertyToID("_MousePosition");
    145.         readonly int _outlineWidthID = Shader.PropertyToID("_OutlineWidth");
    146.         readonly int _stepWidthID = Shader.PropertyToID("_StepWidth");
    147.         readonly int _axisWidthID = Shader.PropertyToID("_AxisWidth");
    149.         public OutlinePass(RenderSettings settings)
    150.         {
    151.             this.settings = settings;
    152.         }
    154.         public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
    155.         {
    156.             base.OnCameraSetup(cmd, ref renderingData);
    157.             _target = renderingData.cameraData.targetTexture;
    158.         }
    160.         public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
    161.         {
    162.             base.Configure(cmd, cameraTextureDescriptor);
    164.             if(settings.outlineMaterial == null)
    165.             {
    166.                 settings.outlineMaterial = new Material(settings.outlineShader != null ? settings.outlineShader : Shader.Find(settings.GetShaderName()));
    167.             }
    168.             int msaa = Mathf.Max(1,QualitySettings.antiAliasing);
    170.             RenderTextureDescriptor silhouetteDescriptor = new RenderTextureDescriptor(){
    171.                 dimension = TextureDimension.Tex2D,
    172.                 graphicsFormat = GraphicsFormat.R8_UNorm,
    174.                 width = cameraTextureDescriptor.width,
    175.                 height = cameraTextureDescriptor.height,
    177.                 msaaSamples = msaa,
    178.                 depthBufferBits = 0,
    180.                 sRGB = false,
    181.                 useMipMap = false,
    182.                 autoGenerateMips = false
    183.             };
    184.             cmd.GetTemporaryRT(_silhouetteBufferID, silhouetteDescriptor, FilterMode.Point);
    186.             RenderTextureDescriptor jfaDescriptor = silhouetteDescriptor;
    187.             jfaDescriptor.msaaSamples = 1;
    188.             jfaDescriptor.graphicsFormat = GraphicsFormat.R16G16_SNorm;
    190.             cmd.GetTemporaryRT(_nearestPointID, jfaDescriptor, FilterMode.Point);
    191.             cmd.GetTemporaryRT(_nearestPointPingPongID, jfaDescriptor, FilterMode.Point);
    193.             ConfigureTarget(_silhouetteBufferID);
    194.             ConfigureClear(ClearFlag.All, Color.clear);
    195.         }
    197.         public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    198.         {
    199.             var mouseViewPortPos =;
    201.             CommandBuffer cmd = CommandBufferPool.Get("JumpFloodOutlinePass");
    202.             using(new ProfilingScope(cmd, new ProfilingSampler("JumpFloodOutlinePass")))
    203.             {
    204.                 context.ExecuteCommandBuffer(cmd);
    205.                 cmd.Clear();
    207.                 cmd.Blit(_meshOcculsionID, _silhouetteBufferID, settings.outlineMaterial, SHADER_PASS_SILHOUETTE_BUFFER_FILL);
    209.                 // Humus3D wire trick, keep line 1 pixel wide and fade alpha instead of making line smaller
    210.                 // slightly nicer looking and no more expensive
    211.                 Color adjustedOutlineColor = settings.outlineColor;
    212.                 adjustedOutlineColor.a *= Mathf.Clamp01(settings.outlinePixelWidth);
    213.                 cmd.SetGlobalColor(_outlineColorID, adjustedOutlineColor.linear);
    214.                 cmd.SetGlobalFloat(_outlineWidthID, Mathf.Max(1f, settings.outlinePixelWidth));
    215.                 cmd.SetGlobalVector(_mousePositionID, mouseViewPortPos);
    217.                 int numMips = Mathf.CeilToInt(Mathf.Log(settings.outlinePixelWidth + 1.0f, 2f));
    218.                 int jfaIter = numMips - 1;
    220.                 if(settings.UseSeparableAxis())
    221.                 {
    222.                     //jfa Init
    223.                     cmd.Blit(_silhouetteBufferID, _nearestPointID, settings.outlineMaterial, SHADER_PASS_JFA_INIT);
    225.                     // jfa flood passes
    226.                     for (int i = jfaIter; i >= 0; i--)
    227.                     {
    228.                         // calculate jump width
    229.                         float stepWidth = Mathf.Pow(2,i) + 0.5f;
    231.                         cmd.SetGlobalVector(_axisWidthID, new Vector2(stepWidth, 0f));
    232.                         cmd.Blit(_nearestPointID,_nearestPointPingPongID, settings.outlineMaterial, SHADER_PASS_JFA_FLOOD_SINGLE_AXIS);
    233.                         cmd.SetGlobalVector(_axisWidthID, new Vector2(0f,stepWidth));
    234.                         cmd.Blit(_nearestPointPingPongID, _nearestPointID, settings.outlineMaterial, SHADER_PASS_JFA_FLOOD_SINGLE_AXIS);
    236.                     }
    237.                 }
    238.                 else
    239.                 {
    240.                     int startBufferID = ( jfaIter % 2 == 0) ? _nearestPointPingPongID : _nearestPointID;
    242.                     cmd.Blit(_silhouetteBufferID, startBufferID, settings.outlineMaterial, SHADER_PASS_JFA_INIT);
    244.                     for(int i = jfaIter; i >= 0; i--)
    245.                     {
    246.                         cmd.SetGlobalFloat(_stepWidthID, Mathf.Pow(2,i) + 0.5f);
    248.                         if(i % 2 == 1)
    249.                             cmd.Blit(_nearestPointID,_nearestPointPingPongID, settings.outlineMaterial, SHADER_PASS_JFA_FLOOD);
    250.                         else
    251.                             cmd.Blit(_nearestPointPingPongID, _nearestPointID, settings.outlineMaterial, SHADER_PASS_JFA_FLOOD);
    252.                     }
    253.                 }
    254.                 cmd.Blit(_nearestPointID, _target, settings.outlineMaterial, SHADER_PASS_JFA_OUTLINE);
    255.             }
    256.             context.ExecuteCommandBuffer(cmd);
    257.             CommandBufferPool.Release(cmd);
    258.         }
    260.         public override void FrameCleanup(CommandBuffer cmd)
    261.         {
    262.             cmd.ReleaseTemporaryRT(_silhouetteBufferID);
    263.             cmd.ReleaseTemporaryRT(_nearestPointID);
    264.             cmd.ReleaseTemporaryRT(_nearestPointPingPongID);
    266.             base.FrameCleanup(cmd);
    267.         }
    269.     }
    273.     AlphaPass alphaPass; // InitPass
    275.     OutlinePass outlinePass; // InitPass
    276.     private RenderTargetHandle alphaTexture;
    277.     [SerializeField] RenderSettings settings = new RenderSettings();
    279.     public override void Create()
    280.     {
    281.         alphaPass = new AlphaPass(RenderQueueRange.opaque, settings);
    283.         outlinePass = new OutlinePass(settings);
    284.     }
    286.     public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    287.     {
    288.         renderer.EnqueuePass(alphaPass);
    289.         renderer.EnqueuePass(outlinePass);
    290.     }
    291. }
    Thanks if you found a solution or if you can explain me if I have something I have misunderstand.
    daneobyrd likes this.