Found myself in need of a shader in CG/Shaderlabs to visually represent units becoming invisible. They must be mostly invisible but still have cues present for the keen eye to look for.


Shader template

We'll start with creating a new Surface shader and naming it Invisibility.shader. I have made many posts before on shaders that covers the basis so for this post we will start with most stuff already setup.


The basis for the shader is a surface shader with a GrabPass. This will get the current backbuffer. We will sample it based on world space uv cordinates and end up with a practically invisible unit. This will good as a start. Then we need to make the effect more visible so we can see the unit, or hints of it, better.


Base shader code

Shader "MageQuest/Invisibility"
{
   Properties
   {
   }

   SubShader
   {
      Tags {
         "RenderType" = "Transparent"
         "Queue" = "Transparent"
         "IgnoreProjector" = "True"
         "ForceNoShadowCasting" = "True"
      }

      LOD 200

      GrabPass {
         "_BackgroundTex"
      }

      CGPROGRAM
      #pragma surface surf Standard
      #pragma target 3.0

      sampler2D _BackgroundTex;

      struct Input
      {
         float4 screenPos;
      };

      void surf(Input IN, inout SurfaceOutputStandard o)
      {
         fixed2 uv = (IN.screenPos.xy) / IN.screenPos.w;
         fixed3 backgroundColor = tex2D(_BackgroundTex, uv).rgb;
         o.Emission = backgroundColor;
         o.Alpha = 0;
      }
      ENDCG
   }
   FallBack "Diffuse"
}


Adding some refraction

Add this to the properties block. We will use it to control the amount of refraction.

_Refraction("Refraction", Range(-1, 1)) = 1


Add a corresponding variable in the CG code

fixed _Refraction;


Add this to the Input struct.

float3 worldNormal;
float3 viewDir;


Refraction using a dot prouct

This is a simple solution based on the view direction of the camera and the normal of the polygon being rendered. A dot product is a way to measure how far apar two vectors are from each other. Update the surf method to add a uv offset when sampling the grab pass texture.

void surf(Input IN, inout SurfaceOutputStandard o)
{
   fixed2 uv = (IN.screenPos.xy) / IN.screenPos.w;
   fixed uvOffset = dot(IN.worldNormal, IN.viewDir) * _Refraction / 10;
   fixed3 backgroundColor = tex2D(_BackgroundTex, uv + uvOffset).rgb;
   o.Emission = backgroundColor;
   o.Alpha = 0;
}


Lets try this out, and slowly adjust the

Refraction
property. The result is good, but the dot product only works in a positive range (for polygons facing the camera anyway) so everything gets offset in the same direction.


Refraction using vector subtraction

This solution is similiar to the previous but is even simpler. We will just calculate the difference between the

viewDir
and
worldNormal
vectors and use that as-is (note: as a fixed3 instead of a fixed.

void surf(Input IN, inout SurfaceOutputStandard o)
{
   fixed2 uv = (IN.screenPos.xy) / IN.screenPos.w;
   fixed3 uvOffset = (IN.worldNormal - IN.viewDir) * _Refraction / 10;
   fixed3 backgroundColor = tex2D(_BackgroundTex, uv + uvOffset).rgb;
   o.Emission = backgroundColor;
   o.Alpha = 0;
}


Much better! It feels more uniform.


Conclusion

When possible, go for the simplest solution. Using the delta between the vectors gave as something nice and three dimenstional to work with. Is this an accurate simulation of how refraction works? No! Does it work well enough for a stylized hobby game? Absolutely!