Open18

Microsoft Mesh関連の話。色々情報飛び交うから備忘録。

miyauramiyaura

ライセンス周り

Immersive Spacesについては以下のいずれかのライセンスが必要

  • Teams Essentials
  • Microsoft 365 Business Basic
  • Microsoft 365 Business Standard
  • Microsoft 365 Business Premium
  • Microsoft 365 E3/E5
  • Office 365 E1/E3/E5

Meshを利用する場合は、Public Prebview期間中はTeams Premiumが別途必要。Questなどで使うアカウントは必要になると思われる。

そのほかにカスタムワールドを作る際のスクリプト使う場合はリソース格納場所にAzureを使うのでサブスクリプションが必要になる。

カスタムワールドはUnityで開発することになる。多分、Teamsのワールド作成はゲーム用途ではないため会社収益によってはUnity ProではなくUnity Industoryが必要になるかもしれない?

https://learn.microsoft.com/en-us/mesh/setup/content/preparing-your-organization?wt.mc_id=WDIT-MVP-5003104#verify-your-licensing

miyauramiyaura

正しいライセンスとpublic previewを有効にすると、Meshは利用可能になる。
ただ、現時点(2023/10/10)ではTeams上のImmersive Spacesは、設定は可能だが利用できない(と色々な人から聞く)。なんか要件が足りない?

miyauramiyaura

開発環境についての話

Unityのバージョンは 2022.3.7f1~。テンプレートはURP 3Dなので。
このほかにUnityの中級から上級スキルが必要(と公式に書いている)

https://learn.microsoft.com/en-us/mesh/develop/getting-started/prerequisites?wt.mc_id=WDIT-MVP-5003104

カスタム空間を作るの際の理解を助けるサンプル

開発を手順を理解するためにはいくつかチュートリアルやサンプルが提供されている。

  • チュートリアルMesh101
  • サンプル

https://learn.microsoft.com/en-us/mesh/develop/getting-started/choose-your-journey?wt.mc_id=WDIT-MVP-5003104

サンプルなどは以下のgithub

https://github.com/microsoft/Mesh-Toolkit-Unity

Mesh Toolkitをインポートするための設定

Mesh Toolkitはパッケージマネージャにスコープの追加が必要
https://learn.microsoft.com/en-us/mesh/develop/build-your-basic-environment/add-the-mesh-toolkit-package?wt.mc_id=WDIT-MVP-5003104

Unityプロジェクトやシーンの作り方

素のMesh用プロジェクトの作り方はこっち
https://learn.microsoft.com/en-us/mesh/develop/build-your-basic-environment/create-a-new-project-or-update?wt.mc_id=WDIT-MVP-5003104

シーンの設定について
https://learn.microsoft.com/en-us/mesh/develop/build-your-basic-environment/set-up-your-scene?wt.mc_id=WDIT-MVP-5003104

マルチプラットフォーム環境で考慮すべきこと

主にパフォーマンスの話でPCだけなら高解像でもいいけど、そうでないなら低解像度使ってね。
https://learn.microsoft.com/en-us/mesh/develop/build-your-basic-environment/build-for-single-and-multiple-platforms?wt.mc_id=WDIT-MVP-5003104

なお、空間設計におけるパフォーマンス上限は以下にまとめられてる。

https://learn.microsoft.com/en-us/mesh/develop/debug-and-optimize-performance/performance-guidelines#performance-optimization?wt.mc_id=WDIT-MVP-5003104

miyauramiyaura

実際やってみる。以下の手順でやってみる。あ、これゼロベースで空間作る時の手順。。。サンプル使う時はまた別ですね。

https://learn.microsoft.com/en-us/mesh/develop/build-your-basic-environment/create-a-new-project-or-update?wt.mc_id=WDIT-MVP-5003104

とりあえずUnity 2022.3.7f1を入れてからスタート。
最初にURP 3Dでプロジェクトつくる。

開いたら、Mesh Toolkitを入れるためにPackage ManagerのScoped Registriesに設定を追加する。
情報は以下のサイトに。
https://learn.microsoft.com/en-us/mesh/develop/build-your-basic-environment/add-the-mesh-toolkit-package?wt.mc_id=WDIT-MVP-5003104

設定後にPackage Managerを開いて「My registries」を選択するとMesh Toolkitが出てくるので、インストール

するとリスタートする?と聞かれる

Meshのプロジェクト設定する?と聞かれる。設定する方を選択するとEditorが再起動する。

起動後Mesh Toolkitがメニューに追加される。

Project ValidationにMRTK3が出てきてる。。。設定にもMRTK3出てるけど、MRTK3使ってる?

メニューの[Mesh Toolkit]-[Environment]を開くとこういう画面が出てくる。
EnvironmentはMeshでは事前に構築している空間のことを指してます。
Microsoft MeshのポータルでログインしたAADでログインすると所属するMeshワールドのEnvironmentにアクセスできる。ワールド作ったらこのパネルからEnvironmentをアップロードする。

