見出し画像

Godot Engine Tips - OpenXRを使用してVRを作る

はじめに

みなさんGodot Engineはご存じでしょうか?
ゲームエンジンといえばUnity/Unreal Engineが一般的ですね。
今回はMITライセンスで提供されているGodot Engineを使ってVRを作ります。

Godot Engineのインストール方法については過去記事を参照ください。
【入門】Godot Engineの始め方:開発環境をつくる|たかとく|note

本記事ではOpenXRに対応した機器に対してGodot Engineでコントローラーの位置を追従して、手のモデルが表示出来るようになるまでの基本的な実装方法を記載します。
プログラム部分に関しては100円の有料Noteになりますのでご注意ください。
⇒大した収益にもならなそうなので無料に変更しました。笑

QuestでVR空間に入れた&コントローラの位置トラッキングも出来た

なお、この記事に書いた基本的な実装まで出来れば、あとは本記事で紹介しているアセットの組み合わせて大抵のことは実現できるかと思います。

VRを作る際のGodot Engineを使うメリット

さて、この記事にたどり着いたエンジニアの方はVRを作っている、またはGodotを使用している方が多いかと思います。

VRを作る際にはUnityまたはUnreal Engineを使う方が多いかと思いますが、以下問題点があります。

  • 最初のスプラッシュスクリーンを消すためにお金がかかる

  • エディタが重い

  • コードを書いてからエディタ反映のビルドに時間がかかる

  • 再生するのにも時間がかかる

  • とにかく実行するのに時間がかかる

その点、Godot Engineは以下メリットがあります。

  • MITライセンスなのでお金はかかりません。

  • エディタはとても軽い。無駄なdll依存も少なく、基本は実行exeファイル1つです。

  • まず、コードエディタがエンジン内蔵。ビルドは爆速。見た目にかかわるシェーダーのコードに関しては書いている途中に次々と見た目に反映される。

  • 再生に至るまでのビルドも爆速。

  • プログラム次第ではあるが、ほとんど時間はかからない。

Godot Engineを使うデメリット

当然デメリットもあります。一番大きなデメリットは、

日本語の情報が少ない

これにつきます。加えてユーザーの少なさからUnityと比較するとアセットが少ないということもあります。
※ただし、基本的にはアセットもMITライセンスで無料で公開されていることが多いです。本当に無料で良いのかこれ?というアセットも多数あります。

Godot EngineでのVRの始め方

では、さっそくGodot EngineでVRを作っていきましょう。
当方の開発環境は以下です。

  • Godot Engine : バージョン3.5.1(Standard)

  • VR機器 : Oculus Quest 2

一点、注意点としてGodot Engineはバージョン4.0を近日中にリリース予定です。バージョン4.0では操作UIや仕組み等、色々と破壊的変更がありますので、バージョン4.0を早々に使う予定の方はご注意ください。

ただ、個人的にはしばらくはバージョン3.5および次に安定版としてリリース予定の3.6を使っていく方が良いと思います。
使いたいアセットが4.0では対応していない、初期バグがあるなど、大型メジャーアップデートの際にはいろいろな問題があることが推測されますので。

さて、まずはプロジェクトの作成です。
後からでも変更は出来ますが、OpenGL ES 2.0でプロジェクトを作成しましょう。
OpenGL ES 3.0の中に一部OpenXRでは機能しないものがあるためです。

OpenGL ES 2.0で始めましょう

プロジェクトを作成したら、まずは必要なアセットをインストールします。
必要なものは以下2つです。

  • OpenXR Plugin

  • Godot XR Tools

OpenXR Pluginは必須です。(Godot4からはゲームエンジン内蔵になったようですね。Godot4では特にインポートしなくても動きました。)
Godot XR Toolsは必須ではないですが、何かと便利な機能がパッケージとして色々と搭載されています。インストールしておきましょう。

そもそもGodotが初めてでアセットのインストールが分からない人のために。。。
アセットは上部のAssetLibから入手できます。

AssetLibは画面上部から移動

検索ウィンドウにOpenXRと入力するとOpenXR Pluginが出てきますのでダウンロードします。

OpenXR Pluginをダウンロード

Godot XR Toolsも同様です。

XR Toolとだけ入力すれば出てきます。

