Hue, saturation, brightness, contrast shader

I spent a lot of time on this shader. I hope it can be useful for someone else. There is my final version of the functions:

inline float3 applyHue(float3 aColor, float aHue)
{
    float angle = radians(aHue);
    float3 k = float3(0.57735, 0.57735, 0.57735);
    float cosAngle = cos(angle);
    //Rodrigues' rotation formula
    return aColor * cosAngle + cross(k, aColor) * sin(angle) + k * dot(k, aColor) * (1 - cosAngle);
}


inline float4 applyHSBEffect(float4 startColor, fixed4 hsbc)
{
    float _Hue = 360 * hsbc.r;
    float _Brightness = hsbc.g * 2 - 1;
    float _Contrast = hsbc.b * 2;
    float _Saturation = hsbc.a * 2;

    float4 outputColor = startColor;
    outputColor.rgb = applyHue(outputColor.rgb, _Hue);
    outputColor.rgb = (outputColor.rgb - 0.5f) * (_Contrast) + 0.5f; 
    outputColor.rgb = outputColor.rgb + _Brightness;        
    float3 intensity = dot(outputColor.rgb, float3(0.299,0.587,0.114));
    outputColor.rgb = lerp(intensity, outputColor.rgb, _Saturation);

    return outputColor;
}

The main problem was with the hue. Hue shift is a 3D-vector rotation. Creating quaternion and converting them to the matrix is much heavier than Rodrigues rotation formula.
Use this shader and be happy.

9 Likes

And how I supposed to use this? :slight_smile:

There are just shader functions. I use this effect for NGUI sprites. Sprite color (tint) is used for passing Hue, brightness, contrast, saturation. But this is my situation. Functions can be used in any other shader.
Here is an example:

HSB.cginc file:

inline float3 applyHue(float3 aColor, float aHue)
{
    float angle = radians(aHue);
    float3 k = float3(0.57735, 0.57735, 0.57735);
    float cosAngle = cos(angle);
    //Rodrigues' rotation formula
    return aColor * cosAngle + cross(k, aColor) * sin(angle) + k * dot(k, aColor) * (1 - cosAngle);
}


inline float4 applyHSBEffect(float4 startColor, fixed4 hsbc)
{
    float _Hue = 360 * hsbc.r;
    float _Brightness = hsbc.g * 2 - 1;
    float _Contrast = hsbc.b * 2;
    float _Saturation = hsbc.a * 2;

    float4 outputColor = startColor;
    outputColor.rgb = applyHue(outputColor.rgb, _Hue);
    outputColor.rgb = (outputColor.rgb - 0.5f) * (_Contrast) + 0.5f;
    outputColor.rgb = outputColor.rgb + _Brightness;      
    float3 intensity = dot(outputColor.rgb, float3(0.299,0.587,0.114));
    outputColor.rgb = lerp(intensity, outputColor.rgb, _Saturation);

    return outputColor;
}

Full shader code. Unlit - Transparent HSB.shader file:

Shader "Unlit/Transparent HSB"
{
    Properties
    {
        _MainTex ("Base (RGB), Alpha (A)", 2D) = "black" {}
    }
 
    SubShader
    {
        LOD 100

        Tags
        {
            "Queue" = "Transparent"
            "IgnoreProjector" = "True"
            "RenderType" = "Transparent"
        }
     
        Cull Off
        Lighting Off
        ZWrite Off
        Fog { Mode Off }
        Offset -1, -1
        Blend SrcAlpha OneMinusSrcAlpha

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
             
            #include "UnityCG.cginc"
            #include "HSB.cginc"
 
            struct appdata_t
            {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
                fixed4 color : COLOR;
            };
 
            struct v2f
            {
                float4 vertex : SV_POSITION;
                half2 texcoord : TEXCOORD0;
                fixed4 color : COLOR;
            };
 
            sampler2D _MainTex;
         
            v2f vert (appdata_t v)
            {
                v2f o;
                o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
                o.texcoord = v.texcoord;
                o.color = v.color;
                return o;
            }
             
            fixed4 frag (v2f i) : COLOR
            { 
                float4 startColor = tex2D(_MainTex, i.texcoord);
                float4 hsbColor = applyHSBEffect(startColor, i.color);
                return hsbColor;
            }
            ENDCG
        }
    }

    SubShader
    {
        LOD 100

        Tags
        {
            "Queue" = "Transparent"
            "IgnoreProjector" = "True"
            "RenderType" = "Transparent"
        }
     
        Pass
        {
            Cull Off
            Lighting Off
            ZWrite Off
            Fog { Mode Off }
            Offset -1, -1
            ColorMask RGB
            //AlphaTest Greater .01
            Blend SrcAlpha OneMinusSrcAlpha
            ColorMaterial AmbientAndDiffuse
         
            SetTexture [_MainTex]
            {
                Combine Texture * Primary
            }
        }
    }
}
4 Likes