Hierarchyの追加メニュー。Mesh Toolkitというグループが追加されていてここにいくつか部品がある。

miyauramiyaura

次はMesh101というチュートリアルをやってみる。手順はここに。

公式にはMesh101について

The Mesh 101 tutorial is a great way to learn about adding Mesh features to a Unity project to create an interactive learning experience.

(機械翻訳)
Mesh 101 チュートリアルは、Unity プロジェクトに Mesh 機能を追加してインタラクティブな学習体験を作成する方法を学ぶのに最適なチュートリアルです。

と書かれているので初めてカスタムするときはここからやるほうが理解しやすいかも。

Chapter.1 概要と準備

https://learn.microsoft.com/en-us/mesh/develop/getting-started/mesh-101-tutorial/mesh-101-01-overview-and-setup?wt.mc_id=WDIT-MVP-5003104

説明の中である程度のUnityスキルがあることを前提にしている記述がある。Unity Visual Scriptingの知識は最終的には必要だけど、チュートリアルについては初心者でも大丈夫。MeshにおけるVisual Scriptingも少しある。

必要スペック

4コア CPU, 8GB RAMくらいは必要

Unity

Unity 2022.3.7f1

手順

まずはMeshアプリをダウンロード。PC版はMicrosoft Storeから。

https://apps.microsoft.com/detail/microsoft-mesh/9NLXZJ1FDBD7?hl=en-us&gl=JP

Mesh Toolkitのサンプルをダウンロードする。Githubで公開されている。

https://github.com/microsoft/Mesh-Toolkit-Unity

リポジトリをローカルに持ってくる(クローンやZipで)。サイズは全部で790MB程度。サンプルのワールドのデータが入ってるから大きめかもしれない。Zipの中にMesh101というのが入っててこれがUnityプロジェクト。任意の場所に展開しておく。

展開したらUnityで開く。チュートリアルはMesh Toolkitの設定も既にされているのでこのまま次に進むことができる。

Chapter.2 プロジェクトの準備をする

公式ドキュメントの方は以下。
https://learn.microsoft.com/en-us/mesh/develop/getting-started/mesh-101-tutorial/mesh-101-02-prepare-the-project?wt.mc_id=WDIT-MVP-5003104

最初にStartingPointシーンを開くと、TextMeshProのリソースをインポートしてというパネルが出るのでインポート。

するとワールドが開く。TestMesh ProのTっていうアイコンが見えにくくて邪魔な時はGizmoから非表示にできるというアドバイスも書いてる、めっちゃ親切。

空間はこういう感じ。風車と建造物がある。

建造物の中にチュートリアルコンテンツがあり、これを操作しながらワールドの作り方を理解する形になってる。

一通り、鑑賞したらhierarchyに以下の2つのオブジェクトを追加する。

  • Playmode Setup
  • Thumbnail Camera

Playmode SetupはUnityのPlaymode時にアバターとして機能します。これによってFPSのキーボード操作で空間内を移動、マウスでオブジェクト操作を行うことができ、作成した環境の動作確認を行います。
Thumbnail Cameraは文字通りなのですが、最終的にUnity上で作った空間はEnvironmentとしてMeshワールドのテンプレートとしてアップロードします。その際にサムネイル画像を添付する必要があります。このオブジェクトをシーン内に配置し好きな画角で配置しておくとアップロード時にカメラで撮影した画像をサムネイルとしてアップロードしてくれます。

つづく

miyauramiyaura

Chapter.3 Meshビジュアルスクリプティングによるインタラクティブ性の追加

Meshのワールドはただ空間だけ作って終わりというわけではなく色々アクションを組み込める。このためにUnityのビジュアルスクリプティング使ってる。

公式ドキュメントの方は以下。

https://learn.microsoft.com/en-us/mesh/develop/getting-started/mesh-101-tutorial/mesh-101-03-visual-scripting?wt.mc_id=WDIT-MVP-5003104

3.1.インタラクションボタンの追加

ビデオの再生と停止を行うためのボタンを追加する手順。
Hierarchyから[Chapter3]-[3.1.video]を展開して、ButtonBaseを追加する。

ButtonBaseはProjectパネルの検索窓に[ButtonBase]を入力して検索対象を[In Packages]にするとPrefabが出てくる。これを展開した[3.1.video]の下に追加。

位置調整して風車の映像の下にボタンを配置。追加したButtonBaseの名前を[PlayVideoButton]に変更する。合わせて、PlayVideoButton配下にあるLabelのTextをPlayに変更。

PlayVideoButtonを選択し、Inspectorパネルから[Script Machine]コンポーネントを追加。これを追加すると最下行に[Mesh Visual Scripting Diagnostics]が追加される。

[Script Machine]コンポーネントにSourceをEmbedに変更して、パラメータに以下を入力。

  • VideoPlayerBehavior
  • Video player behavior definition.