2つインストールが終わったら、Assetを有効化します。(インストールだけでは有効化されません)
プロジェクト⇒プロジェクト設定から、プラグインのタブに移動します。
その中に先ほどインストールした2つのアセットが入っていますので、どちらもステータスを有効に設定します。(なぜかXR PluginはここではGodot OpenXRに名前が変わっていますが大丈夫です)

両方ステータスを有効にする

さて、これで準備は完了です。
作っていきましょう。

まずは、ルートノードとして3Dシーンを配置しておきます。
作成されたノードをダブルクリックして編集し、Mainとでもしておきましょう。

3Dシーンを配置
名前はなんでも良いです

とりあえずシーンは保存しておきます。
名前はこだわりが無ければそのままデフォルトで良いでしょう。

シーンを保存しておく

メインシーンに対して子ノードを追加してきます。

子ノードを追加

まずはARVROriginノードを追加します。
ARVRと検索すると出てきます。

ARVROriginを追加

同じようにARVRControllerを、2つ追加します。
もしくは1つ追加したARVRControllerを右クリック⇒複製(またはノード選択してCtr+D)で同じノードを複製します。

これがVRの基本構成

次にARVRControllerの名前をLeftHandに、ARVRController2はRightHandに名前を変えましょう。その際、RightHand側は右側のインスペクタービューでControllerIDを2に変更しておきます。(1が左手側、2が右手側に対応しています)

右手のControllerIDの変更を忘れずに

次にARVROriginのノードを右クリックし、スクリプトをアタッチします。
スクリプト名はなんでも良いですが、VRControl.gsとでもしておきましょうか。

スクリプトをアタッチ
スクリプト名は分かればなんでも良いです

VRインタフェースの初期化と実行

さて、上記を実行するとプログラミングを組む画面になりましたね。
プログラミング自体は下記コピペで一切問題ないです。

extends ARVROrigin

func _ready():
	var VR= ARVRServer.find_interface("OpenXR")
	if VR and VR.initialize():
		get_viewport().arvr = true
		OS.vsync_enabled = false
	else:
		print("VR Initilize failed.")
		
	_connect_plugin_signals()
		
	pass

func _connect_plugin_signals():
	ARVRServer.connect("openxr_session_begun", self, "_on_openxr_session_begun")
	ARVRServer.connect("openxr_session_ending", self, "_on_openxr_session_ending")
	ARVRServer.connect("openxr_session_idle", self, "_on_openxr_session_idle")
	ARVRServer.connect("openxr_session_synchronized", self, "_on_openxr_session_synchronized")
	ARVRServer.connect("openxr_session_loss_pending", self, "_on_openxr_session_loss_pending")
	ARVRServer.connect("openxr_session_exiting", self, "_on_openxr_session_exiting")
	ARVRServer.connect("openxr_focused_state", self, "_on_openxr_focused_state")
	ARVRServer.connect("openxr_visible_state", self, "_on_openxr_visible_state")
	ARVRServer.connect("openxr_pose_recentered", self, "_on_openxr_pose_recentered")

func _on_openxr_session_begun():
	print("openxr_session_begun")

func _on_openxr_session_ending():
	print("openxr_session_ending")
	
func _on_openxr_session_idle():
	print("openxr_session_idle")
	
func _on_openxr_session_synchronized():
	print("openxr_session_synchronized")
	
func _on_openxr_session_loss_pending():
	print("openxr_session_loss_pending")
	
func _on_openxr_session_exiting():
	print("openxr_session_exiting")
	
func _on_openxr_focused_state():
	print("openxr_focused_state")
	get_tree().paused = false
	
func _on_openxr_visible_state():
	print("openxr_visible_state")
	get_tree().paused = true
	
func _on_openxr_pose_recentered():
	print("openxr_pose_recentered")

え、なにやってるの、これ?って感じですかね。
正直細かく見ていけば、そこまで大したことはしていません。
パッと見て何をやっているか分かった人は、コピペだけして本章は読み飛ばしてOKです。

extends ARVROrigin

func _ready():
	var VR= ARVRServer.find_interface("OpenXR")
	if VR and VR.initialize():
		get_viewport().arvr = true
		OS.vsync_enabled = false
	else:
		print("VR Initilize failed.")
		
	_connect_plugin_signals()
		
	pass

まずはここから。
extends ARVROriginは最初からデフォルトで出ている決まり文句みたいなものなので気にしないでOKです。
(参考:ARVROriginを継承するという意味です)

