お断り
すみません、まだGitHubリポジトリを用意できていないため、後日URLを貼ります。
今の段階では、こんな思想で実装したんだな〜という程度で見てもらえると...
こんにちは
まいけるさんです。
今回は Apple Mackbook Pro の UE5 において、VR Preview みたいなことをして、Mac しかない環境でも簡易的に実機確認するという試みの記事です。
① とあるように、長くなりそうな間に合わなかったので何回かの記事に分けます。
パート ① 時点のモノ
※ 撮るのが下手すぎて明滅注意...!
はじめに
今回、なんでこんなことをしたかったかというと、私自身が新幹線や飛行機で移動したりが多く、今までは GPU 付きの Windows PC を持ち歩いていたのですが...
重い!!
とにかく重く、Mac で開発する必要がある場面もあり、なんとか UE5 で VR 開発できないかな〜ということからこんなことがしたくなったわけですね。
MacBook なら結構楽に持ち運べるし、これで開発できれば PC を 2 台持ち運ばなくても済んで、出張の荷物も減りますし。
ただ、問題として Mac 環境だとAir LinkやSteamVRが動かなかったりでVR Preview において使えないということで、Windows 環境と同じように VR Preview ができないんですよね。
作業してビルドしてインストールして...というものを繰り返す手段もあるのですが、さすがに面倒。
ということで、今回試してみるに至りました。
もっと調べたりしたら出てくるかもしれないですが、アドベントカレンダーの時期ということでネタにもなりそう勉強のためにやってみようという感じで試してみることにしました。
今回やりたいこと
- MacBook Pro において、Quest3 と接続し、VR Preview を行って簡易的にでも実機確認できるようにする
- 実機確認といってもコントローラーを使って掴める、移動できる、程度
- ネットワークがない環境(飛行機など)でも、動かせるように有線接続で確認できるようにしたい
- エンジン改造であったり、実プロジェクトのコードを変えるようなことはしたくない
- 開発は大変でも使用時は楽に確認できるようにしたい
- とりあえず、今の環境だけで動かせれば良い。ただし、Windows での開発がメインのため、最終的にはそちらの環境の邪魔になることは避ける
前提
- バージョン: UE5.7
- PC: MackBookPro M3 メモリは 18GB(今となってはもっと必要でした)
- 実機: Quest3
方針
まずはじめに、やり方としてパッと思いつくのが 2 つの方法でした。
- PixelStreaming を使用する
- プラグインを作成して、なんとか力ずくでも接続する
このうち、PixelSteaming ですが、(そもそも Mac 環境ですんなりいくかどうかもわからないものの)そもそもネットワークが必要ということで、そういった環境がない場所でも作業したい私にとってはちょっと痛い点です。
新幹線はまだなんとかなりますが、飛行機だと基本的には ANA や JAL ぐらいでしかフリー Wifi ないですし、毎回あるわけでもない。
ということで、今回はプラグインを作成して、なんとか力ずくで有線接続でも動かせるようにしていくこととします。
今回の大まかな設計
- USB 接続をするため、adb コマンドによる映像・トラッキングの送受信を行う
- 上記を行うため、Quest 側でもアプリを作成(今回は説明省略。ただし、②以降かGitHubのリポジトリを作ります)
- プラグイン側で行うこと
- HMDの管理
- Quest から送信された HMD 等のトラッキング情報を UE 側へ反映
- ゲームプレイ映像の Stereo 化・エンコード
- 映像のフレーム受け渡し
- Quest アプリ側で行うこと
- トラッキング情報を取得し、UE 側へ受け渡す
- プラグインを経由してきた映像をデコードして表示させる
ネットワーク設計
前述の通り、adbコマンドを利用して情報を送受信します。
[Mac - Unreal Engine] [USB] [Quest - Viewer App]
│ │ │ │
トラッキングデータ受信 <- adb forward localhost:9001 <- トラッキングデータ送信
│ │ │ │
ビデオフレーム送信 -> adb forward localhost:9002 -> ビデオフレーム受信
│ │ │ │
フォルダ設計
UE側のVR Preview関連の実装やOpenXR プラグインなどの実装を参考に
Plugins/
└─ MacToVRPreview/
├─ MacToVRPreview.uplugin
└─ Source/
└─ MacToVRPreview/
├─ Public/
│ ├─ MacToVRPreview.h
│ ├─ MacToVRPreviewHMD.h
│ ├─ MacToVRPreviewTypes.h
│ ├─ MacToVRPreviewFrameCapture.h
│ └─ MacToVRPreviewVideoEncoder.h
└─ Private/
├─ MacToVRPreview.cpp
├─ MacToVRPreviewHMD.cpp // HMDの管理、カメラとの接続やトラッキングの接続等
├─ MacToVRPreviewRenderTargetManager.cpp // ステレオ化したRTの確保/管理
├─ MacToVRPreviewFrameCapture.cpp
└─ MacToVRPreviewVideoEncoder.cpp // H.264エンコードなど
Part①での要点コード
全コードを載せると文章がかなり多くなりそうなので、今回の実装をかいつまんで説明します。
もう少しコード綺麗にしたらGitHubのリポジトリ公開しようと思っていますので、ご興味ある方はそちらをお待ちください...
1) Preview開始〜Stereo有効化
// Source/MacToVRPreview/Private/MacToVRPreviewHMD.cpp
bool FMacToVRPreviewHMD::EnableStereo(bool bStereo)
{
bStereoEnabled = bStereo;
if (bStereo)
{
if (!ListenerSocket) { StartNetworkListener(9001); }
}
else { StopStreaming(); }
return bStereoEnabled;
}
bool FMacToVRPreviewHMD::OnStartGameFrame(FWorldContext&)
{
if (bIsConnectedToQuest && bStereoEnabled && !IsStreaming())
{
StartStreaming();
}
return true;
}
- VR Previewは通常実行できませんが、プラグイン側でHMDと接続したことにしています。本当は、なんらかのボタンを用意して、adbコマンドによる接続が確立するようにするべきですが、とりあえず動作確認優先にしています
- Stereo有効化時にトラッキング受信の待受(9001)を開始
- Quest接続が確立すると映像ストリーミングを開始
3) トラッキング受信(Quest → Mac)
// StartNetworkListener: 127.0.0.1:9001 でListen開始
bool FMacToVRPreviewHMD::StartNetworkListener(int32 Port)
{
ListenerSocket = SocketSubsystem->CreateSocket(NAME_Stream, TEXT("Listener"), false);
...
ListenerSocket->Bind(*Addr);
ListenerSocket->Listen(1);
ReceiverThread.Reset(FRunnableThread::Create(
new FMacToVRPreviewReceiverRunnable(this), TEXT("Receiver")));
}
void FMacToVRPreviewHMD::NetworkReceiverThreadFunc()
{
if (!ClientSocket && ListenerSocket)
{
if (ListenerSocket->HasPendingConnection(bHasPending) && bHasPending)
ClientSocket = ListenerSocket->Accept(TEXT("Quest Client"));
}
if (ClientSocket && ClientSocket->Recv(...))
{
// PacketBufferに追記して...
ProcessReceivedPacket(PacketBuffer.GetData(), TotalPacketSize);
}
}
// 受信パケットを反映(姿勢・IPD)
bool FMacToVRPreviewHMD::ProcessReceivedPacket(const uint8* Data, int32 Size)
{
const auto* Header = reinterpret_cast<const FMacToVRPreviewPacketHeader*>(Data);
if (Header->Type == EMacToVRPreviewPacketType::TrackingData)
{
FMacToVRPreviewTrackingDataRaw Raw; memcpy(&Raw, Payload, sizeof Raw);
auto Tracking = Raw.ToTrackingData(); Tracking.Timestamp = Header->Timestamp;
UpdateTrackingData(Tracking);
}
}
// ここからUE側のカメラに反映ためのもの
bool FMacToVRPreviewHMD::GetCurrentPose(int32 DeviceId, FQuat& OutOri, FVector& OutPos)
{
FScopeLock Lock(&TrackingDataMutex);
OutOri = BaseOrientation.Inverse() * LatestTrackingData.HeadOrientation;
OutPos = (LatestTrackingData.HeadPosition - BasePosition) * WorldToMetersScale;
return true;
}
- adb forwardでQuestアプリ→
localhost:9001に接続、予測ポーズを送信 - HMDは受信スレッドで
LatestTrackingDataへ反映 - ゲーム側は毎フレーム
GetCurrentPose()で最新の頭部姿勢を取得
4) ビデオ送信(Mac → Quest)
// ストリーミング開始(9002 へ接続)
bool FMacToVRPreviewHMD::StartStreaming()
{
VideoSendSocket = SocketSubsystem->CreateSocket(NAME_Stream, TEXT("VideoSend"), false);
Addr->SetIp(TEXT("127.0.0.1")); Addr->SetPort(9002);
VideoSendSocket->Connect(*Addr);
StreamingPipeline = MakeUnique<FMacToVRPreviewStreamingPipeline>();
StreamingPipeline->Initialize(IdealRenderTargetSize.X*2, IdealRenderTargetSize.Y, 90000000, VideoSendSocket);
RenderTargetManager->SetStreamingPipeline(StreamingPipeline.Get());
}
// レンダリング後で最終RTを渡す
void FMacToVRPreviewSceneViewExtension::PostRenderViewFamily_RenderThread(...)
{
FTextureRHIRef StereoRT = RenderTargetManager->GetCurrentRenderTarget();
GraphBuilder.AddPass(RDG_EVENT_NAME("MacToVRPreview_StereoFrameCapture"), ...,
[this, StereoRT](FRHICommandListImmediate& RHICmdList)
{
HMD->CaptureFrame_RenderThread(RHICmdList, StereoRT.GetReference());
});
}
// エンコードと送信
void FMacToVRPreviewStreamingPipeline::SubmitFrame_RenderThread(...)
{
double Timestamp = FApp::GetCurrentTime();
FrameCapture->CaptureFrame_RenderThread(RHICmdList, SourceTexture, Timestamp);
}
void FMacToVRPreviewStreamingPipeline::EncoderThreadFunc()
{
if (FrameCapture->GetCapturedFrame(Captured))
{
FMacToVRPreviewEncodedFrame Encoded;
if (VideoEncoder->EncodeFrame(Input, Encoded))
{
SendEncodedFrame(Encoded);
}
}
}
次回以降について
今回は、とりあえずHMDの動きをUEのカメラと同期させ、映像を送信するところまでを実装しました。
まだ残っている点として
- コントローラーがまだ動かせない...
- レイテンシー酷すぎて酔いそう
- 解像度が...
という課題が残っていますので、②以降はこちらを解消していきたいと思います〜!
また、そもそも力づく・非効率感があり、もう少しうまいことUE側の機能など使えないかなと思っていますので、もう少しちゃんとコードリーディングします...笑