始まり
背景をベタ塗りしたい気分になりました。
テクスチャを書き換えるのは面倒なので、ポストエフェクトで簡易的にやります。
過去にSNNフィルタを作ったので、今回はKuwaharaフィルタを試してみます。
Kuwaharaフィルタとは
分かりやすく言うと平均化フィルタの一種かな。
計算方法の分かりやすい説明はこちらかな。
ポストプロセスマテリアル編
ポストプロセスマテリアルの作り方が分からない場合はアウトラインから見るのおすすめ。
ノードを組む
こんな感じ。

カスタムノードを書く
こんな感じ。
#define IterRowBegin(Block) (IterParams[Block].x)
#define IterRowEnd(Block) (IterParams[Block].y)
#define IterColumnBegin(Block) (IterParams[Block].z)
#define IterColumnEnd(Block) (IterParams[Block].w)
#define ViewportUVToClampedBufferUV(UV, U, V, InvSize) (ClampSceneTextureUV(ViewportUVToSceneTextureUV(UV + float2(U, V) * InvSize, PPI_PostProcessInput0), PPI_PostProcessInput0))
float KernelFullSize = Kernel + (((int)Kernel % 2 == 0) ? 1.0 : 0.0);
int KernelHalfSize = (int)floor(KernelFullSize * 0.5);
// 4回使うので事前にdivしておく
float InvWeight = 1.0 / (float)((KernelHalfSize + 1) * (KernelHalfSize + 1));
float3 Color = float3(0.0, 0.0, 0.0);
float MinVarSum = 1e6; // 最小比較で通すため適当に大きい初期値
int4 IterParams[4] =
{
int4(-KernelHalfSize, 0, -KernelHalfSize, 0),
int4(-KernelHalfSize, 0, 0, KernelHalfSize),
int4( 0, KernelHalfSize, -KernelHalfSize, 0),
int4( 0, KernelHalfSize, 0, KernelHalfSize),
};
UNROLL
for (uint block = 0; block < 4; ++block)
{
float3 Ave = float3(0.0, 0.0, 0.0); // 平均
float3 Var = float3(0.0, 0.0, 0.0); // 分散
LOOP
for (int y = IterRowBegin(block); y <= IterRowEnd(block); ++y)
{
LOOP
for (int x = IterColumnBegin(block); x <= IterColumnEnd(block); ++x)
{
float3 c = SceneTextureLookup(ViewportUVToClampedBufferUV(UV, x, y, InvSize), PPI_PostProcessInput0, 0).rgb;
Ave += c;
Var += Pow2(c);
}
}
Ave *= InvWeight;
Var = (Var * InvWeight - Pow2(Ave));
// 最も小さい分散の平均を採用
float VarSum = VectorSum(Var);
if (VarSum < MinVarSum)
{
MinVarSum = VarSum;
Color = Ave;
}
}
return Color;
計算順の可視化
左上 → 右上 → 左下 → 右下の順に処理しています。

ループ命令を明示的
固定長と動的が混ざっている所為か、コンパイラが最適な展開をしてくれないので明示的に書いています。
UNROLLとLOOPの軽い説明は過去編。
ハード性能に依存するので一例ですが、カーネルを31にした場合、約0.2msほどの差分です。


プラグイン編
リポジトリに纏めている&過去にネタにしたので、特に説明はないです。
ついでにポストプロセスマテリアルも入っています。
GitHub - kafues511/StylizedFilterRenderPipeline: This plugin provides a sample implementation for applying stylized filters to objects in the scene using FSceneViewExtensionBase.This plugin provides a sample implementation for applying stylized filters to objects in the scene using FSceneViewExtensionBase. - kafues511/StylizedFilterRenderPipeline左・右・上・下の領域指定箇所
説明はないんだけど我ながらスマートに書けたので自慢。
for文の始点と終点をIterParamsに格納。
PassParameters->IterParams[0] = FIntVector4(-KernelHalfSize, 0, -KernelHalfSize, 0);
PassParameters->IterParams[1] = FIntVector4(-KernelHalfSize, 0, 0, KernelHalfSize);
PassParameters->IterParams[2] = FIntVector4( 0, KernelHalfSize, -KernelHalfSize, 0);
PassParameters->IterParams[3] = FIntVector4( 0, KernelHalfSize, 0, KernelHalfSize);
領域箇所は4回で固定長、範囲は動的な感じ。
#define IterRowBegin(Block) (IterParams[Block].x)
#define IterRowEnd(Block) (IterParams[Block].y)
#define IterColumnBegin(Block) (IterParams[Block].z)
#define IterColumnEnd(Block) (IterParams[Block].w)
float MinVarSum = 1e6; // 最小比較で通すため適当に大きい初期値
UNROLL
for (uint block = 0; block < 4; ++block)
{
float3 Ave = float3(0.0, 0.0, 0.0); // 平均
float3 Var = float3(0.0, 0.0, 0.0); // 分散
LOOP
for (int y = IterRowBegin(block); y <= IterRowEnd(block); ++y)
{
LOOP
for (int x = IterColumnBegin(block); x <= IterColumnEnd(block); ++x)
{
float3 c = CalcSceneColor(ViewportUVToClampedBufferUV(Input.UV, x, y));
Ave += c;
Var += Pow2(c);
}
}
Ave *= InvWeight;
Var = (Var * InvWeight - Pow2(Ave));
// 最も小さい分散の平均を採用
float VarSum = VectorSum(Var);
if (VarSum < MinVarSum)
{
MinVarSum = VarSum;
Color = Ave;
}
}
おわり!!!
モックレベルならポストエフェクトで良さそうですね。
本番環境の場合は事前にテクスチャに適用が安牌ですかね。
この負荷をランタイムに持ち込むのは憚られるですわ。
