ぼっちプログラマのメモ

UE4とかUE5とかについて書いたり書かなかったり。

UE4のスクリーンショット機能をちょっと高速化してみた

お久しぶりです。
ブログ再開しましたので、改めてよろしくお願いします!

はじめに

f:id:pafuhana1213:20161124230243j:plain
突然ですが、UE4には「標準スクリーンショット」と「高解像度スクリーンショット」の
2種類のスクリーンショット機能が用意されています。
docs.unrealengine.com

使い方は↑を見て頂くとして…使ってみると分かるのですが
スクリーンショットを撮った瞬間にカクついてしまいます。

一瞬とは言えカクつくのは見栄え的によくないですし、
VRコンテンツの場合はそのカクつきが酔いに直結してしまいます…

ということで、エンジン改造して高速化しました
…色々と保証できない + テストを殆どしていないので
ご利用は計画的に!


あ、エンジンはUE4.14.0です。
あ、あと、解説とかいらねえよという方は「まとめ」を見て下さい。

高速化の方針

今回はVR向けに「カクつきが起きない」ことを最優先とします。
ということで、高解像度スクリーンショットは対応しません。
(そもそも複数回レンダリングする手法なので…負荷大き過ぎます…)

まずは、標準スクリーンショットである"shot"コマンドが
どういう流れで処理が走って、どの処理が重いのかを見ます。

  1. "shot"でエンジンコードを検索し、GameViewportClient.cppで文字列解析してることを確認
  2. 走っている処理を追い、bIsScreenshotRequestedというフラグをtrueにしている部分を発見
  3. bIsScreenshotRequestedを返すUnrealClientIsScreenshotRequested関数がどこで使わているかを調べます
  4. UGameViewportClient::ProcessScreenShotsという関数で使わているようです!ゴールは近そうです!
  5. その関数の中を見ると…Save the contents of the array to a png fileというコメントが…!見つけたぞ!世界の歪みを!
// Save the contents of the array to a png file.
TArray<uint8> CompressedBitmap;
FImageUtils::CompressImageArray(Size.X, Size.Y, Bitmap, CompressedBitmap);
FFileHelper::SaveArrayToFile(CompressedBitmap, *ScreenShotName);


この時点で予想できる処理のボトルネックは3つです

  1. 画面上の各ピクセルの色情報取得
  2. pngへの変換処理
  3. 変換したpng画像の保存処理

↑のコードをコメントアウトして、1の処理の負荷を確認します…
「お、あまり負荷ない!」ということで、2と3の処理を高速化します。

↑で載せたコードをコメントアウトし、その部分に新しいコードを追加していきます。

png画像への変換処理をOFF

png画像のほうが色々と便利だとは思うのですが、処理負荷軽減のため、
シンプルにbmp画像で出力します。

ピクセルの色情報は、Bitmap内に保存されています。このデータと
Bitmap画像を出力するCreateBitmap関数を使います。
FFileHelper::CreateBitmap | Unreal Engine API Reference

ScreenShotName = FPaths::GetBaseFilename(ScreenShotName, false);
FFileHelper::CreateBitmap(ScreenShotName.GetCharArray().GetData(), GScreenshotResolutionX, GScreenshotResolutionY, Bitmap.GetData());

これでpng画像ではなく、bmp画像を出力するようになりました。
かなり軽くはなりますが…まだ若干カクつくと思います。さらなる高速化が必要です。

画像出力処理を非同期に

ファイルIO的に重い画像出力処理ですが…not 非同期処理です。
画像出力処理が終わるまでゲーム全体の処理を待つようになっています…

ということで、非同期にして待ち時間をなくすようにしました!
これでカクつきがほぼ無くなりました!簡単!

ScreenShotName = FPaths::GetBaseFilename(ScreenShotName, false);
AsyncTask(ENamedThreads::BackgroundThreadPriority, [ScreenShotName, Size, Bitmap]()
{
    FFileHelper::CreateBitmap(ScreenShotName.GetCharArray().GetData(), Size.X, Size.Y, Bitmap.GetData());
});

…もっと丁寧に組みたい方は↓を参考にするといいと思います
Using AsyncTasks - Epic Wiki


まとめ

GameViewportClient.cppのUGameViewportClient::ProcessScreenShots関数にある

// Save the contents of the array to a png file.
TArray<uint8> CompressedBitmap;
FImageUtils::CompressImageArray(Size.X, Size.Y, Bitmap, CompressedBitmap);
FFileHelper::SaveArrayToFile(CompressedBitmap, *ScreenShotName);

ScreenShotName = FPaths::GetBaseFilename(ScreenShotName, false);
AsyncTask(ENamedThreads::BackgroundThreadPriority, [ScreenShotName, Size, Bitmap]()
{
    FFileHelper::CreateBitmap(ScreenShotName.GetCharArray().GetData(), Size.X, Size.Y, Bitmap.GetData());
});

に書き換えることで、
スクリーンショット使用時のカクつきを軽減することができました!

ただし、色々と保証できない + テストを殆どしていないです!
ご利用は計画的に!




(…プラグイン化試してみよっかな。あまり期待はしないで下さい)