-1

I have a shader that allows me to create and rotate a 2 or 3 color gradient. My problem was that it was very heavy on the GPU, so I moved this part of the code from the fragment shader to the vertex shader:

 fixed4 frag (v2f i) : SV_Target
            {   
                //STARTS HERE
                float2 uv =  - (i.screenPos.xy / i.screenPos.w - 0.5)*2;

                fixed3 c;                        
                #if _BG_COLOR_GRADIENT2
                    c = lerp(_BgColor1,_BgColor3,clampValue(rotateUV(uv.xy,_BgColorRotation*PI).y,_BgColorPosition));                                            
                #elif _BG_COLOR_GRADIENT3
                    c = lerp3(_BgColor1,_BgColor2,_BgColor3,clampValue(rotateUV(uv.xy,_BgColorRotation*PI).y,_BgColorPosition),_BgColorPosition3);
                #endif 
                //ENDS HERE         

                return fixed4(c, i.color.a);
            }

Now my shader looks like this:

Shader "Custom/Gradient"
{
    Properties
    {
        [KeywordEnum(Gradient2, Gradient3)] _BG_COLOR ("Color Type", Float) = 1
        _Color("Color", Color) = (1, 1, 1, 1)
        _BgColor1 ("Start Color",Color) = (0.667,0.851,0.937,1)
        _BgColor2 ("Middle Color",Color) = (0.29, 0.8, 0.2,1)
        _BgColor3 ("End Color",Color) = (0.29, 0.8, 0.2,1)
        [GradientPositionSliderDrawer]
        _BgColorPosition ("Gradient Position",Vector) = (0,1,0)
        _BgColorRotation ("Gradient Rotation",Range(0,2)) = 0
        _BgColorPosition3 ("Middle Size",Range(0,1)) = 0

    }

    SubShader
    {
        Tags{ "Queue" = "Background" "IgnoreProjectors"="True" }

        Blend SrcAlpha OneMinusSrcAlpha
        AlphaTest Greater .01
        ColorMask RGB
        Cull Off Lighting Off ZWrite Off
        BindChannels {
        Bind "Color", color
        Bind "Vertex", vertex
        Bind "TexCoord", texcoord
    }

    Pass
    {
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag

        #pragma shader_feature _BG_COLOR_GRADIENT2 _BG_COLOR_GRADIENT3

        #include "UnityCG.cginc"
        #include "GradientHelper.cginc"

        struct appdata
        {
           float4 vertex : POSITION;
           fixed4 color : COLOR;
        };

        struct v2f
        {                
            float4 pos : SV_POSITION;
            float4 screenPos : TEXCOORD4;
            fixed4 color : COLOR;
        };

        fixed4 _BgColor1;
        fixed4 _BgColor2;
        fixed4 _BgColor3;
        float _BgColorRotation;
        float2 _BgColorPosition;
        float _BgColorPosition3;
        float4 _Color;

        v2f vert (appdata v)
        {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);
            o.screenPos = ComputeScreenPos(o.pos);

            float2 uv =  - (o.screenPos.xy / o.screenPos.w - 0.5)*2;                         

            #if _BG_COLOR_GRADIENT2
                o.color = lerp(_BgColor1,_BgColor3,clampValue(rotateUV(uv.xy,_BgColorRotation*PI).y,_BgColorPosition)) * v.color;                                            
            #elif _BG_COLOR_GRADIENT3
                o.color = lerp3(_BgColor1,_BgColor2,_BgColor3,clampValue(rotateUV(uv.xy,_BgColorRotation*PI).y,_BgColorPosition),_BgColorPosition3) * v.color;
            #endif

            return o;
        }

        fixed4 frag (v2f i) : COLOR {
            return i.color;
        }
        ENDCG
        }
    }
    CustomEditor "Background.Editor.BackgroundGradientEditor"
}

(Here is my shader helper):

#ifndef PI
#define PI 3.141592653589793
#endif
#ifndef HALF_PI
#define HALF_PI 1.5707963267948966
#endif

