top of page

Sprite Shader

  • Writer: Anthony Nguyen
    Anthony Nguyen
  • Aug 21, 2020
  • 4 min read

In this post I will share a quick shader I am using for units in the game.


It is a surface shader with an X-Ray vision effect pass, which I use to show units occluded by 3D meshes with colors indicating their alliance, and other extra fragment functions used for status effects and gameplay functionalities.





Shader "Custom/SpritesCustom"
{
	Properties
	{

		[Header(Shader Settings)]
		_Color("Color", Color) = (1,1,1,1)
		[PerRendererData]_MainTex("Sprite Texture", 2D) = "white" {}
		_Cutoff("Shadow alpha cutoff", Range(0,1)) = 0.5

		[Header(X Ray)]
		_XrayMode("X-Ray Mode", Int) = 1 // 0 - Off, 1 - Outline, 2 - Silhouette
		_AllianceColor("Alliance Color", Color) = (0, 0, 0, 1)
	...
	}
}

For this shader, the main parts are the [PerRendererData] added before _MainTex telling the shader to grab the texture data from SpriteRenderers, an int _XRayMode indicating if we want the unit to be shown with an outline, a full silhoutte, or not at all when occluded behind an object, and a color in which the x-ray is rendered.


This shader requires 2 passes, a transparent pass which renders the x-ray effect, and an opaque pass which renders the sprites.


X-Ray Pass

Tags {
     "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent"
	}
	ZWrite Off
	ZTest Greater
	Cull Off

	CGPROGRAM
	#pragma surface surf Lambert alpha
	#pragma target 3.0

The X-Ray pass requires "Queue" = "Transparent" and "RenderType" = "Transparent" tags since we want to render this along with transparent objects, which happens after opaque objects (in Unity's standard render pipeline). ZWrite should be turned off because we don't need to write this to the depth buffer, ZTest Greater because we want to render this effect on objects further away from the camera based on depth sorting, and Cull off because we want to see the outline regardless regardless of whether our sprite is flipped or not.

#include "../shaders_includes/Custom/SpriteCustom.cginc"

Since this shader can be quite extensive, I went ahead and made a ".cginc" file which contains all the functions used within the shader.

struct Input {float2 uv_MainTex;};

For this pass all we really need is the uvs of the Sprite Texture.

void surf(Input IN, inout SurfaceOutput o) {
	// Turned off
	if (_XrayMode == 0)
	return;

	fixed4 c = sampleTexturePixel(IN.uv_MainTex);
	fixed4 finalColor = xray(c, IN.uv_MainTex.xy);

	o.Albedo = finalColor.rgb;
	o.Alpha = finalColor.a;
	clip(o.Alpha - 0.5f);
}
ENDCG

The surf function simply samples a pixel from the _MainTex and then runs that pixel through an xray() function that filters out pixels surrounded in all four directions (up, down, left, right) in order to create the outline effect.

// in "SpriteCustom.cginc"
...
inline fixed4 outline(float4 input_color, float2 input_texcoord, float4 outlineColor) {
	if (input_color.a != 0) {
	// Get the neighbouring four pixels.
	fixed4 pixelUp	= tex2D(_MainTex, input_texcoord.xy + float2(0, _MainTex_TexelSize.y));
	fixed4 pixelDown = tex2D(_MainTex, input_texcoord.xy - float2(0, _MainTex_TexelSize.y));
	fixed4 pixelRight = tex2D(_MainTex, input_texcoord.xy + float2(_MainTex_TexelSize.x, 0));
	fixed4 pixelLeft = tex2D(_MainTex, input_texcoord.xy - float2(_MainTex_TexelSize.x, 0));

	// If one of the neighbouring pixels is invisible, render outline.
	if (pixelUp.a * pixelDown.a * pixelRight.a * pixelLeft.a == 0)
		return fixed4(1, 1, 1, 1) * outlineColor;
	}
	return (1, 1, 1, 0) * outlineColor;
}
...

This function takes in the color of the current pixel to filter out invisible pixels (alpha == 0), then samples four of its surrounding pixels using the uvs and an offset. Since my sprites are pixel art, simply adding (0, 1), (0, -1), (1, 0), and (-1, 0) as offsets would not result in a pixel perfect outline, _MainTex_TexelSize (Unity's built-in uvs based on the used texture's dimensions) had to be used instead. After sampling the four pixels, I simply take the product of their alpha's to see if the current pixel is completely surrounded, if not we draw that pixel using the outlineColor parameter.


i.e. Green is the current pixel being checked. To know if it is an outline pixel we only need to know if it is not blocked in by one of its neighbors, and since blue has an alpha of 0, we know green is an outline pixel.


Opaque Pass

The opaque pass for this shader doesn't deviate much from a that of a standard surface shader.

Tags{
	"Queue" = "Geometry"
	"RenderType" = "TransparentCutout"
}
ZWrite On
ZTest LEqual
Cull Off

RenderType needs to be set to TransparentCutout in order to render 2D sprites. ZWrite on since we want to save the object's depth information to the depth buffer, which will then be used in the X-Ray pass for comparison with other objects. ZTest LEqual, which is the opposite of the X-Ray pass, since we only render these objects when they are not occluded (closer to the camera view).

#pragma surface surf Lambert addshadow fullforwardshadows

For Sprites to cast and receive shadows, we must use Lambert lighting model and include addshadow fullforwardshadows.

void surf(Input IN, inout SurfaceOutput o) {
	fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
	o.Albedo = c.rgb;
	o.Alpha = c.a;
	clip(o.Alpha - _Cutoff);

	// Color Adjustments
	o.Albedo = turnStatus(o.Albedo);
	o.Albedo = statusEffects(o.Albedo, IN.uv_MainTex);

	// Outline selection
	o.Albedo = selection(o.Albedo, IN.uv_MainTex).rgb;
}

The surf function simply samples the pixel and modifies it (if necessary) based on some extra information.

For example, I could have an extra outline effect for selecting a unit within a group when the player wants to separate or choose a specific target, or a quick color change when a unit ended their turn.











Comments


  • Instagram

©2020. Anh Tri Nguyen.

bottom of page