就像我之前所描述的,HoloToolkit項目是微軟基於Unity內置的底層API封裝的一套工具集合,幫助我們快速使用Unity集成開發HoloLens應用。
本文主要通過源碼研究其中Spatial Mapping的實現,關於底層的API細節,請閱讀我前一篇文章:HoloLens開發手記 - Unity之Spatial mapping 空間映射
0x00 組件結構
Spatial Mapping目錄下有很多內容,其中Prefabs目錄里有我們可以直接使用的預置組件,本文關注的重點是Scripts目錄的腳本。組件目錄結構如下:
本文重點研究SpatialMappingObserver.cs和SpatialMappingSource.cs,這是當前組件的核心內容。
0x01 SpatialMappingObserver.cs
我們先分析其屬性,如下:
//每立方米網格三角形數量,控制mesh質量 public float TrianglesPerCubicMeter = 500f; //當前Observer檢測的空間范圍 public Vector3 Extents = Vector3.one * 10.0f; //刷新時間間隔 public float TimeBetweenUpdates = 3.5f; //用於掃描空間平面的核心組件 private SurfaceObserver observer; //存儲已構建的空間網格對象 private Dictionary<int, GameObject> surfaces = new Dictionary<int, GameObject>(); //SurfaceData隊列,用於生成空間mesh private Queue<SurfaceData> surfaceWorkQueue = new Queue<SurfaceData>(); //為了避免同一時刻生成太多mesh,保證同一時刻只生成一個mesh,這個變量用於判斷當前時刻是否有mesh正在創建 private bool surfaceWorkOutstanding = false; //用於追蹤Observer對象上次更新時間 private float updateTime; //表示當前掃描狀態,Running和Stopped兩種狀態 public ObserverStates ObserverState { get; private set; }
再分析其程序邏輯及流程:
- Awake()方法最先被執行,這里對SurfaceObserver對象進行了初始化。
private void Awake() { observer = new SurfaceObserver(); ObserverState = ObserverStates.Stopped; }
- 接下來是Start()方法,里面設定了掃描的空間范圍。
private void Start() { observer.SetVolumeAsAxisAlignedBox(Vector3.zero, Extents); }
- Update()方法中則會根據當前狀態來調用API請求空間表面信息或者生成mesh對象
private void Update() { if (ObserverState == ObserverStates.Running) { // 如果當前沒有再生成mesh,且SurfaceData中有需要生成mesh的對象 if (surfaceWorkOutstanding == false && surfaceWorkQueue.Count > 0) { SurfaceData surfaceData = surfaceWorkQueue.Dequeue(); // 如果能成功請求到mesh對象,當前任務狀態變為生成mesh中 surfaceWorkOutstanding = observer.RequestMeshAsync(surfaceData, SurfaceObserver_OnDataReady); } //如果當前沒有任務運行且上次更新距現在大於時間間隔,則重新請求SurfaceData數據 else if (surfaceWorkOutstanding == false && (Time.time - updateTime) >= TimeBetweenUpdates) { observer.Update(SurfaceObserver_OnSurfaceChanged); updateTime = Time.time; } } }
- SurfaceObserver_OnDataReady()事件方法用於處理使用SurfaceData請求到的mesh對象信息,用於后續的使用,比如處理其材質效果等。
private void SurfaceObserver_OnDataReady(SurfaceData cookedData, bool outputWritten, float elapsedCookTimeSeconds) { GameObject surface; if (surfaces.TryGetValue(cookedData.id.handle, out surface)) { // 設置 renderer組件的材質. MeshRenderer renderer = surface.GetComponent<MeshRenderer>(); renderer.sharedMaterial = SpatialMappingManager.Instance.SurfaceMaterial;
//是否渲染mesh對象 renderer.enabled = SpatialMappingManager.Instance.DrawVisualMeshes; if (SpatialMappingManager.Instance.CastShadows == false) { renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; } } surfaceWorkOutstanding = false; }
- SurfaceObserver_OnSurfaceChanged()事件方法用於處理SurfaceObserver獲取到的空間表面數據,用於后續的請求mesh對象操作。
private void SurfaceObserver_OnSurfaceChanged(SurfaceId id, SurfaceChange changeType, Bounds bounds, System.DateTime updateTime) { // 判斷當前掃描狀態 if (ObserverState != ObserverStates.Running) { return; } GameObject surface; switch (changeType) { case SurfaceChange.Added: case SurfaceChange.Updated: // 檢測當前表面是否已被掃描過 if (!surfaces.TryGetValue(id.handle, out surface)) { // 創建一個和當前表面關聯的mesh對象 surface = AddSurfaceObject(null, string.Format("Surface-{0}", id.handle), transform); surface.AddComponent<WorldAnchor>(); // 將surface對象加入已知空間表面字典 surfaces.Add(id.handle, surface); } // 請求生成或更新對應的mesh對象 QueueSurfaceDataRequest(id, surface); break; case SurfaceChange.Removed: // 移除關聯的mesh對象 if (surfaces.TryGetValue(id.handle, out surface)) { surfaces.Remove(id.handle); Destroy(surface); } break; } }