Back in the post on SpriteLightKit, we talked about the stencil buffer and we left things off with a homework assignment. I also did the homework assignment and will be sharing the result that I came up with.
First, let’s have a look at what the resulting shaders look like in action. We have one shader for the trees (occluders) and one for the player (occluded).
The full shaders are available below. At first glance they might seem really daunting but in reality they are super, duper simple. For some reason, Unity makes us use shaders for accessing the stencil buffer so because of that we have a lot more code than is really required (in “real life” stencil buffer access has nothing to do with a shader). Basically, all there is in these shaders below is a near copy of the standard sprite shader once for the occluder and twice for the occluded sprites and then our tiny stencil section on each. Lets ignore the sprite shader portion (since its so similar to the Unity default sprite shader) and have a look at the actual stencil portions.
First, the occluder sprites. Below is all of the code that actually matters for the stencil buffer. All it is doing is saying for every pixel we write to the color buffer lets replace the stencil buffer value with 4. So, in essence, anywhere there is an occluder pixel it will write 4 to the stencil buffer. Important side note: in the example the tree is not a solid sprite. It has lots of alpha = 0 portions (like most non-rectangle/square sprites) so in the shader we discard any pixels that are less than alpha 0.1. If you have a solid sprite that is not necessary.
123456
Stencil{Ref4CompAlwaysPassReplace}
Occluded sprites need two passes (once for rendering the silhouette and one for rendering the non-occluded portation) so we will have two different stencil sections for them. The first pass is going to render wherever the stencil buffer value is not equal to 4. So, anywhere that there is not an occluder the first pass will render. The second pass is exactly the opposite: wherever the stencil buffer value is equal to 4 it will render. When it renders it multiplies the output by a dark color to make a silhouette. That’s all there is to it.
12345678910111213
// first passStencil{Ref4CompNotEqual}// second passStencil{Ref4CompEqual}
Below is the full shader to be used for any occluder sprites.
Shader"Sprites/Occluder"{Properties{ [PerRendererData]_MainTex("Sprite Texture",2D)="white"{}_Color("Tint",Color)=(1,1,1,1) [MaterialToggle]PixelSnap("Pixel snap",Float)=0_AlphaCutoff("Alpha Cutoff",Range(0.01,1.0))=0.1}SubShader{Tags{"Queue"="Transparent""IgnoreProjector"="True""RenderType"="TransparentCutout""PreviewType"="Plane""CanUseSpriteAtlas"="True"}CullOffLightingOffZWriteOffBlendOneOneMinusSrcAlphaPass{Stencil{Ref4CompAlwaysPassReplace}CGPROGRAM#pragma vertex vert#pragma fragment frag#pragma multi_compile _ PIXELSNAP_ON#include"UnityCG.cginc"structappdata_t{float4vertex:POSITION;float4color:COLOR;float2texcoord:TEXCOORD0;};structv2f{float4vertex:SV_POSITION;fixed4color:COLOR;half2texcoord:TEXCOORD0;};fixed4_Color;fixed_AlphaCutoff;v2fvert(appdata_tIN){v2fOUT;OUT.vertex=mul(UNITY_MATRIX_MVP,IN.vertex);OUT.texcoord=IN.texcoord;OUT.color=IN.color*_Color;#ifdefPIXELSNAP_ONOUT.vertex=UnityPixelSnap(OUT.vertex);#endifreturnOUT;}sampler2D_MainTex;sampler2D_AlphaTex;fixed4frag(v2fIN):SV_Target{fixed4c=tex2D(_MainTex,IN.texcoord)*IN.color;c.rgb*=c.a;// here we discard pixels below our _AlphaCutoff so the stencil buffer only gets written to// where there are actual pixels returned. If the occluders are all tight meshes (such as solid rectangles)// this is not necessary and a non-transparent shader would be a better fit.clip(c.a-_AlphaCutoff);returnc;}ENDCG}}}
Below is the full shader to be used for any occluded sprites.
Shader"Sprites/Occluded"{Properties{ [PerRendererData]_MainTex("Sprite Texture",2D)="white"{}_Color("Tint",Color)=(1,1,1,1) [MaterialToggle]PixelSnap("Pixel snap",Float)=0_OccludedColor("Occluded Tint",Color)=(0,0,0,0.5)}CGINCLUDE// shared structs and vert program used in both the vert and frag programsstructappdata_t{float4vertex:POSITION;float4color:COLOR;float2texcoord:TEXCOORD0;};structv2f{float4vertex:SV_POSITION;fixed4color:COLOR;half2texcoord:TEXCOORD0;};fixed4_Color;sampler2D_MainTex;v2fvert(appdata_tIN){v2fOUT;OUT.vertex=mul(UNITY_MATRIX_MVP,IN.vertex);OUT.texcoord=IN.texcoord;OUT.color=IN.color*_Color;#ifdefPIXELSNAP_ONOUT.vertex=UnityPixelSnap(OUT.vertex);#endifreturnOUT;}ENDCGSubShader{Tags{"Queue"="Transparent""IgnoreProjector"="True""RenderType"="Transparent""PreviewType"="Plane""CanUseSpriteAtlas"="True"}CullOffLightingOffZWriteOffBlendOneOneMinusSrcAlphaPass{Stencil{Ref4CompNotEqual}CGPROGRAM#pragma vertex vert#pragma fragment frag#pragma multi_compile _ PIXELSNAP_ON#include"UnityCG.cginc"fixed4frag(v2fIN):SV_Target{fixed4c=tex2D(_MainTex,IN.texcoord)*IN.color;c.rgb*=c.a;returnc;}ENDCG}// occluded pixel pass. Anything rendered here is behind an occluderPass{Stencil{Ref4CompEqual}CGPROGRAM#pragma vertex vert#pragma fragment frag#pragma multi_compile _ PIXELSNAP_ON#include"UnityCG.cginc"fixed4_OccludedColor;fixed4frag(v2fIN):SV_Target{fixed4c=tex2D(_MainTex,IN.texcoord);return_OccludedColor*c.a;}ENDCG}}}