XREAL AURAで使うGodot:ネイティブ空間開発の実践ガイド
要点まとめ
GodotでXREAL AURA向けネイティブ空間開発を実践する手順を解説。光学シースルー特有の設計課題とSpatial Containers活用、実機ワークフローを網羅。
現在のXRビルドパイプラインは、デスクトップにテザリングし、APKをエクスポートし、ADB経由でサイドロードし、ヘッドセットを装着し、タイプミスを見つけて、やり直す——という流れになっている。XREAL AURAはそのループを完全に取り除き、Godotもローンチラインナップに含まれ、グラス単体で動作する完全なエディタを備えている。これは空間開発における「イテレーション」の意味を変える。
本ガイドでは、実際のワークフローを解説する。AURAでエディタを動かす手順、OpenXR Spatial Containersを使った空間シーンの構築、光学グラスとVRヘッドセットを区別するパススルー/シースルーモデルの扱い、そして最初のエクスポートで確実に踏む落とし穴までをカバーする。
光学シースルーが設計前提をどう変えるか
XREAL AURAは、Android XR初の光学シースルーデバイスだ。この一文だけで、既存のGodot XRチュートリアルに組み込まれた前提が崩れる。
VRヘッドセットでは、ヘッドセットのカメラがすべてをレンダリングする。ユーザーが見るのは自分の環境だけだ。光学シースルーグラスでは、ユーザーはレンズ越しに現実世界を見て、その上にレンダリングコンテンツが合成される。これはQuestのビデオパススルーとは異なる。ビデオパススルーはカメラで現実を捉えて再表示するため、レイテンシと光量の低下が生じる。光学シースルーには、現実世界に対するコンポジタのレイテンシがゼロである。
レンダリング予算への実務的な影響は次のとおり。
- 現実世界を暗くすることはできない。 透明ディスプレイ上の黒は透明になる。不透明な背景がないため、暗いパネルに依存したコントラスト設計のUIは明るい部屋で機能しない。
- 加算的な見た目のコンテンツが勝つ。 明るい発光コンテンツはよく読めるが、暗く彩度の低いコンテンツは消える。パレットはVRで使う輝度の2〜3倍で設計する。
- 視野角はVRより狭い。 極端な角度に置いたコンテンツは表示領域の外に出る。主要な操作ターゲットは前方中心から概ね±30度以内に置く。
VRで製品を出したことがあるなら、すべてのマテリアルとUIパネルを調整し直すつもりでいてほしい。シーンごとに丸一日の予算を組むこと。
グラス上でGodotエディタをセットアップする
エディタはサイドロードAPKではなくGoogle Playで配布される。つまり、他のアプリと同じように更新できるということだ。
セットアップ手順:
- Play StoreからXREAL AURAにGodotエディタをインストールする。
- コンパニオンのGradleビルド環境アプリをインストールする。これにはAndroidエクスポートツールチェーンが同梱されているため、デバイス上でJDKやAndroid SDKをゼロから構築する必要はない。
- デバイスからビルドを署名・公開する予定がある場合は、Android開発者 IDにサインインする。
- エディタを起動する。単一のフラットウィンドウではなく、マルチウィンドウの空間ワークスペースとして開く。
4つ目のポイントが興味深い。エディタはデスクトップ同等の機能セットを備えつつ、空間内にフローティングウィンドウとして配置される。Sceneツリーをひとつのボリュームに、2Dビューポートを別のボリュームに置ける。
オンデバイスワークフローで実際に得られるもの
正直な答えは、フィールのイテレーションは速くなり、アセット制作のイテレーションは遅くなるということだ。
ターゲットデバイス上でシェーダーを編集し、ノードのトランスフォームを調整し、入力マッピングをテストすれば、エクスポートとテストのサイクル全体が不要になる。そのサイクルは、ビルド時間・転送・再起動を考慮すると通常3〜8分かかる。10回のイテレーションで1時間が消える。
逆に役に立たないのは、大きなテクスチャアトラスのインポート、長いライトマップベイク、CPU負荷の高い処理だ。これらはワークステーションでやるべきだ。現実的な分担は、アセット制作はデスクトップ、シーンアセンブリと調整はグラス上、となる。
OpenXR Spatial Containersで空間コンテンツを構築する
GodotのAndroid XRサポートにはOpenXR Spatial Containersが含まれている。これはシステム管理のボリュームで、アプリはその中に空間コンテンツを配置する。プラットフォーム自身のアナロジーはほぼ正確だ。従来のアプリはデスクトップ上のウィンドウ内に2Dコンテンツを描き、空間アプリは部屋内のコンテナ内に3Dコンテンツを描く。
コードを書く前に理解しておくべきコンテナタイプは2つある。
| コンテナタイプ | 最適な用途 | トレードオフ |
|---|---|---|
| Volume | 実深度を持つ3Dオブジェクト、ワールドロック配置 | システムがボリューム境界をクランプする可能性がある |
| Panel | UIサーフェス、テキスト、2Dゲーム | 固定平面にレンダリングされ、深度の手がかりがフラットになる |
ほとんどの開発者がやらかすミスは、UIにVolumeを選ぶことだ。Volumeコンテナ内のテキストは、システムによって予測不能な方法でスケール・移動され、丁寧に配置したレーダーウィジェットがずれる。UIはPanelコンテナに入れて、配置はシステムに任せるべきだ。
Spatial Container APIを扱う
GodotはOpenXR拡張クラスを通じて空間コンテナを公開する。正確なクラス名はGodotのバージョンに依存するため、本番コードを書く前に、現在のGodotビルドに同梱されている空間コンテナバインディングの機能リストを確認してほしい。以下のパターンは、コンテナベースのXRコンテンツが通常どう構成されるかを反映したものだ。
extends Node3D
signal container_created(handle: int)
# Spatial container setup for Android XR.
# Class names are version-dependent; verify against the
# OpenXR extension bindings shipped with your Godot build.
var volume_container: SpatialContainer
var panel_container: SpatialContainer
func _ready() -> void:
var xr_interface: XRInterface = XRServer.find_interface("OpenXR")
if xr_interface == null or not xr_interface.is_initialized():
push_error("OpenXR interface unavailable on this device.")
return
# Volume container for the 3D board.
volume_container = SpatialContainer.new()
volume_container.container_type = SpatialContainer.TYPE_VOLUME
volume_container.extent = Vector3(0.6, 0.4, 0.6) # 60cm x 40cm x 60cm
if volume_container.request_container() != OK:
push_error("Volume container request failed.")
return
# Panel container for the HUD.
panel_container = SpatialContainer.new()
panel_container.container_type = SpatialContainer.TYPE_PANEL
panel_container.extent = Vector3(0.4, 0.4, 0.02)
panel_container.request_container()
add_child(volume_container)
add_child(panel_container)
container_created.emit(volume_container.get_container_handle())
func _process(_delta: float) -> void:
# Containers can be rejected or resized by the system.
if volume_container and not volume_container.is_active():
_handle_container_loss()
func _handle_container_loss() -> void:
push_warning("Volume container lost - repositioning.")
# Never assume the container survives; re-request on loss.
volume_container.request_container()
重要な行は_handle_container_loss()だ。システム管理コンテナは、定義上、システムによって管理される。ユーザーが離れる、アシスタントがコンテンツを再配置する、デバイスが空間を再利用する。入力処理がコンテナ喪失を乗り越えられなければ、アプリは見えないボードを前に、ボタンを連打するプレイヤーとともに放置される。
ほぼ全員が踏む落とし穴:アンカードリフト
これが具体的な問題であり、最もデバッグ時間を費やすことになる問題だ。
Volumeコンテナに3Dボードを置く。Playを押す。すべて正しく見える。グラスを外し、2分後に再び装着すると、ボードは左に50cmずれ、20度回転している。
なぜか。コンテンツをコンテナではなくワールド原点にアンカーしたからだ。空間デバイス上のワールド原点は安定した場所ではない。セッション開始ポーズを基準に定義される。セッションが再アンカーすると、ワールド原点は移動する。
修正方法は、シーンのすべてのピースをコンテナのトランスフォームの子として保持し、空間コンテンツでグローバル位置を読み書きしないことだ。
# WRONG: assumes world origin is stable
func place_piece(piece: Node3D, board_pos: Vector3) -> void:
piece.global_position = board_pos
# RIGHT: position relative to the container transform
func place_piece(piece: Node3D, board_pos: Vector3) -> void:
piece.position = board_pos # local to container
piece.rotation = Vector3.ZERO
出荷前にプロジェクト全体で.global_positionと.global_transformをgrepしよう。空間コンテンツでヒットした箇所はすべてドリフトバグの可能性がある。この種のバグは従来の3Dやマルチプレイヤー開発にも現れる。レプリケートされたトランスフォームでのアンカーと所有権のミスは、「特定クライアントから見るとオブジェクトがテレポートする」というまったく同じ症状を生む。だからこそ、トランスフォームを絶対値ではなくコンテナローカルとして扱う習慣は、どのGodotネットワークプロジェクトにも持ち込む価値がある。
修正の確認
アンカリングが正しいことを確認するには:
- シーンを起動し、ボードの位置を記録する。
- デバイスを外し、60秒待って、アプリに再入場する。
- ボードは、自分が立っている場所からの相対オフセットではなく、部屋に対する同じ物理的位置に表示されるはずだ。
- デバイスの向きを変えて繰り返す。座った状態と立った状態で。
ドリフトするなら、コンテンツツリーにまだ絶対参照が残っている。ジャンプするが、ジャンプ後は安定するなら、コンテナが再配置され、コンテンツが正しく追従している——これは期待される挙動であり、バグではない。
プレイヤー IDと永続状態の扱い
Android XRの空間アプリはAndroidアプリだ。サスペンドし、レジュームし、ユーザーはデバイスに複数のサインイン済みIDを持ち、セーブデータは他のすべてと同じアカウントエコシステムに存在する。
失敗モードは微妙だ。プレイヤーがグラスで空間プレイし、その後スマートフォンで同じゲームを開く。セーブ状態がデバイスローカルなら、二つの divergent な世界ができ、それらを調整する方法はない。これを修正するのはレンダリングの問題ではなく、アカウントアイデンティティの問題だ。
自力で正しく実装するには、サインインフロー、サーバーサイドのセーブストア、そして誰もが過小評価する部分——2つのデバイスが同じセーブスロットに書き込むときの競合処理——を実装する必要がある。クライアントがバージョンを比較し、競合を明示的に解決するリビジョン対応の読み書きモデルだけが、実ユーザーとの接触に耐えられる。ゼロから構築するのは数週間の仕事だ。horizOn は、このケースに正確に対応するアカウント連携クラウドセーブとリビジョン対応の読み書きを提供し、さらにEmail・匿名・Google・Apple認証にも対応しているため、同じプレイヤーIDをグラスとスマホで引き継げる。
どのバックエンドを使うにせよ、出荷前に競合ポリシーを決定してほしい。最終書き込み優先は静かに進行を破壊する。データを失うより、プレイヤーにプロンプトを出す方がましだ。
XREAL AURAで出荷するためのベストプラクティス
- 環境光を想定して設計する。 すべてのUIパネルを真昼の屋外と薄暗い部屋でテストする。どちらかでテキストが読めなくなるなら、コントラストを上げ、暗い影ではなく明るいアウトラインを追加する。影は透明ディスプレイでは何の効果もない。
- コンテナは小さく、再リクエスト可能に保つ。 60cmのボリュームは、2mのボリュームよりシステムが確実に配置しやすい。すべてのコンテナはいつでも失われる可能性があると想定し、回復パスを最後ではなく最初に書く。
- レンダリング予算を積極的に制限する。 これはグラスであってデスクトップGPUではない。時間的アンチエイリアシングをオフにし、空間解像度でのコストが低いMSAAを採用して、フレームタイム11ms未満(90Hzクラス)を目指す。
- UIはパネルに、コンテンツはボリュームに。 テキストの配置ロジックと戦わない。ボリュームは、プレイヤーが複数の角度から調べる本物の深度を持つもののために確保する。
- 各マイルストーン前に実機で検証する。 グラス上のエディタがあればこれは安上がりだ。シミュレータの結果では、アンカードリフト、輝度の失敗、コンテナ喪失挙動は明らかにならない。
次のステップへ
オンデバイスエディタこそが本当の見出しだ。空間イテレーションループを数分から数秒に圧縮する。これは、デザインを調整することと、デザインを推測することの違いだ。
まず、AURAにエディタとコンパニオンGradleアプリをインストールしよう。それから、新規作成ではなく小さな既存プロジェクトを空間シーンとして再構築する。アンカリングと輝度の問題が、すでに理解しているコンテンツですぐに表面化する。そこから、3DボリュームコンテナとUIパネルを追加し、サスペンド/レジュームサイクルをまたいで両方を安定させる。それが維持できれば、プラットフォームに逆らうのではなく、プラットフォームのために設計する準備が整った。
デバイス間の永続的なプレイヤー状態が検討項目にあるなら、horizOn はクライアント側の競合解決付きアカウント連携クラウドセーブを処理するため、空間セッションとフラットスクリーンセッションの同期を維持できる。