HoloLensでSpatilaMappingに穴を開ける

# やりたかった事

- RoboRaidとかOrigamiのUnderWorldみたいに壁/床の向うにオブジェクトを出したい
- 衝突判定も無効化して壁/床の向うにオブジェクトを投げ込みたい

# 試行錯誤の道

下記等を参考にしたのですが空中で見せる事は出来ても、壁/床の向う側に出そうとするとSpatialMappingに阻まれて隠れてしまいました。

- [Holograms 101](https://developer.microsoft.com/ja-jp/windows/holographic/holograms_101)
- [Case study - Looking through holes in your reality](https://developer.microsoft.com/en-us/windows/holographic/case_study_-_looking_through_holes_in_your_reality)

Holograms 101を見るとSpatialMapping自体を無効化しているようなのですが、一部分だけ無効化する方法は無いものかと頭を捻ってみました。

> いいぜ、HoloLensが何でも思い通りに空間マッピング出来るってなら、まずはそのふざけた空間をぶち殺す。

# 実現方法

下記のような流れでSpatialMappingで自動生成されたMeshに対して穴を開けています。

1. ColliderのOnTriggerEnterで範囲内にSpatialMappingのMeshが入った事を検知
1. 対象Meshが範囲内に完全に内包されていればMeshを無効化
1. 範囲内のMeshに含まれる頂点情報を全て検索
1. 頂点が1つでも含まれる三角形を削除することで穴開け
1. 穴あけをしたMeshでSpatialMappingを更新

また、そこそこ重い処理となるようなので実行した時にカクつかないようにコルーチンを使って、一定時間でUnity側に処理を戻すようにしています。

# SpatialBreaker

そして、出来上がった物がこちらです。

## Unityパッケージ

[SpatialBreaker.unitypackage](https://www.dropbox.com/s/7m2mihjso5lwx3f/SpatialBreaker.unitypackage?dl=0)

## 使い方

1. SpatialBreakerのプレハブをシーンに追加します
2. 大きさと位置を適当に調整します
3. (必要であれば)Unity側に処理を戻すまでの許可フレーム数(AllowUseFrame)をデフォルトの2から変更します

> すごーい！君はMeshを壊せるプレハブなんだね！
> 壁の向こう側にいけるー！　たーのしー！

## 注意点

仕組み上、下記のような問題があります。

1. 処理が重いので範囲が広いとSpatialMappingの再生成(3.5秒)に間に合わない場合がある
1. 再生成後にどうしても1f以上は壁/床が存在する時間がある
1. 頂点を1つでも含む三角形を全て消しているので綺麗には穴が開かない
1. 頂点が含まれず三角形の線分だけが対象領域に掛かっていると削除対象にならない

## ソースコード

```C#:SpatialBreakerController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.VR.WSA;
using HoloToolkit.Unity;

public class SpatialBreakerController : MonoBehaviour {

    public int allowUseFrame = 2;
    private Collider spatialBreakerCollider;

	// Use this for initialization
	void Start () {
        spatialBreakerCollider = GetComponent<Collider>();
	}
	
	// Update is called once per frame
	void Update () {
		
	}

    private void OnTriggerEnter(Collider other)
    {
        StartCoroutine( spatialBreak(other));
    }

    IEnumerator spatialBreak(Collider other)
    {
        if(other.gameObject.transform.parent.gameObject.GetComponent<SpatialMappingManager>() == null)
        {
            //  SpatialMappingManagerの子要素で無ければ何もしない
            yield break;
        }

        float startTime = Time.realtimeSinceStartup;
        float totalStartTime = startTime;
        MeshFilter meshFilter = other.gameObject.GetComponent<MeshFilter>();

        // MeshFilterが無ければ追加する。
        if(meshFilter == null)
        {
            other.gameObject.AddComponent<MeshFilter>();
            meshFilter = other.gameObject.GetComponent<MeshFilter>();
        }
        Mesh mesh = meshFilter.mesh;

        // 対象のメッシュが非表示領域に完全に含まれている場合は非表示にする。
        if(spatialBreakerCollider.bounds.Contains(mesh.bounds.min) &&
            spatialBreakerCollider.bounds.Contains(mesh.bounds.max))
        {
            other.gameObject.SetActive(false);
            yield break;
        }


        Vector3[] vertices = mesh.vertices;

        // 範囲内の頂点のIndexを取得する。
        List<int> insideVertices = new List<int>();

        for(int i=0; i < vertices.Length; i++)
        {
            Vector3 vertexPos = other.gameObject.transform.TransformPoint(vertices[i]);
            if (spatialBreakerCollider.bounds.Contains(vertexPos))
            {
                insideVertices.Add(i);
            }
        }

        List<int> triangles = new List<int>();
        triangles.AddRange(mesh.triangles);

        int count = 0;
        int removeCount = 0;
        foreach(int vertexIndex in insideVertices)
        {
            int searchStartPos = 0;
            int pos = triangles.IndexOf(vertexIndex, searchStartPos);

            //Debug.LogFormat("SpatialBreaker process {0}/{1} complete.", count, insideVertices.Count);
            while (pos != -1)
            {
                int triangleFirstPos = pos - (pos % 3);
                triangles.RemoveRange(triangleFirstPos, 3);
                searchStartPos = triangleFirstPos;
                pos = triangles.IndexOf(vertexIndex, searchStartPos);
                removeCount++;
            }

            count++;
            if(Time.realtimeSinceStartup - startTime > (0.015f * allowUseFrame))
            {
                // 指定された最大フレーム以上時間が掛かっている場合は戻る。
                yield return null;
            }
            // 再開時に処理開始時刻を再度取得
            startTime = Time.realtimeSinceStartup;
        }

        // Meshを再生成
        mesh.Clear();
        mesh.vertices = vertices;
        mesh.triangles = triangles.ToArray();

        mesh.RecalculateNormals();
        mesh.RecalculateBounds();
        meshFilter.mesh = mesh;

        // 衝突しないようにしたいのでColliderのMeshも更新する。
        ((MeshCollider)other).sharedMesh = mesh;

        float totalTime = Time.realtimeSinceStartup - totalStartTime;
        //Debug.LogFormat("SpatialBreaker Done. removeTriangles={0} totalTime={1}",removeCount, totalTime);
    }
}
```

# 最後に

HoloLensの為にUnity/C#の勉強を始めたので諸々勘違いしてたり、定石から外れている部分があるような気がしています。
改善点とか変な部分がありましたらコメント頂けるとありがたいです。