Unityでペルソナ5風のテキストによるマスク

今回のリポジトリ
https://github.com/kamahir0/TextMask

# 序論
皆さんペルソナ５、ご存じですか？

おそらく ご存じかと思います。

**そう、ご存じなんです。**
<br>
特にあのオシャレなUIは有名ですね。
今回はそんなペルソナ5のUI表現の中でも、個人的に印象に残っている**項目選択のUI表現**をUnityで再現してみた、という内容になっております。

# 今回再現するもの
ペルソナ5に限りませんが...
ゲームで「何かを選ぶ」というシーンにおいて、**フォーカスしている項目だけ文字色を変える**という表現はよく見られます。

（例）ポケットモンスター　ソード・シールド
![SwordShield FocusUI.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3780101/5fcac590-5121-b1a8-6703-fa43c1db3424.jpeg)

Unityで実装するのであれば....（仮にカーソルの色を黒として）
並べて表示するTextMeshPro全ての参照を持っておいて、現在フォーカスしているインデックス番目のものだけ文字色を白色に設定する（他は黒色に設定する）みたいな感じでもいけそうです。
<br>
しかし、ペルソナ5をやっていて気付いたことがあります。
<br>
![Persona5MaskedTextSample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3780101/eba1dbe0-eefa-fa3f-848d-1041473c2a88.gif)

例によってこちらもフォーカスしている項目（「行こう」）の文字色が反転しているのですが、よく見ると**その１個下の文字も部分的に反転している**ことが分かります。（「仕方ない」の上端部）

これは先ほど述べたような、フォーカスしている項目だけ異なる文字色に設定するアプローチでは不可能です。

今回はシェーダーのステンシル機能を用いてこの「テキストによるマスク」を再現してみたいと思います。

# 作戦
という訳でここからはシェーダーを書いていきます！！！
<br>
...と思いきや、Unityに標準で搭載されている[UI-Default](https://github.com/TwoTailsGames/Unity-Built-in-Shaders/blob/master/DefaultResourcesExtra/UI/UI-Default.shader)シェーダーで十分行けそうなので普通にソレを使います。
TextMeshPro用のシェーダーは既存のものを改造します（たったの２行ですが...）

なお、今回はフォーカスしていない項目の文字色を白色、している項目の文字色を黒色、カーソルを白色としています。

## カーソル側
Canvas下に2つのImageオブジェクトA, Bを配置します。
Aは普通にカーソルらしく働いてもらう方で、色は白にします。
もう一方のBは**フォント形状にマスクされる**方で、色は黒にします。

B用にマテリアルを新規作成します。
シェーダーは`UI-Default`。作成したらプロパティをいじります。
今回は以下のようにしました。
| プロパティ名 | 値 |
|:-:|:-:|
| Stencil Comparison | 3 |
| Stencil ID | 1 |
| Stencil Operation | 0 |

`Stencil Comparison`はステンシルテストで適用する比較関数に対応している数値で、`3`は`Equal`です。
`Stencil Operation`はステンシルテスト通過時の処理に対応している数値で、`0`は`Keep`です。
`Stencil ID`は適当です。大事なのは**テキスト側と値を合わせておく**こと（比較関数`Greater` `Less`等が絡むなら値は考える必要アリ）。

## テキスト側
Canvas下にTextMeshProオブジェクトを配置します。こいつがマスクする側となるワケです。

ここでHierarchyにおいての配置順は、
上から
 ↓ Image A
 ↓ 任意の数のTextMeshPro
 ↓ ImageB
...となるようにします。

ここでシェーダーを新規作成します。

適当なフォルダに...と言いたいところですが、どうやらTextMeshPro用のシェーダーは`Assets > Text Mesh Pro > Shaders`フォルダにないと使えないっぽいので、そこに。

また今回は上述した通り、既存のものをたった2行程度改造するだけです。なんなら同フォルダにある`TMP_SDF-Mobile.shader`あたりをコピペしちゃいましょう。
<br>
で、具体的にはどう改造するのかというと...
````Shaderlab:TMP_SDF ImageMask.shader
// PIXEL SHADER
fixed4 PixShader(pixel_t input) : SV_Target
{
    ～～～～～～～～～～～～～～～～
    
    //#if UNITY_UI_ALPHACLIP
    clip(c.a - 0.001);
    //#endif
    
    return c;
}
````
**まさかのコメントアウト**

本当にこれだけ。（理由は後述）
<br>
で、こちらもマテリアルを新たに用意します。
TextMeshProのマテリアル追加はややややこしいのですが（や４連）、
簡単に手順を述べると
1. TextMeshProコンポーネントの`FontAsset`をクリック
1. ProjectウィンドウでFontAssetがピクピクするので、ソレの「▷」マークをクリック
1. 開いた中にマテリアルがあるのでクリック
1. Inspectorの左上のアイコンから`右クリック>Create Material Asset`
1. マテリアルが作成されるのでシェーダーを先ほど作ったものに変更

※TextMeshPro用マテリアルのファイル名にはFontAssetの名前を含む必要があることに注意
<br>
で、さっきと同様にプロパティをいじる。ただしTextMeshProのマテリアルのプロパティはそのままじゃ一部表示されないので（なんで？）、Inspectorの右上のボタンからDebugモードにしてください。
今回は以下のようにしました。
| プロパティ名 | 値 |
|:-:|:-:|
| _StencilComp | 8 |
| _Stencil | 1 |
| _StencilOp | 2 |

`_StencilComp`はステンシルテストで適用する比較関数に対応している数値で、`8`は`Always`です。
`_StencilOp`はステンシルテスト通過時の処理に対応している数値で、`2`は`Replace`です。
`_Stencil`は適当です。大事なのは**カーソル側と値を合わせておく**こと（比較関数`Greater` `Less`等が絡むなら値は考える必要アリ）。
<br>
で。
<br>
**完成！**
# 完成形
カーソルをC#スクリプトで適当に動かしてみました。
（サンプルプロジェクトではマウスホイールで選択できます）
![TextMask_forQiita.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3780101/8df124e3-fe2c-b264-74f2-ac300c0121f7.gif)

ちなみにシェーダーコードの改変の必要性についてですが、早い話がこうなります。

![スクリーンショット 2024-04-29 213231.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3780101/73ce3cfd-6007-ea32-b4dc-1a1197066866.png)

あくまで自分の考察ですが、
これは文字の描画において透明になっている部分も実はピクセルシェーダーの処理が破棄されてなくて、ステンシル書き込みしちゃってる感じなのかなあ～と。

`#if UNITY_UI_ALPHACLIP`というのがMaskコンポーネントを使うときなんかに有効になる？んですかね。
エディタでプロパティから変更できたりもしなさげだったので、今回はif部分を削除したという。

以上です！