// Helper Funtions

inline float clampValue(float input, float2 limit) 
{
    float minValue = 1-limit.y;
    float maxValue = 1-limit.x;
    if(input<=minValue){
        return 0;
    } else if(input>=maxValue){
        return 1;
    } else {
        return (input - minValue )/(maxValue-minValue);
    }                       
}

inline float2 rotateUV(fixed2 uv, float rotation) 
{
    float sinX = sin (rotation);
    float cosX = cos (rotation);
    float2x2 rotationMatrix = float2x2(cosX, -sinX, sinX, cosX);
    return mul ( uv, rotationMatrix )/2 + 0.5;
}

inline fixed4 lerp3(fixed4 a, fixed4 b, fixed4 c, float pos, float size){

    float ratio2 = 0.5+size*0.5;
    float ratio1 = 1-ratio2;
    if(pos<ratio1)
        return lerp(a,b,pos/ratio1);
    else if(pos>ratio2)
        return lerp(b,c,(pos-ratio2)/ratio1);               
    else
        return b;
}    
#endif

The performance is great now, but the rotation is totally messed up (most noticeable on the 3 color gradient) and I can't seem to figure it out why.

genpfault
  • 51,148
  • 11
  • 85
  • 139
Roland Szép
  • 879
  • 1
  • 8
  • 19
  • 1
    A fragment shader (FS) outputs the color of a pixel. A vertex shader (VS) outputs values for vertices. The OpenGL engine interpolates values (for the FS) using the outputs of the VS, as your `lerp`ed vars. You have an interpolation from rotated screen coordinates. – Ripi2 Nov 13 '18 at 17:35
  • Can you help me, what should I do then? Thank you in advance! – Roland Szép Nov 13 '18 at 18:07
  • 1
    Pass to the VS coordinates and colors of the vertices of the triangle of colors. The OGL will pass to the FS interpolated values (as by using 'lerp') for every point inside the triangle. No 'lerp' needed. No calcs needed in the FS. If you want a rotated triangle, just rotate the vertices in the VS, which needs you pass also, at least an angle. Don't confuse "triangle rotation" with "color rotation" (a gradient). – Ripi2 Nov 13 '18 at 18:16
  • I am so sorry, I am really new to shaders and I am also not a native English speaker. If you could write down the code for me, I would gladly accept it as an answer. – Roland Szép Nov 13 '18 at 18:30
  • I can't write the code, I know nothing on unity3d. I recommend to study some OpenGL tutorial (for version >= 3.2) even it's written in C++. You'll learn how OpenGL works, specially about the automatic interpolation that is done between the VS and the FS. – Ripi2 Nov 13 '18 at 18:33
  • Okay, Thank you a lot! – Roland Szép Nov 13 '18 at 18:36

1 Answers1

2

I never understand why people want to make their gradients inside the shader, it is quite limited and not necessarily more performant unless you are changing the values every frame. My best solution for this would be to generate the gradient as a texture on the CPU, with the size 1x128. Use the Gradient class which is provided by Unity, and loop:

Texture2D texture = new Texture2D(128, 1);
Color[] pixels = Color[128];
for (int i = 0; i < 128; i++) {
    pixels[i] = gradient.Evaluate(i/127f);
}
texture.SetPixels(pixels);
texture.Apply();

Send it to the shader using:

material.SetTexture("_Gradient", texture)

Then, you can rotate and scroll along this texture all you want using a 2x2 matrix like you did. Just make sure to set texture overflow mode to clamp and not repeat. Remember that you can implement OnValidate() into your behavior to apply value updates in the editor, if you need to update it in build though, you will need to listen to changes some other way.

Using vertex colors would indeed be useful for gradients, since these are interpolated in the hardware... but from my understanding, this is a screen-space effect, and as such you would need the vertices to line up with the actual gradient bands.

Kalle Halvarsson
  • 1,240
  • 1
  • 7
  • 15