Microsoft Build 2024に合わせてMicrosoft MeshのCustom Immersive SpacesのサンプルMesh 201がパワーアップ
先日のMicrosoft Build 2024視聴しましたか?今回のカンファレンスでは、特にAI周りの話が多かったと思います。興味がある方は是非キーノートだけでも見ておくことをお勧めします。
XR関係でいうと、久々にキーノートでMixed Realityのキーワードが出てきていました。「Windows Volumetric Apps on Meta Quest」という、Quest上でWindowsアプリを利用する際に必要に応じて3Dコンテンツをウィンドウから出力して利用できるもののようです。キーノートの中ではXBoxコントローラの設計図から、空間に立体物として3Dモデルを描画するというデモ動画が流れてしました。
さて、そんなMicrosoft Buildなのですが、このカンファレンスに合わせてMicrosoft MeshのサンプルにもOpenAI Serviceを使ったものが登場しました。この記事では実際にサンプルを動かすまでの公式ドキュメントの手順の補足と、Microsoft MeshのCloud Scriptingでの実装について紹介したいと思います。
サンプルであるMesh201については以下にチュートリアルが整理されています。
目次
- 必要な環境と準備
- Meshの開発環境
- Cloud Scriptingに必要な準備
- Mesh 201に必要な各種サービスの準備
- ソースコードの取得
- 実行とデプロイ
- Cloud Scriptingの実装部分について
必要な環境と準備
Mesh201はMicrosoft MeshのCustom Immersive Spacesを構築する際に[Web Slate]という空間内でWebサイトを開くための部品とCloud Scriptingを使った外部サービス連携を実装する際のサンプルにもなっています。
必要な環境としては色々あるのですが、実際にCustom Immersive Spacesを没入体験する必要がないのであれば、Unity上で動作させることができます。
Meshの開発環境
Microsoft MeshのCustom Immersive Spacesの開発には以下のUnityバージョンが必要です。現状はこのバージョン固定となっているようです。また、現在Microsoft MeshはPCとMeta Questに対応したアプリがあるためモジュールもAndroidとWindowsのものが必要になります。
- Unity 2022.3.15f1
- Android
- Windows Build Support(IL2CPP)
Cloud Scriptingに必要な準備
Cloud ScriptingはMicrosoft MeshのCustom Immersive Spacesで利用できる機能の1つです。Microsoft MeshはそのワールドでのインタラクションにUnityのVisual Scriptingをベースとしたノードベース開発が基本です。
ただし、他サービス連携や独自実装が必要なシーンにおいてはCloud側にカスタムコードを展開するCloud Scriptingを利用することが可能です。これはC#実装することができます。
Cloud Scriptingを利用する場合は、クライアント環境に追加で以下のモジュールをインストールしてください。
-
Azure CLI
- Cloud Scriptingを利用するCustom Immersive SpacesをMicrosoft Meshポータルへアップロードする際に必要
-
.NET7 SDK
- Unity Play ModeでCloud Scriptingのコードを実行するローカルサーバ稼働のために必要
Cloud Scriptingを利用するCustom Immersive SpacesをアップロードするとAzure CLIを利用して自動的にAzure App Serviceなど必要なAzureリソースがデプロイされます。この際にAzureのリソースグループやサブスクリプションが必要になります。利用できるリージョンなど含めた詳細な情報は公式ドキュメントで紹介されています。
(補足)Cloud Scriptingについて
Cloud ScriptingはAzure上で動作するWeb APIとして実行されます。Custom Immersive Spacesから呼び出され参加者との同期(必要な場合)を含めてC#で実装した処理をワールド内で実行します。情報の同期や処理の呼出しについては全てMesh側でするため特に開発者が何か気にする必要はありません。
Web APIとして動作する機能のため、Cloud Scripting上のコードからUnityの機能を利用することができません。例えば、スクリプトで3Dオブジェクトを移動する実装を行う場合、monobehaviourのtransformプロパティを利用しますが、直接的にこのような実装すら行うことが出来ません。
このためある程度Unityとの実装を合わせられるようにNodeクラスが提供されています。Cloud Scriptingの実行時はUnityのHierarchy構造と同じ構成をとったNodeクラス構造が生成されます。Cloud ScriptingではこのNodeクラスを介してUnityのHierarchy上のオブジェクトにアクセスします。
![]() 「クラウド スクリプトの基本的な概念 - コンポーネントとメッシュ クラウド スクリプト シーン グラフ」より参照 |
---|
上記の図ではParticleSystem(B)のようなNodeが用意されていない場合の例として書かれています。こういったCloud Scripting側からアクセスできないUnityオブジェクトも存在しますが、実行時にはUnityの階層構造に従ってオブジェクトが管理されているためParticleSystemが消えたりすることはありません。
実装時の考え方としては、.NetのWebAPI実装の考え方と一緒です。IHostedServiceインタフェースを実装します。コンストラクタで初期処理を実装し、StartAsync内でNodeクラスを利用し、Custom Immersive Spaces内の処理に関する実装を行います。例えば、「Node内のボタンオブジェクトにアクセスし、Selectedイベントで文字を表示する」といった実装を行います。
今回のMesh201のサンプルでは、WeatherAPIから3地点の天気情報を取得する処理と、OpenAI Serviceを利用したMesh201のサンプル内でのQA対応が実装されています。
Cloud Scriptingを利用したい場合は空のGameObjectを作成し、[Mesh Cloud Scripting]コンポーネントを追加します。このコンポーネント配下にあるGameObjectは階層構造に対応したNodeクラスが生成され、実装時に利用することが可能になります。
[Mesh Cloud Scripting]コンポーネントはシーンに1つしか配置できません。また、このコンポーネントが配置されたシーンをMeshポータルにアップロードすると自動的にAzureに必要なリソースが生成されるので注意してください(一度作られたAzureサービス群は自分で削除しない限りはリソースとして残ります。)。
Mesh 201に必要な各種サービスの準備
Mesh201は、Cloud Scriptingを利用して2つのサービスを利用します。WeatherAPIはGPS座標の天気を返すWebAPIとして利用します。最小限の情報しかとっていないため無料プランで問題ありません。サインアップしてAPIKeyを取得します。Open AI ServiceについてはAzure上でOpen AI Serviceを作成しLLMをデプロイする必要があります。
- WeatherAPI(https://www.weatherapi.com/)
- アカウント作成後、[サービスURI]と[APIKey]を控えておく
- Azure OpenAI Service
- デプロイ後に、[サービスURI]と[Key]を控えておく
Azure OpenAI ServiceでMesh201用に[gpt-35-turbo]のモデルをデプロイしておきます。詳細な手順は公式ドキュメントに公開されていますが注意点としてはデプロイ名も[gpt-35-turbo]にしておくことを忘れないでください。Mesh201の完成コード内で使用するデプロイ名としてこの名前が使用されています。間違った名前で作った場合は再作成するか、コードの方を修正します。
ソースコードの取得
Microsoft MeshにおけるCustom Immersive SpacesのサンプルはMesh Toolkitのgithubリポジトリに含まれています。以下のGithubからコードを取得します。
Mesh201のサンプルについては[Mesh-Toolkit-Unity/Mesh201]に含まれています。
実行とデプロイ
もし各機能を実装しながら学習したい場合は以下のチュートリアルの手順に従って進めることもできます。
- 第 1 章: 概要、セットアップ、作業の開始。
- 第 2 章: ローカルの非共有 HTML ファイルを WebSlate に読み込む。
- 第 3 章: ローカル共有 HTML ファイルを WebSlate に読み込む。
- 第 4 章: 3D アセットから URL を読み込む。
- 第 5 章: インタラクティブな地球儀をクリックして、ライブ気象データを取得する
- 第 6 章: Azure OpenAI を使用して質問に対する回答を取得する
今回はとりあえず完成後のプロジェクトをUnity上で実行して動作を確認します。
まず、Unityで先ほどダウンロードしたGithubプロジェクトのMesh201フォルダを開きます。
プロジェクトを開いて、[Assets/FinishedProject]シーンを開きます。
次に、[Mesh Cloud Scipting]コンポーネントを選択し、[Inspector]パネルから[Open application folder]ボタンを押すと、Explorerが開きます。(Cloud ScriptingのコードはUnityの実装コードとは別に管理されています)
ExplorerはCloud Scripting用のコードが格納された場所を表示しています。
フォルダの中から[appsettings.UnityLocalDev.json]を探しソースを開きます。ファイルの最下行の方に以下の4行があるので、先ほどの手順で控えておいたWeatherAPIとOpenAI Servicesの接続に関する情報を入力します。
...
"Microsoft.Mesh.CloudScripting": "Information"
}
}
},
"WEATHER_API_URI": "<WEATHER_API_URI>",
"WEATHER_API_KEY": "<WEATHER_API_KEY>",
"AZURE_OPENAI_API_URI": "<AZURE_OPENAI_API_URI>",
"AZURE_OPENAI_API_KEY": "<AZURE_OPENAI_API_KEY>"
}
最後にUnity上でPlay Mode実行します。Cloud Scriptingを利用する場合、ローカルでWebAPI実行するためのプロセスが起動します。プロセスの起動はマシンスペックにもよりますが時間がかかるかもしれません。当然ですが、サービスが立ち上がる前に操作をするとエラーが表示されるので注意してください。
また、サービスが立ち上がってこない場合は.NET7 SDKがインストールされていない場合が考えられるので一度確認してください。
クライアント用のプロセスが稼働しているかは[Mesh Cloud Scipting]コンポーネントで確認できます。[Scripting Server Information]-[running]がtrueであれば問題なく動作します。
スタート地点から180度反転して前進すると、WeatherAPIとAzure OpenAI Serviceのサンプルを体験することができます。地球のモデルをクリックすると3地点の天気情報が表示されます。
次に、丸い[i]ボタンをクリックすると半透明のダイアログが表示されます。例えば、入力テキストに「What am I looking at?」と入力しOKボタンを押下すると、回答が返ってきます。
まだ何かちょっとバグってみるみたいでダイアログが崩れてしまいますが結果は返ってきます。
内容以下の通りです。
You asked: What am I looking at ? Response: You are looking at a globe with the current weather data displayed for three different locations: Lagos, Nigeria; Redmond, Washington, USA; and Dublin, Ireland. The current weather conditions for each of these locations are described in the text provided. |
---|
質問:私は何を見ているのですか? 回答:あなたは3つの異なる場所の現在の気象データが表示された地球儀を見ています: ナイジェリアのラゴス、アメリカのワシントン州レドモンド、アイルランドのダブリンです。それぞれの場所の現在の天候は、提供されたテキストに記載されています。 |
このような感じで様々な外部サービスと連携することが可能です。
Cloud Scriptingの実装部分について
最後にCloud Scriptingの実装内容を確認します。Cloud ScriptingはAzure上でWebサービスとして構成され、メインとなるクラス[App]はIHostedServiceインタフェースを実装しています。基本的な実装は以下の通りです
- Appクラスのコンストラクタで初期処理を実施する
- StartAsyncメソッドでMeshに関する実装を行う
- StopAsuncメソッドで後処理を実装する
コンストラクタ
コンストラクタは初期化処理を行います。コンストラクタの中は3つの引数を持ちます。
変数名 | 概要 |
---|---|
app | ICloudAppicationインターフェースを実装したオブジェクト。このオブジェクトはMesh内での実装に必要な機能を提供します。例えば、シーン内のNodeオブジェクト取得等。 |
Configuration | IConfigurationインターフェースを実装したオブジェクト。外部設定パラメータの情報を取得することができます。 |
logger | デバッグログ等ログ情報出力に必要なオブジェクトです。 |
Mesh201ではWeatherAPIとAzure OpenAI Serviceへの接続に関する情報を外部パラメータで定義しているため、コンストラクタでその情報をフィールド変数に格納しています。また、app,loggerオブジェクトはのちのStartAsyncメソッドで利用する為、フィールド変数に格納します。
public App(ICloudApplication app, IConfiguration configuration, ILogger<App> logger)
{
_app = app;
_logger = logger;
_weatherAPIBaseUrl = configuration.GetValue<string>("WEATHER_API_URI");
_weatherAPIKey = configuration.GetValue<string>("WEATHER_API_KEY");
Uri azureOpenAIResourceUri = new(configuration.GetValue<string>("AZURE_OPENAI_API_URI"));
AzureKeyCredential azureOpenAIApiKey = new(configuration.GetValue<string>("AZURE_OPENAI_API_KEY"));
_openAIClient = new(azureOpenAIResourceUri, azureOpenAIApiKey);
}
StartAsync
開始時に呼び出される処理です。この処理はユーザ毎に実施されます。
Mesh201では以下の処理を実施するために情報を収集します。
- 地球儀のオブジェクトを取得し選択すると天気情報取得のための処理(GetCurrentWeatherメソッド)を呼び出す
- AIAssistantオブジェクトを取得しChatGPTを使った応対を実行する
/// <inheritdoc/>
public Task StartAsync(CancellationToken token)
{
// When a user selects the globe, refresh the current weather
var refreshButton = _app.Scene.FindFirstChild("Earth", true) as TransformNode
?? throw new NullReferenceException("Could not find Earth");
var refreshButtonNode = refreshButton.FindFirstChild<InteractableNode>(true);
if (refreshButtonNode != null)
{
refreshButtonNode.Selected += async (_, _) =>
{
await GetCurrentWeather(_latlong);
};
}
// When a user selects the information button begin a conversation with a LLM
var aiParentNode = _app.Scene.FindFirstChild("5 - AIAssistant", true) as TransformNode
?? throw new NullReferenceException("Could not find infoButtonParent");
var infoButton = aiParentNode.FindFirstChild<InteractableNode>(true);
if (infoButton != null)
{
infoButton.Selected += async (sender, args) =>
{
// Ensure we have weather data before begining the conversation
await GetCurrentWeather(_latlong);
// Display an input dialog for the user to send a message to the LLM
await _app.ShowInputDialogToParticipantAsync("Ask Azure OpenAI", args.Participant).ContinueWith(async (response) =>
{
try
{
if (response.Exception != null)
{
throw response.Exception.InnerException ?? response.Exception;
}
string participantInput = response.Result;
var chatCompletionsOptions = new ChatCompletionsOptions()
{
DeploymentName = "gpt-35-turbo", // Use DeploymentName for "model" with non-Azure clients
Messages =
{
// The system message represents instructions or other guidance about how the assistant should behave
new ChatRequestSystemMessage(
"You are a helpful assistant." +
"You're part of a developer sample for the Mesh Toolkit." +
"Use brief answers, less than 1 paragraph." +
"You can suggest a good location for a wind farm based on current and historical weather data." +
"We're looking at globe with the current weather data displayed for each of these locations: Lagos Nigeria, Redmond WA USA, Dublin Ireland" +
"Current weather conditions for these locations:" + _currentWeatherText
),
new ChatRequestUserMessage(participantInput),
}
};
// Wait for a response from OpenAI based on the user's message
var aiResponse = await _openAIClient.GetChatCompletionsAsync(chatCompletionsOptions);
// Display the first response from the LLM
var responseMessage = aiResponse.Value.Choices[0].Message;
_app.ShowMessageToParticipant($"<i>You asked: {participantInput}</i>\n\nResponse: {responseMessage.Content}", args.Participant);
}
catch (Exception ex)
{
_logger.LogCritical($"Exception during OpenAI request: {ex.Message}");
}
}, TaskScheduler.Default);
};
}
// When a user selects the reset button, reset the weather markers
var resetButton = _app.Scene.FindFirstChild("ResetWeatherButton", true) as TransformNode
?? throw new NullReferenceException("Could not find ResetWeatherButton");
var resetButtonNode = resetButton.FindFirstChild<InteractableNode>(true);
if (resetButtonNode != null)
{
resetButtonNode.Selected += (_, _) =>
{
var weatherMarkersGameObject = _app.Scene.FindFirstChild("WeatherMarkers", true) as TransformNode
?? throw new NullReferenceException("Could not find WeatherMarkers");
// Hide all the weather markers
foreach (var child in weatherMarkersGameObject.Children)
{
child.IsActive = false;
}
};
}
return Task.CompletedTask;
}
以下は今回のサンプルの中で実装されているMesh特有の実装とAzure OpenAI Serviceの実装部分を紹介します。
Custom Immersive Spaces内のオブジェクト(=UnityのHierarchy構造で管理されるオブジェクト)へのアクセス
先に説明した通り、Cloud Scriptingはクラウドのサービスとして動作します。このためCloud ScriptingでCustom Immersive Spaces内のオブジェクト(=UnityのHierarchy構造で管理されるオブジェクト)をMonobehaviourのように扱うことが出来ません。アクセスにはコンストラクタの引数であるappオブジェクトを利用します。
たとえば、Unity上の[MeshCloudScripting/4 - GlobeWithCloudScripting/Earth]オブジェクトにアクセスするためには以下のように実装することで対応するNodeオブジェクトを取得することが出来ます。
var refreshButton = _app.Scene.FindFirstChild("Earth", true) as TransformNode
?? throw new NullReferenceException("Could not find Earth");
Custom Immersive Spaces内のオブジェクトのクリックイベントを登録する
今回のMesh201のサンプルでは地球儀をクリックするとWeatherAPIから情報を取得する処理が実行されます。Mesh ScriptingではクリックイベントはIntaractableNodeクラスを介して操作します。このクラスは、Unity上は[Mesh Interactable Body]コンポーネントが含まれるオブジェクトと対になっています。
今回のサンプルコードでは以下の部分でInteractableNodeオブジェクトを取得しクリックイベントを登録しています。
//Earthという名前のオブジェクトを取得
var refreshButton = _app.Scene.FindFirstChild("Earth", true) as TransformNode
?? throw new NullReferenceException("Could not find Earth");
//Earthオブジェクトは以下のMesh Interactable Bodyをもつコンポーネントを取得
var refreshButtonNode = refreshButton.FindFirstChild<InteractableNode>(true);
if (refreshButtonNode != null)
{
//対象のオブジェクトのクリック時に行う処理を登録
refreshButtonNode.Selected += async (_, _) =>
{
await GetCurrentWeather(_latlong);
};
}
ダイアログやメッセージを表示する
Cloud Scripting内で利用できるappオブジェクトは、Mesh内のコンポーネントのアクセス以外にもいくつか機能を持っています。その1つがダイアログ表示で、Azure OpenAI Serviceでその機能を見ることができます。
//入力ダイアログを表示する(処理待ちのために非同期メソッドとなっている)
_app.ShowInputDialogToParticipantAsync("Ask Azure OpenAI", args.Participant);
//メッセージを表示する
_app.ShowMessageToParticipant("Message", args.participant);
ShowInputDialogToParticipantAsyncメソッドを利用すると以下のような入力ダイアログが表示されます。これによりユーザの入力情報に応じて処理を実行することが可能です。
引数にはメッセージと、表示対象の参加者の情報を指定します。Mesh201のサンプルではクリックした人にダイアログ表示を行うように実装されています。Mesh内ではクリック操作などはワールド内で発生したイベントとして処理されれおり、イベント発生時は[だれが操作したか]を調べることが出来ます。操作した人と表示対象の人を合わせることで今回のような問い合わせダイアログを表示させています。
その他appオブジェクトが持つ機能
上記以外にもMesh内の処理を実装するためにappオブジェクトには便利な機能が含まれています。
プロパティ/メソッド | 概要 |
---|---|
app.Create(name,parent) | 新しくNodeを追加します。引数にはオブジェクト名、および親のNodeを指定することが出来ます。 |
app.ShowMessageToParticipant(message, participant); | 指定の参加者に対してメッセージを表示します。 |
app.ShowMessageToParticipants(message, participants); | 指定の参加者(複数)に対してメッセージを表示します。 |
app.ShowInputDialogToParticipantAsync(message, participant, token) | 入力ダイアログを表示します。非同期メソッドのため、ContinueWithメソッド等を利用することで入力後に処理を実行するなども可能です |
app.Scene | CloudScriptingのルートノードを取得します。ルートノードはUnity上で定義した[Mesh Cloud Scripting]コンポーネントがルート要素になります。また、[Mesh Cloud Scripting]コンポーネントの[App Root override]でルートを変更することも可能です。 |
Azure OpenAI Serviceの実装
Azure OpenAI Service部分の実装は特にMesh特有というわけではないのですが、紹介ます。
最初にAzure OpenAI Serviceへの接続情報と認証情報を利用してOpen AI Service用のOpenAIClientオブジェクトを生成します。
Uri azureOpenAIResourceUri = new(configuration.GetValue<string>("AZURE_OPENAI_API_URI"));
AzureKeyCredential azureOpenAIApiKey = new(configuration.GetValue<string>("AZURE_OPENAI_API_KEY"));
OpenAIClient _openAIClient = new(azureOpenAIResourceUri, azureOpenAIApiKey);
次にAzure OpenAI Service上にデプロイしたモデル名とプロンプト、問い合わせ内容を送信します。
Mesh201のプロンプトはWeatherAPIで問合せした結果も含めているため、表示されている都市の天気についても回答可能となっています。以下はMesh201のコードの抜粋で_currentWeatherTextフィールドの情報はWeatherAPIからのレスポンスになっています。
var chatCompletionsOptions = new ChatCompletionsOptions()
{
DeploymentName = "gpt-35-turbo", // Use DeploymentName for "model" with non-Azure clients
Messages =
{
// The system message represents instructions or other guidance about how the assistant should behave
new ChatRequestSystemMessage(
"You are a helpful assistant." +
"You're part of a developer sample for the Mesh Toolkit." +
"Use brief answers, less than 1 paragraph." +
"You can suggest a good location for a wind farm based on current and historical weather data." +
"We're looking at globe with the current weather data displayed for each of these locations: Lagos Nigeria, Redmond WA USA, Dublin Ireland" +
"Current weather conditions for these locations:" + _currentWeatherText
),
new ChatRequestUserMessage(participantInput),
}
};
// Wait for a response from OpenAI based on the user's message
var aiResponse = await _openAIClient.GetChatCompletionsAsync(chatCompletionsOptions);
// Display the first response from the LLM
var responseMessage = aiResponse.Value.Choices[0].Message;
_app.ShowMessageToParticipant($"<i>You asked: {participantInput}</i>\n\nResponse: {responseMessage.Content}", args.Participant);
このような形で、Mesh空間内に描画したりイベントを制御するといった部分を除けば一般的なC#でのWebAPI開発とそう違いはありません。
StopAsync
サービスと止める時に実行する処理です。リソースの後始末などを実装します。Mesh201のサンプルでは特に後処理としては不要な実装はありません。
/// <inheritdoc/>
public Task StopAsync(CancellationToken token)
{
return Task.CompletedTask;
}
まとめ
今回はMicrosoft Build 2024にあわせて(?)エンハンスされたMicrosoft MeshのチュートリアルMesh201について紹介しました。Cloud Scriptingを利用し外部サービスと連携する簡単なサンプルとなっているのでより高度なワールドを作りたい場合の参考になると思います。
また、Azure OpenAI Serviceで応答する実装も入っています。サンプルの実装を拡張して空間内での行動に基づいたプロンプトを作成できると、かなり面白いAIアシスタントになるかもしれないので興味のある方は是非チャレンジしてみてください。