この記事はTech KAYAC Advent Calendar 2023の6日目の記事です。
面白プロデュース事業部 技術部の藤澤です。主にUnityを扱った案件を担当しています。 この記事では、UnityとMetaQuest3を用いたMRコンテンツの作り方の一例をご紹介します。
10月10日に、待望のMetaQuest3が発売されましたね。VRはもちろん、MR機能がすごい!とXでも話題になっていた印象です。
この盛り上がりに乗じて私もMR開発したい!ということで、モグラ叩きを遊べるMRコンテンツを作ってみました。
Unityバージョンとレンダリングパイプライン
Unityのバージョンは2022.3.12f1
、レンダリングパイプラインはUniversal RP 14.0.9
を採用しました。
UnityでMetaQuestを動かすための環境構築
まずはMetaQuestが使えるようにするために、SDKを導入していきます。
Meta XR Core SDK
Unity AssetStoreからMeta XR Core SDK
というSDKが取得できるので、Unityプロジェクトにインポートしましょう。
少し前まではMeta XR Utilities SDK
というSDKを入れる必要がありましたが、詳細を見るとdeprecatedと書いてあるので、今後はMeta XR Core SDK
を使いましょう。
そこから、色々プロジェクトの設定をする必要があるのですが、SDKにProject SetUp Tool
という便利な機能が含まれており(Project Settings -> Oculus
で該当画面)、Fix All
で自動で設定してくれます。
これで最低限のセットアップは完了です。
Meta XR Interaction SDK
コントローラーやハンドトラッキングの機能も使えるようにします。
Unity AssetStoreからMeta XR Interaction SDK
というSDKが取得できるので、Unityプロジェクトにインポートしましょう。
Meta XR Interaction SDK OVR Integration
後述しますが、Meta XR Interaction SDK OVR Integration
を導入すると、Building Blocks
からモノを掴んだり、投げたりするようなインタラクションを選択できるようになります。
Unity AssetStoreからMeta XR Interaction SDK OVR Integration
というSDKが取得できるので、Unityプロジェクトにインポートしましょう。
以上でSDKの導入は完了です。
Building Blocksで必要な機能を導入する
モグラ叩きMRでは、以下のようなMR、インタラクション機能が必要でした。
- パススルー機能(hmdのカメラから撮影された現実空間の映像を表示する機能)
- スペースを参照する機能(Quest3のスペース設定で検出した現実空間の情報をUnity上で参照する機能)
- 現実空間の手に3Dの手をOverlayする機能
- 3Dオブジェクトを手で掴む機能
以上の機能を、Building Blocks
というツールで導入しました。
エディタのヘッダーから、Oculus -> Tools -> Building Blocks
でツールを開くことができ、実装したい機能を選択すると、コンポーネントのプリセットがHierarchyに展開されます。
これだけで機能を実現できてしまいます。
今回はBuilding Blocks
から以下を導入しました。
- Camera Rig
- Background Passthrough
- Room Model
- Synthetic Hands
- Hand Tracking
- Grabble Item
これで必要な機能は使えるようになりました。ここからは機能群を使ってモグラ叩きを作っていきます。
床にモグラが出てくるようにする
コンテンツの主役であるモグラが、現実空間の床を元気に移動する機能を実装します。
現実空間(スペース)のメッシュを生成する
現実空間の床を移動させるには、現実空間の情報(スペース)がUnityで使える必要があります。スペースはQuest3の設定から事前に登録することができます。
公式のリファレンスを見ると、OVRSceneManager
から現実空間の情報を参照できるようです。(SDKの方ではスペースじゃなくSceneと呼んでいるようです)
Unity Scene Overview: Unity | Oculus Developers
Building Blocks
からRoom Model
を導入していれば、OVRSceneManager
をアタッチしたGameObjectがHierarchy上に存在するはずなので、OVR Scene Anchor
、OVR Scene Volume Mesh Filter
、Mesh Filter
あたりをアタッチしたPrefabを作成し、それをOVRSceneManager
のPrefab Override
にアタッチする(Scene TypeはGLOBAL_MESH
)して実行すると...
あらかじめ登録しておいたスペースのメッシュ情報が生成されるようになります。Coliderをつければ当たり判定も可能です。
また、GlobalMesh(空間全体のメッシュ)以外にも、天井や床、ドア等、特定の空間だけメッシュ化することも可能なようです。
モグラが地面に隠れるようにする
モグラには、移動中は地面の中に、移動が完了するとヒョコッと顔を出して欲しいです。前述でスペースのメッシュは表示できるようになったので、カメラに対してモグラがメッシュよりも奥側にいる時にオクルージョンしてくれるShaderを用意します。
Shader "Unlit/Depth" { SubShader { Tags {"Queue" = "Background+10" } ColorMask 0 ZWrite On Pass { } } }
RGBAチャンネルへの書き込みを行わず、深度バッファにのみ書き込みを行なうことで、現実空間の映像でオクルージョンができるようにします。
このShaderをGlobal Mesh
に設定することで、モグラが地面に隠れるような表現ができるようになります。
現実空間のランダムな座標を選択する
次はスペースの範囲内でランダムな座標が選択できるようにします。
ランダムな座標を決めるにあたって、GlobalMesh(上記で可視化したスペース)の端の座標を知りたいわけですが、どうやらOVRSceneManager
から生成される天井や床のメッシュは、GlobalMeshをすっぽり覆い隠すような大きさと位置で生成されるようです。つまり、天井の頂点となる座標が参照できれば、やりたいことができそうです。
ということで、天井や床に関する情報を参照できないかなと調べたところ、OVRSceneRoom
というクラスからOVRScenePlane
というクラスで天井や床が参照できそうでした。
OVRScenePlane
のドキュメントをさらにみてみると、Boundary
というプロパティがそれっぽい名前をしています。
IReadOnlyList<Vector2> OVRScenePlane.Boundary The vertices of the 2D plane boundary. The vertices are provided in clockwise order and in plane-space (relative to the plane's local space). The X and Y coordinates of the 2D coordinates are the same as the 3D coordinates. To map the 2D vertices (x, y) to 3D, set the Z coordinate to zero: (x, y, 0).
- Boundaryには時計回りの順番で頂点群が格納されているよ
- 平面上のローカル座標になっているよ
- 3次元座標にマッピングする場合は、z軸を0にしてね
とのことなので、Boundaryの座標をOVRScenePlane
からのワールド座標に変換します。
OVRSceneRoom room = FindAnyObjectByType<OVRSceneRoom>(); OVRScenePlane ceiling = room.Ceiling; // 例として1頂点抽出 Vector2 localPoint = ceiling.Boundary[0]; // これが天井のワールド頂点座標 Vector3 worldPoint = ceiling.transform.TransformPoint(localPoint);
これで天井の頂点座標が分かったので、以下のようなロジックでモグラの移動先を決定しました。
Boundary[0]
からBoundary[1]
の範囲でランダムな座標を決定Boundary[1]
からBoundary[2]
の範囲でランダムな座標を決定2.の座標から
Boundary[2]
からBoundary[1]
の放線ベクトルを持つPlaneを生成し、1.の座標からPlaneと逆の法線ベクトルでRaycastを打つ。(XZ軸の位置の決定)Planeにヒットした座標から、床の
OVRScenePlane
と逆の法線ベクトルでRaycastを打ち、GlobalMeshにヒットした座標をモグラの移動位置とする。(Y軸の位置の決定)
モグラが移動する高さを4.の方法で決定している理由は、モグラが床以外にもベッドの上や障害物の上に移動すると、より面白そうと思ったので、GlobalMeshのColliderを利用しました。
選択した座標にいい感じにモグラを移動させる
モグラの移動先を決められるようになったので、次は実際に移動してもらいます。
前述した通り、モグラには床以外にも、ベッドや障害物の上など様々な場所を移動してもらう必要があります。この時、道中で障害物を突き抜けたり高さを無視するような移動をしてしまうと、没入感を損ねてしまいます。
以上の理由から、NavMeshを使ってモグラがいい感じに移動してくれるようにします。
現実空間を表現しているGlobalMesh
はランタイムで生成されるので、ランタイム中にNavMeshをベイクできる必要があります。NavMesh Surfaceを使うと、ランタイムでベイクが可能なので、UPMを使ってPackageをインポートします。
ランタイムでベイクを実行するには、GlobalMesh
が生成されるタイミングも必要になります。ドキュメントをみてみると、 SceneModelLoadedSuccessfully
というコールバックがOVRSceneManager
から参照できるようで、このタイミングであればGlobalMesh
が確実に参照できそうです。
this.sceneManager.SceneModelLoadedSuccessfully += this.OnLoadedScene; private void OnLoadedScene() { OVRSceneRoom room = FindAnyObjectByType<OVRSceneRoom>(); NavMeshSurface navMesh = room.gameObject.AddComponent<NavMeshSurface>(); // NavMeshをGlobalMeshにベイク navMesh.collectObjects = CollectObjects.Children; this.navmesh.BuildNavMesh(); }
ベイクできました。また、前述の方法で決定した目的地が、NavMeshでベイクしたエリアから到達可能か判断することも可能なので、ルートが存在しない場合は、再度目的地を調整するようにしていました。 NavMeshについては、こちらの方の記事がとても分かりやすく、参考にさせて頂きました。
これで高さや障害物を考慮した移動ができるようになりました。
モグラの影を落とす
モグラが地面から顔を出した際、地面に影が落ちるとより没入感が増しそうです。
現実空間を表現しているGlobalMesh
に対してモグラの影だけを描画するようにすれば、現実世界の床にモグラの影が落ちる絵が実現できそうです。
こちらの記事を参考に、以下のようなShaderを実装することで実現しました。
#ifndef CUSTOM_LIGHTING_INCLUDED #define CUSTOM_LIGHTING_INCLUDED void MainLight_float(float3 WorldPos, out float3 Direction, out float3 Color, out float DistanceAtten, out float ShadowAtten) { #ifdef SHADERGRAPH_PREVIEW Direction = float3(0.5, 0.5, 0); Color = 1; DistanceAtten = 1; ShadowAtten = 1; #else float4 shadowCoord = TransformWorldToShadowCoord(WorldPos); Light mainLight = GetMainLight(shadowCoord); Direction = mainLight.direction; Color = mainLight.color; DistanceAtten = mainLight.distanceAttenuation; ShadowSamplingData shadowSamplingData = GetMainLightShadowSamplingData(); float shadowStrength = GetMainLightShadowStrength(); ShadowAtten = SampleShadowmap(shadowCoord, TEXTURE2D_ARGS(_MainLightShadowmapTexture, sampler_MainLightShadowmapTexture), shadowSamplingData, shadowStrength, false); #endif } #endif
天井から穴が開く演出
HoloLensのRoboRaidが大好きだった私としては、MR上で壁が突き抜ける演出は絶対入れたいと思いました。
穴が空いて見える表現
こちらの方の記事を参考に、オブジェクトに対して穴が開いているように見せるShaderとRendererFeatureを実装しました。
主に2つのオブジェクトと、RendererObjectを用いて、空洞を表現しました。
空洞オブジェクトを描画するための結合部分となるオブジェクト
- Stencilが有効で、
Stencil = 1
で、Compare Function
がAlways
、Pass
がReplace
な RenderObjectを紐づけることで、このオブジェクトが描画される部分にStencil = 1
の内容が描画されるようにする。
- Stencilが有効で、
空洞部分を表現するためのオブジェクト
- 以下のShaderGraphのような、メッシュの内側を描画するShaderをアサインしている。
- Depthが有効で、
Depth Test
がAlways
なRenderObjectを紐づけることで、Global Mesh
に隠れても描画されるように設定。 Stencil = 1
なRenderObjectを紐づけることで、結合部分のオブジェクトに空洞が描画されるように設定。
より穴が開いて見えるように
穴を開けらえるようになりましたが、丸い穴だと現実味に欠けます。ヒビが入って、天井が割れるように穴が開くようにブラッシュアップしていきます。
前述の方法は、Cylinderのような厚みのある3Dモデルであれば代用できるので、こんな感じの3Dモデルを作って
砂埃っぽいParticleと破片が飛ぶように調整して、最終的にこちらのような感じになりました。
まとめ
MetaQuest3を使って、モグラ叩きゲームを遊べるMRコンテンツを作ってみました。豊富な機能がSDKから提供されており、面白いコンテンツを作るための可能性に満ちているなと感じました。またARFoundation
と連携するためのOpenXRパッケージが公開されたりと、さらにアップデートが進んでいるようなので、今後が楽しみです。