itch.io is community of indie game creators and players

Adding dynamic outlines to certain objects

Author: Matthew Chadwick

Date: 1/27/2024


In CyberSiege, there are certain objects that the player can interact with. These objects are mostly used to quest objectives, but some are the quest givers themselves. Currently, these non-quest objective objects don't have any indication that they can be interacted with. As a result, some players may not know what to do when they approach the quest giver, which can cause the player to miss out on some defenses in the game. To fix this, I created a post-process material that adds an outline to certain objects to differentiate them from normal objects.

Variation formula shown in the video

This task was harder for me than most of other tasks I've completed for this project. I'm not the greatest at understanding graphics math, so I relied on Google to provide me with the information I needed. I utilized the first part of this tutorial to create the initial outline. The concepts in this tutorial were actually very easy to understand, and I'll explain them as I understand them. To create an outline, we first need to find the edges of the object of which we want to create an outline on. To do this, we basically take samples of each pixel as well as its adjacent neighbors. Using the formula described at 1:38 in the video, we can determine the difference in depth between the center pixel (the Kernel) and its adjacent pixels. If this value exceeds a certain threshold, we can determine that the pixel is part of an edge, and we can then change its color to our outline color. The first part of the edge detection is basically just getting the location of kernel pixel neighbors. During this initial part, we can also factor in the thickness of the line we want. We can do this by utilizing the TexCoord, which will allow us to manipulate the texture, and a scalar value, which determines our outline thickness. To get the information needed for all required pixels, we use simple offsets that move our sample to the left, right, above, and below of our kernel. Once we put all of this together into a material function for organization, we can use it in the next part of our process, which is actually detecting the edges.

Getting the kernel's and neighbor's data

The actual edge detection is also very simple now that we have our kernel information. To start, we need to call the function we created previously and supply it with our outline thickness to get the kernel and its adjacent neighbors. Once we have this, we can sample the depth of each pixel returned from our kernel function. We can do this with the Scene Depth node, which is provided by Unreal. After doing this, we simply follow the formula above, which tells us to add up the neighbor pixels' depth values and subtract the kernel's depth value, multiplied by four. After taking the absolute value of the difference, we can then compare that to our threshold value to determine if we are on an edge or not. All of this math took me a little bit to understand, but to summarize, the "variation" we calculated represents the difference in how far away the kernel pixel is from its neighbors. To represent this, I drew a very bad picture in paint that represents how I see it in my head. In the picture, since the variation of 480 is greater than our threshold of 20, we can determine that we are on the edge of Box "B". If we moved our sample over to right by one, our variation would be 0, which would mean our kernel pixel is not on the edge. To get a useful value from all of this math, in our edge detection material function, if the value exceeds our threshold, we output a 1; otherwise, we output a 0.

Art was never my strong suit, which is why I'm a developer

To tie all of this together, we need to link everything together in a post-process material to achieve our outline. This part is was the easiest to understand, but I still had to do some research to determine how to selectively apply the outline. To start, we need access the buffer that contains all of the pixels that are about to be rendered. This can be done with the SceneTexture node. After getting this buffer, we can then lerp the color values of each pixel between their current value and the outline color value, using the output of the edge detection function as our "alpha". You could technically stop right here since by this point, every object in your scene will have an outline. However, since I wanted only certain objects to be outlined and not every object, I needed to utilize something called CustomDepth. CustomDepth is basically a separate depth buffer that meshes can be sent to instead of the primary depth buffer. If you assign a mesh to this custom depth buffer, the mesh will no longer appear in the primary buffer (I think) and can manipulated separately without affecting every mesh. To make a mesh utilize the custom depth buffer, you must change a boolean value to "true" on the mesh, which is how I was able to make the outline appear only when the player was looking at the mesh". I used the custom depth buffer values and put that into an if statement. This if statement basically says "if there is a depth value present in the custom depth buffer at this location, use the edge outline color; otherwise, use the normal mesh color". The result was a outline with a custom color and thickness that would only appear whenever the player looked at the object.

Leave a comment