【Unity】LightWeightRPでSpriteの色をHSV制御するシェーダー (色相・彩度・明度・コントラスト)

注意：Unity 2019.2.21 です。

LightWeight RP (Unity2Dで光と影を描写するパイプライン) を導入したプロジェクトで、デフォルトのシェーダー (Sprite-Lit-Default) を改変して色相・彩度・明度・コントラストを変更するシェーダーを作った備忘録です。

ライトの影響を受けつつ、スプライトの色を変更できます。

https://twitter.com/nokonoko312/status/1499559114141503488?s=20&t=w26V35Ao6EpTEkU_teu5gA

シフト関数は以下の記事よりお借りしました。
[ShaderでSpriteの色相をシフトする | Unity | kido tech blog](https://kido0617.github.io/unity/2015-01-03-shader-hue-shift/)

シェーダーわかる方は上の記事だけ読めば大丈夫です。
この記事はどこをどう変更したらどうなるかさっぱりわからない初心者が初心者のために残す記事です。


#### 1. シェーダーを複製する

まず、デフォルトのシェーダーを複製します。

LightWeight RPを導入済みの場合、プロジェクトビューから Packages/LightWeight RP/Shaders/2D を開き、Sprite-Lit-Defaultを右クリック>エクスプローラーで表示 でエクスプローラーを開きます。

開いた画面に「Sprite-Lit-Default.shader」があるので、それを任意のフォルダ (Assets/Shaders など) にドラッグアンドドロップします。
ファイル名を「Sprite-Lit-HSV.shader」などに変更します。

#### 2. シェーダーファイルを書き換える
シェーダーファイルを次のように書き換えます。
めんどくさい場合はまるごとコピペすれば大丈夫です。

```c#:Sprite-Lit-HSV.shader
Shader "Lightweight Render Pipeline/2D/Sprite-Lit-HSV" //変更部分 1
{
    Properties
    {
        //追加部分 1 ------------------------------------------
        _Color ("Tint", Color) = (1,1,1,1)
        _Hue ("Hue", Float) = 0         //色相
        _Sat ("Saturation", Float) = 1  //彩度
        _Val ("Value", Float) = 1       //明度
        _Contrast ("Contrast", Range(0, 1.0)) = 0.5 //コントラスト
        //-----------------------------------------------------
        
        _MainTex("Diffuse", 2D) = "white" {}
        _MaskTex("Mask", 2D) = "white" {}
        _NormalMap("Normal Map", 2D) = "bump" {}
    }

    HLSLINCLUDE
    #include "Packages/com.unity.render-pipelines.lightweight/ShaderLibrary/Core.hlsl"
    ENDHLSL

    SubShader
    {
        Tags {"Queue" = "Transparent" "RenderType" = "Transparent" "RenderPipeline" = "LightweightPipeline" }

        Blend SrcAlpha OneMinusSrcAlpha
        Cull Off
        ZWrite Off

        Pass
        {
            Tags { "LightMode" = "Lightweight2D" }
            HLSLPROGRAM
            #pragma prefer_hlslcc gles
            #pragma vertex CombinedShapeLightVertex
            #pragma fragment CombinedShapeLightFragment
            #pragma multi_compile USE_SHAPE_LIGHT_TYPE_0 __
            #pragma multi_compile USE_SHAPE_LIGHT_TYPE_1 __
            #pragma multi_compile USE_SHAPE_LIGHT_TYPE_2 __
            #pragma multi_compile USE_SHAPE_LIGHT_TYPE_3 __

            struct Attributes
            {
                float3 positionOS   : POSITION;
                float4 color        : COLOR;
                float2  uv           : TEXCOORD0;
            };

            struct Varyings
            {
                float4  positionCS  : SV_POSITION;
                float4  color       : COLOR;
                float2	uv          : TEXCOORD0;
                float2	lightingUV  : TEXCOORD1;
            };

            #include "Packages/com.unity.render-pipelines.lightweight/Shaders/2D/Include/LightingUtility.hlsl"

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);
            TEXTURE2D(_MaskTex);
            SAMPLER(sampler_MaskTex);
            TEXTURE2D(_NormalMap);
            SAMPLER(sampler_NormalMap);
            half4 _MainTex_ST;
            half4 _NormalMap_ST;
            //追加部分 2 ------------------------------------------
            float4 _Color;
            half _Hue, _Sat, _Val, _Contrast;
            //-----------------------------------------------------

            #if USE_SHAPE_LIGHT_TYPE_0
            SHAPE_LIGHT(0)
            #endif

            #if USE_SHAPE_LIGHT_TYPE_1
            SHAPE_LIGHT(1)
            #endif

            #if USE_SHAPE_LIGHT_TYPE_2
            SHAPE_LIGHT(2)
            #endif

            #if USE_SHAPE_LIGHT_TYPE_3
            SHAPE_LIGHT(3)
            #endif

            Varyings CombinedShapeLightVertex(Attributes v)
            {
                Varyings o = (Varyings)0;

                o.positionCS = TransformObjectToHClip(v.positionOS);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                float4 clipVertex = o.positionCS / o.positionCS.w;
                o.lightingUV = ComputeScreenPos(clipVertex).xy;
                o.color = v.color * _Color; //変更部分 2 (*を+にすれば加算染色になる)
                
                return o;
            }
            
            //追加部分 3 ------------------------------------------
            float4 shift_col(float4 RGBA, half4 shift)
            {
                float contrast = shift.a * 2;
                float4 RESULT = float4(RGBA);
                float VSU = shift.z*shift.y*cos(shift.x*3.14159265/180);
                float VSW = shift.z*shift.y*sin(shift.x*3.14159265/180);
                
                RESULT.x = (.299*shift.z+.701*VSU+.168*VSW)*RGBA.x
                    + (.587*shift.z-.587*VSU+.330*VSW)*RGBA.y
                    + (.114*shift.z-.114*VSU-.497*VSW)*RGBA.z;
                
                RESULT.y = (.299*shift.z-.299*VSU-.328*VSW)*RGBA.x
                    + (.587*shift.z+.413*VSU+.035*VSW)*RGBA.y
                    + (.114*shift.z-.114*VSU+.292*VSW)*RGBA.z;
                
                RESULT.z = (.299*shift.z-.3*VSU+1.25*VSW)*RGBA.x
                    + (.587*shift.z-.588*VSU-1.05*VSW)*RGBA.y
                    + (.114*shift.z+.886*VSU-.203*VSW)*RGBA.z;
                
                RESULT.xyz = (RESULT.xyz - 0.5f) * contrast + 0.5f;
                
                return (RESULT);
            }
            //-----------------------------------------------------

            #include "Packages/com.unity.render-pipelines.lightweight/Shaders/2D/Include/CombinedShapeLightShared.hlsl"

            half4 CombinedShapeLightFragment(Varyings i) : SV_Target
            {
                half4 main = i.color * SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
                
                //追加部分 4 ------------------------------------------
                half4 shift = half4(_Hue, _Sat, _Val, _Contrast);
                main = float4( shift_col(main, shift) );
                //-----------------------------------------------------
                
                half4 mask = SAMPLE_TEXTURE2D(_MaskTex, sampler_MaskTex, i.uv);

                return CombinedShapeLightShared(main, mask, i.lightingUV);
            }
            ENDHLSL
        }

        Pass
        {
            Tags { "LightMode" = "NormalsRendering"}
            HLSLPROGRAM
            #pragma prefer_hlslcc gles
            #pragma vertex NormalsRenderingVertex
            #pragma fragment NormalsRenderingFragment

            struct Attributes
            {
                float3 positionOS   : POSITION;
                float4 color		: COLOR;
                float2 uv			: TEXCOORD0;
            };

            struct Varyings
            {
                float4  positionCS		: SV_POSITION;
                float4  color			: COLOR;
                float2	uv				: TEXCOORD0;
                float3  normalWS		: TEXCOORD1;
                float3  tangentWS		: TEXCOORD2;
                float3  bitangentWS		: TEXCOORD3;
            };

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);
            TEXTURE2D(_NormalMap);
            SAMPLER(sampler_NormalMap);
            float4 _NormalMap_ST;  // Is this the right way to do this?

            Varyings NormalsRenderingVertex(Attributes attributes)
            {
                Varyings o = (Varyings)0;

                o.positionCS = TransformObjectToHClip(attributes.positionOS);
                o.uv = TRANSFORM_TEX(attributes.uv, _NormalMap);
                o.uv = attributes.uv;
                o.color = attributes.color;
                o.normalWS = TransformObjectToWorldDir(float3(0, 0, 1));
                o.tangentWS = TransformObjectToWorldDir(float3(1, 0, 0));
                o.bitangentWS = TransformObjectToWorldDir(float3(0, 1, 0));
                return o;
            }

            #include "Packages/com.unity.render-pipelines.lightweight/Shaders/2D/Include/NormalsRenderingShared.hlsl"

            float4 NormalsRenderingFragment(Varyings i) : SV_Target
            {
                float4 mainTex = i.color * SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
                float3 normalTS = UnpackNormal(SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, i.uv));
                return NormalsRenderingShared(mainTex, normalTS, i.tangentWS.xyz, i.bitangentWS.xyz, -i.normalWS.xyz);
            }
            ENDHLSL
        }
        Pass
        {
            Tags { "LightMode" = "LightweightForward" "Queue"="Transparent" "RenderType"="Transparent"}

            HLSLPROGRAM
            #pragma prefer_hlslcc gles
            #pragma vertex UnlitVertex
            #pragma fragment UnlitFragment

            struct Attributes
            {
                float3 positionOS   : POSITION;
                float4 color		: COLOR;
                float2 uv			: TEXCOORD0;
            };

            struct Varyings
            {
                float4  positionCS		: SV_POSITION;
                float4  color			: COLOR;
                float2	uv				: TEXCOORD0;
            };

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);
            float4 _MainTex_ST;

            Varyings UnlitVertex(Attributes attributes)
            {
                Varyings o = (Varyings)0;

                o.positionCS = TransformObjectToHClip(attributes.positionOS);
                o.uv = TRANSFORM_TEX(attributes.uv, _MainTex);
                o.uv = attributes.uv;
                o.color = attributes.color;
                return o;
            }

            float4 UnlitFragment(Varyings i) : SV_Target
            {
                float4 mainTex = i.color * SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
                return mainTex;
            }
            ENDHLSL
        }
    }
    Fallback "Hidden/Sprite-Fallback"
}

```

変更部分が2つ、追加部分が4つあります。

変更部分1：
Unityエディター上で表示するシェーダーの名前を Sprite-Lit-Default から Sprite-Lit-HSV に変えています。
```C#
Shader "Lightweight Render Pipeline/2D/Sprite-Lit-HSV"
```

追加部分1：
エディター上から編集可能な変数を追加しました。スクリプトの [SerializeField] みたいなやつです。
```C#
Properties
    {
        //追加部分 1 ------------------------------------------
        _Color ("Tint", Color) = (1,1,1,1)
        _Hue ("Hue", Float) = 0         //色相
        _Sat ("Saturation", Float) = 1  //彩度
        _Val ("Value", Float) = 1       //明度
        _Contrast ("Contrast", Range(0, 1.0)) = 0.5 //コントラスト
        //-----------------------------------------------------
        
        /*略*/
    }
```

追加部分2：
Propertiesで定義した変数はpass内で定義しなおさないと使えません。
初めてシェーダーさわったので、float4ってfloat型変数が4つ並んだベクトルってことか～なるほど～！ってなりました。時間できたらちゃんと勉強したいです。
```C#
float4 _Color;
half _Hue, _Sat, _Val, _Contrast;
```

変更部分2：
出力の色変数に_Color変数を乗算します。
乗算すると色が暗くなり、加算すると色が明るくなります。
お好みで使い分けてください。
```C#
Varyings CombinedShapeLightVertex(Attributes v)
{
   /*略*/
   o.color = v.color * _Color; //変更部分 2 (*を+にすれば加算染色になる)
                
   return o;
}
```

追加部分3：
入力された色を shift(色相・彩度・明度・コントラスト)に変更して返す関数です。記事の最初に乗せた参考サイトからお借りした関数ほぼそのままです。
型とか透明度とかだけちょっといじったのと、コントラスト処理を追加しています。原理はちゃんとわかってないのでわかる人に聞いてください。
```C#
float4 shift_col(float4 RGBA, half4 shift)
{
    float contrast = shift.a * 2;
    float4 RESULT = float4(RGBA);
    float VSU = shift.z*shift.y*cos(shift.x*3.14159265/180);
    float VSW = shift.z*shift.y*sin(shift.x*3.14159265/180);
    
    RESULT.x = (.299*shift.z+.701*VSU+.168*VSW)*RGBA.x
        + (.587*shift.z-.587*VSU+.330*VSW)*RGBA.y
        + (.114*shift.z-.114*VSU-.497*VSW)*RGBA.z;
    
    RESULT.y = (.299*shift.z-.299*VSU-.328*VSW)*RGBA.x
        + (.587*shift.z+.413*VSU+.035*VSW)*RGBA.y
        + (.114*shift.z-.114*VSU+.292*VSW)*RGBA.z;
    
    RESULT.z = (.299*shift.z-.3*VSU+1.25*VSW)*RGBA.x
        + (.587*shift.z-.588*VSU-1.05*VSW)*RGBA.y
        + (.114*shift.z+.886*VSU-.203*VSW)*RGBA.z;
    
    RESULT.xyz = (RESULT.xyz - 0.5f) * contrast + 0.5f;
    
    return (RESULT);
}
```

追加部分4：
エディター上で指定した色相・彩度・明度・コントラストをshiftベクトルにひとまとめにして、追加部分3で追加した関数に渡しています。
ここで、Varyings i はたぶんこのシェーダーの入力なので、入力の色をshift_col関数で変更しています。今回は入力側でshift_col関数を実行しましたが、出力側でも同じなんじゃないかな(要検証)

```C#
half4 CombinedShapeLightFragment(Varyings i) : SV_Target
{
    half4 main = i.color * SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
    
    //追加部分 4 ------------------------------------------
    half4 shift = half4(_Hue, _Sat, _Val, _Contrast);
    main = float4( shift_col(main, shift) );
    //-----------------------------------------------------
    
    half4 mask = SAMPLE_TEXTURE2D(_MaskTex, sampler_MaskTex, i.uv);

    return CombinedShapeLightShared(main, mask, i.lightingUV);
}
```

#### 3. マテリアルを設定する

シェーダーができたので、マテリアルを作成します。
プロジェクトビューの任意のフォルダ (Assets/Materials など) で 右クリック>作成>マテリアル と選ぶと新しくマテリアルが作成されます。名前は何でもいいですが、「Sprite-Lit-HSV」にしておくとわかりやすいです。

マテリアルのインスペクタービューで、Shader を Lightweight Render Pipeline/2D/Sprite-Lit-HSV に変更します。

最後に、マテリアルを任意のスプライトオブジェクトの SpriteRenderer の Material に設定しておわりです。