[Unity] Shader Graph で生成した Shaderを Mask / RectMask2Dに対応させる

# 背景
どうも今川です。
[Sprite 無しで枠線付き角丸四角形の図形を uGUI で描画する方法](https://qiita.com/t_imagawa/items/b5bdca93a220cd4ef492) にて、 Shader Graph で角丸四角形の図形を描画する方法を紹介しましたが、 Shader Graph で生成した Shader を利用すると Mask / RectMask2D が機能しません。
これは、 Shader Graph が、ステンシルなど uGUI のマスクに関するシステムに対応していないからです。

これらを対応するには、Shader Graph で生成した Shader を書き換えて対応する必要があり、その方法を紹介したいと思います。

# 事前準備
- MaskableGraphic を継承したコンポーネントを利用して、マテリアルの更新を行う
- Shader Graph から Shader を生成する

## MaskableGraphic を継承したコンポーネントを利用して、マテリアルの更新を行う
MaskableGraphic を継承したコンポーネントを利用して、 [IMaterialModifier.GetModifiedMaterial](https://docs.unity3d.com/Packages/com.unity.ugui@2.0/api/UnityEngine.UI.IMaterialModifier.html?q=IMaterialModifier#methods) でマテリアルの更新を行います。
これにより、 Mask / RectMask2D のマスクで必要な ShaderProperty に値が渡ります。

要するに、MaskableGraphicを継承してコンポーネントを作成し、[IMaterialModifier.GetModifiedMaterial](https://docs.unity3d.com/Packages/com.unity.ugui@2.0/api/UnityEngine.UI.IMaterialModifier.html?q=IMaterialModifier#methods) を実装したいのであれば、  ```base.GetModifiedMaterial``` で取得されたマテリアルを利用すればいいわけです。

``` cs
_material.CopyMatchingPropertiesFromMaterial(base.GetModifiedMaterial(baseMaterial));
```

## Shader Graph から Shader を生成する

Shader Graph の Inspector View から View Generated Shader を押して生成しコードをコピーしファイルに保存します。
![スクリーンショット 2023-11-26 23.13.32_c.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121445/3d01c59c-2e1c-57bb-4c3d-3631ca173943.png)

Shader Graph で生成される SubShader の Pass は、複数生成される事がありますが
Sprite の描画用の Pass は、 ```"LightMode" = "Universal2D"``` の Pass になります。

# Mask / RectMask2D に対応

では、Mask / RectMask2D の対応方法をそれぞれ紹介していきます。

## Maskの対応

Maskは、ステンシルによるマスクにより実現しています。
そのため、対応方法はステンシルを追加実装するだけです。

- ZTest の変更
- ShaderProperty の追加
- Stencil と ColorMask の追加

### ZTest の変更


*SubShader / Pass* の **ZTest** を ```[unity_GUIZTestMode]``` に変更します。
**unity_GUIZTestMode** にすることで、
> 描画対象のマテリアルが所属するCanvasのRender Modeの設定

に合わせて適切に *ZTest* を設定してくれます。
引用: https://zenigane138.hateblo.jp/entry/2018/06/29/231816
``` diff
            // Render State
            Cull Off
                Blend SrcAlpha OneMinusSrcAlpha, One OneMinusSrcAlpha
-                ZTest LEqual
+                ZTest [unity_GUIZTestMode]
```

### ShaderProperty の追加

*Stencil* と *ColorMask* の設定に必要な ShaderProperty が必要なため、以下を *Properties* に追加します

``` diff
+            _StencilComp("Stencil Comparison", Float) = 8
+            _Stencil("Stencil ID", Float) = 0
+            _StencilOp("Stencil Operation", Float) = 0
+            _StencilWriteMask("Stencil Write Mask", Float) = 255
+            _StencilReadMask("Stencil Read Mask", Float) = 255
+            _ColorMask("Color Mask", Float) = 15
            [HideInInspector][NoScaleOffset]unity_Lightmaps("unity_Lightmaps", 2DArray) = "" {}
            [HideInInspector][NoScaleOffset]unity_LightmapsInd("unity_LightmapsInd", 2DArray) = "" {}
            [HideInInspector][NoScaleOffset]unity_ShadowMasks("unity_ShadowMasks", 2DArray) = "" {}
```

### Stencil と ColorMask の追加

*SubShader / Pass* に **Stencil** と **ColorMask** を追加します。

``` diff
            ZWrite Off

+            Stencil
+            {
+                Ref [_Stencil]
+                Comp [_StencilComp]
+                Pass [_StencilOp]
+                ReadMask [_StencilReadMask]
+                WriteMask [_StencilWriteMask]
+            }
+            ColorMask [_ColorMask]
```

以上で Mask の対応は完了です。

## RectMask2D の対応

RectMask2D は、 *ClipRect(マスク領域)* からマスクするかを判断し、Alpha値を更新する必要があります。
そのため、 ```SurfaceDescription SurfaceDescriptionFunction(SurfaceDescriptionInputs IN)```内の Alpha値の最終出力時に、 RectMask2D に対応するマスク処理を入れることで対応します。

- シェーダキーワード UNITY_UI_CLIP_RECT の追加
- Uniform 変数の追加
- positionOS オブジェクト空間（Object Space) の追加
- RectMask2D に対応するマスク処理の追加

### シェーダキーワード UNITY_UI_CLIP_RECT の追加
RectMask2D によって対象がマスクされる時、 シェーダキーワード UNITY_UI_CLIP_RECT が有効になります。
そのため、 UNITY_UI_CLIP_RECT を利用するために *SubShader / Pass* に宣言します。

``` diff
            // Keywords
+            #pragma multi_compile_local _ UNITY_UI_CLIP_RECT
            #pragma multi_compile_fragment _ DEBUG_DISPLAY
            // GraphKeywords: <None>
```

### Uniform 変数の宣言
RectMask2D のマスク計算に利用する Uniform 変数を宣言します。
今回の Uniform 変数は、マテリアルで設定することはなくシェーダキーワード UNITY_UI_CLIP_RECT が有効の時のみ利用されるため、以下を *SubShader / Pass* に宣言します。

- **_ClipRect** : 表示領域の *positionOS オブジェクト空間（Object Space* です（_ClipRect.xy が開始 _ClipRect.zw が終了）
- **_UIMaskSoftnessX** : Unity2020バージョン頃から追加された RectMask2D の *Softness* パラメーターのX軸、ソフトマスクとして切り抜くX軸の幅の値
- **_UIMaskSoftnessY** : 同じくソフトマスクとして切り抜くY軸の幅の値

``` diff
            // -- Properties used by SceneSelectionPass
            #ifdef SCENESELECTIONPASS
            int _ObjectId;
            int _PassValue;
            #endif

+            // -- Properties used by UnityUiClipRect
+            #ifdef UNITY_UI_CLIP_RECT
+            float4 _ClipRect;
+            float _UIMaskSoftnessX;
+            float _UIMaskSoftnessY;
+            #endif
```

### positionOS オブジェクト空間（Object Space) の追加

RectMask2D のマスクは、フラグメントの位置が *_ClipRect* の範囲内にあるかどうかを判断して、マスクするかどうかを決定します。
そのためフラグメントの位置を取得できるようにする必要があります。また *_ClipRect* は、オブジェクト空間での値のため、同じくオブジェクト空間でのフラグメントの位置にしてあげる必要があります。

**SurfaceDescriptionInputs** に **positionOS** を宣言します。

``` diff
            struct SurfaceDescriptionInputs
            {
+                float3 positionOS;
                float4 uv0;
                float3 TimeParameters;
            };
```

**SurfaceDescriptionInputs BuildSurfaceDescriptionInputs(Varyings input)** にて *positionOS* に設定していきます。

Varyings には *positionWS ワールド空間（World Space）* の値しか含まれていないため オブジェクト空間 に変換してから値を設定します。

``` diff
+                output.positionOS = mul(unity_WorldToObject, float4(input.positionWS, 1.0));
                output.uv0 = input.texCoord0;
                output.TimeParameters = _TimeParameters.xyz; // This is mainly for LW as HD overwrite this value
```

### RectMask2D に対応するマスク処理の追加

```SurfaceDescriptionFunction``` 内で、 RectMask2D に対応するマスク処理の追加します。

はじめに、RectMask2D によるマスクがされていない場合を考えます。
その時は、もともとの Alpha値 のままとなります。

``` diff
                SurfaceDescription SurfaceDescriptionFunction(SurfaceDescriptionInputs IN)
                {
                    SurfaceDescription surface = (SurfaceDescription)0;

                    ...

                    surface.BaseColor = (_OverlayColor_014a506269514c278e10d6700e0df390_OutColor_1_Vector4.xyz);
+#ifdef UNITY_UI_CLIP_RECT
+                    // ここに RectMask2D の処理を書く
+#else
                    surface.Alpha = <もともとの、最終出力のAlpha値>;
+#endif
                    return surface;
                }
```



以上の変数の値を使い、 RectMask2D の Alpha値を求めます。

``` diff
#ifdef UNITY_UI_CLIP_RECT
+                const float2 pixel_size = 1 / float2(1, 1) / abs(mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy));
+                const float4 clamped_rect = clamp(_ClipRect, -2e10, 2e10);
+                const float2 mask_xy = IN.positionOS.xy * 2 - clamped_rect.xy - clamped_rect.zw;
+                const float2 mask_zw = 0.25 / (0.25 * half2(_UIMaskSoftnessX, _UIMaskSoftnessY) + abs(pixel_size.xy));
+                const float4 mask = float4(mask_xy, mask_zw);
+                const half2 m = saturate((_ClipRect.zw - _ClipRect.xy - abs(mask.xy)) * mask.zw);
+                surface.Alpha = <もともとの、最終出力のAlpha値> * m.x * m.y;
#else
                surface.Alpha = <もともとの、最終出力のAlpha値>;
#endif
                return surface;
```

以上で RectMask2D の対応は完了です。

# まとめ

Shader Graph は、 Render pipelines に依存するため、いまだに色々と機能が足りなかったり、
不要な Shader も含めて生成されるなど、モックやデザイナー用に利用するのはいいですが UI用のシェーダーを製品用に利用するにはまだ向いていないように感じます。

Shader Graph を使うのであれば、Shader Graph から生成されたシェーダーをエンジニアの手で適切に、チューニングしてあげることが大切そうです。

また、コストは高そうですが UI向けのチューニングされた Scriptable Render Pipeline を作成し、それを Target として Shader Graph を利用するのもいいと思います。

# 参考文献
- https://zenigane138.hateblo.jp/entry/2018/06/29/231816
- https://qiita.com/gamu/items/5715457adfd7b6f5f5a5