クロスプラットフォームライブラリ「Mixed Reality Toolkit 3」を使ってみよう
この記事は、XR Kaigi 2023で登壇した「Mixed Reality Toolkit 3 で始めるクロスプラットフォーム開発」の中で解説した実装に関する記事です。
2023年9月にリリースされた「Mixed Reailty Tookit 3」の環境構築とUX部品を使ってみたいと思った人向けに作成しました。本記事はMRTK3で構築したコンテンツに対して以下の作業を実施する手順を記載しています。
- 空間マッピング機能追加と可視化
- Dual Render Fusionを使ったARグラス視点のスマホ表示
- Lenovo ThinkReality A3にデプロイ
本記事の手順は事前に以下の記事でUnityプロジェクトを作成済みの状態からスタートすることを前提にしていますが、作業手順自体は汎用的なものなので必要に応じて活用してください。
MRTK3の概要については XR Kaigi内のセッションでも紹介していますが、関連ドキュメントなど詳細は以前に記事にしたものがあるので合わせて参考にしてください。
本記事に関連する記事は複数あります。
Lenovo ThinkReality A3以外のデバイスでのMRTK3を使うことに興味がある方は参考にしてください。
開発環境について
Lenovo VRXにデプロイするために必要なツールとライブラリは以下の通りです。
- ツール
- Unity 2021.3.22f1
- Android Build Support
- Open JDK
- Android SDK & NDK Tools
- Android Build Support
- Mixed Reality Feature Tool
- Unity 2021.3.22f1
- 導入するライブラリ(インポート時に自動導入される依存ライブラリは未記載)
- MRTK3関連
- MRTK Input 3.0.0
- MRTK Spatial Manipulation 3.0.0
- MRTK Standard Assets 3.0.0
- MRTK UX Components 3.1.0
- MRTK UX Core Scripts 3.1.0
- MRTK Graphics Tools 0.5.12
- MRTK Core Definitions 3.0.0
- Mixed Reality OpenXR Plugin 1.9.0
- Snapdragon Spaces
- QHCT Unity Interaction 4.1.0
- Snapdragon Spaces SDK 0.17.0
- Snapdragon Spaces Dual Render Fusion 0.17
- MRTK3関連
Snapdragon Spacesの導入
ThinkReality A3の独自機能を利用するためにはMRTK3に加えてSnapdragon Spacesが必要になります。
Snapdragon Spacesについて
Snapdragon SpacesはQualcommが提供するXRデバイス向けの開発プラットフォームになります。現在このプラットフォームがサポートしているデバイスは以下のLenovo製のXRデバイスとなっています。
- Lenovo ThinkReality A3
- Lenovo VRX
Snapdragon SpacesのSDKについて公式サイトからダウンロードすることが可能です(要無償ユーザ登録)。
現在はUnity以外にもUnreal Engine向けのライブラリも提供されています。
Unityに導入する場合はPackage Managerからインポートします。
最初に[(作成したUnityプロジェクトフォルダ)\ProjectUnity2021.3\Packages]内に[SnapdragonSpaces]というフォルダ名を作成し、その中にダウンロードしたSnapdragon SpacesのSDKファイルを格納します。
次にUnity Editorのメニューから[Window]-[Package Manager]を選択しPackage Managerウインドウを開きます。左上の[+]を押して[add package from tarball ...]を選択しインポートを行ってください。
問題なく導入できると以下のようにQualCommのパッケージとして追加されます。
プロジェクトを作成する
MRTK3でのアプリ実装については以下の記事を参照してUI部分の構築を行ってください。以降はこの手順で作成したプロジェクトを変更しながら機能を追加していきます。
空間マッピングを有効にする
Lenovo ThinkReality A3で空間認識の機能を利用する場合、ARFoundationを経由してLenovo ThinkReality A3の機能を利用できます。
実装については以下の手順に従って設定を行います。
- [MRTK XR Rig]-[Camera Offset]の下に空のGameObjectを追加
- 追加したオブジェクト名をSpatialMappingに変更(任意)
- SpatialMappingのInspectorパネルから以下のコンポーネントを追加/設定する
- [ARMeshManager]を追加しMesh Prefabを設定
- [Spaces AR Mesh Manager Config]を追加
AR Mesh Managerに設定するMeshObjectは以下のコンポーネントを定義したオブジェクトを作成し設定してください。
- Mesh Filter
- Mesh Renderer
- Mesh Collider (※床など空間に干渉するコンテンツを作成する場合に必要)
以上でThinkReality A3でMRTK3と空間マッピングの機能を使ったコンテンツを作ることが可能です。
Dual Render Fusionの設定とARグラス視点の映像の表示
Lenovo ThinkReality A3のARグラス視点を第三者に見せるために撮影する手段として[TR Cast]というアプリを使ったストリーミング配信機能があるのですが、まだ使い勝手がよくなくほとんどの場合でFPS低下、通信切断などでうまく録画することができません。そこで今回はARグラス視点の映像をThinkReality A3が接続されているスマホの画面にDual Render Fusionの機能を利用して描画し、その画面をスマホの録画アプリを使ってきれいにとるという仕組みを作っています。非常に回りくどい手段ではあるのですが現状きれいに動画を撮る手段があまりないため回避策として導入しています。ただ、この機能を利用するためにいくつかSnapdragon Spacesの機能を活用しているのでアプリケーションに導入したい場合はぜひ参考にしてください。
Dual Render Fusionはスマホ接続型のARグラスの特性を最大限活用できる興味深い機能です。この機能を利用するとスマホ画面とARグラスのコンテンツを同時に扱うことができます。合わせてARグラス視点のRGBカメラ映像をコンテンツを合成する実装も行っています。大きくは以下の手順で実現しています。
- スマホ画面用のCameraの追加
- RGBカメラの映像投影用のRaw Imageの作成
- RGBカメラの映像情報を読込Raw Imageへ書き込む処理の実装
- Unity内のARグラス視点のオブジェクト投影用のCameraとRender Textureの追加
- Render Textureを設定したRaw Imageの作成
- 4で作成したRaw Imageをスマホ用カメラのみに描画
- XR Plug-in Managementの設定でDual Render FusionとRGBカメラへのアクセス機能を有効化する
- ThinkReality A3には装着者視点のRGBカメラが付いているのでこの映像をRawImageに書き出します。
- 1のRawImageが最背面になるように奥行を調整しARグラスで描画するコンテンツを手前に配置する
- 2の映像を別の仮想カメラで撮影する。この仮想カメラはARグラスのヘッドセット用のカメラとTransformを同期させる
- 仮想カメラにRender Textureを適用し、Raw Imageに仮想カメラの映像を書きだす
- 5の仮想カメラ映像をスマホ画面のみに描画する
ThinkReality A3には装着者視点のRGBカメラがあるのでこのカメラの映像を背景画像として利用し、実際のコンテンツと合成した映像をスマホの画面に出力することでARグラスの装着者視点の映像に近いものを構築します。今回はRGBカメラの映像の縮尺や画角の調整はしていません。このためたとえばハンドトラッキング等の手のメッシュと実際の手の縮尺は異なります。あくまで、「こんな感じにみえるんだ」というレベルを見ることができる程度の簡易なものと考えてください。(もちろん厳密に調整すれば対応は可能です)
実装手順
実装については先ほども紹介した通りの以下の手順で説明をします。
- スマホ画面用のCameraの追加
- RGBカメラの映像投影用のRaw Imageの作成
- RGBカメラの映像情報を読込Raw Imageへ書き込む処理の実装
- Unity内のARグラス視点のオブジェクト投影用のCameraとRender Textureの追加
- Render Textureを設定したRaw Imageの作成
- 4で作成したRaw Imageをスマホ用カメラのみに描画
- XR Plug-in Managementの設定でDual Render FusionとRGBカメラへのアクセス機能を有効化する
1. スマホ画面用のCameraの追加
シーンの任意の場所に空のGameObjectを作成しCameraコンポーネントを追加します。
この際にいくつか設定を行います。スマホ画面の表示に使うカメラについてはTarget Eyeを[None(Main Display)]に設定します。また、Tagについては[MainCamera]以外を設定してください。MainCameraタグはARグラス用のカメラコンポーネントとして利用されているため予期せぬ動作を引き起こします。
2. RGBカメラの映像投影用のRaw Imageの作成
次にThinkReality A3のRGBカメラの情報にアクセスし映像をRaw Imageに出力します。任意の場所にRaw Imageを追加してください。オブジェクトのサイズは1280x720でそれ以外はデフォルトの設定のままで問題ないです。
3. RGBカメラの映像情報を読込Raw Imageへ書き込む処理の実装
次に先ほど追加Raw ImageにRGBカメラの映像を描画するロジックを構築します。今回は[CameraFrameAccess]コンポーネントとしてその実装を行っています。
この実装についてはSnapdragon SpacesのDual Render Fusionのサンプルの中にあります。サンプルではスマホ画面にオリジナルのUIを構築するものになっています。このUIの中にARグラスのカメラ映像をRaw Imageに書き出すコードなども含まれています。今回はサンプルから必要最小限のコードを作成してます。
Projectパネルの任意のフォルダでCameraFrameAccessという名前でC#スクリプトを作成し以下の実装を行います。
// Copyright (c) 2023 Takahiro Miyaura
// Released under the MIT license
// http://opensource.org/licenses/mit-license.php
using System;
using System.Runtime.InteropServices;
using Qualcomm.Snapdragon.Spaces;
using Reseul.Snapdragon.Spaces.Devices;
using TMPro;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;
namespace Reseul.Snapdragon.Spaces.CameraFrameAccesses {
public class CameraFrameAccess : MonoBehaviour {
[Header("Camera Feed")]
public RawImage CameraRawImage;
public bool RenderUsingYUVPlanes;
private ARCameraManager _cameraManager;
private SpacesARCameraManagerConfig _cameraManagerConfig;
private Texture2D _cameraTexture;
private bool _deviceSupported;
private bool _feedPaused;
private NativeArray<XRCameraConfiguration> _cameraConfigs;
private XRCameraIntrinsics _intrinsics;
private XRCpuImage _lastCpuImage;
public void Awake() {
_cameraManager = FindObjectOfType<ARCameraManager>(true);
_cameraManagerConfig = FindObjectOfType<SpacesARCameraManagerConfig>(true);
}
// Start is called before the first frame update
private void Start() {
_deviceSupported = CheckDeviceSupported();
if (!_deviceSupported) {
OnDeviceNotSupported();
return;
}
if (!CheckSubsystem()) {
return;
}
_deviceSupported = FindSupportedConfiguration();
if (!_deviceSupported) {
OnDeviceNotSupported();
return;
}
_cameraManager.frameReceived += OnFrameReceived;
}
private void OnFrameReceived(ARCameraFrameEventArgs args) {
if (_feedPaused) {
return;
}
if (!_cameraManager.TryAcquireLatestCpuImage(out _lastCpuImage)) {
Debug.Log("Failed to acquire latest cpu image.");
return;
}
UpdateCameraTexture(_lastCpuImage, RenderUsingYUVPlanes);
// Update intrinsics on every frame, as intrinsics can change over time
UpdateCameraIntrinsics();
}
private void UpdateCameraIntrinsics() {
if (!_cameraManager.TryGetIntrinsics(out _intrinsics)) {
Debug.Log("Failed to acquire camera intrinsics.");
}
}
private unsafe void UpdateCameraTexture(XRCpuImage image, bool convertYuvManually) {
var format = TextureFormat.RGBA32;
if (_cameraTexture == null || _cameraTexture.width != image.width || _cameraTexture.height != image.height) {
_cameraTexture = new Texture2D(image.width, image.height, format, false);
}
var conversionParams = new XRCpuImage.ConversionParams(image, format);
var rawTextureData = _cameraTexture.GetRawTextureData<byte>();
var rawTexturePtr = new IntPtr(rawTextureData.GetUnsafePtr());
if (convertYuvManually) {
ConvertYuvImageIntoBuffer(image, rawTexturePtr, format);
} else {
try {
image.Convert(conversionParams, new IntPtr(rawTextureData.GetUnsafePtr()), rawTextureData.Length);
}
finally {
image.Dispose();
}
}
_cameraTexture.Apply();
CameraRawImage.texture = _cameraTexture;
}
private void ConvertYuvImageIntoBuffer(XRCpuImage image, IntPtr targetBuffer, TextureFormat format) {
var bufferSize = image.height * image.width * 4;
var rgbBuffer = new byte[bufferSize];
// Populate buffer with YUV => RGB conversion
var yPlane = image.GetPlane(0);
var uvPlane = image.GetPlane(1);
// Known offset values of test devices.
// AR Foundation's XRCpuImage.Plane does not expose buffer offsets or image offsets
var planeOffsetY = 0;
var planeOffsetUV = 0;
var imageOffsetY = 0;
var imageOffsetX = 0;
var downsamplingStride = (int)_cameraManagerConfig.DownsamplingStride;
for (int row = 0; row < image.height; row++) {
for (int col = 0; col < image.width; col++) {
var adjustedRow = row * downsamplingStride;
var adjustedCol = col * downsamplingStride;
byte y = yPlane.data[
planeOffsetY + ((imageOffsetY + adjustedRow) * yPlane.rowStride) + imageOffsetX + adjustedCol];
var rowOffset = (imageOffsetY + adjustedRow) / 2 * (uvPlane.rowStride);
var colOffset = (imageOffsetX + adjustedCol) / 2 * 2;
var offset = planeOffsetUV + rowOffset + colOffset;
// YUV NV12 to RGB conversion (A3 wrongly reports NV21, no need to swap U & V)
// https://en.wikipedia.org/wiki/YUV#Y%E2%80%B2UV420sp_(NV21)_to_RGB_conversion_(Android)
sbyte u = (sbyte)(uvPlane.data[offset] - 128);
sbyte v = (sbyte)(uvPlane.data[offset + 1] - 128);
var r = y + (1.370705f * v);
var g = y - (0.698001f * v) - (0.337633f * u);
var b = y + (1.732446f * u);
r = r > 255 ? 255 : r < 0 ? 0 : r;
g = g > 255 ? 255 : g < 0 ? 0 : g;
b = b > 255 ? 255 : b < 0 ? 0 : b;
int pixelIndex = ((image.height - row - 1) * image.width) + col;
switch (format)
{
case TextureFormat.RGB24:
rgbBuffer[4 * pixelIndex] = (byte)r;
rgbBuffer[(4 * pixelIndex) + 1] = (byte)g;
rgbBuffer[(4 * pixelIndex) + 2] = (byte)b;
break;
case TextureFormat.RGBA32:
rgbBuffer[4 * pixelIndex] = (byte)r;
rgbBuffer[(4 * pixelIndex) + 1] = (byte)g;
rgbBuffer[(4 * pixelIndex) + 2] = (byte)b;
rgbBuffer[(4 * pixelIndex) + 3] = 255;
break;
case TextureFormat.BGRA32:
rgbBuffer[4 * pixelIndex] = (byte)b;
rgbBuffer[(4 * pixelIndex) + 1] = (byte)g;
rgbBuffer[(4 * pixelIndex) + 2] = (byte)r;
rgbBuffer[(4 * pixelIndex) + 3] = 255;
break;
}
}
}
Marshal.Copy(rgbBuffer, 0, targetBuffer, bufferSize);
}
private bool FindSupportedConfiguration() {
_cameraConfigs = _cameraManager.GetConfigurations(Allocator.Persistent);
return _cameraConfigs.Length > 0;
}
private bool CheckDeviceSupported() {
// Currently not supporting Lenovo VRX
var type = DeviceConfirmProvider.GetCurrentDeviceType();
return type == XRDeviceType.ThinkRealityA3;
}
private void OnDeviceNotSupported() {
Debug.Log("This feature is not currently supported on this device.");
}
private bool CheckSubsystem() {
return _cameraManager.subsystem?.running ?? false;
}
// Update is called once per frame
private void Update() { }
}
}
また、このRGBアクセスの機能については今回はThink Reality A3のみで利用するためにThink Reality A3上で動作している場合のみこの処理が実行されるようにCheckDeviceSupportedメソッドでデバイスのチェックを行います。デバイスの判断はDeviceConfirmProviderを作成しています。
今回はThink Reality A3上で動いているという判断をスマホのモデル名で判断しています。少し安易な方法なので厳密にThink Reality A3で動いているを判断するためにはもう少し別の手段を講じた方がいいかもしれないです。
// Copyright (c) 2023 Takahiro Miyaura
// Released under the MIT license
// http://opensource.org/licenses/mit-license.php
using UnityEngine;
namespace Reseul.Snapdragon.Spaces.Devices{
public enum XRDeviceType {
Unknown,
ThinkRealityA3,
ThinkRealityVRX
}
public class DeviceConfirmProvider {
public static XRDeviceType GetCurrentDeviceType() {
var modelName = SystemInfo.deviceModel.ToLower();
if (modelName.Contains("vrx")) {
return XRDeviceType.ThinkRealityVRX;
}
else if (modelName.Contains("motorola")) {
//TODO: ThinkRealityA3のモデル名がわからないので、とりあえずMotorolaの文字列が入っていたらThinkRealityA3として扱う
return XRDeviceType.ThinkRealityA3;
} else {
return XRDeviceType.Unknown;
}
}
}
}
最後にこの機能を利用するためにコンポーネントを登録します。任意のGameObjectに新しいスクリプト[CameraFrameAccess]を追加します。CameraFrameAccessのプロパティにあるCameraRawImageに先ほど作成したRawImageコンポーネントを割り当てます。
4. Unity内のARグラス視点のオブジェクト投影用のCameraとRender Textureの追加
次に、コンテンツと先ほど作成したRGBカメラ映像が描画されるRaw Imageを合成しスマホに描画するための実装を行います。合成には仮想カメラを用意します。仮想カメラはARグラスのMainCameraと同じ視点でデジタルコンテンツを描画し、最背面に先ほどのRaw Imageを配置することで疑似的に現実空間にデジタルコンテンツが表示される映像を作成します。
任意の場所に空のGameObjectを作成しCameraコンポーネントを追加します。先ほどのCameraコンポーネントと同様タグ名はMainCamera以外に設定します。この仮想カメラの映像はRaw Imageで書きだすためにTarget TexureにRender Textureを割り当てておきます。Render Textureは新規に作成してください。
また、この仮想カメラをARグラスの視点に合わせるためにTransformを同期するスクリプトを作成し追従させます。
// Copyright (c) 2023 Takahiro Miyaura
// Released under the MIT license
// http://opensource.org/licenses/mit-license.php
using UnityEngine;
namespace Reseul.Snapdragon.Spaces.CameraFrameAccesses{
public class TransformFollower : MonoBehaviour {
public Transform transformToFollow;
// Update is called once per frame
void Update() {
if (transformToFollow != null) {
transform.position = transformToFollow.position;
transform.rotation = transformToFollow.rotation;
}
}
}
}
5. Render Textureを設定したRaw Imageの作成
次に先ほどの仮想カメラのRender Textureを書きだすためのRaw Imageを作成します。
任意の場所に空のCanvasを作成し、その下にRaw Imageを生成します。今回は以下のように作成しました。
- [CameraFrameDisplay] → Canvasコンポーネントを含むGameObject
- [ARCamera] → Render Textureを設定したRaw Image。[CameraFrameDisplay]の子オブジェクトとして追加
Raw Imageについてはサイズを1280x720、Textureに先ほどのCameraに設定しているRender Textureを割り当てます。
ここまで実装できれば、ARCameraのRaw ImageにはRGBカメラとコンテンツが合成された映像を描画されます。
6. 4で作成したRaw Imageをスマホ用カメラのみに描画
最後に、5の手順で作成した [CameraFrameDisplay]をスマホ画面のみ表示するように設定します。これはレイヤーを利用します。
CameraFrameDisplayに任意のレイヤー(PhoneUI)を作成し設定しておきます。
あと、最初に作ったスマホ画面用のカメラで表示するレイヤーをCameraFrameDisplayに設定したレイヤーのみ描画するようにします。
逆にARグラスのCameraにはこのレイヤーを描画しないように設定してください。
以上で実装は完了です。最後にXR Plug-in Managementの設定でSnapdragon Spacesのいくつかの機能を有効化して完了です。
7. XR Plug-in Managementの設定でDual Render FusionとRGBカメラへのアクセス機能を有効化する
今回はSnapdragon SpacesのDual Render FusionとCameraFrameAccessの機能を利用しているのでXR Plug-in Managementで該当機能を有効化します。これはSnapdragon Spacesの以下の機能にチェックします。
- Camera Access (Experimental)
- Dual Render Fusion (Experimental)
ThinkReality A3へのデプロイ
ThinkReality A3にデプロイするために設定は次の通りです。
- Build Settingsの変更
- MRTK3の設定
- XR Plugin Management(Open XR) の設定
- デプロイ
1. Build Settingsの変更
Unity Editorのメニューから[File]-[Build Settings]を選択しBuild Settingsパネルを表示します。ThinkReality A3向けのアプリはAndroidを選択する必要があるので、Platformから[Android]を選択し、[Switch Platform]を押下します。
2. MRTK3の設定
次にAndroid用のMRTK3の設定を行います。Unityのメニューから[Edit]-[Project Settings]を選択しプロジェクト設定パネルを表示します。
左のメニューから「MRTK3」を選択します。右側にMRTK3のプロファイル設定についての項目が表示されます。プラットフォームごとのタブが表示されているので「Androidアイコン」のタブが選択されていることを確認してMRTK3のプロファイルを設定してください。
プロファイル設定については今回はデフォルトの設定で特に問題がないためパッケージに入っているデフォルトの[MRTKProfile]を割り当てます。
3. XR Plugin Management(Open XR) の設定
次にThinkReality A3用にXR系の設定を行っていきます。引続き[Project Settings]の中から[XR Plug-in Management]を選択します。プラットフォームごとのタブが表示されているので「Androidアイコン」のタブが選択されていることを確認したうえで以下の項目にチェックが入れてください。
- Initialize XR on Startup
- OpenXR
- Snapdragon Spaces feature group
上記の設定を行った後[Project Validation]の項目をチェックするといくつかの警告が表示されます。右上の[Fix All]を何回か押すことでSnapdragon Spaces向けにプロジェクトの設定が是正されます。
警告をすべて消すためにはいくつか手動パッケージの導入等必要になります。具体的にはデバイス毎のモーションコントローラのモデル表示関する設定です。また、今回はハンドトラッキングを利用するためモーションコントローラ周りの設定で警告が出ます。これは公式でも現状は警告がでてる記載があるので問題ありません。
次に[OpenXR]で必要な設定を行います。[XR Plug-in Management]-[OpenXR]を選択します。以下の設定が行われているかを確認してください。
- Interaction Profileに何も設定しない
- OpenXR Feature Groupsの以下をチェック
- Snapdragon Spaces
- Camera Access (Experimental)
- Dual Render Fusion (Experimental)
- Hand Tracking
- Motion Controller Model
- Spatial Meshing (Experimental)
Snapdragon Spacesではスマホをコントローラとして扱うことも可能です。これについては本記事の最後にSnapdragon Spacesの公式ドキュメントで記載があるMRTK3関連の情報も参照してください。
4. デプロイ
デプロイについてはAndroidアプリのそれと変わりありません。Think Reality A3のスマートフォンを開発者モードでPCにUSB接続しておけばUnityから直接デプロイすることも可能です。もしくはAPKをスマートフォンにコピーしてAPKを直接インストールします。
参考サイト