こんにちは、株式会社Leon Gameworksのキンアジと遠藤です。
前回までは、コンテンツブラウザ上のアセットに対して変更を行うツールの作成方法を解説しました。今回は、ビューポート上の作業を簡略化するツールの作成方法を解説していきます。
0:動作環境
本記事はUE5.3.2を基に執筆しており、エディターの言語は「英語」でスクリーンショットを撮影しております。
本記事で作成するツールのプロジェクト一式のデータは以下からダウンロードできます。
1:スクリプトアクション
1-1:スクリプトアクションとは?
今回はビューポート上で扱える機能の中で、「スクリプトアクション」という機能を使ったツールを紹介します。
スクリプトアクションは、任意のアクターやアセットの右クリックメニューに対して独自のメニューを追加することができる機能です。

スクリプトアクションは、[Editor Utility Blueprint]から簡単に作成できるので、まずは試しに独自のメニューを作成してみます。
1-2:スクリプトアクションの作成
まず、スクリプトアクション用の関数を定義するための[Editor Utility Blueprint]クラスを作成します。
コンテンツブラウザ左上の[+Add]から、[Editor Utilities → Editor Utility Blueprint]を選択します。

すると、親クラスを選択するウィンドウが表示されます。今回はアクター用のスクリプトアクションを作成するため、[Actor Action Utility]を選択してから[Select]をクリックします。

作成したアセットの名前は「EUB_ActorActionUtility」としておきます。
「EUB_ActorActionUtility」をダブルクリックしてアセットを開きます。[Editor Utility Blueprint]は、通常のブループリントクラスと同じようにロジックを作成できます。

メニューを作成するには、「関数」もしくは「カスタムイベント」を作成する必要があります。
最初にテスト用の関数を作成してメニューを作ります。[My Blueprint]タブの[Functions]の右の[+]ボタンをクリックして関数を作成しましょう。名前は「Test Menu」としておきます。

これだけで、スクリプトアクションとしてのメニューを作成する事ができました。試しに、Test Menu関数に[Print String]関数を繋げてからコンパイルをしてみます。

レベル上の適当なアクターを右クリックすると[Scripted Actor Actions]というメニューの中に、上記で作成した[Test Menu]という項目が表示され、クリックすると[Print String]ノードの処理が実行されます。

このように、スクリプトアクションの機能を使うことで、簡単にアクターに対するメニューを追加することができます。では、このスクリプトアクションを用いてより実用的なツールを作成していきます。
2:選択中のカメラから撮影し、画像を出力するツールの作成
2-0:前提
UE5で開発を進めていく上で、よくある要件として「マップ内のスクリーンショットが欲しい」というものがあります。
[Movie Render Queue]というプラグインを使ってより高度な撮影を行うこともできますが、今回は「気軽にその場でキャプチャするツール」を作成したいと思います。
※ Movie Render Queueについては、公式ドキュメントを参照してください。
最も簡単な撮影方法は、ビューポート上で[F9]キーを押してスクリーンショットを撮影することですが、この方法ではビューポートカメラで撮影するため、カメラを動かすと撮影位置が変わってしまうという問題があります。
同じ位置から撮影するには、ビューポート上のカメラアクターの右クリックメニューから[Pilot]というコマンドを実行することでPilotモードになり、配置したカメラの視点をビューポートに表示することができます。

※ 簡易的にスクリーンショットを行う方法については、公式ドキュメントで紹介されています。
そこで今回は「選択中のカメラに対して自動で[Pilot]コマンドを実行し撮影するツール」を作成していきたいと思います。
2-1:撮影機能の作成
まずはツールのベースとなる撮影を行う処理をブループリントで作成していきます。
「1-2.スクリプトアクションの作成」で作成した[EUB_ActorActionUtility]を開き、新規に[Capture Current Viewport]という関数を作成します。

コンソールコマンドをブループリントから実行するには、[Execute Console Command]ノードを使用します。