Note: you can do all that using a single operation.
Build a 3x3 matrix (color transform) and apply it to your vec3 RGB color.

colorRGB *= _Transform3x3;

The matrix can concatenate any number of operation in any order.
And is not llimited hue (luminance preserving or not), levels, contrast, saturation, etcā€¦

This article is over 20 years old now, but you can find a good base implementation.
http://www.graficaobscura.com/matrix/

1 Like

Matrix applying is one operation. But creation of this matrix is not. I used matrix for hue transformation. Creation of transform matrix had too many operations. I converted quaternion to matrix. Shader didnā€™t work on some android devices.

Yes, but the good part is that building the matrix is done outside the pixel shader.
This saves a tremendous amount of pixel shader computation. Build matrix once, apply to all pixel.

The code require in the shader should be this :

fixed4 frag (v2f i): COLOR
{
return mul(_Transform3x3, tex2D(_MainTex, i.texcoord).rgb);
}

1 Like

You are right, sschaem. Iā€™ll try to implement this for NGUI sprites. It should be a good challenge. Itā€™s much easier to use already existed tint color for passing hsb parameters. But easiest is not always the best.

Quite the interesting link, thanks! This in particular was illuminating under ā€œConverting to Luminanceā€:

Seems like UnityCG.cginc OTOH uses:

// Converts color to luminance (grayscale)
inline fixed Luminance( fixed3 c )
{
    return dot( c, fixed3(0.22, 0.707, 0.071) );
}

Now I canā€™t decide what to make of this :smile:

Thanks for sharing Andrey! Brilliant :slight_smile:

I did a contrast shader a while back that had the advantage of being less destructive. You current solution should clamp very quickly. A simple workaround is to apply a ā€œSā€ curve like you would in photoshop, with a simple remap of the range and a power 3 (valvalval), then to modulate the intensity (at the time I used the lerp() like you did for saturation).

best regards

Very useful! A lot of thanks!

@Andrey-Postelzhuk Would this shader be able to solve the contrast degradation problem Iā€™m trying to recreate in this post?

@Ben-BearFish I donā€™t know what a ā€˜contrast degradation problemā€™ is. I just optimized common HSB shader.

1 Like

@Andrey-Postelzhuk Contrast degradation is when the contrast of an image/surface becomes darker the greater the viewing angle is from the center of the object.

So if youā€™re viewing a tv straight on youā€™ll see 100% contrast, but as you move to an angle letā€™s say a 45 degree angle, then the contrast drops (degrades) to 50%. I guess really I need a shader that changes the contrast of the colors on the surface of an object based on the camera viewing angle.

useful! tnx for sharing Andrey!

Thank you all of you. these article very useful for me.
http://www.clonefactor.com/wordpress/program/unity3d/1513/

2 Likes

Thanks a lot for this. Works like a charm :slight_smile:

It keeps black and coloring white, any way to swap it?

Thanks for this Andrey,

Here is a slightly modified version that works for me when I directly copy and paste it into a new .shader file

Shader "Unlit/ColorAdjust"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Hue ("Hue", Range(-360, 360)) = 0.
        _Brightness ("Brightness", Range(-1, 1)) = 0.
        _Contrast("Contrast", Range(0, 2)) = 1
        _Saturation("Saturation", Range(0, 2)) = 1
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };
            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _Hue;
            float _Brightness;
            float _Contrast;
            float _Saturation;
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }
            inline float3 applyHue(float3 aColor, float aHue)
            {
                float angle = radians(aHue);
                float3 k = float3(0.57735, 0.57735, 0.57735);
                float cosAngle = cos(angle);
                //Rodrigues' rotation formula
                return aColor * cosAngle + cross(k, aColor) * sin(angle) + k * dot(k, aColor) * (1 - cosAngle);
            }
            inline float4 applyHSBEffect(float4 startColor)
            {
                float4 outputColor = startColor;
                outputColor.rgb = applyHue(outputColor.rgb, _Hue);
                outputColor.rgb = (outputColor.rgb - 0.5f) * (_Contrast)+0.5f;
                outputColor.rgb = outputColor.rgb + _Brightness;
                float3 intensity = dot(outputColor.rgb, float3(0.299, 0.587, 0.114));
                outputColor.rgb = lerp(intensity, outputColor.rgb, _Saturation);
                return outputColor;
            }
            fixed4 frag (v2f i) : SV_Target
            {
                float4 startColor = tex2D(_MainTex, i.uv);
                float4 hsbColor = applyHSBEffect(startColor);
                return hsbColor;
            }
            ENDCG
        }
    }
}
8 Likes

Thank you so much guys, it works perfectly! :smile:

Saved my bacon. Much thanks you guys. Better than the latest default URP post processing.