シンプルなImageEffect　その12。HSV調整

TwitterにアップしたHSV調整イメージエフェクトのシェーダコードを貼っておきます。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">先日RTした「ニーアらしさ」を出せるように、彩度を調整できるImageEffectシェーダ書いた。<br>ついでにHSVの微調整も出来るようにしました。<br><br>HSVtoRGB変換は基礎的なやつだけど、そういえばQiitaにまだ記事を書いてなかったので今日の夜にでも書きます。 <a href="https://t.co/a1lC3mvFTx">pic.twitter.com/a1lC3mvFTx</a></p>&mdash; MIYAKE (@ScreenPocket) <a href="https://twitter.com/ScreenPocket/status/1289837792285999104?ref_src=twsrc%5Etfw">August 2, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

きっかけはyokotaro氏のツイート
「ニーアらしさとは「Photoshopで彩度を半分にすること」です」
というツイートを見た事。
10個近くImageEffectを投稿してきましたが、そういえばHSVを調整できるイメージエフェクトを作ってなかったですね。。
HSV調整ができれば、ImageEffect上で「ニーアらしさ」を実装できるはず。

手順としては、RGBカラーをHSV(色相、彩度、明度)空間に変換して、調整し再びRGBカラーに戻します。
RGB→HSV、HSV→RGBの変換式についてはググれば山の様に出てきますが、
せっかくなのでUnityEngine.Color.RGBtoHSV()、UnityEngine.Color.HSVtoRGB()のコードを参考にして実装します。
※下記コードで言うとRGBToHSVHelper(),RGBtoHSV(), HSVtoRGB()が参考＆修正部分です。

```glsl:hsv.shader
Shader "ScreenPocket/ImageEffect/HSV"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _HueOffset("Hue Offset", Range(-1,1)) = 0
        _SaturationMultiply("Saturation Multiply", Range(0,2)) = 1
        _SaturationOffset("Saturation Offset", Range(-1,1)) = 0
        _ValueMultiply("Value Multiply", Range(0,2)) = 1
        _ValueOffset("Value Offset", Range(-1,1)) = 0
    }
    SubShader
    {
        // No culling or depth
        Cull Off ZWrite Off ZTest Always

        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;
            };

            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            half3 RGBToHSVHelper(
                half offset,
                half dominantColor,
                half colorOne,
                half colorTwo)
            {
                half3 hsv;
                hsv.z = dominantColor;
                if (hsv.z == 0.0)
                {
                    hsv.x = 0;
                    hsv.y = 0;
                    return hsv;
                }
                
                half num1 = colorOne <= colorTwo ? colorOne : colorTwo;
                half num2 = hsv.z - num1;
                if (num2 != 0.0)
                {
                    hsv.y = num2 / hsv.z;
                    hsv.x = offset + (colorOne - colorTwo) / num2;
                }
                else
                {
                    hsv.y = 0.0f;
                    hsv.x = offset + (colorOne - colorTwo);
                }
                
                hsv.x /= 6.0;
                if ( hsv.x >= 0.0)
                    return hsv;
                hsv.x += 1.0;
                return hsv;
            }
            
            half3 RGBtoHSV(half3 rgb)
            {
                if (rgb.b > rgb.g && rgb.b > rgb.r)
                    return RGBToHSVHelper(4.0, rgb.b, rgb.r, rgb.g);
                if (rgb.g > rgb.r)
                    return RGBToHSVHelper(2.0, rgb.g, rgb.b, rgb.r);

                return RGBToHSVHelper(0.0, rgb.r, rgb.g, rgb.b);
            }

            half3 HSVtoRGB(half3 hsv)
            {
                if (hsv.y == 0.0)
                {
                    return hsv.zzz;
                }
                
                if (hsv.z == 0.0)
                {
                    return half3(0,0,0);
                }

                half num1 = hsv.y;
                half num2 = hsv.z;
                half f = hsv.x * 6.0;
                int num3 = (int)floor(f);
                half num4 = f - (half)num3;
                half num5 = num2 * (1.0 - num1);
                half num6 = num2 * (1.0 - num1 * num4);
                half num7 = num2 * (1.0 - num1 * (1.0 - num4));
                
                switch (num3)
                {
                case -1:
                    return saturate(half3(num2,num5,num6));
                case 0:
                    return saturate(half3(num2,num7,num5));
                case 1:
                    return saturate(half3(num6,num2,num5));
                case 2:
                    return saturate(half3(num5,num2,num7));
                case 3:
                    return saturate(half3(num5,num6,num2));
                case 4:
                    return saturate(half3(num7,num5,num2));
                case 5:
                    return saturate(half3(num2,num5,num6));
                case 6:
                    return saturate(half3(num2,num7,num5));
                default:
                    return half3(0,0,0);
                }
            }

            half _HueOffset;
            half _SaturationMultiply;
            half _SaturationOffset;
            half _ValueMultiply;
            half _ValueOffset;
            sampler2D _MainTex;

            half4 frag (v2f i) : SV_Target
            {
                half4 col = tex2D(_MainTex, i.uv);
                half3 hsv = RGBtoHSV(col.rgb);

                hsv.x = frac(hsv.x + _HueOffset);
                hsv.y = saturate(hsv.y * _SaturationMultiply + _SaturationOffset );
                hsv.z = saturate(hsv.z * _ValueMultiply + _ValueOffset );
                
                col.rgb = HSVtoRGB(hsv);
                return col;
            }
            ENDCG
        }
    }
}
```
ポイントはHSVのH(色相)に関してだけは周期性があるのでsaturate()ではなくfrac()を使っている所でしょうか。
あと、計算式はLDR用に元コードの一部のHDR用if文を端折っていたりします。
※HLSL化した時に面倒くさいので今回からfixedは使わないようにしました。fixed化したい場合は該当箇所を書き換えてください。

_SaturationMultiplyに0.5を入れれば「ニーアらしさ」をリスペクト出来るかと。
他にも色相調整でガラッと雰囲気を変えることも出来るので、色々試してみたいですね。