[Command]ピンにて[HighResShot]と必要な引数の文字列を指定し、コマンドを実行します。
※ [HighResShot]コマンドについては、公式ドキュメントを参照してください。
今回はファイル名と解像度を指定できるようにするため、[Capture Current Viewport]関数に引数を3つ追加します。


引数で受け取った情報を組み合わせてコマンドを作成します。[HighResShot]コマンドの引数は、[filename]のみ引数名を指定しての代入となり、それ以外のパラメータは引数に入れた順番によって扱いが変わります。
今回の場合は、下のような規則でコマンドを作成します。
HighResShot filename=[ファイル名] [解像度X]x[解像度Y]

コンパイルをしてからスクリプトアクションを実行してみます。
ビューポート上の適当なアクターを選択し、右クリックから[Scripted Actor Actions → Capture Current Viewport]を選択します。

すると、Capture Current Viewport関数の引数が一覧されたポップアップウィンドウが表示されるので、それぞれ任意の値を入力して[OK]を押します。

実行すると、プロジェクトの「Saved\Screenshots\WindowsEditor」以下に現在のビューポートの描画がPNGファイルとして保存されます。

これでベースとなる撮影機能は作成できたので、次に「選択中のカメラによる連続した撮影」機能を実装していきます。
2-2:選択中のカメラによる連続撮影ツールの作成
作成の前に、今回の実装上の注意点について1つ記述しておきます。
HighResShotによる撮影は、同じフレーム内では1度のみしか行えません。そのため、[For Each Loop]等によって複数のカメラに対して撮影を行う場合は「撮影」→「次のフレームを待つ」→「撮影」のように1フレーム待つ必要があります。
また、次のフレームを待つには[Async Editor Delay]を実行する必要がありますが、関数の中では使用ができないため、連続撮影は「カスタムイベント」を使って実装していきます。
では、早速ツールを作成していきます。再び「EUB_ActorActionUtility」を開き、イベントグラフに新規でカスタムイベントを作成します。
名前は[Capture By Selected Cameras]とします。カスタムイベントを使用してスクリプトアクションを作成する場合は、カスタムイベントの詳細パネルから[Call In Editor]にチェックを入れます。

カスタムイベントの引数にInteger型の[ResX]と[ResY]を追加します。

カスタムイベントに[Async Editor Delay]ノードを接続して、[Minimum Frames]を[1]に設定します。

次に選択中のアクターを取得するため、[Get Selected Level Actors]ノードと繋ぎます。

選択中のアクターを取得するには、[Get Selected Actors]もしくは[Get Selected Level Actors]ノードで取得できます。

選択中のアクターから「Camera Actor」のみに限定するために、[Filter by Class]ノードでフィルタリングします。[Object Class]を「Camera Actor」に設定し、[Filter Type]を[Include]に設定します。

取得した「Camera Actor」に対して順番にPilotモードへ切り替えてから撮影を行う処理を書きます。Pilotモードへ切り替えるには、[Level Editor Subsystem]の[Pilot Level Actor]を使用します。

[Filter by Class]ノードの戻り値の配列を[For Each Loop]を使用せずに要素を取り出していくために、内部的な数値のカウンタ用として[Local Capture Counter]という名前でInteger型の変数を作成します。

[Branch]ノードを作成し、[Filter by Class]ノードに繋ぎ、[Length → Local Capture Counter]で比較し、BranchのConditionに接続します。

[Length → Local Capture Counter]がTrueの場合に撮影処理を行いたいので、[Branch]ノードに[Pilot Level Actor]を接続します。[Actor to Pilot]には、[Filter by Class]ノードの戻り値の配列から[Local Capture Counter]変数を使って取り出した要素を接続します。

「2-1:撮影機能の作成」で作成した[Capture Current Viewport]を[Pilot Level Actor]に接続します。
[File Name]には、[Camera Actor]のアウトライナー上の名前を指定したいので、[Get Actor Label]ノードで[Camera Actor]の名前を取得し[File Name]に繋げます。[ResX]と[ResY]は[Capture By Selected Camera]の引数と接続します。

