はじめに
この投稿はグレンジ Advent Calendar 2022の12日目(12/12)の記事です。
こんにちは!
株式会社グレンジでクライアントエンジニアをしているGamu(@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公式のダウンロードアーカイブからビルトインシェーダをダウンロードすることで入手できます。
例えば、当記事が対象としているUnity 2021.3.10f1のビルトインシェーダはこれです。
ダウンロードしたら解凍し、DefaultResourcesExtra/UIフォルダに入っているUI-Default.shader
をコピペして新規UIシェーダを作成していきましょう。
また、UI/Default
シェーダの実装に詳細なコメントをつけて解説して下さっている方もいます。
比較画面
シーン上に比較のため2つのUIを用意しました。
左はマテリアルに何も指定していないImageコンポーネントを持つUIで、自動的にUI/Default
シェーダが使われます。
右はオリジナルのUIシェーダを割り当てたマテリアルを指定したImageコンポーネントを持つUIです。
チェック項目1: 透過されない
見てすぐに分かる通り、ボタン画像の透過されていてほしい部分が透過されない状況です。
解決方法
Unity 2020.1以前
シェーダのBlendで、Src:SrcAlpha、Dst:OneMinusSrcAlphaにします。
次のように感じでコードを変更します。
Pass
{
+ Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
Unity 2020.1以降
シェーダのBlendで、Src:One、Dst:OneMinusSrcAlphaにします。
さらに、ピクセルシェーダで出力するRGBに不透明度を乗算します。
次のようにコードを変更します。
Pass
{
+ Blend One OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
+ col.rgb *= col.a;
return col;
}
解決方法の解説
Blendとは、シェーダで出力した値と描画先に既に格納されている値とを、どのように合成するか指定するものです。
合成の式は次のようになります。
合成結果 = シェーダで出力した値 * 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)を使って合成するのが標準となったようです。
チェック項目2: ImageコンポーネントのColorが反映されない
ImageコンポーネントのColorを白以外に変えてもボタンに反映されていない状況です。
解決方法
頂点カラーをピクセルシェーダに渡すための処理をシェーダのいくつかの場所に加えます。
次のようにコードを変更します。
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
+ half4 color : COLOR;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
+ half4 color : COLOR;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
+ o.color = v.color;
return o;
}
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のメッシュの頂点カラーとしてシェーダに情報が送信されています。
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セマンティクスが付いたフィールドを定義する必要があります。
セマンティクスとは、データのラベルのようなもので、特に頂点シェーダの引数に使う構造体においては、型や名前に関係なく特定のデータは特定のセマンティクスが付いたフィールドに渡されてきます。
頂点カラーはCOLORセマンティクスが付いたフィールドに渡されるようになっています。
COLOR は、頂点ごとの色で、一般的には、 float4 です。
ImageコンポーネントのColorが頂点カラーでシェーダに渡されてくることは分かりましたが、最終的に使用したいのはピクセルシェーダなので、頂点シェーダからピクセルシェーダにデータを渡す処理も必要になります。
チェック項目3: RectMask2Dコンポーネントでマスクされない
RectMask2Dコンポーネントの配下に置いた時に、右のUIだけマスクが掛からない状況です。
解決方法
まず、ImageコンポーネントのMaskableにチェックが入っていることを確認します。
OFFであればONにします。
シェーダキーワードUNITY_UI_CLIP_RECT
を宣言します。
そして、UNITY_UI_CLIP_RECT
の有効無効による分岐処理をシェーダのいくつかの場所に加えます。
次のようにコードを変更します。
#pragma vertex vert
#pragma fragment frag
+#pragma multi_compile_local UNITY_UI_CLIP_RECT
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
half4 color : COLOR;
+ float4 mask : TEXCOORD1;
};
sampler2D _MainTex;
float4 _MainTex_ST;
+float4 _ClipRect;
+float _UIMaskSoftnessX;
+float _UIMaskSoftnessY;
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;
}
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文を使用するよりもパフォーマンス向上を期待できます。
UNITY_UI_CLIP_RECT
キーワードが有効な時は、Uniform変数の_ClipRect
、_UIMaskSoftnessX
、_UIMaskSoftnessY
にマスクに関するデータが渡されてきます。
3つのUniform変数の名前は、一文字でもtypoするとデータが渡されないので注意して下さい。
頂点シェーダではこれらのUniform変数のデータを使用して、その頂点座標におけるマスク用のパラメータを計算します。
ピクセルシェーダでは頂点シェーダから渡されたマスク用のパラメータを使用してマスク度合を計算し、結果を不透明度に乗算します。
チェック項目4: Maskが適用されない
Maskコンポーネントの配下に置いた時に、右のUIだけマスクが掛からない状況です。
解決方法
シェーダのPropertiesにステンシルテスト用のプロパティを定義します。
SubShaderブロックの直下にStencilブロックを定義します。
シェーダパスでColorMaskを指定します。
次のようにコードを変更します。
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
}
SubShader
{
+ Stencil
+ {
+ Ref [_Stencil]
+ Comp [_StencilComp]
+ Pass [_StencilOp]
+ ReadMask [_StencilReadMask]
+ WriteMask [_StencilWriteMask]
+ }
Pass
{
Blend One OneMinusSrcAlpha
+ ColorMask [_ColorMask]
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
解決方法の解説
RectMask2Dコンポーネントでは座標を使用して出力する不透明度にマスク結果を乗算していましたが、Maskコンポーネントではステンシル(Stencil)テストという機能を使用しており、根本的に値を出力させていません。
ステンシルテストとは、ステンシルと呼ばれる専用の値を使用してピクセル単位で条件分岐を行い、テストに合格した場合だけシェーダを動作させるという機能です。
逆を言えば、テストに不合格した場合はシェーダが動作しません。
Maskコンポーネントでは、このステンシルの値とステンシルテストの合格条件を自動的に設定してくれます。
ただし、Propertiesブロックに定義した_StencilComp
、_Stencil
、_StencilOp
、_StencilWriteMask
、_StencilReadMask
、_ColorMask
のいずれかを一文字でもtypoしていると設定をしてくれないので注意して下さい。
この辺りの仕様は公式ドキュメントには明記されていませんが、StencilMaterialクラスの実装を読むことで理解できます。
ちなみに、Maskコンポーネントでマスクを掛けようとして失敗した場合は、次のようなヒントをくれたりします。
これだけだとマスクに失敗した理由かどうか分らんがな。
おわりに
他にも沢山のUIシェーダならではのエラーがありますが、普遍的なものはこのくらいでしょうか。
シェーダに詳しい人であれば何ということもない内容だと思いますが、シェーダを勉強し始めた人にとってはエラーメッセージも出ないエラーは沼りやすいものです。
この記事を読んで不具合が解決したという人がいれば幸いです。
参考