func _ready(): ~ pass自体も最初から出ているものですね。
これはノードが呼ばれたときに最初に実行されるコードを記載するところです。主に初期化に使用します。
ということで_ready内にVRの初期化処理を記載するわけです。

var VR= ARVRServer.find_interface("OpenXR")

これはVRという変数にARVRServerからOpenXRというインターフェースを探し出して割り当てるという処理です。こうすることでVR.***という形でVR関係の機能のパラメータを取得したり、逆にパラメータを割り当てたりすることが出来ます。

if VR and VR.initialize():
		get_viewport().arvr = true
		OS.vsync_enabled = false

ifは基本的なプログラミングなので解説するまでもないですね。先ほどの変数宣言でOpenXRのインターフェースが正しく変数VRに割り当てられると、if VRはTrueになります。
また、VR.initialize()ではOpenXRの初期化を行います。初期化が正しく完了するとTrueが返ってきますので、これをifのAND条件として設定します。
つまりこのif文は「正しくVRインターフェースが割り当てられ、初期化が正しく完了した場合。」という条件になります。

初期化が完了した際には、get_viewport().arvr = trueでVRモードを有効にします。
さらにOS.vsync_enabled = falseでOSによるフレームレートの自動調整機能を無効にします。これをしないと、ゲームエンジン側の処理と描画処理のタイミングが合わず、映像が滑らかにならないという問題が出てきます。
これはVRにおいては致命的で、視点を移動するときにカクカクするとVR酔いの原因になります。

else:
		print("VR Initilize failed.")

else:の方はもしちゃんと初期化できてなかったら、デバックログに失敗したことを通知するようにしています。

_connect_plugin_signals()

これはただの関数の実行です。この関数自体は次で定義されています。

func _connect_plugin_signals():
	ARVRServer.connect("openxr_session_begun", self, "_on_openxr_session_begun")
	ARVRServer.connect("openxr_session_ending", self, "_on_openxr_session_ending")
	ARVRServer.connect("openxr_session_idle", self, "_on_openxr_session_idle")
	ARVRServer.connect("openxr_session_synchronized", self, "_on_openxr_session_synchronized")
	ARVRServer.connect("openxr_session_loss_pending", self, "_on_openxr_session_loss_pending")
	ARVRServer.connect("openxr_session_exiting", self, "_on_openxr_session_exiting")
	ARVRServer.connect("openxr_focused_state", self, "_on_openxr_focused_state")
	ARVRServer.connect("openxr_visible_state", self, "_on_openxr_visible_state")
	ARVRServer.connect("openxr_pose_recentered", self, "_on_openxr_pose_recentered")

さて、GDScriptに慣れていない人が一番意味が分からないものがあるとしたらここですかね。

ARVRServer.connect("openxr_session_begun", self, "_on_openxr_session_begun")

まずARVRServerは初期化でも使ったARVRのオブジェクトクラスと呼ばれるものです。
オブジェクトクラスに対し、.connect("シグナル名" ,self ,"関数名")とするとそのオブジェクトクラスから該当するシグナルが出てきたとき、指定の関数を実行するイベント関数を設定することが出来ます。

試しにOculud Questで実行した場合のログは以下です。
★は私が操作/行動した部分です。

<ログ>
★VRゴーグルを装着した状態でスタート
openxr_session_idle
openxr_session_begun
openxr_session_synchronized
openxr_visible_state
openxr_focused_state
★VRゴーグルを外した
openxr_visible_state
openxr_session_synchronized
openxr_session_ending
openxr_session_idle
★VRゴーグルを装着した
openxr_session_begun
openxr_session_synchronized
openxr_visible_state
openxr_focused_state
★Oculusボタンでメニューを表示
openxr_visible_state
★Oculusボタンでメニューを解除
openxr_focused_state
★Oculusボタンでメニューを表示
openxr_visible_state
★Oculusメニューからアプリケーションを終了
openxr_session_synchronized
openxr_session_ending
openxr_session_idle
openxr_session_exiting

これを見ると、

  • VRゴーグル装着 & プレイ : focused_state

  • VRゴーグル装着 & メニュー表示 : visible_state

  • VRゴーグル非装着 : visible_state → ・・・ → session_idle

  • アプリケーション終了 : visible_state →・・・session_exiting

このようにプレイ中はfocused_state、それ以外はvisible_stateを経由することが分かります。

