UIシェーダのエラーチェックリスト

# はじめに
この投稿は[グレンジ Advent Calendar 2022](https://qiita.com/advent-calendar/2022/grenge)の12日目(12/12)の記事です。

こんにちは！
[株式会社グレンジ](https://www.grenge.co.jp/)でクライアントエンジニアをしているGamu([@AblerBiri](https://twitter.com/ablerbiri))です。

私が今年貯めてきた小ネタを投稿していこうと思います。
今回はその第1弾です。

当記事は、UIシェーダを書いていた時に「コンパイルエラーではないけれど何か見た目がおかしい！」ということがあったらこれから挙げることを確認しておこうね！と啓蒙するものです。

### 対象読者
- Unityでシェーダを勉強している人
- Unityでシェーダを記述できるけどUIシェーダを書いて沼にハマってる人

### 動作環境
- Windows 10 home 64 bit
- Unity 2021.3.10f1
- 2D Sprite 1.0.0
- Universal RP 12.1.7 (Built-in RPでも動作する内容です)

# 結論を言えば
結論を言えば、Unityの`UI/Default`シェーダの実装をコピペすれば良い話です。
`UI/Default`シェーダは、Unity公式のダウンロードアーカイブからビルトインシェーダをダウンロードすることで入手できます。

https://unity.com/releases/editor/archive


例えば、当記事が対象としているUnity 2021.3.10f1のビルトインシェーダはこれです。
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225590/2c731ed0-088c-c0b6-5719-b2df1b4ba051.png">

ダウンロードしたら解凍し、DefaultResourcesExtra/UIフォルダに入っている`UI-Default.shader`をコピペして新規UIシェーダを作成していきましょう。

また、`UI/Default`シェーダの実装に詳細なコメントをつけて解説して下さっている方もいます。

https://zenn.dev/heppoko/articles/6d04f106bdb1d1

# 比較画面
シーン上に比較のため2つのUIを用意しました。
左はマテリアルに何も指定していないImageコンポーネントを持つUIで、自動的に`UI/Default`シェーダが使われます。
右はオリジナルのUIシェーダを割り当てたマテリアルを指定したImageコンポーネントを持つUIです。
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225590/d1e3e346-7240-bd14-4059-94508b3172d4.png">

# チェック項目1: 透過されない
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225590/653cdf65-f70d-4b31-4c95-acf431dbbce9.png">

見てすぐに分かる通り、ボタン画像の透過されていてほしい部分が透過されない状況です。

### 解決方法
**Unity 2020.1以前**
シェーダのBlendで、Src:SrcAlpha、Dst:OneMinusSrcAlphaにします。
次のように感じでコードを変更します。
```diff_hlsl:Blend変更
Pass
{
+   Blend SrcAlpha OneMinusSrcAlpha
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
```

**Unity 2020.1以降**
シェーダのBlendで、Src:One、Dst:OneMinusSrcAlphaにします。
さらに、ピクセルシェーダで出力するRGBに不透明度を乗算します。
次のようにコードを変更します。
```diff_hlsl:Blend変更
Pass
{
+   Blend One OneMinusSrcAlpha
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
```
```diff_hlsl:ピクセルシェーダ
fixed4 frag (v2f i) : SV_Target
{
    fixed4 col = tex2D(_MainTex, i.uv);
+   col.rgb *= col.a;
    return col;
}
```

### 解決方法の解説
Blendとは、シェーダで出力した値と描画先に既に格納されている値とを、どのように合成するか指定するものです。

https://docs.unity3d.com/ja/2021.3/Manual/SL-Blend.html

合成の式は次のようになります。
```
合成結果 = シェーダで出力した値 * Src + 描画先に格納されている値 * Dst
```

そして、Blendの指定フォーマットは次のようになります。
```
Blend Src Dst
```

つまり、BlendのSrcで指定した値がシェーダで出力した値に乗算され、Dstで指定した値が描画先に既に格納されている値に乗算され、その加算結果が最終的に格納される値となります。

Blendオプションを指定しないと、Src:One、Dst:Zeroになるようなのですが、この場合は**透明度を考慮しない上書き処理**になってしまいます。
そのため、Blendオプションで透明度を考慮するように指定する必要があります。
その指定こそが、Src:SrcAlpha、Dst:OneMinusSrcAlphaです。

しかし、Unity 2020.1で次のバグの修正に関連して、出力する前に透明度を乗算した値(Pre Multiplied Transparency)を使って合成するのが標準となったようです。

https://issuetracker.unity3d.com/issues/transparent-ui-gameobject-ignores-opaque-ui-gameobject-when-using-rendertexture

# チェック項目2: ImageコンポーネントのColorが反映されない
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225590/6c52fdbe-4d3e-3da7-7a49-89f5f9e950a9.gif">

ImageコンポーネントのColorを白以外に変えてもボタンに反映されていない状況です。

### 解決方法
頂点カラーをピクセルシェーダに渡すための処理をシェーダのいくつかの場所に加えます。
次のようにコードを変更します。
```diff_hlsl:頂点シェーダの引数に使う構造体
struct appdata
{
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
+   half4 color : COLOR;
};
```
```diff_hlsl:頂点シェーダの戻り値に使う構造体
struct v2f
{
    float2 uv : TEXCOORD0;
    float4 vertex : SV_POSITION;
+   half4 color : COLOR;
};
```
```diff_hlsl:頂点シェーダ
v2f vert (appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
+   o.color = v.color;
    return o;
}
```
```diff_hlsl:ピクセルシェーダ
fixed4 frag (v2f i) : SV_Target
{
-   fixed4 col = tex2D(_MainTex, i.uv);
+   fixed4 col = tex2D(_MainTex, i.uv) * i.color;
    col.rgb *= col.a;
    return col;
}
```

### 解決方法の解説
ImageコンポーネントのColorは、UIのメッシュの頂点カラーとしてシェーダに情報が送信されています。

https://docs.unity3d.com/ja/2019.2/ScriptReference/UI.Graphic-color.html

>The builtin UI Components use this as their vertex color. Use this to fetch or change the Color of visual UI elements, such as an Image.

シェーダ側で頂点カラーの情報を取得するためには、頂点シェーダの引数に使う構造体にCOLORセマンティクスが付いたフィールドを定義する必要があります。
セマンティクスとは、データのラベルのようなもので、特に頂点シェーダの引数に使う構造体においては、**型や名前に関係なく特定のデータは特定のセマンティクスが付いたフィールドに渡されて**きます。

https://docs.unity3d.com/ja/2021.3/Manual/SL-ShaderSemantics.html

頂点カラーはCOLORセマンティクスが付いたフィールドに渡されるようになっています。

https://docs.unity3d.com/ja/2021.3/Manual/SL-VertexProgramInputs.html

>COLOR は、頂点ごとの色で、一般的には、 float4 です。

ImageコンポーネントのColorが頂点カラーでシェーダに渡されてくることは分かりましたが、最終的に使用したいのはピクセルシェーダなので、頂点シェーダからピクセルシェーダにデータを渡す処理も必要になります。

# チェック項目3: RectMask2Dコンポーネントでマスクされない
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225590/8f93df3c-c2bf-03ac-82d2-f15a47499657.png">

RectMask2Dコンポーネントの配下に置いた時に、右のUIだけマスクが掛からない状況です。

### 解決方法
まず、ImageコンポーネントのMaskableにチェックが入っていることを確認します。
OFFであればONにします。
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225590/b2ff7fd0-a2cf-ba13-cfaa-81bc9f9c0c86.png">

シェーダキーワード`UNITY_UI_CLIP_RECT`を宣言します。
そして、`UNITY_UI_CLIP_RECT`の有効無効による分岐処理をシェーダのいくつかの場所に加えます。
次のようにコードを変更します。
```diff_hlsl:プリプロセッサ
#pragma vertex vert
#pragma fragment frag
+#pragma multi_compile_local UNITY_UI_CLIP_RECT
```
```diff_hlsl:頂点シェーダの戻り値に使う構造体
struct v2f
{
    float2 uv : TEXCOORD0;
    float4 vertex : SV_POSITION;
    half4 color : COLOR;
+   float4 mask : TEXCOORD1;
};
```
```diff_hlsl:Uniform変数宣言
sampler2D _MainTex;
float4 _MainTex_ST;
+float4 _ClipRect;
+float _UIMaskSoftnessX;
+float _UIMaskSoftnessY;
```
```diff_hlsl:頂点シェーダ
v2f vert(appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    o.color = v.color;

+   #ifdef UNITY_UI_CLIP_RECT
+   float2 pixelSize = o.vertex.w;
+   pixelSize /= float2(1, 1) * abs(mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy));
+   float4 clampedRect = clamp(_ClipRect, -2e10, 2e10);
+   float2 maskXY = v.vertex.xy * 2 - clampedRect.xy - clampedRect.zw;
+   float2 maskZW = 0.25 / (0.25 * half2(_UIMaskSoftnessX, _UIMaskSoftnessY) + abs(pixelSize.xy));
+   o.mask = float4(maskXY, maskZW);
+   #else
+   o.mask = 0;
+   #endif
    return o;
}
```
```diff_hlsl:ピクセルシェーダ
fixed4 frag(v2f i) : SV_Target
{
    fixed4 col = tex2D(_MainTex, i.uv) * i.color;
+   #ifdef UNITY_UI_CLIP_RECT
+   half2 m = saturate((_ClipRect.zw - _ClipRect.xy - abs(i.mask.xy)) * i.mask.zw);
+   col.a *= m.x * m.y;
+   #endif
    col.rgb *= col.a;
    return col;
}
```

### 解決方法の解説
Image等のMaskableGraphicを継承するコンポーネントがRectMask2Dコンポーネントを持つオブジェクトの配下にある時、`UNITY_UI_CLIP_RECT`キーワードの有効状態が更新されます。
この時、maskableがOnの時はキーワードが有効に、Offの時はキーワードが無効になります。
キーワードが有効な時は、RectMask2Dによって適切にマスクされるようシェーダで処理します。

シェーダキーワードとは、シェーダの機能の一部分を条件によって分岐させたい時に使用するものです。
一貫して結果が不変であるような条件を使って処理を分岐させたい場合は、if文を使用するよりもパフォーマンス向上を期待できます。

https://docs.unity.cn/ja/2021.3/Manual/SL-MultipleProgramVariants.html

https://light11.hatenadiary.com/entry/2018/07/08/134440

`UNITY_UI_CLIP_RECT`キーワードが有効な時は、Uniform変数の`_ClipRect`、`_UIMaskSoftnessX`、`_UIMaskSoftnessY`にマスクに関するデータが渡されてきます。
3つのUniform変数の名前は、**一文字でもtypoするとデータが渡されない**ので注意して下さい。


頂点シェーダではこれらのUniform変数のデータを使用して、その頂点座標におけるマスク用のパラメータを計算します。
ピクセルシェーダでは頂点シェーダから渡されたマスク用のパラメータを使用してマスク度合を計算し、結果を不透明度に乗算します。

# チェック項目4: Maskが適用されない
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225590/f05bcdb2-6081-b39c-be3e-a94acca4e0a7.png">

Maskコンポーネントの配下に置いた時に、右のUIだけマスクが掛からない状況です。

### 解決方法
シェーダのPropertiesにステンシルテスト用のプロパティを定義します。
SubShaderブロックの直下にStencilブロックを定義します。
シェーダパスでColorMaskを指定します。
次のようにコードを変更します。
```diff_hlsl:プロパティ定義
Properties
{
    _MainTex ("Texture", 2D) = "white" {}
        
+   _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
}
```
```diff_hlsl:Stencilブロック
SubShader
{
+   Stencil
+   {
+       Ref [_Stencil]
+       Comp [_StencilComp]
+       Pass [_StencilOp]
+       ReadMask [_StencilReadMask]
+       WriteMask [_StencilWriteMask]
+   }
```
```diff_hlsl:ColorMask
Pass
{
    Blend One OneMinusSrcAlpha
+   ColorMask [_ColorMask]
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
```

### 解決方法の解説
RectMask2Dコンポーネントでは座標を使用して出力する不透明度にマスク結果を乗算していましたが、Maskコンポーネントではステンシル(Stencil)テストという機能を使用しており、根本的に値を出力させていません。

ステンシルテストとは、ステンシルと呼ばれる専用の値を使用してピクセル単位で条件分岐を行い、テストに合格した場合だけシェーダを動作させるという機能です。
逆を言えば、テストに不合格した場合はシェーダが動作しません。

https://nn-hokuson.hatenablog.com/entry/2017/05/02/185320

Maskコンポーネントでは、このステンシルの値とステンシルテストの合格条件を自動的に設定してくれます。
ただし、Propertiesブロックに定義した`_StencilComp`、`_Stencil`、`_StencilOp`、`_StencilWriteMask`、`_StencilReadMask`、`_ColorMask`の**いずれかを一文字でもtypoしていると設定をしてくれない**ので注意して下さい。
この辺りの仕様は公式ドキュメントには明記されていませんが、StencilMaterialクラスの実装を読むことで理解できます。

ちなみに、Maskコンポーネントでマスクを掛けようとして失敗した場合は、次のようなヒントをくれたりします。
~~これだけだとマスクに失敗した理由かどうか分らんがな。~~
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225590/db2618ad-fdc9-401b-a41a-309a63f22523.png">

# おわりに
他にも沢山のUIシェーダならではのエラーがありますが、普遍的なものはこのくらいでしょうか。
シェーダに詳しい人であれば何ということもない内容だと思いますが、シェーダを勉強し始めた人にとってはエラーメッセージも出ないエラーは沼りやすいものです。
この記事を読んで不具合が解決したという人がいれば幸いです。

# 参考
https://assetstore.unity.com/packages/2d/gui/icons/simple-button-set-01-153979

https://zenn.dev/heppoko/articles/6d04f106bdb1d1

https://docs.unity3d.com/ja/2021.3/Manual/SL-Blend.html

https://issuetracker.unity3d.com/issues/transparent-ui-gameobject-ignores-opaque-ui-gameobject-when-using-rendertexture

https://docs.unity3d.com/ja/2019.2/ScriptReference/UI.Graphic-color.html

https://docs.unity3d.com/ja/2021.3/Manual/SL-ShaderSemantics.html

https://docs.unity3d.com/ja/2021.3/Manual/SL-VertexProgramInputs.html

https://docs.unity.cn/ja/2021.3/Manual/SL-MultipleProgramVariants.html

https://light11.hatenadiary.com/entry/2018/07/08/134440

https://nn-hokuson.hatenablog.com/entry/2017/05/02/185320