こんにちは、ナレコム菅井です。
今回はMR and Azure311のアプリを作っていきます。
開発環境は以下の通りです。
・Windows
・Unity 2017.4.11f1
・Visual Studio 2017
・HoloLens
Microsoft GraphはMicrosoft内の様々なサービスにアクセスするためのAPIです。目標はMicrosoft Graphを利用してMicrosoft accountにログインし、HoloLens上でカレンダーを確認できるようにすることです。それではさっそく始めて行きましょう!
0.準備
まずはじめに、Microsoft Application Registration Portalでアプリケーションを作ります。
1. Microsoft Application Registration Portalにログインし、[アプリの追加]をクリックします。初めてのアプリか否かによって[アプリの追加]の位置が異なるかと思います。
2. アプリケーション名を決めて、[create]をクリック。
3. アプリケーション IDを保存します。後々使うのでアプリケーション IDがある場所を覚えておくか、どこかにメモしておきましょう。
4. 同じ画面のまま[プラットフォーム]から[プラットフォームの追加]をクリックします。アプリの種類を選ぶ画面がポップしてくるので、[ネイティブ アプリケーション]選択します。
5. 続いて同じ画面の下の方にある[Microsoft Graphのアクセス許可]から[委任されたアクセス許可]の隣の[追加]ボタンをクリックします。
6. Microsoftのどのサービスへのアクセスを許可したいかを選択する画面がポップします。今回は[Calendars.Read]にチェックをつけ、[Ok]をクリックします。
7. 最後に[保存]します。
1.Unity の設定
続いてUnityの設定を行っていきたいと思います。以下の手順で進めていきましょう。
1.Unityを開き、[New]から新しいプロジェクトをつくる。
名前をMSGraphMRとして、[Create project]をクリックします。
2. [Build Settings..]からさまざまな項目を編集していく。
[File]->[Build Settings..]を開きます。
a.プラットフォームの変更
[PC, Mac & Linux Standalone]を[Universal Windows Platform]に変更し、[Switch Platform]をクリックします
b.[Player Settings..]を編集する
[Player Settings..]をクリックします。
そのあと[Other Settings]、[Publishing Settings]->[Capabilities]、[XR Settings]を以下のように設定してきます。
続いて、[Unity C#]にチェックを入れます。
3. Sceneの保存
[Add Open Scenes]をクリックし、[新しいフォルダー]からScenesと名前をつけて、この中にMSGraphMR_sceneとして保存します。
[Build Settings..]の変更は以上です。
4. Microsoft Graphパッケージのインポート。
まず、MSGraph LabPlugins.zip fileをダウンロードします。[Assets]->[Import Package]->[Custom Package]をクリックします。ダウンロードしたMSGraph LabPlugins.zip fileをフォルダの中から探し選択し、[Import]をクリックします。
5. インポートした中身を確認する。
まず、[Plugins]の中にある[Microsoft.Identity.Client] の中身です。
続いて、[Plugins]->[WSA]の中にある[Microsoft.Identity.Client] の中身です。
2.カメラの設定
[Hierarchy]パネルの[Main Camera]の[Inspector] を編集していきます。
a. Transform
————————-
Position | 全て0
————————-
Rotation | 全て0
————————-
Scale | 全て1
————————-
b. Camera
———————————-
Clear Flags | Solid Color
———————————-
Background | 黒
———————————-
3.スクリプトの作成
ここでは以下の4つのスクリプトを作っていきたいと思います。
・MeetingUI
・Graph
・GazeInput
・Interactions
スクリプトの作り方はまず、すべてのスクリプトをまとめておくフォルダーを作成します。[Project]->[Create]をクリックし、[Folder]を選択して新しいフォルダーを作ります。名前をScriptsとします。続いて[Project]->[Create]->[C# Script]をクリックし、クラス名をつけていきます。
・MeetingUI
このクラスではUI の詳細を記述していきます。コードは以下の通りです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
using System.Collections.Generic; using UnityEngine; using System; public class MeetingsUI : MonoBehaviour { /// <summary> /// Allows this class to behave like a singleton /// </summary> public static MeetingsUI Instance; /// <summary> /// The 3D text of the scene /// </summary> private TextMesh _meetingDisplayTextMesh; /// <summary> /// Called on initialization /// </summary> void Awake() { Instance = this; } /// <summary> /// Called on initialization, after Awake /// </summary> void Start() { // Creating the text mesh within the scene _meetingDisplayTextMesh = CreateMeetingsDisplay(); } /// <summary> /// Set the welcome message for the user /// </summary> internal void WelcomeUser(string userName) { if (!string.IsNullOrEmpty(userName)) { _meetingDisplayTextMesh.text = $"Welcome {userName}"; } else { _meetingDisplayTextMesh.text = "Welcome"; } } /// <summary> /// Set up the parameters for the UI text /// </summary> /// <returns>Returns the 3D text in the scene</returns> private TextMesh CreateMeetingsDisplay() { GameObject display = new GameObject(); display.transform.localScale = new Vector3(0.03f, 0.03f, 0.03f); display.transform.position = new Vector3(-3.5f, 2f, 9f); TextMesh textMesh = display.AddComponent<TextMesh>(); textMesh.anchor = TextAnchor.MiddleLeft; textMesh.alignment = TextAlignment.Left; textMesh.fontSize = 80; textMesh.text = "Welcome! \nPlease gaze at the button" + "\nand use the Tap Gesture to display your meetings"; return textMesh; } /// <summary> /// Adds a new Meeting in the UI by chaining the existing UI text /// </summary> internal void AddMeeting(string subject, DateTime dateTime, string location) { string newText = $"\n{_meetingDisplayTextMesh.text}\n\n Meeting,\nSubject: {subject},\nToday at {dateTime},\nLocation: {location}"; _meetingDisplayTextMesh.text = newText; } } |
・Graph
このクラスはMicrosoftアカウントから取得したい情報(今回はカレンダーの情報)を取ってくる役割を担います。このクラスでは先ほど0.準備で取得したアプリケーション IDを使います。コードは以下の通りです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 |
using System.Collections; using System.Collections.Generic; using UnityEngine; using Microsoft.Identity.Client; using System; using System.Threading.Tasks; #if !UNITY_EDITOR && UNITY_WSA using System.Net.Http; using System.Net.Http.Headers; using Windows.Storage; #endif public class Graph : MonoBehaviour { /// <summary> /// The object hosting the scheduled meetings /// </summary> [Serializable] public class Rootobject { public List<Value> value; } [Serializable] public class Value { public string subject { get; set; } public StartTime start { get; set; } public Location location { get; set; } } [Serializable] public class StartTime { public string dateTime; private DateTime? _startDateTime; public DateTime StartDateTime { get { if (_startDateTime != null) return _startDateTime.Value; DateTime dt; DateTime.TryParse(dateTime, out dt); _startDateTime = dt; return _startDateTime.Value; } } } [Serializable] public class Location { public string displayName { get; set; } } /// <summary> /// Insert your Application Id here /// </summary> private string _appId = "--Insert your アプリケーションID--"; /// <summary> /// Application scopes, determine Microsoft Graph accessibility level to user account /// </summary> private IEnumerable<string> _scopes = new List<string>() { "User.Read", "Calendars.Read" }; /// <summary> /// Microsoft Graph API, user reference /// </summary> private PublicClientApplication _client; /// <summary> /// Microsoft Graph API, authentication /// </summary> private AuthenticationResult _authResult; /// <summary> /// Begin the Sign In process using Microsoft Graph Library /// </summary> internal async void SignInAsync() { #if !UNITY_EDITOR && UNITY_WSA // Set up Grap user settings, determine if needs auth ApplicationDataContainer localSettings = ApplicationData.Current.LocalSettings; string userId = localSettings.Values["UserId"] as string; _client = new PublicClientApplication(_appId); // Attempt authentication _authResult = await AcquireTokenAsync(_client, _scopes, userId); // If authentication is successfull, retrieve the meetings if (!string.IsNullOrEmpty(_authResult.AccessToken)) { // Once Auth as been completed, find the meetings for the day await ListMeetingsAsync(_authResult.AccessToken); } #endif } /// <summary> /// Attempt to retrieve the Access Token by either retrieving /// previously stored credentials or by prompting user to Login /// </summary> private async Task<AuthenticationResult> AcquireTokenAsync( IPublicClientApplication app, IEnumerable<string> scopes, string userId) { IUser user = !string.IsNullOrEmpty(userId) ? app.GetUser(userId) : null; string userName = user != null ? user.Name : "null"; // Once the User name is found, display it as a welcome message MeetingsUI.Instance.WelcomeUser(userName); // Attempt to Log In the user with a pre-stored token. Only happens // in case the user Logged In with this app on this device previously try { _authResult = await app.AcquireTokenSilentAsync(scopes, user); } catch (MsalUiRequiredException) { // Pre-stored token not found, prompt the user to log-in try { _authResult = await app.AcquireTokenAsync(scopes); } catch (MsalException msalex) { Debug.Log($"Error Acquiring Token: {msalex.Message}"); return _authResult; } } MeetingsUI.Instance.WelcomeUser(_authResult.User.Name); #if !UNITY_EDITOR && UNITY_WSA ApplicationData.Current.LocalSettings.Values["UserId"] = _authResult.User.Identifier; #endif return _authResult; } /// <summary> /// Build the endpoint to retrieve the meetings for the current day. /// </summary> /// <returns>Returns the Calendar Endpoint</returns> public string BuildTodayCalendarEndpoint() { DateTime startOfTheDay = DateTime.Today.AddDays(0); DateTime endOfTheDay = DateTime.Today.AddDays(1); DateTime startOfTheDayUTC = startOfTheDay.ToUniversalTime(); DateTime endOfTheDayUTC = endOfTheDay.ToUniversalTime(); string todayDate = startOfTheDayUTC.ToString("o"); string tomorrowDate = endOfTheDayUTC.ToString("o"); string todayCalendarEndpoint = string.Format( "https://graph.microsoft.com/v1.0/me/calendarview?startdatetime={0}&enddatetime={1}", todayDate, tomorrowDate); return todayCalendarEndpoint; } /// <summary> /// Request all the scheduled meetings for the current day. /// </summary> private async Task ListMeetingsAsync(string accessToken) { #if !UNITY_EDITOR && UNITY_WSA var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var response = await http.GetAsync(BuildTodayCalendarEndpoint()); var jsonResponse = await response.Content.ReadAsStringAsync(); Rootobject rootObject = new Rootobject(); try { // Parse the JSON response. rootObject = JsonUtility.FromJson<Rootobject>(jsonResponse); // Sort the meeting list by starting time. rootObject.value.Sort((x, y) => DateTime.Compare(x.start.StartDateTime, y.start.StartDateTime)); // Populate the UI with the meetings. for (int i = 0; i < rootObject.value.Count; i++) { MeetingsUI.Instance.AddMeeting(rootObject.value[i].subject, rootObject.value[i].start.StartDateTime.ToLocalTime(), rootObject.value[i].location.displayName); } } catch (Exception ex) { Debug.Log($"Error = {ex.Message}"); return; } #endif } } |
・GazeInput
このクラスはHoloLensをつけたユーザーのGaze情報を管理するスクリプトです。コードは以下の通りです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
using System.Collections; using System.Collections.Generic; using UnityEngine; /// <summary> /// Class responsible for the User's Gaze interactions /// </summary> [System.Serializable] public class GazeInput : MonoBehaviour { [Tooltip("Used to compare whether an object is to be interacted with.")] internal string InteractibleTag = "SignInButton"; /// <summary> /// Length of the gaze /// </summary> internal float GazeMaxDistance = 300; /// <summary> /// Object currently gazed /// </summary> internal GameObject FocusedObject { get; private set; } internal GameObject oldFocusedObject { get; private set; } internal RaycastHit HitInfo { get; private set; } /// <summary> /// Cursor object visible in the scene /// </summary> internal GameObject Cursor { get; private set; } internal bool Hit { get; private set; } internal Vector3 Position { get; private set; } internal Vector3 Normal { get; private set; } private Vector3 _gazeOrigin; private Vector3 _gazeDirection; /// <summary> /// Start method used upon initialisation. /// </summary> internal virtual void Start() { FocusedObject = null; Cursor = CreateCursor(); } /// <summary> /// Method to create a cursor object. /// </summary> internal GameObject CreateCursor() { GameObject newCursor = GameObject.CreatePrimitive(PrimitiveType.Sphere); newCursor.SetActive(false); // Remove the collider, so it doesn't block raycast. Destroy(newCursor.GetComponent<SphereCollider>()); newCursor.transform.localScale = new Vector3(0.05f, 0.05f, 0.05f); Material mat = new Material(Shader.Find("Diffuse")); newCursor.GetComponent<MeshRenderer>().material = mat; mat.color = Color.HSVToRGB(0.0223f, 0.7922f, 1.000f); newCursor.SetActive(true); return newCursor; } /// <summary> /// Called every frame /// </summary> internal virtual void Update() { _gazeOrigin = Camera.main.transform.position; _gazeDirection = Camera.main.transform.forward; UpdateRaycast(); } /// <summary> /// Reset the old focused object, stop the gaze timer, and send data if it /// is greater than one. /// </summary> private void ResetFocusedObject() { // Ensure the old focused object is not null. if (oldFocusedObject != null) { if (oldFocusedObject.CompareTag(InteractibleTag)) { // Provide the 'Gaze Exited' event. oldFocusedObject.SendMessage("OnGazeExited", SendMessageOptions.DontRequireReceiver); } } } private void UpdateRaycast() { // Set the old focused gameobject. oldFocusedObject = FocusedObject; RaycastHit hitInfo; // Initialise Raycasting. Hit = Physics.Raycast(_gazeOrigin, _gazeDirection, out hitInfo, GazeMaxDistance); HitInfo = hitInfo; // Check whether raycast has hit. if (Hit == true) { Position = hitInfo.point; Normal = hitInfo.normal; // Check whether the hit has a collider. if (hitInfo.collider != null) { // Set the focused object with what the user just looked at. FocusedObject = hitInfo.collider.gameObject; } else { // Object looked on is not valid, set focused gameobject to null. FocusedObject = null; } } else { // No object looked upon, set focused gameobject to null. FocusedObject = null; // Provide default position for cursor. Position = _gazeOrigin + (_gazeDirection * GazeMaxDistance); // Provide a default normal. Normal = _gazeDirection; } // Lerp the cursor to the given position, which helps to stabilize the gaze. Cursor.transform.position = Vector3.Lerp(Cursor.transform.position, Position, 0.6f); // Check whether the previous focused object is this same. If so, reset the focused object. if (FocusedObject != oldFocusedObject) { ResetFocusedObject(); if (FocusedObject != null) { if (FocusedObject.CompareTag(InteractibleTag)) { // Provide the 'Gaze Entered' event. FocusedObject.SendMessage("OnGazeEntered", SendMessageOptions.DontRequireReceiver); } } } } } |
・Interactions
このクラスはシーン内のボタンを作ったり、エアタップに反応したりする機能をつけるためのスクリプトです。コードは以下の通りです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.XR.WSA.Input; public class Interactions : GazeInput { /// <summary> /// Allows input recognition with the HoloLens /// </summary> private GestureRecognizer _gestureRecognizer; /// <summary> /// Called on initialization, after Awake /// </summary> internal override void Start() { base.Start(); // Register the application to recognize HoloLens user inputs _gestureRecognizer = new GestureRecognizer(); _gestureRecognizer.SetRecognizableGestures(GestureSettings.Tap); _gestureRecognizer.Tapped += GestureRecognizer_Tapped; _gestureRecognizer.StartCapturingGestures(); // Add the Graph script to this object gameObject.AddComponent<MeetingsUI>(); CreateSignInButton(); } /// <summary> /// Create the sign in button object in the scene /// and sets its properties /// </summary> void CreateSignInButton() { GameObject signInButton = GameObject.CreatePrimitive(PrimitiveType.Sphere); Material mat = new Material(Shader.Find("Diffuse")); signInButton.GetComponent<Renderer>().material = mat; mat.color = Color.blue; signInButton.transform.position = new Vector3(3.5f, 2f, 9f); signInButton.tag = "SignInButton"; signInButton.AddComponent<Graph>(); } /// <summary> /// Detects the User Tap Input /// </summary> private void GestureRecognizer_Tapped(TappedEventArgs obj) { if (base.FocusedObject != null) { Debug.Log($"TAP on {base.FocusedObject.name}"); base.FocusedObject.SendMessage("SignInAsync", SendMessageOptions.RequireReceiver); } } } |
記述し終わったあと、Interactionsスクリプトを[Hierarchy]パネルの[MainCamera]にD&Dします。
また、記述したコード内でタグを用いて識別するので、[MainCamera]にタグをつけたいと思います。[Main Camera]の[Inspector]->[Tag]からタグを追加します。
名前をSignInButtonとして保存します。
4.ビルド
[File]->[Build Settings..]->[Build]の順に選択します。新しいフォルダー(App)を作ります。このフォルダーを選択し、保存します。
この後の手順についてはこちらを参照してください。
HoloLensで実行した時の様子です。何も予定を追加していないのですが、無事動作が確認できました。
以上となります。 お疲れ様でした。