There are many techniques for adding outlines to objects in games but they each come with their own trade-offs. This is particularly true when it comes to creating dynamic outlines for flat 2D objects where the quality of the available approaches is often less than desirable. Most of the dynamic outlining techniques seem to have been developed primarily with 3D assets in mind, or have other shortcomings that make them undesirable for certain use cases.
Tom Mathews and I had been looking for a solution to this problem for a while. For our use case we wanted something that worked with 2D characters making use of skeletal animation and mesh deformation and we wanted the outlines to be chunky and smooth.
We tried a variety of existing solutions but ran into problems with each of them. Here is a quick summary of the main issues that led us to look for an alternative:
- Many 3D techniques make use of mesh normals and therefore aren’t applicable to 2D.
- Screen space effects are difficult to customise for specific objects and good quality thick outlines are difficult to achieve.
- Approaches that create multiple duplicated versions of the sprite do not work effectively when you want thick outlines as the duplicates begin to separate.
- Shaders like this colour the inside of the geometry, hiding the artwork, especially when using thick outlines.
Our solution is to allow the artist to draw the outlines into the original artwork at a reduced alpha value. In engine the character is then rendered out with a shader utilising dual alpha cutoffs to allow the outline to appear only around the outside of the character. This approach gives the artist complete control over the outlines and allows the outline to be any thickness while remaining smooth. In this post I’m going to cover most of the technical details of the shader. For more information on the art work flow you can check out Tom’s art workflow guide.
This technique requires the art you want to outline to be done in a specific way. This unfortunately means that it can not be applied to existing artwork without first modifying them. The process isn’t too complicated and the control we get over the outline as well as the quality of the final outline made the trade-off worthwhile.
To create compatible art you simply draw whatever outline you want into the original textures but set at a lower alpha value than the main artwork. In game a custom shader will cause this outline to appear at full opacity and behind the main texture, preventing unwanted inner lines from drawing over the top of the character. Unfortunately you will be able to see the inner in your animation package however you can reduce the visual impact those lines have by choosing a very low alpha.
This shader has a few key advantages over other solutions. The major advantage is in the quality of outlines produced when a thick outline is required.
Unlike other solutions the outline does not encroach on the original artwork. This approach also gives the artist total control over outline thickness as well as allowing for variable outline thickness at different points on the character.
Outline colours can be easily customised per character by using separate materials unlike in screen space solutions.
The technique should also be very fast, with no extra geometry created, only 1 texture sample required in each pass and no use of additional cameras or render targets.
The main disadvantage to using this shader is that the art must be specifically prepped for use with the shader as previously described.
Internal lines are visible when not using the shader, and therefore in any external animation package. This can be distracting, however the visual impact of this can be reduced by using a very low alpha value on the outline in the original texture since it will be overridden in engine anyway.
Blending artifacts can appear on the edges of the artwork if the outline colour in the texture does not match the neighbouring pixels of the main artwork. This can be solved by ensuring these pixels match. More details can be found in this slide of Tom’s guide.
The full source for the shader is available here.
***Please note!*** This shader makes use of the Offset tag which seems to be misbehaving in Unity 5.5. I currently have a bug report open and am waiting for a solution from the devs. I will update this post when I have more information.
For now this shader should work fine on 5.4. I might look at an alternate solution that actually changes the vertex positions if this fails to be resolved, but I’d rather avoid that additional complexity if possible.
This version of the shader uses Unity’s standard lighting model, but it should be relatively easy to convert any other alpha cutoff shader to do something similar. Below I will go over the key points that you would need to add to your own shader to support this technique. Note that for shaders that are not surface shaders secondary passes are added differently than I have described.
The shader works similarly to a traditional alpha cutoff shader, but with two alpha cutoff values instead of one. The first cutoff works as normal, controlling what pixels are visible. The second cutoff is used to control what pixels constitute the outline. As an example, if your normal artwork is 100% opaque and your outline is 30% opaque you might set your normal cutoff to 0.8 and your outline cutoff to 0.25.
In unity you can achieve a dual cutoff by creating a default surface shader, duplicating the CGPROGRAM so you have two passes and adding a clip function to each to do the alpha clipping with the appropriate cutoff. Something like the following:
clip (albedo.a – _Cutoff);
Note that two different cutoff values are defined in the full shader, one for each pass.
_Cutoff (“Main Alpha Cutoff”, Range(0,1)) = 0.5
_OutlineCutoff (“Outline Alpha Cutoff”, Range(0,1)) = 0.25
Make sure to reference the correct cutoff in each pass.
At this point the outline will still draw over the top of the rest of the artwork. To solve this we can add an Offset tag to the main artwork pass to force its z depth in front of the outline.
Offset -3, [_LineOffset]
Exposing the offset value as a parameter allows it to be tweaked for individual characters to ensure the outline works correctly for overlapping pieces in the case of a character using skeletal animation, or to customise the behaviour when two outlined characters overlap. The -3 controls how the offset behaves when the image is viewed at grazing angles. This feature isn’t very well documented but -3 seemed to work best for me. Feel free to experiment with different values. If you are only ever viewing the character from straight on then 0 is probably a better value.
The final step is to colour the outline. This step is technically optional, you could simply put whatever outline colour you want to use directly into the texture, however colouring the outline can be useful if you want to change the colour at runtime. It also allows you to have the colour of the outline in the texture match the colour of the nearby pixels in the main artwork which helps reduce some potential blending artifacts that can otherwise occur as previously mentioned. Tom explores two approaches to creating your art with matching outline colours here.
To implement this the albedo colour is set to the value of a property (_OutlineColor) rather than the colour derived from the texture.
o.Albedo = _OutlineColor.rgb;
Simple! There really isn’t much to it, but it is pretty effective.
Some minor things to note:
I recommend only applying a shadow caster pass to the outline pass (by adding “addshadow” to the surface pragma). Adding a shadow pass to the main pass can cause shadows to appear on the outline which looks strange.
Both passes have a “Cull Off” tag. This is optional and only present to allow the shader to work with Spine.
Here is a comparison of a character created in spine to work with this shader. The standard Unity shader using alpha cutoff (Left) and our dual cutoff outline shader (right).
Making use of an alpha gradient in the outline would allow control over outline width without having to modify the texture. This could also allow for runtime animation of the outline width by animating the outline alpha cutoff.
Potentially possible to extend this technique to support alpha’d edges, but doing this might also require the outlines to be drawn at a higher alpha value.
At the moment the outline pass uses the same lighting as the main pass. This doesn’t need to be true. The outline could just as easily be unlit or incorporate any other effect that might be desired.
That’s pretty much all there is to it. I recommend checking out Tom’s art workflow reference for some more detail on setting up artwork to work with this shader. Hopefully someone will find this useful!