Edit Graphを押すと確認ダイアログが出てくるので[Change now]を押す。

Script Graphが出てくる。

ここからVisual Scriptingになる。ビデオの再生/停止については以下の処理をスクリプトにしてる。

  1. オブジェクトの変数としてisPlaying(Boolean)を1つもつ
  2. ボタンが押されたかどうかMesh Interactable BodyコンポーネントのIs Selectedを監視
  3. 2の状態が変更された時isPlayingの値をtrue→false,false→trueと交互に変更
  4. isPlayingの値を監視
  5. 4の状態が変更された場合に、以下の条件分岐による処理を実施
    1. false:ビデオを再生し、ボタンのラベルを"Stop"に変更
    2. true:ビデオを停止し、ボタンのラベルを"Play"に変更

完成図は以下。

Script Graphについては、右クリックまたは、Graph上にある処理の入力または出力の端子をドラッグ&ドロップすることでつなげる処理を選択できる。
例えば、上記のビデオを追加する最初の方の手順としては以下のような形になっている。

開いたScript Graphに[PlayVideoButton]直下の[Button]をドラッグ&ドロップして[Button]を選択すると、Graphの中にButtonのGameObjectが追加される。

検索ボックスに"Is Selected"と入力すると絞り込まれるので、"Mesh Interactable Body: IsSelected"を追加。

動作検証する場合は、Hierarchyに[PlaymodeSetup]の追加する。このオブジェクトはいわゆるアバターとして機能するデバッグ実行用のオブジェクト。PlayModeにして先ほど追加したボタンを押すことで、ビデオが再生したり、止まったりしたら成功。

なお、このボタンを押して再生/停止する動作をこの空間に参加している全員で同期したい場合は、Script Machineに[Local Script Scope]を追加し、[Share visual script variables on this Game Object]にチェックを入れる。こうすることでVisual Scripting内の変数(今回だとisPlaying)がワールド内で共通変数として扱われる。イメージとしてはStaticプロパティ化されると思うとわかりやすいかも。

miyauramiyaura

機能の続き

Station 3.2: 情報ダイアログを表示する

公式は以下のところに。

https://learn.microsoft.com/en-us/mesh/develop/getting-started/mesh-101-tutorial/mesh-101-03-visual-scripting?wt.mc_id=WDIT-MVP-5003104#station-32-trigger-an-info-dialog

いわゆるボタンを押すと出てくるダイアログの部品ですね。
Hierarchyから[3.2 - Info Dialog]-[InfoButtonWrapper]を選択しInspectorで[Local Script Scope]コンポーネントを確認してみると、以下のように[Share visual script variables on this Game Object]のチェックが外れている。これはこのオブジェクトの変数が参加者毎に管理されます。参加者おのおのの操作による状態変更が可能になるというものです。チェックを入れると空間で共有の状態変更になる。

次に[InfoButtonWrapper]の[Script Machine]コンポーネントの[Edit Graph]を押下して編集パネルを開きます。今回は3.1の最初に行った「ボタンが押されたら処理を実行する」という部分の実装までが行われています。ここから作業をスタートする感じですね。

ボタンがおされたらダイアログを表示する処理を作るので[Show Dialog]を検索して追加。「Message」と書かれているぶぶんがダイアログ表示に出てくるメッセージなのでここに"Did you know that the world's largest wind turbine has blades longer than a football field?"と入力。その次のところに表示するボタンの種類を設定。ボタンは色々選択できるのと複数候補を設定する場合(よくあるOKとキャンセル等)複数入れることができる。今回はContinueのみ。

実行すると、"3.2 - Info Dialog Trigger"と書かれたパネルの上のアイコンを押すとダイアログメッセージが出てきて設定した情報とボタンが表示される。今回は以降の処理を書いていないので表示して終わるだけ。

Station 3.3: タービン発動機へのテレポート

公式は以下。

https://learn.microsoft.com/en-us/mesh/develop/getting-started/mesh-101-tutorial/mesh-101-03-visual-scripting?wt.mc_id=WDIT-MVP-5003104#station-33-teleport-to-the-turbine-generator

ボタンを押すとテレポートする実装例。このワールドには風力発電機があり、発動機を近くで見るための空間もある。そこへスクリプトで移動するための実装。

[3.3 - Teleport]-[TeleportToWindTurbineButton]を選択し、InspectorからScript Machineを開く。

開くとある程度実装済み

テレポートは出入口が必要でこれから追加するのは発動機前のポイント。まずはhierarchyの[Chapter3]-[3.3 - Teleport]-[TravelGroup]の下に空オブジェクトを配置して名前を”TeleportLocationWindTurbine”に変更する。

テレポート地点のオブジェクトは[Travel Point]コンポーネントを追加しておく。ちなみに、上位のオブジェクトは[Travel Point Group]コンポーネントを持ってます。
テレポート地点のオブジェクトの座標が移動したときの空間座標となるので座標も設定。

  • Position (6,58,61)
  • Rotation (0,270,0)

