UrhoSharpで作るHoloLens用の積木アプリ - SpatialMappingと簡単なイベント

#HoloLens向けアプリケーションの新しいプラットフォーム
今までHoloLens用のアプリケーション開発といえばUnityでしたが、Xamarinファミリーの「UrhoSharp」というクロスプラットフォーム向け3D/2Dエンジンが提供されるようになりました。これによって排他モードのHoloLensアプリケーションの作る手法が１つ増えることになります。今回もっとも単純といえるHoloLens用の積木アプリを実装することでUrhoSharpの一部の機能を紹介します。

## UrhoSharpについて
UrhnoSharpはXamarinで提供されおり、クロスプラットフォームでの3D/2Dのゲームや描画を可能にするエンジンです。クロスプラットフォームのXamarinの特徴も引き継いでおりAndroid,iOS等としても提供が可能です。
Xamarinの一部として提供されているためUnity（条件によってはUnityは有償）とは異なり無償で利用できることを売りにしています。
ただし、実際にHoloLensのアプリケーションを構築するにあたっては色々課題が多く、今のところUnity+Mixed Reality Toolkit - Unityでの開発が崩されることはないかなという感じがします。

### UrhoSharpの特徴

UrhoSharpの特徴をいくつか記載します。特徴の１つ「原則すべてコーディング」が何よりも課題になりそうです。というよりもMixed Reality Toolkit - Unityという現代のMixed Realityの神器に慣れているのが主な原因ですが。

* 無償で利用できる
* 原則すべてコーディングで作成する


### オブジェクトの構造
基本的な概念はUnityのHierarchy構造を模しています。ルート要素がSceneとなっていてその下にUnityのGameObjectに相当するUrhno.Nodeクラスのオブジェクトがぶら下がります。NodeにはUnityと同様Componentを設定することができ、これによってオブジェクトの機能を表現することになります。なお、Sceneにぶら下がるものは基底クラスがUrhoObjectとなっています。


