Object specific silhouette outline using URP custom Render Feature
In my last freelance job I had the opportunity to create a cool outline effect in URP that needed some creative solutions to achieve the specific requirements presented by the client, so I thought it would be an interesting article to share with the Unity developers community.
This effect was part of a collaboration with Funtra, a new inspiring mobile games company and a really awesome team that I really enjoyed working with.
This article discusses one solution in a series of features I helped implement in their upcoming game “Heist Hunters”.
The brief I got from the art director was to create an outline effect for specific gameplay objects, such as the player car, obstacles and pickups.
The outline should only be visible on the object exterior, meaning a silhouette, without interior object lines like in the “classic” outline effects.
The brief I got from the technical director is to get the effect with the minimum draw calls without hurting the frame rate and staying above 30 FPS (we did do a short optimization session, but that is a topic for another article).
In order to get the desired effect I used an edge detection method called Robert’s Cross. In short, the shader checks 4 pixels in a cross pattern around each pixel, if the value difference between the diagonals is higher than a specified threshold value, the sampled pixels will be painted. There are many online resources that provide details about this technique if you are looking for a deeper explanation or tutorial.
(left image — edge detected, right image — no edge detected)
Usually developers use the camera depth normal textures to detect edges and cross reference them to clean up the results. Using the depth normal solution we would get the “classic” outline, and in this case we are only interested in the silhouette of the objects.
We need a different kind of texture to feed the edge detection so we only get the silhouette. For this we can create an object color id texture where each object has a different color, so the edge detection algorithm can detect the edges between objects.
(left image — depth pass, right image — Color ID pass)
The game utilizes Unity’s URP which means we are creating a custom render feature that creates 3 passes. The first pass is the Object Color ID, the second pass is for occluding objects like the floor or pickups, and the third is the edge detection.
In the Object Color ID and Occluders pass we filter the objects using a layer mask so only the desired objects will receive the effects.
At first thought I had the idea of using Unity’s Material Property Block (MPB) which allows each object to receive a unique color while still using the same material so they can all be batched and sent together to the GPU to save draw calls.
However, the game relies on the SRP batcher to reduce and optimize draw calls, and the MPB dose not work with the SRP batcher and will actually create more draw calls when using it in this setup.
We could give each object its own material and the SRP batcher would have handled that fine, but it would be very inconvenient and messy to have so many materials in the project just for this effect.
The solution we came up with is to create a premade texture with a given set of unique colors, then create a UV layout for each object and position the UVs in a unique pixel position corresponding to its unique ID.
I created a small editor tool that generates a texture where each pixel has a unique color, so we can easily adjust the amount of unique colors according to the scene needs.
The UV generation is managed with 2 scripts: a “Object Color ID Controller” and a “Generate Object ID” script, which is attached to the game objects that require a unique ID.
The Controller finds all of the objects that need a unique ID and gives them one, then each Generator gets all the child meshes of that object, calculates the desired position according to the ID given to it, and generates the UV.
Now we can simply assign the material to all of the objects we want to be outlined in the Render Feature Color ID pass, and send the result to the silhouette pass and its shader.
At this point the line was uniformed, and looked a bit odd, mainly because it stayed with the same thickness no matter how far the object was from the camera.
The final touch was to make the silhouette thinner and more transparent the further the object is from the camera. To accomplish this I used the camera depth texture to modulate the thickness of the line and to change the alpha of the line as it moves away from the camera.
Thank you for reading and hope you found it helpful :)