How to create a 4-corner gradient shader?

I want to create a four-corner gradient shader.

I saw this post but I don’t want to do it via texture or setting vertex colors if possible because I’m gonna be changing the corners’ colors every frame.

It’d be a great help if someone could point me in the right direction.

You just need to define four colors on the shader and lerp between them based on UVs.

fixed4 colorTop = lerp(_ColorA, _ColorB, i.uv.x);
fixed4 colorBottom = lerp(_ColorC, _ColorD, i.uv.x);
return lerp(colorTop, colorBottom, i.uv.y);

2 Likes

I’m not trying to lerp vertically, I want to lerp from each corner, so four unique colors.
[Edit] What @bgolus posted works, I’m an idiot

As far as I can see, it does that.

1 Like

Ah, you’re right. I miss-read it, thanks and sorry haha.
3179393--242405--upload_2017-8-10_14-25-41.jpg
3179393--242404--FourCornerGradient.gif

2 Likes

That’s what that code does. That’s bilinear sampling written out using 3 lerps (which, yes, bilinear is 3 lerps, trilinear is actually 6 lerps!)

You’re totally right, I misread, sorry and thanks!

Using stuff on this thread, and another shader, I was able to figure out a shader that works for me. I hope others find it helpful too. (Screen space gradient, UV texture)

Shader "Unlit/Transparent Color Gradient" {
    Properties{
        _MainTex("Base (RGB) Trans (A)", 2D) = "white" { }
        _ColorA("Color Bottom Left", Color) = (1.000000,1.000000,1.000000,1.000000)
        _ColorB("Color Top Left", Color) = (0.000000,1.000000,1.000000,1.000000)
        _ColorC("Color Bottom Right", Color) = (1.000000,0.000000,1.000000,1.000000)
        _ColorD("Color Top Right", Color) = (1.000000,1.000000,0.000000,1.000000)
    }
 
        SubShader{
            LOD 100
            Tags{ "QUEUE" = "Transparent" "IGNOREPROJECTOR" = "true" "RenderType" = "Transparent" }
            Pass{
            Tags{ "QUEUE" = "Transparent" "IGNOREPROJECTOR" = "true" "RenderType" = "Transparent" }
            Blend SrcAlpha OneMinusSrcAlpha
    
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 2.0
            #include "UnityCG.cginc"
            #pragma multi_compile_fog
            #define USING_FOG (defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2))

            // uniforms
            float4 _MainTex_ST;

            // vertex shader input data
            struct appdata {
                float3 pos : POSITION;
                float3 uv0 : TEXCOORD0;
            };

            // vertex-to-fragment interpolators
            struct v2f {
                fixed4 color : COLOR0;
                float2 uv0 : TEXCOORD0;
                #if USING_FOG
                    fixed fog : TEXCOORD1;
                #endif
                float4 pos : SV_POSITION;
                float4 screenPos: TEXCOORD2;
            };

            // vertex shader
            v2f vert(appdata IN) {
                v2f o;
                half4 color = half4(0,0,0,1.1);
                float3 eyePos = mul(UNITY_MATRIX_MV, float4(IN.pos,1)).xyz;
                half3 viewDir = 0.0;
                o.color = saturate(color);
                // compute texture coordinates
                o.uv0 = IN.uv0.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                // fog
                #if USING_FOG
                    float fogCoord = length(eyePos.xyz); // radial fog distance
                    UNITY_CALC_FOG_FACTOR(fogCoord);
                    o.fog = saturate(unityFogFactor);
                #endif
                // transform position
                o.pos = UnityObjectToClipPos(IN.pos);
                o.screenPos = ComputeScreenPos(o.pos);
                return o;
            }

            // textures
            sampler2D _MainTex;
            fixed4 _ColorA;
            fixed4 _ColorB;
            fixed4 _ColorC;
            fixed4 _ColorD;

            // fragment shader
            fixed4 frag(v2f IN) : SV_Target{
                fixed4 col;
                fixed4 tex, tmp0, tmp1, tmp2;
                // SetTexture #0
                tex = tex2D(_MainTex, IN.uv0.xy);

                float2 screenPosition = (IN.screenPos.xy / IN.screenPos.w);

                //float2 screenUV = IN.screenPos.xy / IN.screenPos.w;
                fixed4 colorTop = lerp(_ColorA, _ColorB, screenPosition.y);
                fixed4 colorBottom = lerp(_ColorC, _ColorD, screenPosition.y);
                fixed4 color =  lerp(colorTop, colorBottom, screenPosition.x);

                col.rgb = tex * color;
                col.a = tex.a * color.a;
                // fog
                #if USING_FOG
                    col.rgb = lerp(unity_FogColor.rgb, col.rgb, IN.fog);
                #endif
                return col;
            }

            // texenvs
            //! TexEnv0: 01010102 01050106 [_MainTex] [_Color]
            ENDCG
        }
    }
    Fallback "Standard"
}

