はじめに
UGUIの標準のアウトラインやシャドウ機能は、乗算合成で色を作っているため、基本的に黒以外の色を指定しづらかったりします。
白いアウトラインを表示するためにはどうするべきか
最初に言ってしまうと、標準のUI-Default.shaderでは白い境界線を表示する事はできません。
基本的に乗算合成のため、テクスチャの色*指定した色(頂点カラー)*マテリアルのカラー、と掛け算で色が合成される事となり、
例えばテクスチャの色が青(R:0,G:0,B:1)だった場合に、赤、緑色成分(RG)に色が入る((R:1,G:1,B:1)になる)ことはあり得ません。
シェーダを用意する
という事で、最終的に色は指定したもの(EffectColor)を使い、アルファ成分についてはテクスチャ側の値を使うシェーダを準備しましょう。
今回は無理矢理でも白いアウトラインを出したいだけなので、余り汎用性の高い実装は行いません。
環境としては、
- Unity 2022.1.2f
- builtin_shaders-2022.1.3f1
を使用します。
builtin_shaders-2022.1.3f1をDLし、UI-Default.shaderをコピペして一部だけを書き換えましょう。
Shader "ScreenPocket/UI/Default ColorEffect"
{
Properties
{
[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
_Color ("Tint", Color) = (1,1,1,1)
_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
[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
}
SubShader
{
Tags
{
"Queue"="Transparent"
"IgnoreProjector"="True"
"RenderType"="Transparent"
"PreviewType"="Plane"
"CanUseSpriteAtlas"="True"
}
Stencil
{
Ref [_Stencil]
Comp [_StencilComp]
Pass [_StencilOp]
ReadMask [_StencilReadMask]
WriteMask [_StencilWriteMask]
}
Cull Off
Lighting Off
ZWrite Off
ZTest [unity_GUIZTestMode]
Blend One OneMinusSrcAlpha
ColorMask [_ColorMask]
Pass
{
Name "Default"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 2.0
#include "UnityCG.cginc"
#include "UnityUI.cginc"
#pragma multi_compile_local _ UNITY_UI_CLIP_RECT
#pragma multi_compile_local _ UNITY_UI_ALPHACLIP
struct appdata_t
{
float4 vertex : POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
uint vid : SV_VertexID;
};
struct v2f
{
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
float2 texcoord : TEXCOORD0;
float4 worldPosition : TEXCOORD1;
float4 mask : TEXCOORD2;
UNITY_VERTEX_OUTPUT_STEREO
};
sampler2D _MainTex;
fixed4 _Color;
fixed4 _TextureSampleAdd;
float4 _ClipRect;
float4 _MainTex_ST;
float _UIMaskSoftnessX;
float _UIMaskSoftnessY;
v2f vert(appdata_t v)
{
v2f OUT;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
float4 vPosition = UnityObjectToClipPos(v.vertex);
OUT.worldPosition = v.vertex;
OUT.vertex = vPosition;
float2 pixelSize = vPosition.w;
pixelSize /= float2(1, 1) * abs(mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy));
float4 clampedRect = clamp(_ClipRect, -2e10, 2e10);
float2 maskUV = (v.vertex.xy - clampedRect.xy) / (clampedRect.zw - clampedRect.xy);
OUT.texcoord = TRANSFORM_TEX(v.texcoord.xy, _MainTex);
OUT.mask = float4(v.vertex.xy * 2 - clampedRect.xy - clampedRect.zw, 0.25 / (0.25 * half2(_UIMaskSoftnessX, _UIMaskSoftnessY) + abs(pixelSize.xy)));
OUT.color = v.color * _Color;
return OUT;
}
fixed4 frag(v2f IN) : SV_Target
{
const half alphaPrecision = half(0xff);
const half invAlphaPrecision = half(1.0/alphaPrecision);
IN.color.a = round(IN.color.a * alphaPrecision)*invAlphaPrecision;
//half4 color = IN.color * (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd);
half4 color = (tex2D(_MainTex, IN.texcoord.xy) + _TextureSampleAdd);//←☆少しだけ変更
color = lerp(IN.color * color, half4(IN.color.rgb, color.a), 1 - IN.color.a); //←☆追加したのはこの1行
#ifdef UNITY_UI_CLIP_RECT
half2 m = saturate((_ClipRect.zw - _ClipRect.xy - abs(IN.mask.xy)) * IN.mask.zw);
color.a *= m.x * m.y;
#endif
#ifdef UNITY_UI_ALPHACLIP
clip(color.a - 0.001);
#endif
color.rgb *= color.a;
return color;
}
ENDCG
}
}
}
さて、変更したのは
//half4 color = IN.color * (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd);
half4 color = (tex2D(_MainTex, IN.texcoord.xy) + _TextureSampleAdd);//←☆少しだけ変更
color = lerp(IN.color * color, half4(IN.color.rgb, color.a), 1 - IN.color.a); //←☆追加したのはこの1行
だけです。
コメントが元のコード。その下2行が変更後のコードです
何をやっているか解説すると
- 1行目と2行目の差はIN.colorの乗算を除去している点です
- 3行目はlerp()で2色を線形補間しています
- 3行目の1色目(lerp()の第1引数)は、2行目で除去したIn.colorの乗算を復帰させて今までと同じ色(UI-Defaultそのまま)を表示します
- 3行目の2色目(lerp()の第2引数)は、RGBはIn.colorの色を見て、アルファ成分はcolor成分の値を表示します
- 3行目の線形補間の閾値(lerp()の第3引数)は、In.color.aの逆の値を流し込みます。
つまり、IN.colorのアルファ値が0の時に、指定した色RGBとテクスチャのAしか見なくなるシェーダが出来上がったわけです。
即ち、このシェーダを持ったマテリアルををImageにつけて
- 元のImageのAlpha値を1にすることで元の画像を表示
- Outlineのアルファ値を0にすることで、Outline側はOutlineで指定した色+テクスチャのアルファ値を表示
することが出来るという事ですね。
載せてみる
という事で、上記のシェーダを持ったマテリアルをImageにつけて、白いアウトラインを付けてみましょう。
正しく、まっ白なアウトラインが表示されていることが確認できるかと思います
ポイントは下記です
- 赤線を引き忘れましたが、Imageにマテリアルが設定されています
- EffectColorに白が指定されています
- Outlineのアルファは0が指定されています。これがこのシェーダを使用する上での制約です。
注意点
上にも書いた通り、「画像の実体か、効果(Outline)か」を判定するためにアルファ値を使用しているせいで、アルファアニメーションは使用できません。
ただ、アルファ値を操作する必要が無い事が分かっているならば十分使用に耐えられるかと思います。
終わりに
という事で、若干無理矢理ですが白いアウトラインの実現方法でした。
本当はもっと別のパラメータで、Image実体と効果の切り分けを判定できればアルファアニメーションも出来てよかったのですが、
ちょっとすぐには対応出来なかった(というかガッツリOutlineを書き換えれば出来そうですが)のと、
そもそもOutlineと半透明は相性が悪い(半透明にするとアウトラインの重ね合わせ色が出てしまう)ので今回はこんな感じにしておきます。
もしもっといいパラメータが有れば、Twitterで突っ込み頂ければ幸いです。