BLOGブログ

2021.01.13UE4/ UMG

[UE4]RetainerBoxを用いたUMG Jitterへの対処

執筆バージョン: Unreal Engine 4.26

こんにちは。エンジニアの小倉です。
今回は、UMG Jitterと呼ばれる現象に対して、簡易的な対処方法を紹介します。

0. 目次

1. UMG Jitterとは
2. 現象の再現方法
3. UVスクロールによる対処
4. ピクセル単位の移動と組み合わせる
5. 注意事項

1. UMG Jitterとは

この現象は、以下のページで話題にあがっています。
UE4 answerhub
Unreal Engine Forums

上記によると、UMG Jitterは主にウィジェットを平行移動・拡縮した際に、それらの変形がピクセルにスナップするために、ウィジェットがジリついて見える現象とのことです。これはRenderTransformを変更した場合だけでなく、ScrollBoxなどの子ウィジェットで内部的に平行移動した場合にも起こります。

これはUMGのどこでも起きる現象ですが、常に小さく動き続けるようなウィジェットでしか悪目立ちしません。多くの場合、ウィジェットは常に動きませんし、短い時間のアニメーションではほぼ気づきません。
よってここでは「現象が悪目立ちするウィジェット」に対して、Blueprintで簡易的に対処する方法を紹介します。また、今回は主に平行移動によって起きるUMG Jitter現象について紹介し、拡縮については取り扱いません。

2. 現象の再現方法

まず、大きさの異なるTextをHorizontalBoxに配置した「W_JitterTestA」を作成します。

次に、このウィジェットに以下のような平行移動の処理を追加します。
これは毎フレームX座標値を加算して、HorizontalBoxを平行移動させる処理です。

PIEで確認すると以下のようになり、ジリついていることが確認できます。

3. UVスクロールによる対処

UMG Jitterはウィジェットが平行移動したとき、新しい座標がピクセルにスナップして座標がとぶことで起こります。これを解決するには、ピクセルにスナップしない機能(例えばマテリアルなど)を使う必要があります。ここでは、RetainerBoxのEffect Materialを用いたUVスクロールによって、ウィジェットを擬似的に平行移動させる方法を紹介します。

まず、RetainerBoxで使うための以下のようなEffect Materialを作成します。
Translationが平行移動のパラメータで、TextureがRetainerBoxから渡される子ウィジェットの描画結果のテクスチャです。

W_JitterTestAを複製してW_JitterTestBを作成し、ウィジェットの親にRetainerBoxを追加します。
EffectMaterialには上記のマテリアルを設定します。

次に、平行移動の処理を「RenderTranslationの変更」から「DynamicMaterialInstanceのパラメータの変更」に書き換えます。ここで、ウィジェットの平行移動とUVスクロールによる平行移動を一致させるためにはDPIを考慮する必要があるので、GetViewportScaleの値を乗算します。

PIEで確認すると以下のようになりました。
上がSetRenderTranslationによる平行移動、下がUVスクロールによる平行移動です。

ジリつきはありませんが、左端から「UVスクロールによってループしてきた右端」が表示されています。
上の例では少しわかりにくいですが、この方法はウィジェット自体が平行移動しているわけではなく、RetainerBoxで描画されたウィジェットの表示位置をズラしているだけです。このためRetainerBoxの範囲外にある描画されていないウィジェットは、(実際には移動していないため)表示できないという問題があります。

4. ピクセル単位の移動と組み合わせる

表示するウィジェットがRetainerBoxの描画範囲に収まっているならUVスクロールによる方法でも問題ありませんが、実際にはClippingなどにより描画範囲が削られることもあり、汎用的に使うのは難しいです。そこで「2. 現象の再現方法」と「3. UVスクロールによる対処」の2つの方法を組み合わせて問題に対応します。具体的には、ピクセル単位の移動はウィジェットの平行移動で行い、サブピクセル単位の移動はUVスクロールで行います。イメージとしては、以下のように平行移動の値を整数部と小数部に分けて、整数部の移動をSetRenderTranslationで行い、小数部の移動をUVスクロールで行います。

はじめに、W_JitterTestBを複製してW_JitterTestCを作成し、平行移動の処理を以下のように変更します。

順に説明します。
まず、以下が平行移動の値を更新する処理になります。さきほど「整数部と小数部に分けて別々に処理する」と説明しましたが、実際にはViewportScaleによってピクセルの大きさ(「ピクセル単位」と呼んでいる基準の量)は変動します。ここで、Baseという変数はViewportScaleの逆数で、1ピクセルの大きさとなります(ViewportScaleの値はDPIであり、逆数をとると1ピクセルの大きさが得られます)。
もしViewportScaleが1なら、単純に平行移動の値を整数部の値と小数部の値に分けて処理できますが、ViewportScaleが1でない場合はViewportScaleの逆数を用いて、ピクセル単位の平行移動の値と、サブピクセル単位の平行移動の値をそれぞれ計算する必要があります。

ピクセル単位の平行移動の値は、以下のように計算します。

ピクセル単位の平行移動はSetRenderTranslationで行います。以下のように実装しました。

サブピクセル単位の平行移動の値は、以下のように計算します。

サブピクセル単位の平行移動はUVスクロールで行います。以下のように実装しました。 ※ 最後にBaseとGetViewportScaleノードと乗算していますが、説明のためにノードを残しているだけで、実際にはBaseとGetViewportScaleの乗算は1になるためこの部分の計算は不要です。

PIEで確認すると以下のようになりました。
一番下が2つの方法の組み合わせによる平行移動で、ジラつきがなく、またUVスクロールによるループもないことを確認できます。

5. 注意事項

この方法には、2つの問題があります。
1. RetainerBoxを使用した時点で見た目が少し変わる
2. RetainerBoxの描画範囲の端がチラつくことがある

1.はRetainerBoxを使うなら妥協が必要です。2.は以下のような現象ですが、UVスクロールを使うとどうしても見えることがあります。気になる場合は、端を表示しないようマスクなどで対処します。