最後に”TeleportLocationWindTurbine”を非活性にしてHierarchyの作業終わり。

さっきのScript Graphを開いて追加の処理を書く。上段の処理は前の手順でもやった「ボタンが押された処理」とほぼ同じ。ボタンが押されるとTeleportNowフラグを立てる処理。
追加する処理はボタンが押されたら"TeleportLocationWindTurbine"をActiveにして[Travel to point]で"TeleportLocationWindTurbine"の座標に移動。移動が完了したらTeleportNowのフラグをfalseに変更する。完成した図は以下。

これを実行すると、テレポートボタンを押すと、タービン近くの座標に飛んで発動機を鑑賞できる。

miyauramiyaura

Mesh 101 Tutorial Chapter 4: オブジェクトの移動と空間内での物理演算とMesh Interactableを使ったアニメーション操作

公式は以下。

https://learn.microsoft.com/en-us/mesh/develop/getting-started/mesh-101-tutorial/mesh-101-04-physics?wt.mc_id=WDIT-MVP-5003104

今回はミニチュアを使って、海に風力発電機を設置するというもの。なんだか懐かしい。

使うのは以下の4章のところ。

Station 4.1: つかんで離す動作

公式は以下

https://learn.microsoft.com/en-us/mesh/develop/getting-started/mesh-101-tutorial/mesh-101-04-physics?wt.mc_id=WDIT-MVP-5003104#station-41-grab-and-release

4章の空間においてある風力発電機のオブジェクトをつかんで移動、はなして配置するための設定をいれる。対象は以下のタービンたちが操作可能になる。

WindTurbine1以外はすでに設定が完了しているのでよくわからないときは他の風力発動機をみるといいかも。[WindTurbine1]を選択した状態で[Inspecter]パネルから以下の[Mesh Interactable Setup]コンポーネントを追加し、パラメータにある[Manipulate]をチェック。あと、RigidbodyのFreeze rotationをすべてチェック(横転防止)にして完了。
テストするときはPlaymodeにして右の方を向くと4章の空間にテレポートするボタンがあるのでそれを押して移動。

あとはタービンがドラッグ&ドロップで移動できることを確認。

Station 4.1: Animation Trigger

公式は以下

https://learn.microsoft.com/en-us/mesh/develop/getting-started/mesh-101-tutorial/mesh-101-04-physics?wt.mc_id=WDIT-MVP-5003104#station-42-animation-trigger

Animation Triggerというのは、イメージはColliderの動作が近い。ある範囲にオブジェクトが入るとそれをTriggerとして、オブジェクトのアニメーションを実行したりできる。今回だと海の上にタービンを持っていくと風が吹いて風車部分が回るといったもの。まずは"AnimationTrigger"を選択しBox Colliderの[Is Trigger]にチェックを入れる

この範囲にタービンがはいったら風車を回す実装を入れる。再び"WindTurbine1"を選択し、Inspectorにて[Script Machine]を追加。今回は他のタービンと同じ処理なのですでにあるGraphから[SPWindTurbineScript]を参照して使用する。ただ、個別に変更する必要があるため、[Source]横にある[Convert]ボタンを押下して"Embed"に変更する。

そのあとスクリプトを修正する。作成済みのスクリプトは以下のようになっており、ColliderのTrigger EnterとTrigger Exitのイベントを監視し、AnimationTriggerのオブジェクトと接触していたらアニメーションを実行/停止するという処理になっています。だた、最後のアニメーションの実行のところがThisになっていて、このままだと動かないです。Animatorは子オブジェクトである[Windmill_Turbine_001:Propellors10]にあるため、これを設定してあげる必要があります。

設定は簡単で上記でThisになってる所に[Windmill_Turbine_001:Propellors10]を入れるだけで完了です。

実行して海の上にタービンを持っていくとくるくる回れば成功です。もしうまくいかなければ他のタービンを参考にして間違ってる所を見るといいと思います。

あと、この4章のシナリオの想定として”同じ空間にいる人同士でタービンの配置を検討する”という形なので、タービンの操作は同じ空間で同期されている必要があります。なので、さらに[Local Script Scope]の[Share visual script valiables on this Game Object]にチェックを入れて空間内で同期するように設定します。

Station 4.3: Constraining Bodies

公式は以下。

https://learn.microsoft.com/en-us/mesh/develop/getting-started/mesh-101-tutorial/mesh-101-04-physics?wt.mc_id=WDIT-MVP-5003104#station-43-constraining-bodies

最後によくあることへの対策。このタービン自由につかんで動かせるので投げてみたり、海に設置するときに奥行失敗して床に落としたりと色々困ったことが起きると思います。
それを防ぐためにあらかじめ移動できる範囲を縛っておく機能がありそれがこの節の内容。

既に領域は設定済みで、こういう形で机の上に合わせたBox Colliderが設置済み。