撮影の処理をくり返せるようにループさせます。[Capture Current Viewport]の後に[Increment]ノードを接続し、[Local Capture Counter]変数に加算します。

1フレーム間を空けてから次の撮影を実行するため、[Async Editor Delay]を使ってフレーム更新を待ちます。[Async Editor Delay]ノードの[Minimum Frames]は[1]を設定します。

[Async Editor Delay]の[Complete]ピンから[Branch]ノードに接続し、ループさせるようにします。

全ての撮影完了時にPilotモード状態で終わってしまうため、[Branch]ノードの[False]ピンに[Eject Pilot Level Actor]ノードを繋げてビューポートカメラの視点に戻します。

最後に、[Show Message Dialog]ノードを使用して撮影が完了したことを示すメッセージを表示します。

これで撮影ツールが完成しました。
早速試してみましょう。レベル上の適当な場所に[Camera Actor]を3つ配置します。

配置した[Camera Actor]を全て選択し、右クリックメニューから[Scripted Actor Actions → Capture By Selected Cameras]を選択して撮影を実行します。

これで、選択中のカメラから自動的に撮影するツールができました。

2-3:使用者を考慮したツール
さて、ここまでで紹介したツールは「最低限」の実装となっており、いくつか改善点があります。
1:撮影中かどうかの判断と途中キャンセル
撮影の開始から終了までビューポートが絶え間なく切り替わることはありますが、カメラが同じ場所にある場合にはビューポートの見た目はほとんど変わらず、場合によってはツールが正常に動いているかの判断がつきません。
また、大量に撮影する場合に途中でキャンセルしようと思ってもできません。
2:ガーベジコレクションによる撮影の中断
UE5には「ガーベジコレクション」という、定期的に使用していないオブジェクトを破棄する機能が備わっています。
スクリプトアクションによる処理は、揮発的なインスタンスが作成されますが、このインスタンスはガーベジコレクションによって破棄される対象となるため、運悪く撮影が止まってしまうことがあります。
これらの点を「Editor Utility Task」を使用して改善していきます。
「Editor Utility Task」は、実行中エディタの右下にポップアップが表示され、実行が終わると完了を通知してくれます。また、実行するインスタンスは「Editor Utility Subsystem」に保持されるため、ガーベジコレクションでインスタンスが消滅しません。

今回の撮影ツールと「Editor Utility Task」は相性が良いため、作成したツールを「Editor Utility Task」へ移行し、より使いやすいものにしていきましょう。
「Editor Utility Task」用の「Editor Utility Blueprint」アセットを作成するため、コンテンツブラウザ左上の[+Add]から、[Editor Utilities → Editor Utility Blueprint]を選択します。

親クラスを選択するウィンドウから、[Editor Utility Task]を選択して[Select]をクリックします。

アセット名は「EUB_CaptureBySelectedCamerasTask」とし、アセットを開きます。
「Editor Utility Task」では、タスクが開始されたときに実行される[Event Begin Execution]イベントから処理を繋ぎ、終了時には[Finish Executing Task]ノードを実行します。

「EUB_ActorActionUtility」の[Capture By Selected Cameras]イベントで作成したノードをコピーし、「EUB_CaptureBySelectedCamerasTask」へ移植します。カスタムイベントのかわりに[Event Begin Execution]イベントを接続し、[Show Message Dialog]のかわりに[Finish Executing Task]ノードを接続します。

[Local Capture Counter]変数が定義されていないので、参照ノードを右クリックし、[Create variable ‘LocalCaptureCounter’]を選択して変数を作成します。

移植前にカスタムイベントの引数に接続されていた2つのIntegerピンは、「EUB_CaptureBySelectedCamerasTask」内にそれぞれ[ResX][ResY]という名前で変数を作成してそれぞれ参照させます。

また、「Editor Utility Task」は途中でキャンセルすることができるようにポップアップ内に[Cancel]ボタンが用意されています。[Cancel]ボタンを押すとポップアップの表示は完了となりますが、内部的な処理はそのままになってしまい、撮影は止まらないためキャンセル対応を追加で行う必要があります。
[Cancel]ボタンが押されると、[Event Cancel Requested]イベントが実行されます。また、[Cancel]ボタンが押されたかどうかは[Was Cancel Requested]ノードで取得できます。

