Unity 2019.3.0f1 URP 7.1.6 January 7th 2020


The final result

Introduction

Imagine we give our shader the image below on the left and we want the shader to generate an outline for us. We could simply detect if there is a sudden color discontinuity, and if that is the case, draw an outline. However, it is possible that not all sides have a different color like in the second image. In that case we will need to take a look at the orientation of the face (the normal vector). In the last image, the background has the same color as some of the sides of the structure, in this case we will need to take a look at a depth difference in the scene.




The solution to creating an effective outline shader that will work for all geometry and colors, is to take into account color, normal vector and/or depth discontinuities. But how do we detect these discontinuities?


Depth + Normals Texture

The Universal Render Pipeline generates a texture called the _CameraColorTexture. This texture gives us a look at all the colors in the scene without any post-processing. We can use it to detect color disconinuities in the scene. A second texture that can be generated by URP, is the _CameraDepthTexture. This texture gives us a color gradient representing the depth of objects in the scene. It uses the red channel so that's why the texture is tinted red.



_CameraColorTexture
_CameraDepthTexture
_CameraDepthNormalsTexture

A third commonly used texture is the _CameraDepthNormalsTexture which is a texture that combines the depth and normals information of the scene into 1 texture. Sadly, URP does not generate a depth + normals texture. However, it is possible for us to modify the pipeline to generate this texture for us so that we can sample it and use this information in our outline shader. To do this, we will be modifying the pipeline by adding a Scriptable Renderer Feature.


Scriptable Renderer Feature

To generate the _CameraDepthNormalsTexture we will be creating our own custom scriptable renderer feature. A scriptable renderer feature can be used to inject render passes into the renderer. We will be creating a script called DepthNormalsFeature.cs. This script will include the ScriptableRendererFeature as well as the ScriptableRendererPass.

The code is as follows.


Now in our renderer asset we can add the DepthNormalsFeature to our renderer features list by using the little plus icon.



One more important thing that you should do is check the Depth Texture box in your pipeline asset. This should be done because we will be using this generated depth texture in our outline shader so we want the rendering pipeline to generate it for us.


Instead of using the _CameraDepthTexture for depth, we could also just use the _CameraDepthNormalsTexture and decode both normals and depth information from it since the texture contains both. However, I believe that the _CameraDepthTexture provides greater precision when it comes to depth values. That's why we will be getting depth from the _CameraDepthTexture, normals from the _CameraDepthNormalsTexture and color from the _CameraColorTexture.


Outline Shader

The next step is to sample this newly generated _CameraDepthNormalsTexture in our shader. We will be using the Custom Function node in shader graph to create the bulk of our outline shader. The custom function node refers to a file called Outline.hlsl with a function called Outline.


The custom function node


The code for the Outline.hlsl file can be found below.


Now if you were to apply this shader to an object, you would get an undesired effect. This is because we will be using this shader as an image effect. This is useful if we want the outline to be applied to all of the objects in the scene.


Image Effect

To add the outline as a screen-space image effect, we will be creating another renderer feature with a script called OutlineFeature.cs.

The code looks like this.


We add this pass to our renderer as a renderer feature and so now we have 2 renderer features. Make sure to assign the outline material to the material slot in the OutlineFeature renderer feature.



And that's it! You should now see an outline in your scene and be able to control the outline settings through the outline material inspector.



Per-object Outline

In the case that we only want an outline applied to a specific object instead of all the objects in the scene as an image effect, we need to do the following. First, we remove the 2nd renderer feature so we now only have the DepthNormalsFeature as our renderer feature. Next, we modify the Outline.hlsl file slightly.



Instead of blending with the scene color and returning a float4, we will simply return the edge variable which is a float1. Please note that I removed outlines based on color in this example, this is because I was having strange issues with it. I will try to resolve these at a later time.

We can use the following node setup. It's important to note that now, in the custom function node, the name should be set to OutlineObject instead of Outline since we renamed our hlsl function to OutlineObject


This node setup will lerp between an outline color and the main texture based on where the outline is. And that should do it! You can now apply the material to an object and only that object will have an outline.


Useful Resources

Here are some extra resources that helped me write this tutorial.

https://roystan.net/articles/outline-shader.html
https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@7.1/manual/universalrp-builtin-feature-comparison.html


Download the source files