Simple See-Through Shader for 2D Sprites in 3D World

I am currently making a game.This is quite simple, It’s a stealth game where the graphics are paper-mario like.So I had the choice: I could have used a 3D Plane with a material containing my texture, and mess around with only 3D objects, but there was still a problem with that: I wanted to use the simple animation of the unity 2D sprites. What I chose was a 3D GameObject with a capsule collider, Rigidbody, Navmesh agent, etc.. AND a sprite renderer attached to him. So I am able to use a simple spritesheet for the animation system. The problem came when I had to make the player slightly visible through walls (because, you know, stealth game…) I had to make a shader.

The only experience I had so far with shaders was a vertex/fragment shader for OpenGL. The Unity shaders are quite different, so I had to learn a lot of new things. And most of the shaders on the internet are for 3D projects or 2D projects, but never for a mix of both.

1. A Basic Sprite Shader

I found this basic sprite shader on GitHub, it helped me a lot to understand how unity shaders works.

Basically, here’s the structure of a shader…

Shader "<name>"
{
	<optional: Material properties>
	<One or more SubShader definitions>
	<optional: custom editor>
	<optional: fallback>
}

You can find more information about that right here.

In our case, the syntax will look like this:

We’ve got one subshader with two pass: one for the visible part of the sprite, and the other for the grey silhouette hidden by our 3D objects.

We will also need two properties:

Properties
{
	[HideInInspector]
	_MainTex    ("Sprite Texture", 2D)          = "white" {}
	_GhostColor ("See-through Color", Color)    = (0.2, 0.2, 0.2, 1)
}

we use that [HideInInspector] tag because we don’t want the user but the sprite renderer to tell the shader which texture to use, so we disallow the user to have access to it from the editor.

_GhostColor is the only property the user will have access to, in order to choose the color of the player’s Ghost.

The properties are defined, we can start to make our subshader!

2. SubShader

SubShader
{
  Tags
  {
    "Queue"="Transparent"
    "PreviewType"="Plane"
  }

we choose the render queue tag Transparent so it will be rendered after the other objects (but before the overlay) we also set the PreviewType as a Plane because we want the material to appear as a sprite material in the editor.

More informations about SubShader tags…

Cull    Off
Blend   One OneMinusSrcAlpha

CGINCLUDE
...
ENDCG

Pass
{
...
}

ZTest   Greater
ZWrite  Off

Pass
{
...
}

Cull Off will tell the shader to not use culling, so both sides of the sprite will be visible Blend One OneMinusSrcAlpha will tell the shader to use alpha blending aka transparency.

CGINGLUDE is the header of all our future CGPROGRAM it is here to prevent us from duplicating code, it makes the code more readable.

you can also see that before the second Pass we have ZTest Greater which tell the shader to apply this program to only the parts that are greater on the Z-buffer (the hidden parts) and ZWrite Off which will prevent our sprites from hiding each others.

3. The Programs

CGINCLUDE
#include "UnityCG.cginc"

sampler2D   _MainTex;
fixed4      _GhostColor;

struct appdata_t
{
  float4 vertex : POSITION;
  float4 color    : COLOR;
  float2 texcoord : TEXCOORD0;
};

struct v2f
{
  float4 vertex : SV_POSITION;
  fixed4 color    : COLOR;
  float2 texcoord : TEXCOORD0;
};

We first declare our global parameters corresponding to the properties of the shader (_MainTex and _GhostColor).

then we declare two structs, appdata_t for the vertex part of the shader and v2f for the fragment part.

4. The Vertex Shader

#pragma vertex vert
v2f vert(appdata_t IN)
{
  v2f OUT;
  OUT.vertex      = UnityObjectToClipPos(IN.vertex);
  OUT.texcoord    = IN.texcoord;
  OUT.color       = IN.color;
  return OUT;
}
ENDCG

#pragma vertex vert tells the shader to use the vert() function as the vertex part. the vertex part is only there to turn each element of your game into a projection on your 2D screen. In the function we don’t need to change the vertex UVs neither the vertex colors so we return the sames as the input ones.

the vertex shader is in the CGINCLUDE because it is the same for both of our Pass (shown part and hidden part)

5. The Fragment Shaders

CGPROGRAM
#pragma fragment frag
fixed4 frag(v2f IN) : SV_Target
{
  fixed4 c = tex2D(_MainTex, IN.texcoord) * IN.color;
  c.rgb *= c.a;
  return c;
}
ENDCG

In the first fragment shader, all we do is output the fragment color corresponding to the texture and the alpha (Transparency).

CGPROGRAM
#pragma fragment frag
fixed4 frag(v2f IN) : SV_Target
{
  fixed4 c = tex2D(_MainTex, IN.texcoord);
  return _GhostColor * _GhostColor.a * c.a;
}
ENDCG

For the hidden part, it’s even easier, we return the _GhostColor processed with the alpha of the texture.

And that’s all, really, we’re done here!

Full Script: https://gist.github.com/cfazilleau/3a44f3f3af19e491e0b3cabcc65054b4


A shader by Clément FAZILLEAU