このBox Colliderに[Containment Field]コンポーネントを設定します。このコンポーネントは同じコンポーネントに設定されているColliderのイベントを観測し接触したユーザが指定した対象のオブジェクトであれば、通さないぞというのをする部品ですね。オブジェクトの条件は以下のように選択できます。

今回は[Object Name]を選択して先頭に"WindTurbine"があればという条件にします。実行してみてテーブルより外にタービンが動かせなければ完成。

miyauramiyaura

5章~7章は以下のような感じ。

  • 5章 : 作成した環境をMeshワールドにアプロードしてMeshポータルに上がるかを確認する
  • 6章:ポータルに上げた環境をMesh Appで体験する
  • 7章:イベントとして設定する

基本的には各章の公式を見ることでだいたいできる。

Mesh 101 Tutorial Chapter 5: テスト用の環境を用意する

公式は以下。
https://learn.microsoft.com/en-us/mesh/develop/getting-started/mesh-101-tutorial/mesh-101-05-make-environment-available?wt.mc_id=WDIT-MVP-5003104

Unityのメニューにある[Mesh Toolkit]-[Environments]を選択。

ログインに成功すると以下のようになる。

パラメータを設定していく。

  • Internal NameとDescriptionを入力
  • Mesh worldは作成済みワールドを選択
  • Capacityは同時参加ユーザ数を設定。デフォルト16なのでそのまま

最後に[Create Asset]を押す。うまくいくと以下のような表示が出てくるので、Closeで閉じる。

すると環境をUploadするタブに移行する。
Select a scene項目を押すと、どのUnityのシーンを利用するか選択することができるので、[StartingPoint]を設定する。

Custom ThunmnailsのチェックはMeshワールドに環境をアップロードしたサムネイルを自分で作成したものに変えたい場合に利用する。「画像を用意してアップロードする」か、「空間内にサムネイル用のカメラを用意してそこから撮影したものをサムネイルにする」を選択できる。今回はThumnails Cameraから作成を選択。

プラットフォームはPC,Android両方選択(わかりにくいですがハイライトされたグレーにする)

Build & Publishを実行。完了するとこんな感じ。

Mesh ポータルにはこういう感じで出てきます。

Mesh 101 Tutorial Chapter 6: Meshアプリ内で環境をテストする

公式は以下。
https://learn.microsoft.com/en-us/mesh/develop/getting-started/mesh-101-tutorial/mesh-101-06-test-your-environment?wt.mc_id=WDIT-MVP-5003104

Meshアプリで実行する場合は、先にインストールしたPC版(もしくはQuest)を起動します。ログイン後にイベントのアイコンをクリックしてドラフトを作成するとワールドに入ることができます。
公式の手順がわかりやすいので割愛

Mesh 101 Tutorial Chapter 7: イベントを作成し、他の人を招待する

公式は以下。
https://learn.microsoft.com/en-us/mesh/develop/getting-started/mesh-101-tutorial/mesh-101-07-create-an-event-and-invite-others?wt.mc_id=WDIT-MVP-5003104

これも6章と同じで割愛。イベントについてはMeshポータルで設定する。

miyauramiyaura

プロジェクト開いたら手順に従ってMesh Toolkitのレジストリを登録して、Package ManagerからMesh Toolkitを入れる。
インストールしたらメニューが表示されるので、以下を選択して設定を反映

  • [Mesh Tookit]-[Configure]-[Apply Project Settings]
  • [Mesh Tookit]-[Configure]-[Apply Default Font Settings]

空のプロジェクト開いたら初期設定。

  • HierarchyからGloval Volumeを削除
  • Projectパネルで[MeshEmulatorSetup]を検索してHierarchyに追加
  • Planeを追加。Scaleは(10,10,10)、Layerを[GroundCoolision]に設定
  • Hierarchyで右クリックメニューを出して、[Mesh Toolkit]-[Thumnail Camera]を追加
  • Hierarchyで右クリックメニューを出して、[Mesh Toolkit]-[TravelPonit]を追加、Radiusを8に。

公式のやり方に従ってとりあえず、Cubeを回転するコードを書く。

  • 空オブジェクトを追加
    • 名前を[Mesh Could Scripting]
    • Inspectorパネルで[Mesh Cloud Scripting]を追加
  • [Mesh Could Scripting]オブジェクトの下にCubeを追加する。Positionは(0,1,3).
  • 追加した[Cube]オブジェクトにInspectorパネルから[Mesh Interactable Setup]コンポーネントを追加する
miyauramiyaura

実装は以下の手順

[Mesh Cloud Scripting]を選択し[Open Application Folder]を押下する。エクスプローラーが開くので、App.csを開く。

クラスに以下の変数を追加する。

App.cs
private float _angle = 0;

App.StartAsyncメソッドを実装する