The texture is applied based on UVs, not screen space, but the colour is purely based on screen location. Transparency works too, which is pretty cool. This is the first actually working shader I’ve ever made, so sorry if there’s any bugs.

1 Like

Also the best way to do “ifs” in shaders… Lerp everything.


https://twitter.com/bgolus/status/1235254923819802626?s=20

2 Likes

Well to be honest, I always forget the rules that make the compiler branch or not with ifs. Especially since different platforms have different rules. So I just lerp everything.

The TLDR version, is assume it’s never going to branch, unless it’s on a platform where that’s fast enough to not be a problem.

You mean assume it’s always going to branch?

I suspect they mean that it will not branch because the compilers are pretty smart.

Step was actually faster for ES2.x mobiles though, across a fair few. I think that may of been the result of compilers not being as good back then or branches doing both sides of a calculation anyway, which as you can imagine, made things worse back then. This is definitely 10 years + ago so it shows how information can get stale.

Yeah, it’s almost never a branch.

AFAIK this was true for all GLES 2.0 hardware as they had a hardware level step() function. Also, they don’t support branches, at all, so an if was never a branch. On some devices an if would automatically get compiled into a lerp for you.

On modern GPUs & shader compilers, step(y, x) produces identical compiled shader code as x >= y ? 1.0 : 0.0 or float val = 0.0; if (x >= y) val = 1.0;.

This (usually) compiles to a single ge (Greater than or Equal comparison) instruction. Which isn’t a branch. It’s just picking between two already calculated values.

On modern GPUs, even ones that support branches, the vast majority of if statements end up as a comparison instruction where “both sides” always get calculated, and then the result of one “side” is thrown out.

You might think using a real branch would solve this, but you’d be wrong. Branches still do “both sides” of the calculation. At least most of the time. GPUs run groups of pixels at a time, usually in 8x8 or similar sized blocks. These are called “waves” or “warps” depending on the GPU. If any pixel in the warp needs one side of a branch, all pixels in the warp pay the cost of calculating that side of the branch. So if not all pixels within a warp do the same branch, all pixels are now paying the cost of both sides of the branch, plus the additional cost of the branch instruction itself. If all pixels within a warp are only doing one side of the branch, than they only pay the cost of that one side, plus the cost of the branch instruction. The main issue is the branch instruction is itself not free, so the work being skipped needs to be significantly more costly than the branch instruction is adding. And most of the time the shader compiler will guess that it won’t be, and will compile the shader to not use a branch.

If which side of the branch can be guaranteed before the shader runs, like specifically if the value being compared is a material property against a constant or another material property, then the cost of the branch instruction is much less. Basically free at that point, and you really do only pay the cost of one side of the branch. This is basically the only time a shader compiler will for sure actually use a branch.

But here’s the last kicker. If the shader compiler thinks it’ll be faster, sometimes that step might be turned into a real branch. Because as I eluded to above, the compiler doesn’t differentiate between a step, a ternary (comp ? a : b), or an if.

Too bad you can still find brand new phones made with a (literally) 10 years old mobile GPU:
https://blucellphones.us/blu-c6l-2020-specifications/