![image.png](https://qiita-image-store.s3.amazonaws.com/0/152548/c0f800f8-faba-965f-208b-a52482872a1c.png)

### UrhoSharpでのアプリケーションの実行フロー
基本的にはUrhoSarpプロジェクトにMainメソッドを実装し、エントリーポイントとします。Mainメソッドの中ではUrho.Applicationを継承したクラスをインスタンス化しアプリケーションとして実行を開始します。

### Urho.Applicationクラス
Urhoでのアプリケーションの動作を記述するクラスになります。HoloLens上で動かすアプリとしてはこのクラスを継承したUrho.SharpReality.StereoApplicationクラスを使って開発します。

#### 基本的な考え方
UnityのGameObjectと概念は同じになっています。初期化コードをStartメソッド内で実装し、以降Updateメソッドで随時処理を行うという形です。少し違うのは、イベント周りや空間マッピング等一部の処理がメソッドで定義されています。よく使いそうな主要なものは以下のものです。

##### 制御系メソッド
Urho.SharpReality.StereoApplicationクラスには以下の制御系メソッドが用意されています。

| メソッド名 | 機能 |
|:-:|:--|
| Start | 初回に１度だけ呼び出されるメソッド。この中で初期化処理を実装します。  |
| OnUpdate  | 繰返し呼び出されるメソッド。UnityでいうUpdate相当です。引数のtimeStepに前回からの差分時間が入ってます。  |


##### イベントに関するメソッド
Urho.SharpReality.StereoApplicationクラスには以下のイベント処理用のメソッドが用意されています。

| メソッド名 | 機能 | 補足 |
|:-:|:--|:--|
| OnGestureTapped  | AirTap時に呼び出されます。  |EnableGestureTappedプロパティをtrueにすることで有効化|
| OnGestureDoubleTapped  | ダブルAirTap時で呼び出されます。  |EnableGestureHoldプロパティをtrueにすることで有効化|
| OnGestureHoldStarted| AirTap&Hold開始時に呼び出されます。  |EnableGestureHoldプロパティをtrueにすることで有効化|
| OnGestureHoldCompleted| AirTap&Hold完了時に呼び出されます。  |EnableGestureHoldプロパティをtrueにすることで有効化|
| OnGestureHoldCanceled| AirTap&Hold中、指の検知をロストしたとき時に呼び出されます。  |EnableGestureHoldプロパティをtrueにすることで有効化|
| OnGestureManipulationStarted| AirTap&Hold後、指移動の開始時に呼び出されます。  |EnableGestureManipulationプロパティをtrueにすることで有効化|
| OnGestureManipulationUpdated| 指移動の開始中に呼び出されます。  |EnableGestureManipulationプロパティをtrueにすることで有効化|
| OnGestureManipulationCompleted| AirTap&Hold時に呼び出されます。  |EnableGestureManipulationプロパティをtrueにすることで有効化|
| OnGestureManipulationCanceled| マニピュレーション中、指の検知をロストしたとき時に呼び出されます。  |EnableGestureManipulationプロパティをtrueにすることで有効化|
| OnSurfaceAddedOrUpdated| 空間マッピングで表面の検出または更新が発生した場合に呼び出されます。  |StartSpatialMappingで空間マッピングを有効化した場合に|

##### そのほか便利なメソッドとプロパティ

| メソッド/プロパティ名 | 機能 |
|:-:|:--|
| StartSpatialMapping | 空間マッピングを開始します。  |
| StopSpatialMapping  | 空間マッピングの終了します。  |
| RegisterCortanaCommands| Cortanaのコマンドを登録します。登録はコマンドの文字列と処理するメソッドの組で登録します。|
| TextToSpeech  | 引数のテキストを音声で読み上げます（おっさん声）。  |
| Scene  | オブジェクトのルートとなるシーンを取得します。 |
| DirectionalLight| シーン内に存在するライティングを取得します。 |
| LeftCamera(RightCamera)  | カメラを取得します。"StereoApplication"が指す通り、左右別々にカメラオブジェクトを持っています。実際にHoloLens等でカメラ関連の情報を使う場合はどちらか一方で対応可能です。  |
| EnableGestureTapped  | Tapの使用可/不可を設定します。  |
| EnableGestureHold  | Tap&Holdの使用可/不可を設定します。  |
| EnableGestureManipulation| Manipulationの使用可/不可を設定します。  |

## 積み木のアプリを作る

ド定番ですがHoloLensの美味しい機能を使う積木のアプリをUrhoSharpで書いてみました。どんな実装になるかの参考にしてください。

### サンプルコード
動くコードはgithubに公開しています。

[https://github.com/TakahiroMiyaura/UrhnoBlockStackingSamples](https://github.com/TakahiroMiyaura/UrhnoBlockStackingSamples)

### 開発環境
開発環境は以下の通りです。

* Windows 10 Pro CU
* Visual Studio 2015 Update 3 Community Edition

### 積み木アプリを作る

UrhoSharpで積木アプリを作る手順は次の通りです。作業自体は特に普通のVisual Studioでの開発とそれほど変わりません。

1. Visual Studioで新規プロジェクトを作成する。
2. UrhoSharpでプログラムを書く

#### 1.Visual Studioで新規プロジェクトを作成する。

HoloLens用のUrhoSharpプロジェクトはすでに公開されているオンラインから入手可能です。
まずVisual Studioを起動し「ファイル」-「新規作成」-「プロジェクト」で新しいプロジェクトのダイアログを開きます。
ペインから「オンライン」を選択し検索ウインドウで「UrhoSharp」を入力すると「UrhoSharp For HoloLens」が出てくるので任意の場所とプロジェクト名で生成します。生成するとそのままビルドできる状態のサンプルアプリになっており、地球の周りに月が回っているオブジェクトが表示されます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/152548/1713155c-532e-040f-d2d1-a9a9c7a00aff.png)


#### 2.UrhoSharpでプログラムを書く

実際に積木アプリを書きます。まず起動の起点となるMainメソッドを実装します。
積木アプリの実装を行ったクラスをCoreApplicationクラスで実行開始するだけです。

```csharp:Programs.cs
// Copyright(c) 2017 Takahiro Miyaura
// Released under the MIT license
// http://opensource.org/licenses/mit-license.php
using System;
using Windows.ApplicationModel.Core;
using Urho;

namespace UrhnoBlockStackingSamples
{
    internal class Program
    {
        [MTAThread]
        private static void Main()
        {
            var appViewSource = new UrhoAppViewSource<BlockStackingSamples>();
            CoreApplication.Run(appViewSource);
        }
    }
}
```

次は積木アプリの実装になります。大きくはStartメソッドの実装、タップ時にキューブを生成する実装、空間マッピングの実装の３つになります。空間マッピングは先ほどのStartSpatialMappingメソッドを呼ぶことで実行可能なのですが、積木アプリのように床の上に置くなどを実現しようとすると表面にコライダーを敷き詰めないと実現できません。このための実装が必要になります。
まず、Startメソッドについては以下のように実装を行いました。
今回は空間マッピングを利用するためStartSpatialMappingを呼び出しています。設定については、10m四方をマッピング対象として1スケール単位で1000分解能、カラーは赤で設定しています。
また、Cortanaのコマンドでgenerateを発言したらタップと同じ処理を実行するように登録しました。

```csharp:BlockStackingSamples.cs
// Copyright(c) 2017 Takahiro Miyaura
// Released under the MIT license
// http://opensource.org/licenses/mit-license.php
protected override async void Start()
{
    // Create a basic scene, see StereoApplication
    base.Start();

    generateCube = new Queue<Node>();

    environmentNode = Scene.CreateChild();

    // Gesture Enabled.
    EnableGestureTapped = true;

    // Scene has a lot of pre-configured components, such as Cameras (eyes), Lights, etc.
    DirectionalLight.Brightness = 1f;
    DirectionalLight.Node.SetDirection(new Vector3(-1, 0, 0.5f));

    // Set Cortana Command
    await RegisterCortanaCommands(new Dictionary<string, Action>()
    {
        {"generate",OnGestureTapped}
    });

    // Set spatial Mapping. 
    spatialMaterial = Material.FromColor(Color.Red);
    spatialMaterial.SetTechnique(0, CoreAssets.Techniques.NoTextureUnlitVCol, 1, 1);
    await StartSpatialMapping(new Vector3(10, 10, 10), 1000, Color.Red);
}
```

次に、タップ時の動作を実装します。実装している内容自体は至極簡単で空のNodeを作成後にBox,RigidBody,CollisionShapeコンポーネントを追加して各設定を行います。形状はキューブ、色は白で重力で落下するように設定を行います。

```csharp:BlockStackingSamples.cs
// Copyright(c) 2017 Takahiro Miyaura
// Released under the MIT license
// http://opensource.org/licenses/mit-license.php
public override void OnGestureTapped()
{
    //Create Empty UrhoObject.
    var child = Scene.CreateChild("Cube"+Guid.NewGuid());
    generateCube.Enqueue(child);
    if (generateCube.Count > 20)
    {
        var dequeue = generateCube.Dequeue();
        Scene.RemoveChild(dequeue);
        dequeue.Dispose();
    }
            
    //Set position and scale.
    child.Position = HeadPosition + RightCamera.Node.WorldDirection * 2f;
    child.Scale = new Vector3(0.2f, 0.2f, 0.2f);

    //add Compoent of Box shape.
    var component = child.CreateComponent<Box>();
            
    //set box color.
    component.Material = Material.FromColor(Color.White);

    //set rigidbody and useGravity.
    var rigidBody = child.CreateComponent<RigidBody>();
    rigidBody.Mass = 1f;
    rigidBody.RollingFriction = 0.5f;
    rigidBody.UseGravity = true;

    //set collision.
    var shape = child.CreateComponent<CollisionShape>();
    shape.SetBox(Vector3.One, Vector3.Zero, Quaternion.Identity);
}
```

以上で、キューブの生成は可能になります。あとは空間マッピングの結果を用いて床や壁にコライダーを付けて完成です。空間マッピングを有効にするとマッピングの状況の変化に応じてOnSurfaceAddedOrUpdatedメソッドが呼び出されます。このメソッドの中でマッピングの表面にメッシュを張り付けコライダーを設定することで床の上に載せる動きを実現します。

```csharp:BlockStackingSamples.cs
// Copyright(c) 2017 Takahiro Miyaura
// Released under the MIT license
// http://opensource.org/licenses/mit-license.php
public override void OnSurfaceAddedOrUpdated(SpatialMeshInfo surface, Model generatedModel)
{
    var isNew = false;
    StaticModel staticModel = null;

    //check already created by Id.
    var node = environmentNode.GetChild(surface.SurfaceId, false);
    if (node != null)
    {
        //get StaticModel.
        staticModel = node.GetComponent<StaticModel>();
    }
    else
    {
        //if it is new,create node.
        isNew = true;
        node = environmentNode.CreateChild(surface.SurfaceId);
        staticModel = node.CreateComponent<StaticModel>();
    }
    node.Position = surface.BoundsCenter;
    node.Rotation = surface.BoundsRotation;
    staticModel.Model = generatedModel;

    if (isNew)
    {
        //set collisionShape.
        staticModel.SetMaterial(spatialMaterial);
        var rigidBody = node.CreateComponent<RigidBody>();
        rigidBody.RollingFriction = 0.5f;
        rigidBody.Friction = 0.5f;
        var collisionShape = node.CreateComponent<CollisionShape>();
        collisionShape.SetTriangleMesh(generatedModel, 0, Vector3.One, Vector3.Zero, Quaternion.Identity);
    }
}
```

## 最後に

UrhoSharpのHoloLensのサンプルはGitで公開されています。
[https://github.com/xamarin/urho-samples/tree/master/HoloLens](https://github.com/xamarin/urho-samples/tree/master/HoloLens)
そこも参考にしてください。動かしてみた感じでは若干動きが滑らかな感じがしてUnityよりも軽量？という印象もあります。が何より素ですべて実装しないといけないところが大変です。Unityでの開発ノウハウがかなりたまってきたうえにMixed Reality Toolkit - Unityという強力なユーティリティがすでにある以上現状UrhoSharpへの移行というのはなかなかないのかもしれないです。
その上、現状3Dのデザイナがないので、エミュレータ等とりあえず動かして確認するという手間が一番面倒なのかもしれないです。