App.cs
public Task StartAsync(CancellationToken token)
{
    // First we find the TransformNode that corresponds to our Cube gameobject
    var transform = _app.Scene.FindFirstChild<TransformNode>();

    // Then we find the InteractableNode child of that TransformNode
    var sensor = transform.FindFirstChild<InteractableNode>();

    // Handle a button click
    sensor.Selected += (_, _) =>
    {
        // Update the angle on each click
        _angle += MathF.PI / 8;
        transform.Rotation = new Rotation { X = 1, Y = 0, Z = 0, Angle = _angle };
    };

    return Task.CompletedTask;
}
miyauramiyaura

とりあえず、これでデプロイしてみる。
メニューから[Mesh Toolkit]-[Environment]を選択。

Mesh Portalの設定されているアカウントでサインインし、Create Environmentタブで必要項目を入力。今回はCloud Scriptingを使うので、[Setup Cloud Scripting Configuration]を展開してAzureのサブスクリプションやリソースグループを設定する。ServiceModeは今回は開発用なのでDevにする。正式版の時はProdでやる。これで環境を作成。

[Update Environment]タブで先ほど作った環境を選択し、UnityのSampleSceneを選択(今回は最初にあるSampleSceneで作った)。その下のクラウド環境は先ほど入れた情報が転記されているはず。
ThumbnailはSceneの中にそれ用のカメラを設定しているので[Generate from Thumnail Camera]を選択する。
あとこの環境を使うPlatformを選択。今回はPCとAndroid両方選択しておく。
後は[Build & Publish]を押してビルドしてみる。

とエラーが出た。

UnityのConsoleにもエラーがでていて今回はAzureへのログインが失敗していた。Consoleログを見る限りazコマンドの問題のようで自分の環境見たらAzure CLIのセットアップがまだだった。

インストールは以下の手順で実施。OSの再起動して環境設定を反映。
https://learn.microsoft.com/ja-jp/cli/azure/install-azure-cli-windows?tabs=azure-cli?wt.mc_id=WDIT-MVP-5003104

再度実行するとさらにエラーが出ていた。UnityのConsoleログを見ると以下のようなものが出ていた。
MeshのUploaderはCloud Scripting使う場合は自動的に指定したリソースグループ配下にAzureのサービスを構築します。以下のエラーはログインしたときのトークンとテナントのIDがあっていないために認証エラーを出していた。これは私の環境が特殊だから起きちゃったパターンですね。。。
Meshに使っているサービスは組織アカウントなのですが、自身の組織用のテナントに加えて、個人アカウントのテナントにアクセスできるようにしています。az loginしたときにデプロイ先とログイン時のテナントがずれてしまって、トークンがあっていないというエラーが以下の内容です。

Azure.RequestFailedException: The access token is from the wrong issuer 'https://sts.windows.net/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxf3/'. It must match the tenant 'https://sts.windows.net/xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx295/' associated with this subscription. Please use the authority (URL) 'https://login.windows.net/xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx295' to get the token. Note, if the subscription is transferred to another tenant there is no impact to the services, but information about new tenant could take time to propagate (up to an hour). If you just transferred your subscription and see this error message, please try back later.

一応別途コマンドプロンプトから以下のコマンドでテナントを調整。

az login --tenant xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx295

これで無事にデプロイ完了しました。

miyauramiyaura

Azure環境はどうなっているかというとこういう形です。

要注意な点が、VM。スペックがPremium v2 P2V2(月額22,000円超)の構成です。おそらくですが、実際に空間内に多くに人が同時アクセスしてその環境全部に対して同期処理を行うのでこういうスペック構成になっているんだと思います。ただ、検証するだけならこんなにスペックいらないのでさっさとスペック落としてもいいかも。

Prodでデプロイするといくつかサービスが増えている。

miyauramiyaura

Cloud ScriptingからAzure Digital Twinsにちょっかい出す実装やってみる。まずはAzure Digital Twins構築する。モデルは「ワールドがあって、その中にアクションのON/OFFできるオブジェクトが存在する」というシンプルな構造。DTDLはこんな感じ。

world.json
{
  "@id": "dtmi:example:world;1",
  "@type": "Interface",
  "displayName": "world",
  "contents": [
    {
      "@type": "Relationship",
      "@id": "dtmi:example:world:rel_objects;1",
      "name": "Contains",
      "displayName": "World has objects",
      "target": "dtmi:example:object;1"
    }
  ],
  "@context": "dtmi:dtdl:context;2"
}
object.json
{
  "@id": "dtmi:example:object;1",
  "@type": "Interface",
  "displayName": "Object",
  "contents": [
    {
      "@type": "Property",
      "name": "Action",
      "schema": "boolean"
    }
  ],
  "@context": "dtmi:dtdl:context;2"
}

Azure Digital Twins構築する。
今回はAzure CLIでコマンド実行する。

az login
az account set --subscription "<your-Azure-subscription-ID>"

このサブスクリプションで初めてADT使う場合は以下のコマンドをたたく

