Search code examples
unity-game-engine3dgpuhlsl

Avoiding an "if" when arbitrarily clamping a value in a HLSL Shader


I'm not experienced in writing shaders, and I've put together a small fragment shader which does chroma keying (makes a certain color and colors similar to it transparent when playing a video):

Shader "Equinox/ChromaKeyShader5" {
    Properties {
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _MaskCol ("Mask Color", Color)  = (1.0, 0.0, 0.0, 1.0)
        _Threshold1 ("Threshold 1", Range(0,1)) = 0.8
        _Threshold2 ("Threshold 2", Range(0,1)) = 0.6
    }

    SubShader {
        CGINCLUDE
            #include "UnityCG.cginc"

        ENDCG

        Pass {
            ZTest Less
            Cull Off
            ZWrite Off
            Lighting Off
            Fog { Mode off }
            Blend SrcAlpha OneMinusSrcAlpha


            CGPROGRAM
                #pragma vertex vert_img
                #pragma fragment frag
                #pragma fragmentoption ARB_precision_hint_fastest
                #pragma alpha

                uniform sampler2D _MainTex;
                uniform float4 _MaskCol;
                uniform float _Threshold1;
                uniform float _Threshold2;

                half4 frag (v2f_img i) : COLOR {
                    half4 c = tex2D(_MainTex, i.uv);
                    half d = distance(c.rgb, _MaskCol.rgb);

                    d = clamp(d, 0, 1); // Do I need it?

                    // TODO: remove if
                    if (d > _Threshold1) {
                        d = 1;
                    } else if (d < _Threshold2) {
                        d = 0;
                    }

                    return half4(c.rgb, d);
                }
            ENDCG
        }
    }
    Fallback off
}

I'm worried about the performance of this if block, with respect to GPU paralelism. Is there a native feature which does that kind of clamping, or another way to write it without a conditional operation?


Solution

  • Your way of clamping is more of a cut-off than a clamp. Doing

    d = clamp(d, _Threshold2, _Threshold1);
    

    would be the normal way of clamping - however, what that method does is more akin to

    if(d > _Threshold1)
        d = _Threshold1;
    else if(d < _Threshold2)
        d = _Threshold2;
    

    If you really want to keep your cut-off behaviour (that sets d to 0 or 1 if it goes below or above those thresholds, instead of setting it to the threshold values), there is no intrinsic function for that.

    Also, while an if in a shader looks scary, shader compiler actually have two different ways to handle this kind of branching: Flattened and Dynamic branching. Dynamic branching is the scary one, which really impacts performance because it doesn't work with the SIMD style of parallism GPUs use. Flattened branching, on the other side, is significantly cheaper in most cases: With that, the GPU will actually execute all the branches of your if, and then lerp between the results based on the branching condition.

    In case of your if, the shader compiler will most likely choose to flatten the branching, which turns your if into something like

    d = lerp(lerp(d, 0, d < _Threshold2), 1, d > _Threshold1);
    

    Note that this rewriting is done automatically by your shader compiler, so for readability it might be best to keep the if and leave the optimisation to the compiler.