下の例のように、油絵っぽい見た目にするポストエフェクト用のシェーダーをUnityで実装します。
元ネタは、こちらの記事で、桑原さんという方が考案した画像フィルターだそうです。この記事では、pythonを使って実装されているので、Unityのshaderで実装したものを紹介します。
内容の詳細は上述の記事に書いてありますので、ここでは、実際のコードと簡単な解説コメントを載せておきます。(記事を読んでいることを前提に解説コメントは書いてあります)
Shader "Filter/KuwaharaFilter"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
// 大きな値になる程、油絵感が増す
_Size ("Filter Size", Range(1, 25)) = 5
}
SubShader
{
// No culling or depth
Cull Off ZWrite Off ZTest Always
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
sampler2D _MainTex;
uniform float4 _MainTex_TexelSize;
fixed _Size;
fixed4 frag (v2f i) : SV_Target
{
// uvのテクセルの座標を取得
float uTexel = _MainTex_TexelSize.x;
float vTexel = _MainTex_TexelSize.y;
// 4エリアの正方形の1辺の長さを求める
int areaSize = floor(_Size / 2);
// 1辺の長さが0なら何もしない
if (areaSize == 0) {
return tex2D(_MainTex, i.uv);
}
// 各エリアの全ピクセルのrgb値から平均と分散を求める
// a : 左上
fixed3 aavg = fixed3(0,0,0);
fixed3 avar = fixed3(0,0,0);
for (int av = 1; av <= areaSize; av++) {
for (int au = 1; au <= areaSize; au++) {
fixed3 pick = tex2D(_MainTex, i.uv + float2(-au*uTexel, av*vTexel));
aavg += pick;
avar += pick * pick;
}
}
aavg /= areaSize * areaSize;
avar = avar / (areaSize * areaSize) - aavg * aavg;
// b : 右上
fixed3 bavg = fixed3(0,0,0);
fixed3 bvar = fixed3(0,0,0);
for (int bv = 1; bv <= areaSize; bv++) {
for (int bu = 1; bu <= areaSize; bu++) {
fixed3 pick = tex2D(_MainTex, i.uv + float2(bu*uTexel, bv*vTexel));
bavg += pick;
bvar += pick * pick;
}
}
bavg /= areaSize * areaSize;
bvar = bvar / (areaSize * areaSize) - bavg * bavg;
// c : 左下
fixed3 cavg = fixed3(0,0,0);
fixed3 cvar = fixed3(0,0,0);
for (int cv = 1; cv <= areaSize; cv++) {
for (int cu = 1; cu <= areaSize; cu++) {
fixed3 pick = tex2D(_MainTex, i.uv + float2(-cu*uTexel, -cv*vTexel));
cavg += pick;
cvar += pick * pick;
}
}
cavg /= areaSize * areaSize;
cvar = cvar / (areaSize * areaSize) - cavg * cavg;
// d : 右下
fixed3 davg = fixed3(0,0,0);
fixed3 dvar = fixed3(0,0,0);
for (int dv = 1; dv <= areaSize; dv++) {
for (int du = 1; du <= areaSize; du++) {
fixed3 pick = tex2D(_MainTex, i.uv + float2(du*uTexel, -dv*vTexel));
davg += pick;
dvar += pick * pick;
}
}
davg /= areaSize * areaSize;
dvar = dvar / (areaSize * areaSize) - davg * davg;
// 各rgb値について、4エリアで最も分散が小さいエリアの平均値を取得
// r
fixed r = lerp(aavg.r, bavg.r, step(bvar.r, avar.r));
r = lerp(r, cavg.r, step(cvar.r, min(avar.r, bvar.r)));
r = lerp(r, davg.r, step(dvar.r, min(cvar.r, min(avar.r, bvar.r))));
// g
fixed g = lerp(aavg.g, bavg.g, step(bvar.g, avar.g));
g = lerp(g, cavg.g, step(cvar.g, min(avar.g, bvar.g)));
g = lerp(g, davg.g, step(dvar.g, min(cvar.g, min(avar.g, bvar.g))));
// b
fixed b = lerp(aavg.b, bavg.b, step(bvar.b, avar.b));
b = lerp(b, cavg.b, step(cvar.b, min(avar.b, bvar.b)));
b = lerp(b, davg.b, step(dvar.b, min(cvar.b, min(avar.b, bvar.b))));
// 取得したrgb値を最終的な色としてて出力
fixed4 col = fixed4(r,g,b,1);
return col;
}
ENDCG
}
}
}
fragmentシェーダー内で、Kuwahara Filterの処理が実装されています。アルゴリズムとしてはかなり単純な内容ですが、見た目の変化は大きくいい感じです。
Sizeの値を大きくするほど、油絵感は増しますが、サンプリングするピクセルの個数も増えて処理的には重くなっていくので注意が必要です。
以上、桑原さんのKuwahara Filterの紹介でした!
趣味でゲームのプラグインなどを作成して遊んでいるものです。
記事を拝見させて頂き、大変勉強になりました。
こちらの記事で紹介されているプログラムを使用させていただくことは可能でしょうか?もし、利用可能な場合、利用条件などがあれば教えて頂けないでしょうか?
アルゴリズム自体論文で公開されているものなので、自由に使用して頂いて問題ございません!