az provider register --namespace 'Microsoft.DigitalTwins'

Azure Digital TwinsとかIoTサービス用の拡張機能を追加する。

az extension add --upgrade --name azure-iot --allow-preview false

Azure Digiltal Twinsを構築する

#リソースグループを作成
az group create --location <region> --name <name-for-your-resource-group>

#Azure Digital Twinsのサービスを作る
az dt create --dt-name <name-for-your-Azure-Digital-Twins-instance> --resource-group <your-resource-group> --location <region> --mi-system-assigned true

#ユーザにロールを割り当てる
az dt role-assignment create --dt-name <your-Azure-Digital-Twins-instance> --assignee "<Azure-AD-user-principal-name-of-user-to-assign>" --role "Azure Digital Twins Data Owner"

モデルを作成する。今回は世界とオブジェクト。

#モデルの作成
az dt model create --dt-name <name-for-your-Azure-Digital-Twins-instance>.api.jpe.digitaltwins.azure.net --models objects.json
az dt model create --dt-name <name-for-your-Azure-Digital-Twins-instance>.api.jpe.digitaltwins.azure.net --models world.json

モデルに対するツインを作成する。世界の中にオブジェクトが含まれるというモデルのなのでリレーションも作成。

#ツインの作成
az dt twin create --dt-name <name-for-your-Azure-Digital-Twins-instance>.api.jpe.digitaltwins.azure.net --dtmi "dtmi:example:world;1" --twin-id world
az dt twin create --dt-name <name-for-your-Azure-Digital-Twins-instance>.api.jpe.digitaltwins.azure.net --dtmi "dtmi:example:object;1" --twin-id object1
#リレーションの作成
az dt twin relationship create --dt-name <name-for-your-Azure-Digital-Twins-instance>.api.jpe.digitaltwins.azure.net --relationship-id r_world --relationship Contains --twin-id world --target object1
miyauramiyaura

Azure Digiltal Twinsでよく使うパターンで今回はやってみる。

Http Request → Azure Functions → Azure DIgital Twins → SignalR → 他のClientアプリ

とりあえず、Azure Functionsのプロジェクトをつくる。

https://learn.microsoft.com/ja-jp/azure/azure-functions/create-first-function-cli-csharp?tabs=windows%2Cazure-cli#create-a-local-function-project

最初にプロジェクトを作成。作るの2つの関数

  • HTTP triggerでAzure Digiltal Twinsを更新する
  • Ecent Grid triggerでAzure Digiltal TwinsのTwinsが更新されたらSignalRで同報通信
> func init MeshADTFunctionProj --worker-runtime dotnet
> cd .\MeshADTFunctionProj\
> dotnet add package Azure.DigitalTwins.Core
> dotnet add package Azure.Identity
> dotnet add package Microsoft.Azure.EventGrid
> dotnet add package Microsoft.Azure.WebJobs.Extensions.EventGrid
> dotnet add package Microsoft.Azure.WebJobs.Extensions.SignalRService
> dotnet add package Newtonsoft.Json
> func new --name UpdateTwinsData --template "HTTP trigger" --authlevel "anonymous"
> func new --name SignalRFunctions --template "EventGridTrigger"

コードはこんな感じで書き換え

UpdateTwinsData.cs
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Azure.DigitalTwins.Core;
using Azure;
using Azure.Identity;
using Newtonsoft.Json.Linq;

namespace MeshADTFunctionProj
{
    public static class UpdateTwinsData
    {

        private static readonly string adtInstanceUrl = Environment.GetEnvironmentVariable("ADT_SERVICE_URL");
        private const string adtAppId = "[your-adt-appid]";
        [FunctionName("UpdateTwinsData")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {

            if (adtInstanceUrl == null) log.LogError("Application setting \"ADT_SERVICE_URL\" not set");
            try
            {
                // Authenticate with Digital Twins
                var cred = new ManagedIdentityCredential();
                var client = new DigitalTwinsClient(new Uri(adtInstanceUrl), cred);
                log.LogInformation($"ADT service client connection created.");

                // {"twinsid":"myTwinId","Action":true}
                string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
                JObject data = (JObject)JsonConvert.DeserializeObject(requestBody);
                string twinId = (string)data["twinsid"];
                var action = data["Action"];
                log.LogInformation($"recieved data for twinId: {twinId} and action: {action}");

                var updateTwinData = new JsonPatchDocument();
                var twin = client.GetDigitalTwin<BasicDigitalTwin>(twinId);

                if(twin.Value.Contents.TryGetValue("Action", out object value))
                {
                    updateTwinData.AppendReplace("/Action", action.Value<bool>());
                }
                else
                {
                    updateTwinData.AppendAdd("/Action", action.Value<bool>());
                }
                await client.UpdateDigitalTwinAsync(twinId, updateTwinData);

                log.LogInformation($"Updated twin data for {twinId} with action: {action}");
            }
            catch (Exception ex)
            {
                log.LogError($"Error in ingest function: {ex.Message}");
                return new BadRequestObjectResult("HTTP Trigger [\"UpdateTwinsData\"] is failed");
            }

            return new OkObjectResult("HTTP Trigger [\"UpdateTwinsData\"] executed successfully");
        }
    }

}
SignalRFunctions.cs
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.EventGrid.Models;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Extensions.EventGrid;
using Microsoft.Azure.WebJobs.Extensions.SignalRService;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;

namespace MeshADTFunctionProj
{
    public static class SignalRFunctions
    {
        public static string twinsId;
        public static bool action;

