Unreal Engine 5.9 Pixel Streaming WebXR VR クラウドゲーミング実装完全ガイド
UE5.9 Pixel Streaming の WebXR VR 対応を実装し、クラウドゲーミングで高品質 VR 体験を実現する技術を詳解します。
約15分で読めますUnreal Engine 5.9 で新たに対応した Pixel Streaming の WebXR VR 機能により、ハイエンド VR 体験をブラウザから直接ストリーミングできるようになりました。この記事では、2026年4月にリリースされた UE5.9 の Pixel Streaming WebXR 統合を活用し、クラウドゲーミング環境で低遅延・高品質な VR アプリケーションを実装する方法を、実装パターン・最適化テクニック・トラブルシューティングまで含めて解説します。
従来の VR 開発ではハイエンド GPU を搭載したローカル PC が必須でしたが、Pixel Streaming と WebXR の統合により、ユーザーは Quest 3 や Pico 4 などのスタンドアロン VR ヘッドセットのブラウザから、クラウド上の UE5 アプリケーションに直接アクセスできます。この技術により、デバイスの性能制約を超えた VR 体験が可能になりました。
UE5.9 Pixel Streaming WebXR 対応の新機能
UE5.9(2026年4月リリース)では、Pixel Streaming プラグインに WebXR API のネイティブサポートが追加されました。これにより、以下の機能が実現されています。
主要な新機能:
- WebXR Device API 統合: ブラウザの WebXR API とエンジンの VR レンダリングパイプラインの双方向バインディング
- ステレオレンダリングストリーミング: 左右眼の独立したビューポートを WebRTC で効率的に送信
- 6DoF トラッキング同期: ヘッドセットとコントローラーの位置・回転データのリアルタイム伝送(遅延10ms以下)
- ハンドトラッキング対応: WebXR Hand Input API との統合による指トラッキング
- Adaptive Bitrate for VR: VR 特有の高フレームレート要求に対応したビットレート制御
以下のダイアグラムは、Pixel Streaming WebXR のアーキテクチャ全体像を示しています。
flowchart TD
A["VR ヘッドセット<br/>(Quest 3 / Pico 4)"] -->|WebXR API| B["ブラウザ<br/>(Chromium-based)"]
B -->|WebRTC ストリーム| C["Pixel Streaming<br/>Signaling Server"]
C -->|WebSocket| D["UE5.9 Cirrus Server"]
D -->|IPC| E["UE5.9 アプリケーション<br/>(Dedicated Server)"]
E -->|Stereo Rendering| F["Left Eye Viewport"]
E -->|Stereo Rendering| G["Right Eye Viewport"]
F -->|H.264/VP9 Encode| H["Video Encoder<br/>(NVENC)"]
G -->|H.264/VP9 Encode| H
H -->|WebRTC Track| D
B -->|6DoF Tracking Data| D
D -->|Input Events| E
E -->|Scene Update| I["VR カメラ<br/>Transform"]
I -->|Render Target| F
I -->|Render Target| G
この構成では、ブラウザの WebXR API がヘッドセットのトラッキングデータを取得し、WebRTC データチャネル経由で UE5 サーバーに送信します。サーバー側では VR カメラの Transform を更新し、ステレオレンダリングされた映像を NVENC でエンコードして返送します。
実装に必要な環境:
- Unreal Engine 5.9.0 以降(Pixel Streaming プラグイン有効化)
- NVIDIA GPU(RTX 3060 以上推奨、NVENC 対応必須)
- Node.js 18.x 以降(Cirrus Signaling Server 用)
- WebXR 対応ブラウザ(Chrome 120+, Edge 120+)
- VR ヘッドセット(Meta Quest 3, Pico 4, HTC Vive XR Elite 等)
プロジェクトセットアップと基本実装
UE5.9 で Pixel Streaming WebXR プロジェクトを構築する手順を解説します。
プラグイン有効化と初期設定
- Pixel Streaming プラグインの有効化
エディタで Edit > Plugins > Pixel Streaming を検索し、有効化します。UE5.9 以降では WebXR 拡張が自動的に含まれます。
- プロジェクト設定の変更
Project Settings > Engine > Rendering で以下を設定します。
Forward Shading: Enabled(VR 最適化のため)
Instanced Stereo: Enabled(ステレオレンダリング高速化)
Mobile Multi-View: Disabled(Pixel Streaming では非対応)
Default RHI: DirectX 12(NVENC 最適化)
- VR プラグインの有効化
Plugins > Virtual Reality で以下を有効化します。
- OpenXR(統一 VR API)
- Pixel Streaming WebXR Extension(UE5.9 新規追加)
WebXR 対応 Player Controller の実装
ブラウザから送信される WebXR トラッキングデータを処理する PlayerController を C++ で実装します。
// WebXRPlayerController.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "PixelStreamingWebXRInterface.h"
#include "WebXRPlayerController.generated.h"
UCLASS()
class AWebXRPlayerController : public APlayerController
{
GENERATED_BODY()
public:
AWebXRPlayerController();
protected:
virtual void BeginPlay() override;
virtual void Tick(float DeltaTime) override;
private:
UPROPERTY()
UPixelStreamingWebXRInterface* WebXRInterface;
// WebXR からのトラッキングデータ受信
UFUNCTION()
void OnWebXRPoseUpdate(const FWebXRPose& Pose);
// コントローラー入力処理
UFUNCTION()
void OnWebXRInputSourceUpdate(const FWebXRInputSource& InputSource);
// VR カメラの Transform 更新
void UpdateVRCamera(const FTransform& HeadTransform);
UPROPERTY()
class UCameraComponent* VRCamera;
UPROPERTY()
class UMotionControllerComponent* LeftController;
UPROPERTY()
class UMotionControllerComponent* RightController;
};
// WebXRPlayerController.cpp
#include "WebXRPlayerController.h"
#include "Camera/CameraComponent.h"
#include "MotionControllerComponent.h"
#include "PixelStreamingModule.h"
AWebXRPlayerController::AWebXRPlayerController()
{
PrimaryActorTick.bCanEverTick = true;
// VR カメラコンポーネント作成
VRCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("VRCamera"));
VRCamera->bUsePawnControlRotation = false;
// モーションコントローラー作成
LeftController = CreateDefaultSubobject<UMotionControllerComponent>(TEXT("LeftController"));
LeftController->MotionSource = FName("Left");
RightController = CreateDefaultSubobject<UMotionControllerComponent>(TEXT("RightController"));
RightController->MotionSource = FName("Right");
}
void AWebXRPlayerController::BeginPlay()
{
Super::BeginPlay();
// Pixel Streaming WebXR インターフェース取得
if (IPixelStreamingModule::IsAvailable())
{
WebXRInterface = IPixelStreamingModule::Get().GetWebXRInterface();
if (WebXRInterface)
{
// WebXR イベントのバインド
WebXRInterface->OnPoseUpdate.AddDynamic(this, &AWebXRPlayerController::OnWebXRPoseUpdate);
WebXRInterface->OnInputSourceUpdate.AddDynamic(this, &AWebXRPlayerController::OnWebXRInputSourceUpdate);
UE_LOG(LogTemp, Log, TEXT("WebXR interface initialized successfully"));
}
}
}
void AWebXRPlayerController::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// WebXR セッションがアクティブな場合のみ処理
if (WebXRInterface && WebXRInterface->IsSessionActive())
{
// フレームごとのトラッキング更新はイベント駆動で処理
}
}
void AWebXRPlayerController::OnWebXRPoseUpdate(const FWebXRPose& Pose)
{
// ヘッドセットの Transform を VR カメラに適用
FTransform HeadTransform;
HeadTransform.SetLocation(Pose.Position);
HeadTransform.SetRotation(Pose.Orientation);
UpdateVRCamera(HeadTransform);
// コントローラーの Transform 更新
if (Pose.LeftHandTransform.IsSet())
{
LeftController->SetWorldTransform(Pose.LeftHandTransform.GetValue());
}
if (Pose.RightHandTransform.IsSet())
{
RightController->SetWorldTransform(Pose.RightHandTransform.GetValue());
}
}
void AWebXRPlayerController::OnWebXRInputSourceUpdate(const FWebXRInputSource& InputSource)
{
// ボタン・トリガー・スティック入力処理
if (InputSource.Handedness == EWebXRHandedness::Left)
{
if (InputSource.TriggerValue > 0.5f)
{
// 左トリガー処理
OnLeftTriggerPressed();
}
}
else if (InputSource.Handedness == EWebXRHandedness::Right)
{
if (InputSource.GripValue > 0.5f)
{
// 右グリップ処理
OnRightGripPressed();
}
}
}
void AWebXRPlayerController::UpdateVRCamera(const FTransform& HeadTransform)
{
if (VRCamera)
{
// ワールド空間での絶対位置に変換
FVector WorldLocation = GetPawn()->GetActorLocation() + HeadTransform.GetLocation();
FRotator WorldRotation = HeadTransform.GetRotation().Rotator();
VRCamera->SetWorldLocationAndRotation(WorldLocation, WorldRotation);
}
}
このコードは、ブラウザの WebXR API から送信されるヘッドセットとコントローラーのトラッキングデータを受信し、UE5 の VR カメラと MotionController コンポーネントに反映します。FWebXRPose 構造体には位置・回転・速度・加速度が含まれており、高精度な VR インタラクションが実現できます。
Cirrus Signaling Server の WebXR 拡張設定
UE5.9 の Cirrus Server には WebXR 対応のための新しい設定オプションが追加されています。
Engine/Source/Programs/PixelStreaming/WebServers/SignallingWebServer/config.json を編集します。
{
"UseFrontend": true,
"UseMatchmaker": false,
"UseHTTPS": true,
"UseAuthentication": false,
"LogToFile": true,
"LogVerbose": true,
"HomepageFile": "player.html",
"AdditionalRoutes": {},
"EnableWebXR": true,
"WebXRConfig": {
"RequireSecureContext": true,
"SupportedSessionModes": ["immersive-vr", "immersive-ar"],
"SupportedFeatures": [
"local-floor",
"bounded-floor",
"hand-tracking",
"hit-test"
],
"StereoStreamConfig": {
"VideoCodec": "H264",
"MaxBitrate": 50000000,
"MinBitrate": 10000000,
"TargetFramerate": 90,
"AdaptiveBitrateEnabled": true,
"LeftEyeStreamId": "left-eye",
"RightEyeStreamId": "right-eye"
},
"TrackingDataChannel": {
"Protocol": "unreliable",
"MaxRetransmits": 0,
"UpdateRateHz": 90
}
}
}
設定の解説:
EnableWebXR: WebXR 機能を有効化(デフォルトは false)SupportedSessionModes: サポートする WebXR セッションタイプ(VR, AR)StereoStreamConfig.VideoCodec: H264 または VP9(NVENC 対応のため H264 推奨)StereoStreamConfig.TargetFramerate: VR では 90fps が最低ライン(Quest 3 は 120fps 対応)TrackingDataChannel.Protocol: トラッキングデータは遅延最小化のため unreliable 設定
サーバー起動コマンド:
# Windows
Start_SignallingServer.bat --EnableWebXR
# Linux
./Start_SignallingServer.sh --EnableWebXR
以下のシーケンス図は、WebXR セッション確立からストリーミング開始までの通信フローを示しています。
sequenceDiagram
participant Browser as ブラウザ<br/>(WebXR API)
participant Cirrus as Cirrus Server
participant UE5 as UE5 Application
Browser->>Browser: navigator.xr.requestSession('immersive-vr')
Browser->>Cirrus: WebSocket接続<br/>(wss://server:8888)
Cirrus->>UE5: セッション開始通知
UE5->>UE5: ステレオレンダリング初期化
UE5->>Cirrus: WebRTC Offer (SDP)
Cirrus->>Browser: WebRTC Offer 転送
Browser->>Browser: WebRTC Answer 生成
Browser->>Cirrus: WebRTC Answer (SDP)
Cirrus->>UE5: WebRTC Answer 転送
UE5->>Browser: ICE Candidates 交換
Browser->>UE5: ICE Candidates 交換
UE5-->>Browser: ステレオビデオストリーム開始<br/>(90fps H.264)
Browser->>UE5: トラッキングデータ送信<br/>(90Hz unreliable)
loop VR セッション中
Browser->>UE5: 6DoF Pose Update
UE5->>UE5: VR カメラ更新
UE5->>UE5: ステレオレンダリング
UE5-->>Browser: 左右眼ビデオフレーム
Browser->>Browser: WebXR レイヤー合成
end
この通信フローでは、WebRTC の SDP 交換後に2つのビデオトラック(左眼・右眼)と1つのデータチャネル(トラッキング)が確立されます。
ステレオレンダリング最適化とフレームレート維持
VR では 90fps(理想は 120fps)を維持することが不可欠です。Pixel Streaming 環境でこれを実現するための最適化手法を解説します。
Instanced Stereo Rendering の設定
UE5.9 では Instanced Stereo により、左右眼を1回の描画コールで処理できます。
Project Settings > Engine > Rendering:
VR > Instanced Stereo: Enabled
VR > Round Robin Occlusion Queries: Enabled
VR > Hidden Area Mesh: Enabled(Quest 3 等で FOV 外をカリング)
C++ での動的設定:
void AMyVRGameMode::InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage)
{
Super::InitGame(MapName, Options, ErrorMessage);
// Instanced Stereo 強制有効化
static IConsoleVariable* InstancedStereoCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("vr.InstancedStereo"));
if (InstancedStereoCVar)
{
InstancedStereoCVar->Set(1);
}
// Mobile Multi-View 無効化(Pixel Streaming 非対応)
static IConsoleVariable* MobileMultiViewCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("vr.MobileMultiView"));
if (MobileMultiViewCVar)
{
MobileMultiViewCVar->Set(0);
}
// VR Pixel Density(解像度スケール)
static IConsoleVariable* PixelDensityCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("vr.PixelDensity"));
if (PixelDensityCVar)
{
// Quest 3 推奨値: 1.2(ネイティブ解像度より少し上)
PixelDensityCVar->Set(1.2f);
}
}
NVENC エンコーダ設定の最適化
Pixel Streaming の映像エンコード設定を VR 向けに調整します。
DefaultEngine.ini に以下を追加:
[/Script/PixelStreaming.PixelStreamingSettings]
; VR 向け高フレームレート設定
CaptureFramerate=90
EncoderTargetBitrate=50000000
EncoderMaxBitrate=80000000
EncoderMinBitrate=10000000
; NVENC 専用設定(RTX GPU 必須)
EncoderCodec=H264
EncoderPreset=P4
EncoderProfile=High
EncoderRateControl=CBR
EncoderMultipass=FullResolution
EncoderGOPSize=90
; ステレオストリーム設定
EnableStereoStreaming=true
LeftEyeStreamPriority=High
RightEyeStreamPriority=High
; 遅延削減
EncoderLowLatencyMode=true
WebRTCMaxFPS=120
WebRTCMinFPS=72
WebRTCStartBitrate=30000000
設定の解説:
EncoderPreset=P4: NVENC の低遅延プリセット(P1〜P7 で P4 が品質と速度のバランスが良い)EncoderMultipass=FullResolution: 2パスエンコードで品質向上(RTX 4000 シリーズ以降推奨)EncoderGOPSize=90: キーフレーム間隔を1秒に設定(90fps の場合)WebRTCStartBitrate=30000000: 初期ビットレート 30Mbps(VR は高ビットレート必須)
C++ での動的ビットレート制御:
void AWebXRPlayerController::AdjustBitrateBasedOnLatency()
{
if (!WebXRInterface) return;
// RTT(往復遅延時間)取得
float CurrentRTT = WebXRInterface->GetCurrentRTTMs();
IPixelStreamingModule& PixelStreamingModule = IPixelStreamingModule::Get();
if (CurrentRTT < 20.0f)
{
// 遅延が低い → ビットレート上げて品質向上
PixelStreamingModule.SetEncoderTargetBitrate(60000000); // 60Mbps
}
else if (CurrentRTT > 50.0f)
{
// 遅延が高い → ビットレート下げて安定性確保
PixelStreamingModule.SetEncoderTargetBitrate(20000000); // 20Mbps
}
UE_LOG(LogTemp, Log, TEXT("RTT: %.2f ms, Adjusted bitrate"), CurrentRTT);
}
フレームペーシング戦略
VR では GPU が 90fps でフレームを生成しても、ネットワーク遅延でクライアント側の表示がずれる可能性があります。UE5.9 の新機能「Predictive Frame Timing」を使用します。
void AWebXRPlayerController::EnablePredictiveFrameTiming()
{
if (!WebXRInterface) return;
// Predictive Frame Timing 有効化(UE5.9 新機能)
WebXRInterface->SetPredictiveFrameTimingEnabled(true);
// 予測時間(ミリ秒)設定
// RTT/2 + エンコード時間 + デコード時間 の推定値
float PredictionTimeMs = 25.0f; // 典型的なローカルネットワークでの値
WebXRInterface->SetFramePredictionTimeMs(PredictionTimeMs);
UE_LOG(LogTemp, Log, TEXT("Predictive Frame Timing enabled with %f ms prediction"), PredictionTimeMs);
}
Predictive Frame Timing は、クライアント側でフレームが表示されるタイミングを予測し、その時点での頭部位置を使ってレンダリングすることで、体感遅延を大幅に削減します。
ハンドトラッキングとインタラクション実装
UE5.9 Pixel Streaming WebXR は、Quest 3 などのハンドトラッキング機能にも対応しています。
WebXR Hand Input API 統合
ブラウザ側で WebXR Hand Input API から取得した手のジョイントデータを UE5 に送信します。
フロントエンド JavaScript(player.html 内):
let xrSession = null;
let handTracking = null;
async function startVRSession() {
const sessionInit = {
requiredFeatures: ['local-floor', 'hand-tracking'],
optionalFeatures: ['bounded-floor']
};
xrSession = await navigator.xr.requestSession('immersive-vr', sessionInit);
// Hand Tracking サポート確認
if (xrSession.enabledFeatures.includes('hand-tracking')) {
console.log('Hand tracking supported');
handTracking = true;
}
xrSession.requestAnimationFrame(onXRFrame);
}
function onXRFrame(time, xrFrame) {
const session = xrFrame.session;
const referenceSpace = session.referenceSpace;
// Hand tracking データ取得
if (handTracking) {
const inputSources = session.inputSources;
for (const inputSource of inputSources) {
if (inputSource.hand) {
const handData = processHandJoints(xrFrame, inputSource.hand, referenceSpace);
// UE5 に送信(WebRTC Data Channel 経由)
sendHandTrackingData(handData);
}
}
}
session.requestAnimationFrame(onXRFrame);
}
function processHandJoints(xrFrame, hand, referenceSpace) {
const joints = [];
// 25個の手のジョイント(WebXR Hand Input 仕様)
const jointNames = [
'wrist',
'thumb-metacarpal', 'thumb-phalanx-proximal', 'thumb-phalanx-distal', 'thumb-tip',
'index-finger-metacarpal', 'index-finger-phalanx-proximal', 'index-finger-phalanx-intermediate', 'index-finger-phalanx-distal', 'index-finger-tip',
// ... 他のジョイント
];
for (const jointName of jointNames) {
const joint = hand.get(jointName);
if (joint) {
const pose = xrFrame.getJointPose(joint, referenceSpace);
if (pose) {
joints.push({
name: jointName,
position: {
x: pose.transform.position.x,
y: pose.transform.position.y,
z: pose.transform.position.z
},
rotation: {
x: pose.transform.orientation.x,
y: pose.transform.orientation.y,
z: pose.transform.orientation.z,
w: pose.transform.orientation.w
},
radius: pose.radius
});
}
}
}
return {
handedness: hand.handedness, // 'left' or 'right'
joints: joints
};
}
function sendHandTrackingData(handData) {
// Pixel Streaming WebRTC Data Channel へ送信
if (webRtcDataChannel && webRtcDataChannel.readyState === 'open') {
const message = {
type: 'handTracking',
data: handData
};
webRtcDataChannel.send(JSON.stringify(message));
}
}
UE5 側でのハンドトラッキングデータ受信と可視化:
// HandTrackingComponent.h
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "HandTrackingComponent.generated.h"
USTRUCT(BlueprintType)
struct FHandJoint
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly)
FName JointName;
UPROPERTY(BlueprintReadOnly)
FVector Position;
UPROPERTY(BlueprintReadOnly)
FQuat Rotation;
UPROPERTY(BlueprintReadOnly)
float Radius;
};
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class UHandTrackingComponent : public UActorComponent
{
GENERATED_BODY()
public:
UHandTrackingComponent();
UFUNCTION(BlueprintCallable)
void UpdateHandJoints(const TArray<FHandJoint>& Joints, bool bIsLeftHand);
UFUNCTION(BlueprintPure)
FVector GetFingerTipPosition(FName FingerName, bool bIsLeftHand) const;
protected:
virtual void BeginPlay() override;
private:
UPROPERTY()
TArray<FHandJoint> LeftHandJoints;
UPROPERTY()
TArray<FHandJoint> RightHandJoints;
// ジョイント可視化用メッシュ
UPROPERTY()
TArray<UStaticMeshComponent*> LeftHandMeshes;
UPROPERTY()
TArray<UStaticMeshComponent*> RightHandMeshes;
void CreateJointMeshes();
};
// HandTrackingComponent.cpp
#include "HandTrackingComponent.h"
#include "Components/StaticMeshComponent.h"
#include "Engine/StaticMesh.h"
UHandTrackingComponent::UHandTrackingComponent()
{
PrimaryComponentTick.bCanEverTick = false;
}
void UHandTrackingComponent::BeginPlay()
{
Super::BeginPlay();
CreateJointMeshes();
}
void UHandTrackingComponent::CreateJointMeshes()
{
// 各ジョイントに小さな球体メッシュを配置(デバッグ用)
UStaticMesh* SphereMesh = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Sphere"));
for (int i = 0; i < 25; i++) // 25ジョイント
{
// 左手
UStaticMeshComponent* LeftMesh = NewObject<UStaticMeshComponent>(GetOwner());
LeftMesh->SetStaticMesh(SphereMesh);
LeftMesh->SetWorldScale3D(FVector(0.01f)); // 1cm 球体
LeftMesh->RegisterComponent();
LeftHandMeshes.Add(LeftMesh);
// 右手
UStaticMeshComponent* RightMesh = NewObject<UStaticMeshComponent>(GetOwner());
RightMesh->SetStaticMesh(SphereMesh);
RightMesh->SetWorldScale3D(FVector(0.01f));
RightMesh->RegisterComponent();
RightHandMeshes.Add(RightMesh);
}
}
void UHandTrackingComponent::UpdateHandJoints(const TArray<FHandJoint>& Joints, bool bIsLeftHand)
{
if (bIsLeftHand)
{
LeftHandJoints = Joints;
// メッシュ位置更新
for (int i = 0; i < FMath::Min(Joints.Num(), LeftHandMeshes.Num()); i++)
{
LeftHandMeshes[i]->SetWorldLocationAndRotation(
Joints[i].Position,
Joints[i].Rotation
);
}
}
else
{
RightHandJoints = Joints;
for (int i = 0; i < FMath::Min(Joints.Num(), RightHandMeshes.Num()); i++)
{
RightHandMeshes[i]->SetWorldLocationAndRotation(
Joints[i].Position,
Joints[i].Rotation
);
}
}
}
FVector UHandTrackingComponent::GetFingerTipPosition(FName FingerName, bool bIsLeftHand) const
{
const TArray<FHandJoint>& Joints = bIsLeftHand ? LeftHandJoints : RightHandJoints;
FString TipJointName = FingerName.ToString() + TEXT("-tip");
for (const FHandJoint& Joint : Joints)
{
if (Joint.JointName.ToString() == TipJointName)
{
return Joint.Position;
}
}
return FVector::ZeroVector;
}
WebXR PlayerController でデータチャネルメッセージを受信:
void AWebXRPlayerController::OnDataChannelMessage(const FString& Message)
{
// JSON パース
TSharedPtr<FJsonObject> JsonObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Message);
if (FJsonSerializer::Deserialize(Reader, JsonObject) && JsonObject.IsValid())
{
FString MessageType = JsonObject->GetStringField(TEXT("type"));
if (MessageType == TEXT("handTracking"))
{
TSharedPtr<FJsonObject> DataObject = JsonObject->GetObjectField(TEXT("data"));
FString Handedness = DataObject->GetStringField(TEXT("handedness"));
bool bIsLeftHand = (Handedness == TEXT("left"));
const TArray<TSharedPtr<FJsonValue>>& JointsArray = DataObject->GetArrayField(TEXT("joints"));
TArray<FHandJoint> Joints;
for (const TSharedPtr<FJsonValue>& JointValue : JointsArray)
{
TSharedPtr<FJsonObject> JointObj = JointValue->AsObject();
FHandJoint Joint;
Joint.JointName = FName(*JointObj->GetStringField(TEXT("name")));
TSharedPtr<FJsonObject> PosObj = JointObj->GetObjectField(TEXT("position"));
Joint.Position = FVector(
PosObj->GetNumberField(TEXT("x")),
PosObj->GetNumberField(TEXT("y")),
PosObj->GetNumberField(TEXT("z"))
);
TSharedPtr<FJsonObject> RotObj = JointObj->GetObjectField(TEXT("rotation"));
Joint.Rotation = FQuat(
RotObj->GetNumberField(TEXT("x")),
RotObj->GetNumberField(TEXT("y")),
RotObj->GetNumberField(TEXT("z")),
RotObj->GetNumberField(TEXT("w"))
);
Joint.Radius = JointObj->GetNumberField(TEXT("radius"));
Joints.Add(Joint);
}
// HandTrackingComponent に渡す
if (HandTrackingComp)
{
HandTrackingComp->UpdateHandJoints(Joints, bIsLeftHand);
}
}
}
}
この実装により、ユーザーがコントローラーなしで手指だけで VR 空間内のオブジェクトを操作できます。Quest 3 の場合、ハンドトラッキングは 60Hz で更新されるため、自然なジェスチャー認識が可能です。
パフォーマンス計測とトラブルシューティング
VR Pixel Streaming の品質を定量的に評価し、問題を特定する方法を解説します。
メトリクス収集の実装
リアルタイムでフレームレート・遅延・パケットロスを計測する仕組みを実装します。
// VRStreamingMetrics.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "VRStreamingMetrics.generated.h"
USTRUCT(BlueprintType)
struct FVRMetrics
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly)
float CurrentFPS;
UPROPERTY(BlueprintReadOnly)
float EncoderLatencyMs;
UPROPERTY(BlueprintReadOnly)
float NetworkLatencyMs;
UPROPERTY(BlueprintReadOnly)
float TotalLatencyMs;
UPROPERTY(BlueprintReadOnly)
float PacketLossPercent;
UPROPERTY(BlueprintReadOnly)
int32 CurrentBitrate;
UPROPERTY(BlueprintReadOnly)
float FrameTimeBudgetMs; // 90fps = 11.11ms
};
UCLASS()
class AVRStreamingMetrics : public AActor
{
GENERATED_BODY()
public:
AVRStreamingMetrics();
UFUNCTION(BlueprintCallable)
FVRMetrics GetCurrentMetrics() const;
protected:
virtual void BeginPlay() override;
virtual void Tick(float DeltaTime) override;
private:
void CollectMetrics();
void LogMetricsToFile();
FVRMetrics CurrentMetrics;
UPROPERTY()
class UPixelStreamingWebXRInterface* WebXRInterface;
// メトリクス履歴(グラフ化用)
TArray<float> FPSHistory;
TArray<float> LatencyHistory;
float MetricsCollectionInterval = 0.1f; // 100ms ごと
float TimeSinceLastCollection = 0.0f;
};
// VRStreamingMetrics.cpp
#include "VRStreamingMetrics.h"
#include "PixelStreamingModule.h"
#include "HAL/PlatformTime.h"
AVRStreamingMetrics::AVRStreamingMetrics()
{
PrimaryActorTick.bCanEverTick = true;
}
void AVRStreamingMetrics::BeginPlay()
{
Super::BeginPlay();
if (IPixelStreamingModule::IsAvailable())
{
WebXRInterface = IPixelStreamingModule::Get().GetWebXRInterface();
}
}
void AVRStreamingMetrics::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
TimeSinceLastCollection += DeltaTime;
if (TimeSinceLastCollection >= MetricsCollectionInterval)
{
CollectMetrics();
TimeSinceLastCollection = 0.0f;
}
}
void AVRStreamingMetrics::CollectMetrics()
{
if (!WebXRInterface) return;
// FPS 計測
CurrentMetrics.CurrentFPS = 1.0f / GetWorld()->GetDeltaSeconds();
// エンコーダー遅延(NVENC 処理時間)
CurrentMetrics.EncoderLatencyMs = WebXRInterface->GetEncoderLatencyMs();
// ネットワーク遅延(RTT)
CurrentMetrics.NetworkLatencyMs = WebXRInterface->GetCurrentRTTMs();
// 総遅延(エンコード + ネットワーク + デコード)
CurrentMetrics.TotalLatencyMs = CurrentMetrics.EncoderLatencyMs + CurrentMetrics.NetworkLatencyMs;
// パケットロス率
CurrentMetrics.PacketLossPercent = WebXRInterface->GetPacketLossPercent();
// 現在のビットレート
CurrentMetrics.CurrentBitrate = WebXRInterface->GetCurrentBitrate();
// フレームタイムバジェット(90fps = 11.11ms)
CurrentMetrics.FrameTimeBudgetMs = 1000.0f / 90.0f;
// 履歴に追加
FPSHistory.Add(CurrentMetrics.CurrentFPS);
LatencyHistory.Add(CurrentMetrics.TotalLatencyMs);
// 履歴を100サンプルに制限
if (FPSHistory.Num() > 100)
{
FPSHistory.RemoveAt(0);
}
if (LatencyHistory.Num() > 100)
{
LatencyHistory.RemoveAt(0);
}
// 警告ログ
if (CurrentMetrics.CurrentFPS < 72.0f)
{
UE_LOG(LogTemp, Warning, TEXT("VR FPS below acceptable threshold: %.2f"), CurrentMetrics.CurrentFPS);
}
if (CurrentMetrics.TotalLatencyMs > 50.0f)
{
UE_LOG(LogTemp, Warning, TEXT("VR Total Latency too high: %.2f ms"), CurrentMetrics.TotalLatencyMs);
}
if (CurrentMetrics.PacketLossPercent > 1.0f)
{
UE_LOG(LogTemp, Warning, TEXT("VR Packet Loss detected: %.2f%%"), CurrentMetrics.PacketLossPercent);
}
}
FVRMetrics AVRStreamingMetrics::GetCurrentMetrics() const
{
return CurrentMetrics;
}
Blueprint から呼び出して UI に表示:
// VR HUD Blueprint のイベントグラフ
Event Tick
-> Get VR Streaming Metrics Actor
-> Get Current Metrics
-> Set Text (FPS Text): "FPS: {CurrentFPS}"
-> Set Text (Latency Text): "Latency: {TotalLatencyMs} ms"
-> Set Text (Bitrate Text): "Bitrate: {CurrentBitrate / 1000000} Mbps"
よくある問題と解決策
1. フレームレートが 60fps を下回る
原因:
- GPU がレンダリングに時間がかかりすぎている
- Dynamic Resolution が有効になっていない
解決策:
; DefaultEngine.ini
[/Script/Engine.RendererSettings]
r.DynamicRes.OperationMode=2
r.DynamicRes.TargetedGPUHeadRoomPercentage=20
r.DynamicRes.MinResolutionFraction=0.5
r.DynamicRes.MaxResolutionFraction=1.0
C++ での動的解像度調整:
void AWebXRPlayerController::AdjustDynamicResolution()
{
float CurrentFPS = 1.0f / GetWorld()->GetDeltaSeconds();
static IConsoleVariable* DynamicResCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("r.DynamicRes.OperationMode"));
if (CurrentFPS < 72.0f)
{
// 解像度を下げる
static IConsoleVariable* MinResCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("r.DynamicRes.MinResolutionFraction"));
MinResCVar->Set(0.7f);
UE_LOG(LogTemp, Log, TEXT("Lowering resolution to maintain framerate"));
}
else if (CurrentFPS > 100.0f)
{
// 余裕があれば解像度を上げる
static IConsoleVariable* MaxResCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("r.DynamicRes.MaxResolutionFraction"));
MaxResCVar->Set(1.2f);
}
}
2. トラッキング遅延が大きい(50ms 以上)
原因:
- WebRTC データチャネルが reliable モードになっている
- ネットワーク帯域が不足している
解決策:
Cirrus Server の config.json で確認:
"TrackingDataChannel": {
"Protocol": "unreliable", // ← これが "reliable" になっていないか確認
"MaxRetransmits": 0
}
C++ でデータチャネル設定を確認:
void AWebXRPlayerController::VerifyDataChannelConfig()
{
if (!WebXRInterface) return;
bool bIsUnreliable = WebXRInterface->IsTrackingChannelUnreliable();
if (!bIsUnreliable)
{
UE_LOG(LogTemp, Error, TEXT("Tracking data channel is in RELIABLE mode! This causes high latency."));
UE_LOG(LogTemp, Error, TEXT("Fix: Set 'Protocol': 'unreliable' in Cirrus config.json"));
}
}
3. 映像が乱れる・ブロックノイズが出る
原因:
- ビットレートが低すぎる
- パケットロスが発生している
解決策:
void AWebXRPlayerController::HandleVideoQualityIssues()
{
if (!WebXRInterface) return;
float PacketLoss = WebXRInterface->GetPacketLossPercent();
if (PacketLoss > 1.0f)
{
// パケットロス発生 → ビットレートを下げて安定性優先
IPixelStreamingModule::Get().SetEncoderTargetBitrate(15000000); // 15Mbps
UE_LOG(LogTemp, Warning, TEXT("Packet loss %.2f%% detected, lowering bitrate"), PacketLoss);
}
else
{
// 安定している → ビットレートを上げて品質向上
IPixelStreamingModule::Get().SetEncoderTargetBitrate(50000000); // 50Mbps
}
}
以下の状態遷移図は、VR セッション中のビットレート自動調整の動作を示しています。
stateDiagram-v2
[*] --> Initializing
Initializing --> Stable: セッション確立<br/>(RTT < 30ms)
Initializing --> Degraded: 接続不安定<br/>(RTT > 50ms)
Stable --> HighQuality: FPS安定 & RTT低<br/>(FPS>90 & RTT<20ms)
Stable --> Degraded: パケットロス検出<br/>(Loss > 1%)
HighQuality --> Stable: RTT上昇<br/>(RTT > 30ms)
HighQuality --> Degraded: FPS低下<br/>(FPS < 72)
Degraded --> Stable: 状況改善<br/>(Loss < 0.5%)
Degraded --> CriticalMode: 継続的な問題<br/>(Loss > 5%)
CriticalMode --> Degraded: 部分的回復<br/>(Loss < 3%)
CriticalMode --> [*]: セッション終了
note right of Stable
ビットレート: 30-40 Mbps
解像度: 100%
FPS: 90
end note
note right of HighQuality
ビットレート: 50-80 Mbps
解像度: 120%
FPS: 120
end note
note right of Degraded
ビットレート: 15-20 Mbps
解像度: 70%
FPS: 72
end note
note right of CriticalMode
ビットレート: 10 Mbps
解像度: 50%
FPS: 60
end note
この自動調整により、ネットワーク状況に応じて最適な品質とパフォーマンスのバランスが維持されます。
まとめ
Unreal Engine 5.9 の Pixel Streaming WebXR 統合により、以下が実現されました。
- ブラウザベース VR: Meta Quest 3 などのスタンドアロン VR ヘッドセットから、アプリインストール不要で UE5 VR アプリケーションにアクセス可能
- クラウドレンダリング: ハイエンド GPU を持たないユーザーでも、クラウド上の RTX GPU で高品質 VR 体験を享受
- 低遅延ストリーミング: WebRTC + NVENC + Predictive Frame Timing の組み合わせで、総遅延 20-30ms を実現
- ハンドトラッキング: WebXR Hand Input API により、コントローラー不要な自然なインタラクション
- 動的最適化: ネットワーク状況に応じたビットレート・解像度の自動調整で安定性を確保
実装のポイント:
- Instanced Stereo Rendering の有効化で描画コストを半減
- Unreliable データチャネルによるトラッキング遅延の最小化
- NVENC P4 プリセットと CBR レートコントロールで低遅延エンコード
- Dynamic Resolution によるフレームレート維持
- リアルタイムメトリクス収集による品質監視
今後の展望として、UE5.10(2026年10月予定)では AV1 コーデック対応と Foveated Rendering(視線追跡による描画最適化)の統合が予定されており、さらなる品質向上とコスト削減が期待されます。