Projector shader with angle limitation

Hi,

when using a projector for projecting bullet holes, the bullet holes will look ugly when the surface has a corner as seen in the picture below:
2651046--186795--Projector.png

The projector used is set to orthographic. I was wondering if it is possible to write a shader for the projector material that checks if the normal of the surface exceeds a certain limit in respect to the projectors angle. The bullet hole is then only rendered where the angle is below this limit.
If the shader would be attached on the bullet hole seen in the picture above, the streched part woud be invisible.

I have some very basic shader knowledge (I wrote some basic surface shaders that mix some textures etc.), but this is currently out of my league. Please let me know if this is technically possible and if so, how I can realize this!

Thanks :slight_smile:

Guys,

I just managed to get it running on my own:

The shader checks the normals of the object, if it exceeds the angle that was set via a property, the projector material is not rendered. The projector material still bends with the surface, if the angle limit is not exceeded. So the shader only removes the ugly stretching of the decal at sharp angles!

I wanted to share my modified version of Unity’s “ProjectorAdditive” shader (please let me know, if the shader could be optimized somehow):

Shader "Custom/ProjectorAdditiveAngleCutoff"
{
    Properties
    {
        _Color ("Main Color", Color) = (1,1,1,1)
        _ShadowTex ("Cookie", 2D) = "" {}
        _FalloffTex ("FallOff", 2D) = "" {}
        _AngleLimit ("Angle Limit (rad)", Float) = 0
    }
   
    Subshader
    {
        Tags {"Queue"="Transparent"}
        Pass
        {
            ZWrite Off
            AlphaTest Greater 0
            ColorMask RGB
            Blend SrcAlpha OneMinusSrcAlpha
            Offset -1, -1
   
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fog
            #include "UnityCG.cginc"
           
            struct v2f {
                float4 uvShadow : TEXCOORD0;
                float4 uvFalloff : TEXCOORD1;
                half projAngle : TEXCOORD2;
                UNITY_FOG_COORDS(2)
                float4 pos : SV_POSITION;
            };
           
            float4x4 _Projector;
            float4x4 _ProjectorClip;
            half3 projNormal;

            inline half angleBetween(half3 vector1, half3 vector2)
            {
                return acos(dot(vector1, vector2) / (length(vector1) * length(vector2)));
            }

            v2f vert (float4 vertex : POSITION, float3 normal : NORMAL)
            {
                v2f o;
                o.pos = mul (UNITY_MATRIX_MVP, vertex);
                o.uvShadow = mul (_Projector, vertex);
                o.uvFalloff = mul (_ProjectorClip, vertex);
                projNormal = mul (_Projector, normal);
                o.projAngle = abs(angleBetween(half3(0,0,-1), projNormal));
                UNITY_TRANSFER_FOG(o,o.pos);
                return o;
            }
           
            fixed4 _Color;
            sampler2D _ShadowTex;
            sampler2D _FalloffTex;
            half _AngleLimit;
           
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 texS = tex2Dproj (_ShadowTex, UNITY_PROJ_COORD(i.uvShadow));
                texS.rgba *= _Color.rgba;
   
                fixed4 texF = tex2Dproj (_FalloffTex, UNITY_PROJ_COORD(i.uvFalloff));
                fixed4 res = texS * texF.a * step(_AngleLimit, i.projAngle);

                UNITY_APPLY_FOG_COLOR(i.fogCoord, res, fixed4(0,0,0,0));
                return res;
            }
            ENDCG
        }
    }
}

Edit: Made some optimizations (moved angle calculation into the vertex shader and replaced if statement in the fragment shader with a step() function)!

1 Like

For future reference, step is implemented as an if statement in the shader compiler; step(y, x) produces an identical shader code to (x >= y ? 1 : 0)

Also if statements aren’t as evil as you’ve heard they are.

Thanks for the hint! So, usually I would assume that using an IF-ELSE statement would be way faster because the whole texture sampling/projection isn’t done when not needed (i.e. when the angle is below the angle limit). So would the following code execute faster?

Like this:

fixed4 frag (v2f i) : SV_Target
            {
                if (i.projAngle > _AngleLimit)
                {
                    fixed4 texS = tex2Dproj (_ShadowTex, UNITY_PROJ_COORD(i.uvShadow));
                    texS.rgba *= _Color.rgba;
    
                    fixed4 texF = tex2Dproj (_FalloffTex, UNITY_PROJ_COORD(i.uvFalloff));
                    fixed4 res = texS * texF.a;
    
                    UNITY_APPLY_FOG_COLOR(i.fogCoord, res, fixed4(0,0,0,0));
                    return res;
                }
                else
                {
                    return fixed4(0, 0, 0, 0);
                }
            }

You might think so, but no. That will most likely be almost exactly the same as your previous shader using step. A confusing part of if statements in shaders is most of the time using an if doesn’t prevent code from running like it does with CPU side code; both sides get calculated and then it chooses the result.

I understand… Thanks for the explanation :wink: