ドロップシャドウと勾配値
同様に,ドロップシャドウも実装できるでしょうか。
すぐに思いつく方法
アウトラインを描画するときは周囲のピクセルをサンプリングして,そのピクセルがアウトラインの範囲にあるかどうかを判定していました。同様に,特定の方向のみにあるピクセルをサンプリングしてやれば,特定の方向のみに影を描画することが可能なような気がします。
例えば,次のようにフラグメントシェーダーを書き換えてみましょう。
code:text_shadow.shader(c)
half4 frag (v2f i) : SV_Target
{
half a0 = tex2D(_MainTex, i.uv).a;
half4 col = lerp(_ShadowColor, i.color, a0);
float2 delta = _MainTex_TexelSize.xy * _Spread;
half a1 = max(tex2D(_MainTex, i.uv + delta * 1.0).a,
tex2D(_MainTex, i.uv - delta * 0.5).a);
half aa = max(a0, a1));
col.a *= aa;
return col;
}
結果はどうでしょうか。
https://gyazo.com/7f03a395f89a95a488e8d1819a8b0e9b
おや?
一部の文字の影の方向がおかしいですね。
何故でしょうか。フォントのテクスチャアトラスを見てみましょう。
https://gyazo.com/5081bcb6ed82597b3122dc3a8b1f4c04
原因は明白です。アトラスには文字がそれぞれ回転して詰め込まれているので,テクスチャ座標で特定の方向にサンプリング点をずらしたつもりでもそれぞれバラバラな方向になってしまうのです。
UV座標勾配値
ではどうすれば良いのでしょうか。
テクスチャ座標を $ uv とすれば,通常は右側のピクセルは $ +u 方向にあり,上方のピクセルは $ +v 方向にあります。しかし今回それは期待できません。実は,こんな時のためにフラグメントシェーダーには,「周囲と比べた場合,特定のパラメータが,どちらの方向に増減しているのか」を調べる関数が用意されています。これが 勾配値 と呼ばれるもので,ddx()およびddy()として呼び出すことができます。数学が得意な方には 偏微分関数 と言ったほうが理解が早いかもしれません。以下に数式を示します。単純ですね。
$ \mathrm{ddx}(f(x,y)) = \nabla f_{x} = \frac{\partial f}{\partial x}
$ \mathrm{ddy}(f(x,y))=\nabla f_{y}=\frac{\partial f}{\partial y}
ここで $ f として $ uvについて勾配値を求めると,例えば「右隣のピクセルは,$ uvの値がこのくらい違う」ということが分かります。このことから,影を付けたい方向のテクセルをサンプリングするには,$ uvをどのくらいずらせば良いのかが分かります。
具体的には,影の方向へのベクトルを $ V_{s} とすると,これを勾配で回転させます。
$ delta_{UV} = \nabla f \times V_{s} = \left( \begin{array}{c} \frac{\partial f}{\partial x} \\ \\ \frac{\partial f}{\partial y} \end{array} \right) \times V_{s}
この数式,そのまま実装してみましょう。
code:Text-Shadow.shader(c)
Shader "GUI/Text-Shadow"
{
Properties {
_MainTex ("Font Texture", 2D) = "white" {}
HDR _Color ("Text Color", Color) = (1,1,1,1) HDR _ShadowColor ("Shadow Color", Color) = (0,0,0,1) _ShadowDistance ("Shadow Distance", Vector) = (1,1,0,0)
}
SubShader {
Tags {
"Queue"="Transparent"
"IgnoreProjector"="True"
"RenderType"="Transparent"
"PreviewType"="Plane"
}
Lighting Off Cull Off ZTest Always ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Pass {
CGPROGRAM
#pragma shader_feature AUTO_OUTLINE_COLOR struct appdata_t {
float4 vertex : POSITION;
half4 color : COLOR;
float2 uv : TEXCOORD0;
};
struct v2f {
float4 vertex : SV_POSITION;
half4 color : COLOR;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
half4 _Color;
half4 _ShadowColor;
float2 _ShadowDistance;
v2f vert (appdata_t v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.color = v.color * _Color;
o.uv = TRANSFORM_TEX(v.uv,_MainTex);
return o;
}
half4 frag (v2f i) : SV_Target
{
half4 col = i.color;
half4 scol = abs(col - half4(1,1,1,0));
half4 scol = _ShadowColor;
half a0 = tex2D(_MainTex, i.uv).a;
// derivertive of texture coordinate
float2 du = ddx(i.uv);
float2 dv = ddy(i.uv);
// build rotate matrix
float2x2 uv_rot_matrix = { du, dv };
// rotate sampling delta
float2 suv = mul(_ShadowDistance.xy, uv_rot_matrix);
half a1 = max(tex2D(_MainTex, i.uv + suv * 1.0).a,
tex2D(_MainTex, i.uv + suv * 0.5).a);
col = lerp(col, scol, saturate(a1-a0));
half aa = max(a0, a1);
col.a *= aa;
return col;
}
ENDCG
}
}
}
ちょっと手抜きでサンプリング点は2箇所なのですが,これは任意に増やせます。
実行結果は次の通り。
https://gyazo.com/34973808e263ecb46a91928b991333da
完璧ですね。
勾配の方向
ひとつだけ注意点があります。それは勾配値が取られる方向です。
実は勾配値はスクリーン座標で計算されます。フラグメントシェーダーで計算される,実際の隣のピクセルの値を元に単なる減算で求めるという実装になっているためです。なので,影が付く位置はオブジェクトの回転や傾きに関係なく常にスクリーンに対して一方向になります。通常はそれで問題無い場合が多いかと思いますが,それでは困る場合は他の手法が必要です。
2018/4/27