What is a decal?

Ever played a shooter where bullet holes appear as fire at a wall? Seen blood splattered or footprints across the floor? Then you've most likely seen decals. In game development a decal is a mesh with a texture rendered into a scene that takes no space of its own but is instead projected down onto the underlying geometry. In essence you are rendering stuff onto other stuff.


Example of a decal texture being projected down onto the underlying geometry.

Creating decal shaders in Unity

For some reason Unity has no decal shader available out-of-the-box which I find rather funny because I have based this article upon one written by Unity themselves back 2015 (Extending Unity 5 rendering pipeline: Command Buffers). There are probably plenty of implementation available in the Asset Store as well but they are not particularly hard to create from scratch so why pay for something when you can learn to create it yourself?


This shader relies on the DepthBuffer to be enabled. If you're game uses Deferred rendering this is already enabled, otherwise you have to enable it manually.

(Unity: Enable camera Depth Buffer)


In this article we will create an unlit decal shader (meaning it wont interact with the lightning from its environment). In my work-in-progress game MageQuest these decals will be used for spell casting reticles and direction indicators and hence needs to always be visible regardless of lightning conditions. The theory from this article can however easily be applied to create a Unity surface shader instead.


Textures

To following article needs a texture to be used. Preferably a .png with an alpha channel enabled.

If you don't have a texture to use, download this one and put it in your Unity project.


Create a new shader

The basis of this effect is an Unlit shader. Created one in Unity by Create -> Shader -> Unlit shader and name it DecalUnlit.shader

Shader "MageQuest/DecalUnlit"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Color("Color", Color) = (1, 1, 1, 1)
    }

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

        Pass
        {
            Fog { Mode Off }
            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma target 3.0
            #pragma vertex vert
            #pragma fragment frag
            #pragma exclude_renderers nomrt

            #include "UnityCG.cginc"

            struct v2f
            {
                float4 pos : SV_POSITION;
                half2 uv : TEXCOORD0;
                float4 screenUV : TEXCOORD1;
                float3 ray : TEXCOORD2;
                half3 orientation : TEXCOORD3;
            };

            sampler2D _MainTex;
            fixed4 _Color;

            sampler2D _CameraDepthTexture;

            v2f vert (appdata_base v)
            {
                v2f o;
                UNITY_INITIALIZE_OUTPUT(v2f, o);
                o.pos = UnityObjectToClipPos(v.vertex);
                o.screenUV = ComputeScreenPos(o.pos);
                o.ray = UnityObjectToViewPos(v.vertex).xyz * float3(-1, -1, 1);
                o.orientation = mul((float3x3)unity_ObjectToWorld, float3(0, 1, 0));
                o.uv = v.texcoord;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                i.ray = i.ray * (_ProjectionParams.z / i.ray.z);
                float2 uv = i.screenUV.xy / i.screenUV.w;
                float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv);
                depth = Linear01Depth(depth);
                float4 vpos = float4(i.ray * depth, 1);
                float3 wpos = mul(unity_CameraToWorld, vpos).xyz;
                float3 opos = mul(unity_WorldToObject, float4(wpos, 1)).xyz;

                clip(float3(0.5, 0.5, 0.5) - abs(opos.xyz));

                i.uv = (opos.xz + 0.5);

                fixed4 col = tex2D(_MainTex, i.uv) * _Color;
                return col;
            }
            ENDCG
        }
    }
}


Add a second property for color that can be used to tint the final color of the decal.

_Color("Color", Color) = (1, 1, 1, 1)


Set the tag section so have this material render in the transparent part in the render queue (after all geometry) and make sure it's not a shadow caster.

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


Removing fog

Pretty self explanatory.

Fog { Mode Off }


Making the shader transparent

Disable writing to the z-buffer. This is always a must when rendering transparent materials.

ZWrite Off


Set the blend mode of the shader to write its output additively tot he back buffer.

Blend SrcAlpha OneMinusSrcAlpha


This fragment shader will require a lot of extra data so the v2f struct is extended with lots of extra fields all of which are then calculated in the vertex shader.

struct v2f {
  float4 pos : SV_POSITION;
  half2 uv : TEXCOORD0;
  float4 screenUV : TEXCOORD1;
  float3 ray : TEXCOORD2;
  half3 orientation : TEXCOORD3;
};


When the cameras depths texture is enabled, Unity will automatically populate the

_CameraDepthTexture 
sampler for all shaders using it. You don't need to expose it via the properties section.

sampler2D _CameraDepthTexture;


The vertex shader does a lot of vector maths using built in Unity methods (from

#include "UnityCG.cginc"
). I wont go into details exactly what's happening here (partly because I don't fully understands it, but hey it works ;) ).

v2f vert (appdata_base v){
  v2f o;
  UNITY_INITIALIZE_OUTPUT(v2f, o);
  o.pos = UnityObjectToClipPos(v.vertex);
  o.screenUV = ComputeScreenPos(o.pos);
  o.ray = UnityObjectToViewPos(v.vertex).xyz * float3(-1, -1, 1);
  o.orientation = mul((float3x3)unity_ObjectToWorld, float3(0, 1, 0));
  o.uv = v.texcoord;
  return o;
}


fixed4 frag (v2f i) : SV_Target {
  i.ray = i.ray * (_ProjectionParams.z / i.ray.z);
  float2 uv = i.screenUV.xy / i.screenUV.w;
  float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv);
  depth = Linear01Depth(depth);
  float4 vpos = float4(i.ray * depth, 1);
  float3 wpos = mul(unity_CameraToWorld, vpos).xyz;
  float3 opos = mul(unity_WorldToObject, float4(wpos, 1)).xyz;

  clip(float3(0.5, 0.5, 0.5) - abs(opos.xyz));
   i.uv = (opos.xz + 0.5);

  fixed4 col = tex2D(_MainTex, i.uv) * _Color;
  return col;
 }

Unity: Built in shader helper functions.


Create a material an try the shader

Create a cube and apply a material using this shader to it.

Notes: This shader does not take UV coordinates from the mesh into consideration but instead bases them upon the screen position of the objects. I found it best to only used cubes to apply decals in the scene. The cube must intersect with some other geometry in the scene to appear.