How to make an Outline in Unreal Engine

Introduction

Hi there.
At the time of writing this article, I couldn’t find any tutorial or resource that explained how to create an outline effect that worked exactly the way I wanted. So, I had to figure it out myself.

This article provides a step-by-step explanation of how the highlight effect works in our game, Take Notes. However, it’s not necessarily aimed at beginners.

How is it Different from Other Approaches?

In the left image, you can see the approach used in many YouTube tutorials, where the outline appears even behind other objects. However, I wanted the outline to highlight only visible areas, not act as an x-ray. This intended effect is shown in the right image.

What is Scene Texel Size?

First of all, to create this effect, we need to understand what scene texel size is, as it’s crucial for controlling the outline’s width.

In simple terms, scene texel size is the size of a single pixel relative to the screen resolution. It divides 1 by the screen’s width and height in pixels, resulting in a 2D vector like this:
(1/ScreenWidth, 1/ScreenHeight).
For a Full HD screen, it looks like this:
(1/1920, 1/1080), giving an output of (0.000520, 0.000925).

What is Screen Position

The second important concept we need to understand is screen position. A screen position is represented as a gradient, as shown below. It encodes values ranging from (0,0) at the top-left corner to (1,1) at the bottom-right corner. As the name suggests, these values indicate location on the screen.

With scene texel size and screen position defined, we can move on to the first part of the post-process responsible for creating the outline.

Outline Width

The first three nodes are straightforward. As explained earlier, Scene Texel Size returns a 2D vector representing pixel size, so by multiplying it by 3, we set the outline width to 3 pixels.

Translating Screen Position

To achieve the outline effect, we need to translate the screen position four times—once for each corner of the screen. The mask and append nodes allow us to manipulate values to accomplish this. The add nodes on the right handle this translation, and I’ll explain why this is necessary in a moment.

To clarify, here are the B inputs for each add node, where „sts” stands for scene texel size:

(-3sts, 3sts)
( 3sts, 3sts)
( 3sts,-3sts)
(-3sts,-3sts)


In essence, this means we’re shifting the screen position by 3 pixels in each direction, with output from the add nodes.

Scene Textures Translation

In the image above, you’ll see five node groups that perform the same function but sample different screen areas, thanks to the previously translated screen position connected to the UV inputs. This setup allows us to sample information from specific screen regions where we want the outline to appear.

Note that we also sample the original texture without any translation, as we’ll need it later.

I am going to explain what a single group actually does, but first, we need to understand what we are sampling.

What is Scene Depth?

Scene depth is one of the scene textures we can sample, and it tells us how far objects on the screen are from the camera. A value of 0, which appears as black, represents the closest possible distance to the camera

(Output divided by 1000 )
What is Custom Depth?

Custom depth is a scene texture similar to scene depth, but it only applies to meshes on which we specifically enable it. This allows us to use it as a mask for what should be outlined.

(Output divided by 100000000)
Creating a Mask for the Outline

However, the problem is that other objects do not block custom depth, and we do not want an x-ray effect. Fortunately, this segment will help us address that issue.

What you see in the image is one of the node groups discussed above. By subtracting one texture from another and performing these calculations, we create a mask that looks like the one shown in the image below—exactly what we need.

The step node takes the Y value and outputs 1 if it’s greater than or equal to the X value; if it’s less than X, it outputs 0.

The rest of the math is self-explanatory, but I encourage you to experiment to see what each of the nodes brings to the table.

We need to do this five times

Remember that we do this five times in total, four of which are translated. Take a look at this visualization to understand the outputs we get.

If our screen is 5×5 pixels, our object to outline is 3×3 pixels, and we want the outline to be 1 pixel wide, then it would look like this:

Merging

As you can see in the previous image, we take an untranslated texture and subtract each of the translated textures from it separately before adding the results together.

The images below illustrate how this works:

1. The first image shows the untranslated view of the object to outline.

2. The second image displays the translated view; however, remember that we do this for every corner, not just the bottom left as shown in the image.

3. The third image shows the subtraction of the untranslated view from the translated view, again done separately for each corner.

4. The fourth image represents the combined result of all four corners.

In practice, it looks like the image below:

What is a Post Process Input?

For the last part, we need to understand what a Post Process Input is. It’s simply a scene texture that represents the view from the camera before any post-processing effects are applied.

Displaying the Outline

Now, we need to add our outline into the game, which we do with the help of a Lerp node. We connect our black-and-white outline to the alpha input and, based on that, we Lerp between the Post Process Input and the chosen color. That’s it—now we have the outline in the game.

Additional Note: The Saturate node clamps any value between 0 and 1. We need it here because, as you may have noticed in the visualization above, translated views can overlap, resulting in values greater than 1. Without clamping, some parts would start to glow, which we don’t want.

Here’s what the complete graph looks like:

How to Make Objects Highlight on Mouse Over

Unfortunately, a detailed explanation is outside the scope of this article. However, I’ll briefly explain how it’s done in Take Notes. We need two things to make it work.

First, we need to add our post-process material to a post-process volume that covers the entire scene.

Second, we need a way to toggle the custom depth on the meshes we want to highlight. In our case, each frame we run a trace line from the camera, and if it hits an actor of the interactive class, we send a signal through an interface implemented in that class. This interface sets the 'Render Custom Depth’ boolean to ON for its static mesh component and turns it OFF after a short delay.

If you need a detailed tutorial, you can find one on YouTube by searching something like „Unreal Engine highlight effect”

Final Thoughts

Let’s summarize how it works:
We need five sampling locations for our textures: one untransformed view and four transformed, each shifted to a different corner of the screen by the width of the outline. Then, we sample both the custom depth and scene depth at each location. By subtracting one from the other and applying some math, we create a mask of the object in five different states. We subtract the untransformed mask from each transformed mask and add them together. Finally, we use a lerp node to blend between the camera view and the highlight color, based on the mask we created.

I hope this article was helpful and will assist you in implementing a similar concept in your own game.