[UE4]nDisplayをやってみる

##概要
UE4.20からnDisplayと言われる複数ディスプレイに同次レンダリングをサポートする機能が追加されております。PCのGPUからの出力だけでは一つのマシンに負荷が偏ってしまうほか、かなりゴリゴリのマシンを用意しなくてはいけません。ですが、このnDisplayを使うと複数のマシンをネットワークを介してレンダリングを負荷を分散させることができます。
最初のプロジェクト新規作成画面でnDisplayのテンプレートがありますが、今回は理解の為にも1から自分で中身を掘り下げ設定していきたいと思います。
テンプレートを使った利用方法はボクも良くお世話になっている[ヒストリア先生](https://historia.co.jp/archives/10812/)の方で紹介されております。
2020/05/25現在、いまだプラグインはBeta番となっておりますが扱い方をまとめていきたいと思います。

![000.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163667/0de78c20-74f0-3993-5444-0e71215a39c4.jpeg)

##準備
それではまず基本的な、というより必須となる設定から見ていきましょう。やらなくてはいけないのは以下の項目です。
・プラグインの有効化、再起動
・nDisplayの有効化
・ワールドにnDisplayClusterSettingsを入れる
・.cfgファイル(設定ファイル)の追加、編集
・デフォルトのPawnにはDisplayClusterPawnを、ゲームモードにはDisplayClusterGameModeを継承させる

順番にやっていきましょう。まずはプラグインの有効化から、 Edit->Plugins でPluginsウィンドウを開き、 Searchで "nDisplay" と入力すれば該当のプラグインが表示されると思います。

![001.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163667/42785f6e-0dba-6430-d6cd-6c06ba028e75.jpeg)

Enabledにチェックを入れ、再起動を促されるのでそのまま再起動しましょう。
次に、 Editor->ProjectSetting->Plugins に nDisplayという項目が追加されていると思うので、このMainの項目もチェックを入れます。もし何も再起動を促す通知がきたら再起動してください

![002.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163667/b8cdf4e1-e69e-2d36-087b-e8a282eb166e.jpeg)

ワールドに必須となるnDisplayClusterSettingsを入れます。これはエディター上で確認する際に設定ファイルを読み込んでプラグインに情報を渡す役割が在ります。
プラグインのクラスになるので、 View Options の設定を変えて nDisplayのC++クラスをエディタから確認できるようにします。</p>

![003.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163667/0e41e6c6-1782-0865-bce3-312105761cd2.jpeg)

このアクタをワールドに追加し、Config fileとNodeID、Show projection screensという項目が確認できます。
・Config file : ウィンドウ設定ファイルである.cfgファイルのパス 例えばContent直下に設定ファイルが在るなら、 "Content/Config/test.cfg" のように入力する
・Node ID : .cfg内で設定している各クラスタノードの一つを入力
・Show projection screens : エディタでのプレビュー時に .cfg ファイルで設定しているウィンドウのプレビューを表示する

![004.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163667/285cc761-47ea-21d4-d5d0-7052b10fa720.jpeg)

.cfgファイルは [公式](https://docs.unrealengine.com/ja/Engine/Rendering/nDisplay/Configuration/index.html)が一番参考になりますが、このサンプル用のファイルを記載しておきます

```config
# info Unrealのバージョン情報
[info] version="23"

# cluster node 各クラスタを定義する
# id : ユニークID
# addr : 実行するコンピューターのアドレス IPv4のみ対応
# window : アプリケーションのメインウィンドウサイズと位置を指定するコンフィグID名をつける
# sound : 音を鳴らすかどうか、デフォルトはFalse
# port_cs port_ss port_ce、それぞれクラスタ同期用、スワップ同期用、クラスタイベント用の各ポート番号
# → デフォルトのポートはそれぞれ 14001 14002 14003
# master : クラスタ内のマスターノードかどうかを定義 全クラスタ内で一つだけ宣言できる
[cluster_node] id="node_left" addr="127.0.0.1" window="wnd_left" master="true" sound="true" port_cs="14001" port_ss="14002" port_ce="14003"
[cluster_node] id="node_right" addr="127.0.0.1" window="wnd_right" sound="true"

# window アプリケーションのウィンドウサイズを定義する
# id : Cluster nodeで定義したユニークID
# fullscreen : 全画面で表示するかどうか
# WinX WinY ResX ResY それぞれウィンドウ開始位置と解像度サイズ
# viewports : アプリケーション内でレンダリングする領域を指定するユニークID
# postprocess : 出力画像にポストプロセスを行いたい場合、このユニークIDを指定する
[window] id="wnd_left" fullscreen="false" WinX="0" WinY="0" ResX="640" ResY="480" viewports="vp_LT"
[window] id="wnd_right" fullscreen="false" WinX="640" WinY="0" ResX="640" ResY="480" viewports="vp_RT"


# viewport アプリケーションの内のビューポートを定義する
# id : windowで定義したユニークID
# X Y : アプリケーションウィンドウのスクリーンスペースのビューポート内の左上の座標
# width height : レンダリングされたフレームの幅と高さ
# projection : ビューポートに描画する必要なあるビューを定義するユニークなID
# camera : このビューポートに表示させたいカメラのID
# buffer_ratio : 画質品汁 0 ~ 1
[viewport] id="vp_LT" X="0" Y="0" width="300" height="220" projection="proj_sample_LT"
[viewport] id="vp_RT" X="0" Y="0" width="300" height="220" projection="proj_sample_RT"


# projection 投影方法を定義する
# id : viewportで定義したユニークID
# type : 投影タイプ
# → simple 視錘台をビューポートにレンダリングする
# → easyblend : https://www.scalabledisplay.com/  を使用して構成したビューポート
# → mpcdi : https://vesa.org/vesa-standards/  を使用して構成したビューポート
# screen : アプリケーションがビューポートにレンダリングする必要のある3D空間視錘台を定義するユニークID
[projection] id="proj_sample_LT" type="simple" screen="scr_LT"
[projection] id="proj_sample_RT" type="simple" screen="scr_RT"


# screen 各スクリーンサイズを定義する
# id : projectionで定義したユニークID
# loc : 親を基準としたVR空間でのスクリーンの中心位置
# rot : スクリーンの向き　ピッチ、ヨー、ロール
# size : ローカルX軸とY軸に沿った長方形スクリーンの合計サイズ（メートル）
# parent : オブジェクトの親として機能する必要のある scene_nodeのユニークID
# → 親を基準としない場合、VRルートが基準となる
[screen] id="scr_LT" loc="X=1.5,Y=0,Z=1.5" rot="P=0,Y=0,R=0" size="X=3,Y=3" parent="screen_LT"
[screen] id="scr_RT" loc="X=1,Y=0,Z=1" rot="P=0,Y=0,R=0" size="X=1,Y=1" parent="screen_RT"


# postprocess ウィンドウに対する追加処理
# 現在サポートしているのは OutputRemap の一つのみ
# file : UVマッピングされた平面ジオメトリを含む.objが必要
# [postprocess] id=pp_1 type="OutputRemap" file="remap_1.obj"


# camera
# id : ユニークID
# loc : 親を基準としたカメラの中心位置
# tracker_id : VRデバイスを定義するInputのユニークID
# tracker_ch : tracker_idを使用した際に、このパラメータでnDisplayがトラッキングデータを読み取るデバイスのチャネルを指定する
# parent : オブジェクトの親として機能する必要のある scene_nodeのユニークID
# eye_swap : ステレオコピックモード(VRの両眼)
# eye_dist : eye_swapを使用した際の視差距離
# force_swap : モノスコピックモード(360°)
[camera] id="camera_static" loc="X=0,Y=0,Z=1.7" tracker_id="VRPNTracking" tracker_ch="0" parent="eye_level" eye_swap="false" eye_dist="0.064" force_offset="0"


# scene_node
# id : ユニークID
# loc : 親を基準としたVR空間でのシーンノードの中心位置
# rot : 向き
# parent : 親のscene_node
# tracker_id : InputのユニークID
# tracker_ch : tracker_idを使用した際のチャネル
[scene_node] id="screen_LT" loc="X=0.Y=0,Z=0" rot="P=0,Y=0,R=0"
[scene_node] id="screen_RT" loc="X=0.Y=0,Z=0" rot="P=0,Y=0,R=0"


# input 入力デバイス
# id : ユニークID
# type : VRPN入力デバイスタイプ
# → tracker はトラッキング デバイス。
# → analog は軸データを生成するデバイス。
# → button はブール ボタン データを生成するデバイス。
# → keyboard は標準のコンピュータ キーボード。
# addr : 特定のデバイスを操作するVRPNサーバのアドレス DeviceName@ADDRESS::PORT
[input] id="VRPNTracking" type="analog" addr="Mouse0@127.0.0.1" loc="X=-1.5,Y=0,Z=3.4" rot="P-0,Y=0,R=0" front="X" right="Y" up="-Z"


# input_setup inputの追加パラメータ
# id : inputで定義したユニークID
# ch : チャネル
# bind : バインドされているキー
[input_setup] id="controller" ch="0" bind="nDisplay Button 0"


# general 一般設定
# swap_sync_policy : 0,1,2
# → 同期なし、ソフトウェアのスワップ同期、NVスワップロック(OpenGLのみ)
[general] swap_sync_policy="1"


# Network
# cln_conn_tries_amount : 接続試行回数
# cln_conn_retry_delay : 接続試行に挟むディレイ時間
# game_start_timeout : マスタへの接続試行時間
# barrier_wait_timeout : 正常につながっているか
[network] cln_conn_tries_amount="10" cln_conn_retry_delay="1000" game_start_timeout="30000" barrier_wait_timeout="5000"
```

最後に、ゲームモードとPawn(操作キャラ)の設定をします。この2つはそれぞれDisplayClusterGameModeとDisplayClusterPawnを継承しなくてはなりません。
なので、現在はCharacterを継承してデフォルトのクラスを作成するには自分でコードを書かないといけないかと思います。
Blueprintの新規作成画面からそれぞれ作成します。
  
![005.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163667/86e3ab85-fb4b-856f-dda4-d73c0e176974.jpeg)

![006.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163667/22fe8346-e615-4288-56a5-bb1b2b1a9fd2.jpeg)

この作成したゲームモードが、ワールド内のDisplayCLusterSettingsを探し、設定ファイルをプラグイン内のマネージャークラスに通知します     そしてマネージャークラスが設定ファイルを読み込んで、DisplayClusterPawnを継承しているクラスに設定されたスクリーンを反映させる仕組みになっています。
勿論、BlueprintではなくC++スクリプトで作成する場合も上記のクラスを継承する必要があります。
また、スクリプト内で nDisplayのAPIを使う際は Build.cs に "DisplayCluster" と追記します。
分かりづらいかもですが、各クラスの関係的にはこんな感じのイメージです。

![007.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163667/1940700e-337d-8c02-9aef-1d157cf4f8d5.jpeg)

このDisplayClusterManagerのスクリプトを見ればわかりますが、 InitializeDisplayClusterActor 関数で全ての初期化を行っています。     また、設定ファイルの読み込みは DisplayClusterConfigManager のスクリプトを見ればわかるかと思います。
では作ったBlueprintを設定して次の項目に進みます。
ここまで正しくできていれば、まだContent内に.cfgファイルを作成していないので、エディタの"プレイ"を押してもエラーで開始しないはずです。
上記のcfgサンプルを試しに test.cfg としてContent直下に置いてみて再度プレイしてみてください。
ちなみにUE4.22とUE4.23とではこの設定ファイルの書き順が変更されているのでファイルの共有はできません、ご注意ください。ここに書いてあるファイルはUE4.23以降対応です。

![008.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163667/57517030-34eb-63e7-463d-29abf7f6e0e0.jpeg)


##LauncherとListener
nDisplayには各アプリケーションを起動させるLauncherと、命令を受ける子機であるListenerというアプリケーションが存在します。
アプリケーションをパッケージ化させて、全く同じ階層にこのListenerアプリを配置させます。先ほどのアプリをビルドしてパッケージ化させましょう。
同じ階層に配置するとこんな感じになります。 Listenerは結構シンプルなC#プロジェクトなので、自分の好きな言語で改造してみるのもいいかもしれません。

![009.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163667/f6e3bb32-3998-c390-6cef-e52fa3fb8ff6.jpeg)

また、LauncherとListenerは以下のディレクトリに存在します。

![010.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163667/676b12bf-4b33-f0dc-5856-89b61d68e5b1.jpeg)

使用時は以下のようなイメージになるかなと思います。

![011.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163667/395a660f-8318-73c0-b2d3-a45e8dc20239.jpeg)

各マシンにあるListenerは起動するだけでOKです。
Launcherのこまかな使い方は割愛しますが、必須な項目は少なく、使用する.cfgファイルと起動するアプリケーション(さきほどパッケージ化したアプリでLauncherを起動しているマシンにあるアプリケーションのsパス)を選択するだけです。

![012.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163667/0c05d8a4-3758-4834-cde3-a5694fcd591c.jpeg)

コンシューマゲームにはまず使われないとは思いますが、UEを使ったインスタレーション作品とかにガンガン使っていきたいですね。また、VR作品向けにも実際にHMDを被っている体験者の視点や別角度からのカメラを表示できたりするのでイベント系にも重宝するかもしれません