Outline shaders, especially for 2D is something that i'm very interested in. These can highlight objects that the player needs to interact with or follow. So I decided to jump in and learn how to make them.
First, we need some properties to know: what the sprite we will be outlining, if we'll be outlining it and what colour it'll be.
[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {} [PerRendererData] _Outline ("Outline", Float) = 0 [PerRendererData] _OutlineColor ("Outline Color", Color) = (1,1,1,1)
Also adding in our pass definitions, telling the pass what to do and what not to do. We don't want depth, lighting, or anything that can alter the outline in the game space.
Then we need fill out our tags which will tell our shader how it will work and render the final image.
Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" "PreviewType" = "Plane" "CanUseSpriteAtlas" = "True" }
Cull Off Lighting Off ZWrite Off Blend One OneMinusSrcAlpha
In our pass CG PROGRAM, we want to indicate our comnpilation directives.
#pragma vertex vert #pragma fragment frag #pragma shader_feature ETC1_EXTERNAL_ALPHA #include "UnityCG.cginc"
Then we are going to pass in our vertex data and run it on each of the verticies of the sprite. Since we're making an outline for a 2D sprite, this isn't going to be too flash.
struct v2f { float4 vertex : SV_POSITION; float4 color : COLOR; float2 texcoord : TEXCORD0; }; float _Outline; fixed4 _OutlineColor; v2f vert (appdata_base IN) { v2f OUT; OUT.vertex = UnityObjectToClipPos(IN.vertex); OUT.texcoord = IN.texcoord; return OUT; }
Now this is where we begin to create the outline. In the fragment shader, we basically first get the pixel's colour from the "SampleSpriteTexture" function. This is a custom function which returns the colour of the sent pixel coordinate.
Then we check if we're supposed to draw an outline and if the pixel is not transparent. If so, then we get the colour of the adjacent up, down, left and right pixels of the current one. If any of those pixels are transparent, then we make this pixel the outline colour.
fixed4 frag (v2f IN) : SV_Target { fixed4 c = SampleSpriteTexture(IN.texcoord) * IN.color; if(_Outline > 0 && c.a != 0) { fixed4 pixelUp = tex2D(_MainTex, IN.texcoord + fixed2(0, _MainTex_TexelSize.y)); fixed4 pixelDown = tex2D(_MainTex, IN.texcoord - fixed2(0, _MainTex_TexelSize.y)); fixed4 pixelLeft = tex2D(_MainTex, IN.texcoord - fixed2(_MainTex_TexelSize.x, 0)); fixed4 pixelRight = tex2D(_MainTex, IN.texcoord + fixed2(_MainTex_TexelSize.x, 0)); if(pixelUp.a * pixelDown.a * pixelLeft.a * pixelRight.a == 0) { c.rgba = fixed4(1, 1, 1, 1) * _OutlineColor; } } c.rgb *= c.a; return c; }
So pretty much, if the pixel is at the edge of the sprite or surrounded by a transparent pixel, colour the pixel the outline colour. This is an inside outline compared to a shader that draws the outline outside of the sprite. Which one is better? It depends on what you're making and how much pixel real estate you wish to give up. For pixel art games, this one probably isn't the best, yet for games with larger sprites the issue won't matter that much.
Now, having this shader as it is wont do anything. So we need to add it into Unity...
Implementing it into Unity
First, we need to make a new Material and set the shader to "Sprites/Outline". This material can then be applied to sprite renderers. Yet... it doesn't do anything.
To fix this, we need to make a script. This script can be called SpriteOutline and it will transfer values that we enter such as colour and outline size to the material.
[ExecuteInEditMode] public class SpriteOutline : MonoBehaviour { public Color colour = Color.white; private SpriteRenderer sr; void OnEnable () { sr = GetComponent<SpriteRenderer>(); UpdateOutline(true); } void OnDisable () { UpdateOutline(false); } void Update () { UpdateOutline(true); } void UpdateOutline (bool outline) { MaterialPropertyBlock mpb = new MaterialPropertyBlock(); sr.GetPropertyBlock(mpb); mpb.SetFloat("_Outline", outline ? 1.0f : 0); mpb.SetColor("_OutlineColor", colour); sr.SetPropertyBlock(mpb); } }
Make sure that at the top of the script the [ExecuteInEditMode] tag is added. This makes it so that the shader can be visible while not in play mode.
For our variables, we have the colour that the outline will be and the sprite renderer, of the sprite we're affecting.
The enable and disable functions get the sprite renderer component, as well as updating or not updating the outline.
In the UpdateOutline function, we transfer those valuables to the outline material. The "outline" float is either 1 or 0 and determines whether or not the outline is on or off. Then we just need to apply the script to the object with the outline material and...
It's complete. We can alter the colour of the outline in the editor.
Using it in the future
Now that the outline shader is done (and works), it's ready to be implemented. At the moment though, I don't have any games that would use this shader, but hopefully in the future that will be a possibility.
Things like highlighting items, making enemies visible through walls, etc, will all be possible by this shader. It may need tweaking to allow for seeing it through walls and outlining away from the sprite (not inside it), but that's for the future. This was just a learning experience in seeing how these sort of things are made, how difficult it is and how well it works.