        [FunctionName("negotiate")]
        public static SignalRConnectionInfo GetSignalRInfo(
            [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequest req,
            [SignalRConnectionInfo(HubName = "dttelemetry")] SignalRConnectionInfo connectionInfo)
        {
            return connectionInfo;
        }

        [FunctionName("broadcast")]
        public static Task Run(
            [EventGridTrigger] EventGridEvent eventGridEvent,
            [SignalR(HubName = "dttelemetry")] IAsyncCollector<SignalRMessage> signalRMessages,
            ILogger log)
        {
            JObject eventGridData = (JObject)JsonConvert.DeserializeObject(eventGridEvent.Data.ToString());
            if (eventGridEvent.EventType.Contains("Twin.Update"))
            {
                try
                {
                    twinsId = eventGridEvent.Subject;
                    
                    var data = eventGridData.SelectToken("data");
                    var patch = data.SelectToken("patch");
                    foreach(JToken token in patch)
                    {
                        if(token["path"].ToString() == "/Action")
                        {
                            action = token["value"].ToObject<bool>();

                            log.LogInformation($"setting alert to: {action}");
                            var property = new Dictionary<object, object>
                            {
                                {"TwinsID", twinsId },
                                {"Alert", action }
                            };
                            return signalRMessages.AddAsync(
                                new SignalRMessage
                                {
                                    Target = "PropertyMessage",
                                    Arguments = new[] { property }
                                });
                        }
                    }
                }
                catch (Exception e)
                {
                    log.LogInformation(e.Message);
                    return null;
                }
            }
            return null;

        }

}
miyauramiyaura

SignalRの構築

> # Create the Azure SignalR Service resource
> az signalr create --name <signalR-service_name> --resource-group <name-for-your-resource-group> --sku Free_F1 --unit-count 1 --service-mode Default

> # Get the SignalR primary connection string 
> primaryConnectionString=$(az signalr key list --name $signalRSvc --resource-group $resourceGroup --query primaryConnectionString -o tsv)
> echo "$primaryConnectionString"

Azure Functionsのデプロイ

> #Azure FunctionsのビルドとZip化
> cd <development_path>
> dotnet publish -c Release -p:UseAppHost=false
> Compress-Archive -Path <development_path>\bin\Release\net6.0\publish\* -DestinationPath MeshADTFunctions.zip

> #Azure  ストレージアカウントの作成
> az storage account create --name <storage_name> --location <region> --resource-group <name-for-your-resource-group> --sku Standard_LRS

> #Azure Functionsサービスの構築(サーバレス)
> az functionapp create --consumption-plan-location <region>--name MeshADTFunctions --os-type Linux --resource-group <name-for-your-resource-group> --storage-account <storage_name> --functions-version 4 --runtime dotnet --runtime -version 6

> #Appplication Insightsの接続
>az functionapp connection create app-insights --resource-group <your-resource-group> --name <your-function-app-name> --target-resource-group  <your-resource-group>--app-insights <your-function-app-name> --secret

> #作成した関数アプリのZipデプロイ
> az functionapp deployment source config-zip -g <resource_group> -n <app_name> --src <zip_file_path>



> #Azure FunctionsからAzure Digital Twinsへのアクセス許可を設定
> az functionapp identity assign --resource-group <your-resource-group> --name <your-function-app-name>
> az dt role-assignment create --dt-name <your-Azure-Digital-Twins-instance> --assignee "<principal-ID>" --role "Azure Digital Twins Data Owner"

> #SignalRサービスへの接続
> az functionapp connection create signalr --resource-group <your-resource-group> --name <your-function-app-name> --target-resource-group <your-resource-group> --signalr <signalR-service_name> --system-identity

> #Azure Functionsのアプリ設定追加(Azure Digital Twinsのホスト名設定)
> az functionapp config appsettings set --name <functionApp_name> --resource-group <name-for-your-resource-group> --settings "ADT_SERVICE_URL=https://<name-for-your-Azure-Digital-Twins-instance>.api.jpe.digitaltwins.azure.net"

> #Azure Functionsのアプリ設定追加(SignalRの接続文字列設定)
> az functionapp config appsettings set --resource-group <your-resource-group> --name <your-function-app-name> --settings "AzureSignalRConnectionString=<your-Azure-SignalR-ConnectionString>"