【UE5】Kuwaharaフィルタを作ってみた – Unreal Engine 5.4

Unreal Engine

水ノ茉の宣伝

準備中...
ゲームを作る予定なの
水ノ茉こおり

始まり

背景をベタ塗りしたい気分になりました。

テクスチャを書き換えるのは面倒なので、ポストエフェクトで簡易的にやります。

過去にSNNフィルタを作ったので、今回はKuwaharaフィルタを試してみます。

Kuwaharaフィルタとは

分かりやすく言うと平均化フィルタの一種かな。

計算方法の分かりやすい説明はこちらかな。

Kuwahara filterとかいう明らかに日本人の名前な画像フィルターに出会い、試してみたらすごかったので紹介する。 - Qiita
6/15追記あとがきの提案について書きました 写真表現としての桑原フィルターの提案#はじめにKuwahara filter(桑原フィルター)とは桑原フィルターは桑原道義さんという大学教授(W…
Kuwahara filter - Wikipedia

ポストプロセスマテリアル編

ポストプロセスマテリアルの作り方が分からない場合はアウトラインから見るのおすすめ。

【UE5】アウトラインを描いてみた ポストプロセスマテリアル編 - Unreal Engine 5.3
始まり描画エンジニアになりたい方の入門編にもちょうどよいコンテンツ。そう、アウトライン(輪郭線)(尚主観。異論は認めよう。)それを幾つかの実装方法で描いていきます。アウトライン(輪郭線)とは読んで字の如く。描画まわりの専門用語が一切必要の無...

ノードを組む

こんな感じ。

カスタムノードを書く

こんな感じ。

#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の軽い説明は過去編。

【UE5】Customノードでも使える命令 BRANCH FLATTEN UNROLL LOOP について
始まり現在はレンダリングまわりに従事している筆者さんですが、最初から描画まわりに興味のあるタイプではなかったです。入社してからシェーダーとか諸々触り始めました。全く興味がない訳でもないけど、学生時代はゲームエンジンから作らされていた関係でそ...

ハード性能に依存するので一例ですが、カーネルを31にした場合、約0.2msほどの差分です。

命令をコンパイラにお任せ
命令を明示的に指定

プラグイン編

リポジトリに纏めている&過去にネタにしたので、特に説明はないです。

【UE5】アウトラインを描いてみた プラグイン編 - Unreal Engine 5.3
始まり描画エンジニアになりたい方の入門編にもちょうどよいコンテンツ。そう、アウトライン(輪郭線)(尚主観。異論は認めよう。)それを幾つかの実装方法で描いていきます。アウトライン(輪郭線)とは読んで字の如く。描画まわりの専門用語が一切必要の無...

ついでにポストプロセスマテリアルも入っています。

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 FSceneViewExten...

左・右・上・下の領域指定箇所

説明はないんだけど我ながらスマートに書けたので自慢。

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;
	}
}

おわり!!!

モックレベルならポストエフェクトで良さそうですね。

本番環境の場合は事前にテクスチャに適用が安牌ですかね。

この負荷をランタイムに持ち込むのは憚られるですわ。

関連