[Branch]ノードの判定処理に[Was Cancel Requested]の値も合わせて判定することでキャンセル判定を追加できます。
画像のように[Was Cancel Request]の値を[Not Boolean]ノードで反転させ、[AND]ノードで条件を追加します。

ここまで完了したら、「EUB_CaptureBySelectedCamerasTask」を閉じて「EUB_ActorActionUtility」を開きます。
[Capture By Selected Cameras]イベントから「EUB_CaptureBySelectedCamerasTask」のタスクを実行するように組みます。
[Editor Utility Task]を実行するには、[Construct Object from Class]で「EUB_CaptureBySelectedCamerasTask」のインスタンスを作成し、[Editor Utility Subsystem]の[Register and Execute Task]ノードを使用してタスクを実行します。
また、タスクの実行前に解像度のパラメータをインスタンスの変数に渡します。

コンパイルをしてからスクリプトアクションを試してみましょう。撮影中は右下にポップアップが表示され、ガーベジコレクションにも削除されないようになりました。

2-4:トラブルシューティング
最後に撮影ツールを使うにあたって、よく挙がる問題と対策方法を記載しておきます。
1:描画更新回数の不足
Auto Exposureによる露出調整やUE5から実装されたLumenによるGlobal Illumination、Final Gather等の描画処理は、1フレームの更新では収まりきらず数フレームの間待機しないと撮影したい視点にならないこともあります。
この問題に対応するには、撮影前に[Async Editor Delay]等で任意のフレーム分描画を回すか、撮影を待つような独自のしくみをC++で追加する必要があります。
2:リアルタイムでのレンダリングが無効の場合
昨今ではリモートワークが増えてリモートデスクトップによる開発が増えていると思いますが、UE5ではリモートデスクトップ等でエディタが起動されているときはデフォルトでリアルタイムでのレンダリングがOFFになっています。この状態だとビューポートの描画が省略されてしまい、撮影結果が正常に出力されないことがあります。
この問題を解決するには、ビューポート左上から[Realtime]にチェックを入れるか、[Level Editor Subsystem]の[Editor Invalidate Viewports]ノードを使用すると、フレームの描画を更新してくれます。

3:「Region」や「Level」の表示/非表示
World Partitionを使用したマップのRegionや、ストリーミング方式のマップ内に所属しているLevelは、撮影する際にそれぞれ表示/非表示を制御することがあります。
World Partitionを使用したマップで、RegionをLocation Volume化している場合は[Get All Actor Of Class]で取得することができます。また、Location Volumeの[Load]や[Unload]ノードでRegionに所属するアクターをロード/アンロードして表示/非表示を切り替えることができます。
※World Partitionについては、公式ドキュメントを参照してください。

ストリーミング方式のマップ内に所属しているサブレベルは、[Set Level Visibility]ノードで表示/非表示を切り替えることができます。画像のように引数の[Level]に[Get Streaming Level → Get Loaded Level]ノードを渡すことで取得できます。

3:まとめ
いかがでしたでしょうか? 今回はスクリプトアクションを使用した撮影ツールのレシピを紹介しました。
本連載では、まだまだ様々なレシピを紹介していきますので、ぜひみなさん活用してUEでより良い開発を目指していただければと思います!
本記事で作成したツールのプロジェクト一式のデータは、以下からダウンロードできます。

株式会社Leon Gameworks
●公式サイト
www.leon-game.co.jp
●X(Twitter)
@Leon_Gameworks

トンコツ(遠藤俊太)
●トンコツ開発ブログ
shuntaendo.hatenablog.com
●X(Twitter)
@tonkotsu3656

キンアジ
●キンアジのブログ
kinnaji.com
● X(Twitter)
@kinnaji_blog
TEXT_トンコツ&キンアジ(Leon Gameworks)
EDIT_小村仁美 / Hitomi Komura(CGWORLD)