func _on_openxr_focused_state():
	print("openxr_focused_state")
	get_tree().paused = false
	
func _on_openxr_visible_state():
	print("openxr_visible_state")
	get_tree().paused = true

そのため、focusedとvisibleの関数にはget_tree().paused = true/falseを入れています。
get_tree().paused = trueはゲーム全体を止める、get_tree().paused = falseはゲーム全体を止めたものを解除するものです。

get_tree().pausedで具体的に何を止めるかというのは、各ノードのインスペクター:Pause Modeで設定できます。
なお、このスクリプトが割り当てられているARVROriginを止めてしまうと、get_tree().paused = falseで復帰できなくなってしまうので、ARVROriginのPoseModeはProcessにしておきましょう。これでget_tree().pausedには影響されません。
※逆にPauseMode:Stopにすると明示的にget_tree()で止めるノードに指定できます。PauseModeはデフォルトでは上位ノードの設定を継承するようになっているので、なんか止まらないなと思ったら確認してみましょう。

ARVROriginのPauseModeはProcessにしておきましょう

ちなみにこのシグナルを接続してVRのステータスを取得する方法は公式ドキュメントやリファレンス等でも記載されていません。
Oculusストアへアプリを出す要件として、Oculusメニュー表示中は「ゲーム全体を止めること」みたいなものがあるので、記載しておいた方が良いと思うんですがね・・・。

手のモデルの追加

さて、この状態で再生しても良いのですが、ただ青い空と地平線を見るだけの寂しいものになってしまいますので、コントローラーの位置に応じて手のモデルも表示出来るようにしましょう。

ここで便利ツール集として導入したGodot XR Toolsが活躍します。
ファイルシステムビューのaddonsからgodot-xr-toolsを開きます。この中のhandsのフォルダを開きます。

手のモデルが格納されているフォルダ

この中のHand_low_L.gltf/Hand_low_R.gltfのいずれかをダブルクリックします。

Handのローポリモデル

すると、確認ダイアログが出てくるので「新規の継承」を選択します。
すると、新規のシーンとして手のモデルの編集画面になります。

新規の継承を選択

特に編集する必要が今はないので、そのまま作成されたシーンをLeftHandやRightHandの名称でシーンを保存します。もう片方の手も同様に作業しましょう。

LeftHand.tscn/RightHand.tscnとしてシーン保存

その後、ファイルシステムビューから先ほど保存したLeftHand/RightHandのシーンをコントローラーの子ノードとして配置します。
これでLeftHand/RightHandとしてトラッキングされたコントローラーの位置に手のモデルが表示されるようになりました。

各コントローラーノードに手モデルを配置

他にもGodot XR Toolsには便利ツールが大量に仕込まれており、ほぼそのまま使うことが出来るものがほとんどですので、一通り見てみると良いかと思います。

いよいよ実行!

これで準備は整いました。Questの場合はUSBデバックを有効にしたうえでOculusLiftを起動してください。
他のVR機器の詳細は分かりませんが、同様に開発者モードみたいなものがあるかと思いますので、それを有効にすれば同様に再生できるかと思います。(詳細は各機器のマニュアル等を参照してください)

再生は右上の再生マークから出来ます。
何かダイアログが表示されたら現在のシーンを利用を選択してください。

シーン再生ボタンは右上

コントローラーを動かすと手が追従される基本的なVRアプリが作成できました。ちなみにQuestの場合だとデフォルトで手の位置がずれているので、そこはHand_low_L/Rの位置や角度の調整で対応してください。

QuestでVR空間に入れた&コントローラの位置トラッキングも出来た

Oculus Questの場合だと、Oculus LiftでPCのGodot編集画面をVR画面側に投影するとゴーグルを外すことなく、編集⇒再生⇒編集⇒・・・と繰り返せますので効率的に作業できます。

さらにこの作業を他ゲームエンジンでやると、ビルドの時間が10秒近くかかります。ちょっと作業してビルド待って確認して、調整してビルド待って確認して・・・と作業すると、1回のビルドは10秒程度ですが、やはりチリツモで開発効率は悪いです。

その点、Godot Engineであれば、この程度であればビルドは一瞬で終わりますので、こういう細かい調整の作業効率は他ゲームエンジンにはない魅力があります。

これからVRアプリを作ってみたいという方はGodot Engineを検討に入れてみてはどうでしょうか?

この記事が気に入ったらサポートをしてみませんか?