Search code examples
unity-game-engineshaderhlslcg

Prevent transparent areas being shaded by projection shader


I'm trying to make a decal shader to use with a projector in Unity. Here's what I've put together:

Shader "Custom/color_projector"
{
    Properties {
        _Color ("Tint Color", Color) = (1,1,1,1)
        _MainTex ("Cookie", 2D) = "gray" {}
    }
    Subshader {
        Tags {"Queue"="Transparent"}

        Pass {

            ZTest Less
            ColorMask RGB
            Blend One OneMinusSrcAlpha
            Offset -1, -1

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct v2f {
                float4 uvShadow : TEXCOORD0;
                float4 pos : SV_POSITION;
            };

            float4x4 unity_Projector;
            float4x4 unity_ProjectorClip;

            v2f vert (float4 vertex : POSITION)
            {
                v2f o;
                o.pos = UnityObjectToClipPos (vertex);
                o.uvShadow = mul (unity_Projector, vertex);
                return o;
            }

            sampler2D _MainTex;
            fixed4 _Color;

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 tex = tex2Dproj (_MainTex, UNITY_PROJ_COORD(i.uvShadow));
                return _Color * tex.a;

            }
            ENDCG
        }
    }
}

This works well in most situations:

enter image description here

However, whenever it projects onto a transparent surface (or multiple surfaces) it seems to render an extra time for each surface. Here, I've broken up the divide between the grass and the paving using grass textures with transparent areas:

enter image description here

I've tried numerous blending and options and all of the ZTesting options. This is the best I can get it to look.

From reading around I gather this might be because the a transparent shader does not write to the depth buffer. I tried adding ZWrite On and I tried doing a pass before the main pass:

Pass {
   ZWrite On
   ColorMask 0
}

But neither had any effect at all.

How can this shader be modified so that it only projects the texture once on the nearest geometries?

Desired result (photoshopped):

enter image description here


Solution

  • The problem is due to how projectors work. Basically, they render all meshes within their field of view a second time, except with a different shader. In your case, this means that both the ground and the plane with the grass will be rendered twice and layered on top of each other. I think it could be possible to fix this using two steps;

    First, add the following to the tags of the transparent (grass) shader:

    "IgnoreProjector"="True"
    

    Then, change the render queue of your projector from "Transparent" to "Transparent+1". This means that the ground will render first, then the grass edges, and finally the projector will project onto the ground (except appearing on top, since it is rendered last).

    As for the blending, i think you want regular alpha blending:

    Blend SrcAlpha OneMinusSrcAlpha
    

    Another option if you are using deferred rendering is to use deferred decals. These are both cheaper and usually easier to use than projectors.