Hola !
En el post anterior expliqué como utilizar Spatial Understanding para tener más control sobre el proceso de escaneo que realiza Hololens. En ese ejemplo, se realizaban los siguientes pasos
- Cuando se lanzaba la App, Hololens comenzaba el proceso de escaneo
- Cuando se llegaba a un mínimo de planos encontrados, se finalizaba el mismo
- En todo momento se mostraba el estado del escaneo en un FPS
El ejemplo de hoy es la continuación del anterior, en el que una vez finalizados los pasos anteriores
- Buscaremos una superficie de tamaño 1×1 en el piso, cuando se haga AirTap / Click en un holograma
La siguiente animación muestra el proceso de scanning (X6 speed, no apto para personas que se mareen) y luego el mapeo en el piso donde se puede ver como se “pintan” los mosaicos del tamaño encontrado.

Estos son los pasos a seguir.
- Create a 3D project in Unity3D
- Import HoloToolkit package
- Configure project to
- Support HoloLens projects (UWP, VR, etc)
- enable Spatial Mapping feature
- Clean Scene elements
- Add
- Hololens Camera
- Cursor With Feedback
- Input Manager
- Spatial Mapping
- Spatial Understanding
- FPS Display
- Add Empty element
- Rename to CodeManagers
- Add new C# Script named “GameStartScanner.cs”
- Add Empty Element
- Rename to HoloCollection
- Add 3D Cube element to HoloCollection
- Rename to “InteractiveCube”
- Set this properties to Cube
- Position: x: 0, y:0, z:1.2
- Scale: x: 0.2, y:0.2, z:0.2
- Add a new C# Script to the cube named “Scanner Analyzer”
El proyecto debe quedar similar al siguiente

Y el script para la nueva clase está debajo, y lo mejor es revisar un par de apuntes interesantes sobre la misma
- Esta clase está basada en el ejemplo de Spatial Mapping de HoloToolkit
- La misma usa clases auxiliares que son parte del proyecto,
- AnimatedBox.cs
- AnimationCurve3.cs
- GameStartScanner.cs
- Line.cs
- LineData.cs
- ScannerAnalyzer.cs
- La funcionalidad de esta clase se activa cuando la clase que creamos en el post anterior “GameStartScanner.cs” termina el proceso de scan del entorno
- En el Update se verifica si se está “buscando” una superficie de 1×1 en el suelo, en caso afirmativo se dibuja la misma
- El proceso de búsqueda se realiza en “OnInputClicked”, que se activa cuando hacemos AirTap o click sobre el Cube
- En esta funcion también se define el tamaño de superficie a buscar con las variables, minWidthOfWallSpace y var minHeightAboveFloor
- En la línea 54 comienza el proceso de búsqueda utilizando los tamaños a buscar y las funciones
- SpatialUnderstanding.Instance.UnderstandingDLL.PinObject(_resultsTopology);
- SpatialUnderstandingDllTopology.QueryTopology_FindPositionsOnFloor()
- En el 1er paso se crea un puntero en memoria con todos los elementos de trabajo y el 2do paso es el que “filtra” por los que estén en el suelo y con el tamaño deseado
- Lo siguiente es dibujar los frames en el piso, este código está basado en lo ejemplos de Spatial Mapping. Son líneas y líneas que merecen una buena tarde refactoring.
Código de ejemplo
| using System.Collections.Generic; | |
| using HoloToolkit.Unity.InputModule; | |
| using UnityEngine; | |
| using HoloToolkit.Unity; | |
| public class ScannerAnalyzer : MonoBehaviour, IInputClickHandler | |
| { | |
| const int QueryResultMaxCount = 512; | |
| const int DisplayResultMaxCount = 32; | |
| private List<AnimatedBox> _lineBoxList = new List<AnimatedBox>(); | |
| private SpatialUnderstandingDllTopology.TopologyResult[] _resultsTopology = new SpatialUnderstandingDllTopology.TopologyResult[QueryResultMaxCount]; | |
| private LineData _lineData = new LineData(); | |
| private string _spaceQueryDescription; | |
| public TextMesh DebugDisplay; | |
| public static bool AnalyzerEnabled; | |
| public Material MaterialLine; | |
| void Update() | |
| { | |
| if (!AnalyzerEnabled) return; | |
| if (DebugDisplay != null) | |
| DebugDisplay.text = _spaceQueryDescription; | |
| // Queries | |
| if (SpatialUnderstanding.Instance.ScanState == SpatialUnderstanding.ScanStates.Done) | |
| { | |
| //Update_Queries(); | |
| } | |
| // Lines: Begin | |
| LineDraw_Begin(); | |
| // Drawers | |
| var needsUpdate = false; | |
| needsUpdate |= Draw_LineBoxList(); | |
| // Lines: Finish up | |
| LineDraw_End(needsUpdate); | |
| } | |
| public void OnInputClicked(InputClickedEventData eventData) | |
| { | |
| if (!SpatialUnderstanding.Instance.AllowSpatialUnderstanding) | |
| { | |
| return; | |
| } | |
| var minWidthOfWallSpace = 1f; | |
| var minHeightAboveFloor = 1f; | |
| // Query | |
| var resultsTopologyPtr = SpatialUnderstanding.Instance.UnderstandingDLL.PinObject(_resultsTopology); | |
| var locationCount = SpatialUnderstandingDllTopology.QueryTopology_FindPositionsOnFloor( | |
| minWidthOfWallSpace, minHeightAboveFloor, | |
| _resultsTopology.Length, resultsTopologyPtr); | |
| // Output | |
| var visDesc = "Find Positions On Floor"; | |
| var boxFullDims = new Vector3(minWidthOfWallSpace, 0.025f, minHeightAboveFloor); | |
| var color = Color.red; | |
| ClearGeometry(); | |
| // Add the line boxes (we may have more results than boxes – pick evenly across the results in that case) | |
| var lineInc = Mathf.CeilToInt((float)locationCount / (float)DisplayResultMaxCount); | |
| var boxesDisplayed = 0; | |
| for (var i = 0; i < locationCount; i += lineInc) | |
| { | |
| var timeDelay = (float)_lineBoxList.Count * AnimatedBox.DelayPerItem; | |
| _lineBoxList.Add( | |
| new AnimatedBox( | |
| timeDelay, | |
| _resultsTopology[i].position, | |
| Quaternion.LookRotation(_resultsTopology[i].normal, Vector3.up), | |
| color, | |
| boxFullDims * 0.5f) | |
| ); | |
| ++boxesDisplayed; | |
| } | |
| // Vis description | |
| if (locationCount == boxesDisplayed) | |
| { | |
| _spaceQueryDescription = string.Format("{0} ({1})", visDesc, locationCount); | |
| } | |
| else | |
| { | |
| _spaceQueryDescription = string.Format("{0} (found={1}, displayed={2})", visDesc, locationCount, boxesDisplayed); | |
| } | |
| } | |
| #region Line and Box Drawing | |
| protected void LineDraw_Begin() | |
| { | |
| _lineData.LineIndex = 0; | |
| for (var i = 0; i < _lineData.Lines.Count; ++i) | |
| { | |
| _lineData.Lines[i].isValid = false; | |
| } | |
| } | |
| private bool Draw_LineBoxList() | |
| { | |
| var needsUpdate = false; | |
| for (var i = 0; i < _lineBoxList.Count; ++i) | |
| { | |
| needsUpdate |= Draw_AnimatedBox(_lineBoxList[i]); | |
| } | |
| return needsUpdate; | |
| } | |
| protected void LineDraw_End(bool needsUpdate) | |
| { | |
| if (_lineData == null) | |
| { | |
| return; | |
| } | |
| // Check if we have any not dirty | |
| var i = 0; | |
| while (i < _lineData.Lines.Count) | |
| { | |
| if (!_lineData.Lines[i].isValid) | |
| { | |
| needsUpdate = true; | |
| _lineData.Lines.RemoveAt(i); | |
| continue; | |
| } | |
| ++i; | |
| } | |
| // Do the update (if needed) | |
| if (needsUpdate) | |
| { | |
| Lines_LineDataToMesh(); | |
| } | |
| } | |
| private void Lines_LineDataToMesh() | |
| { | |
| // Alloc them up | |
| var verts = new Vector3[_lineData.Lines.Count * 8]; | |
| var tris = new int[_lineData.Lines.Count * 12 * 3]; | |
| var colors = new Color[verts.Length]; | |
| // Build the data | |
| for (var i = 0; i < _lineData.Lines.Count; ++i) | |
| { | |
| // Base index calcs | |
| var vert = i * 8; | |
| var v0 = vert; | |
| var tri = i * 12 * 3; | |
| // Setup | |
| var dirUnit = (_lineData.Lines[i].p1 – _lineData.Lines[i].p0).normalized; | |
| var normX = Vector3.Cross((Mathf.Abs(dirUnit.y) >= 0.99f) ? Vector3.right : Vector3.up, dirUnit).normalized; | |
| var normy = Vector3.Cross(normX, dirUnit); | |
| // Verts | |
| verts[vert] = _lineData.Lines[i].p0 + normX * _lineData.Lines[i].lineWidth + normy * _lineData.Lines[i].lineWidth; colors[vert] = _lineData.Lines[i].c0; ++vert; | |
| verts[vert] = _lineData.Lines[i].p0 – normX * _lineData.Lines[i].lineWidth + normy * _lineData.Lines[i].lineWidth; colors[vert] = _lineData.Lines[i].c0; ++vert; | |
| verts[vert] = _lineData.Lines[i].p0 – normX * _lineData.Lines[i].lineWidth – normy * _lineData.Lines[i].lineWidth; colors[vert] = _lineData.Lines[i].c0; ++vert; | |
| verts[vert] = _lineData.Lines[i].p0 + normX * _lineData.Lines[i].lineWidth – normy * _lineData.Lines[i].lineWidth; colors[vert] = _lineData.Lines[i].c0; ++vert; | |
| verts[vert] = _lineData.Lines[i].p1 + normX * _lineData.Lines[i].lineWidth + normy * _lineData.Lines[i].lineWidth; colors[vert] = _lineData.Lines[i].c1; ++vert; | |
| verts[vert] = _lineData.Lines[i].p1 – normX * _lineData.Lines[i].lineWidth + normy * _lineData.Lines[i].lineWidth; colors[vert] = _lineData.Lines[i].c1; ++vert; | |
| verts[vert] = _lineData.Lines[i].p1 – normX * _lineData.Lines[i].lineWidth – normy * _lineData.Lines[i].lineWidth; colors[vert] = _lineData.Lines[i].c1; ++vert; | |
| verts[vert] = _lineData.Lines[i].p1 + normX * _lineData.Lines[i].lineWidth – normy * _lineData.Lines[i].lineWidth; colors[vert] = _lineData.Lines[i].c1; ++vert; | |
| // Indices | |
| tris[tri + 0] = (v0 + 0); tris[tri + 1] = (v0 + 5); tris[tri + 2] = (v0 + 4); tri += 3; | |
| tris[tri + 0] = (v0 + 1); tris[tri + 1] = (v0 + 5); tris[tri + 2] = (v0 + 0); tri += 3; | |
| tris[tri + 0] = (v0 + 1); tris[tri + 1] = (v0 + 6); tris[tri + 2] = (v0 + 5); tri += 3; | |
| tris[tri + 0] = (v0 + 2); tris[tri + 1] = (v0 + 6); tris[tri + 2] = (v0 + 1); tri += 3; | |
| tris[tri + 0] = (v0 + 2); tris[tri + 1] = (v0 + 7); tris[tri + 2] = (v0 + 6); tri += 3; | |
| tris[tri + 0] = (v0 + 3); tris[tri + 1] = (v0 + 7); tris[tri + 2] = (v0 + 2); tri += 3; | |
| tris[tri + 0] = (v0 + 3); tris[tri + 1] = (v0 + 7); tris[tri + 2] = (v0 + 4); tri += 3; | |
| tris[tri + 0] = (v0 + 3); tris[tri + 1] = (v0 + 4); tris[tri + 2] = (v0 + 0); tri += 3; | |
| tris[tri + 0] = (v0 + 0); tris[tri + 1] = (v0 + 3); tris[tri + 2] = (v0 + 2); tri += 3; | |
| tris[tri + 0] = (v0 + 0); tris[tri + 1] = (v0 + 2); tris[tri + 2] = (v0 + 1); tri += 3; | |
| tris[tri + 0] = (v0 + 5); tris[tri + 1] = (v0 + 6); tris[tri + 2] = (v0 + 7); tri += 3; | |
| tris[tri + 0] = (v0 + 5); tris[tri + 1] = (v0 + 7); tris[tri + 2] = (v0 + 4); tri += 3; | |
| } | |
| // Create up the components | |
| if (_lineData.Renderer == null) | |
| { | |
| _lineData.Renderer = gameObject.AddComponent<MeshRenderer>() ?? | |
| gameObject.GetComponent<Renderer>() as MeshRenderer; | |
| _lineData.Renderer.material = MaterialLine; | |
| } | |
| if (_lineData.Filter == null) | |
| { | |
| _lineData.Filter = gameObject.AddComponent<MeshFilter>() ?? gameObject.GetComponent<MeshFilter>(); | |
| } | |
| // Create or clear the mesh | |
| Mesh mesh; | |
| if (_lineData.Filter.mesh != null) | |
| { | |
| mesh = _lineData.Filter.mesh; | |
| mesh.Clear(); | |
| } | |
| else | |
| { | |
| mesh = new Mesh { name = "Lines_LineDataToMesh" }; | |
| } | |
| // Set them into the mesh | |
| mesh.vertices = verts; | |
| mesh.triangles = tris; | |
| mesh.colors = colors; | |
| mesh.RecalculateBounds(); | |
| mesh.RecalculateNormals(); | |
| _lineData.Filter.mesh = mesh; | |
| // If no tris, hide it | |
| _lineData.Renderer.enabled = (_lineData.Lines.Count != 0); | |
| // Line index reset | |
| _lineData.LineIndex = 0; | |
| } | |
| protected bool Draw_AnimatedBox(AnimatedBox box) | |
| { | |
| // Update the time | |
| if (!box.Update(Time.deltaTime)) | |
| { | |
| return false; | |
| } | |
| if (box.IsAnimationComplete) | |
| { | |
| // Animation is done, just pass through | |
| return Draw_Box(box.Center, box.Rotation, box.Color, box.HalfSize, box.LineWidth); | |
| } | |
| // Draw it using the current anim state | |
| return Draw_Box( | |
| box.AnimPosition.Evaluate(box.Time), | |
| box.Rotation * Quaternion.AngleAxis(360.0f * box.AnimRotation.Evaluate(box.Time), Vector3.up), | |
| box.Color, | |
| box.HalfSize * box.AnimScale.Evaluate(box.Time), | |
| box.LineWidth); | |
| } | |
| protected bool Draw_Box(Vector3 center, Quaternion rotation, Color color, Vector3 halfSize, float lineWidth = Line.DefaultLineWidth) | |
| { | |
| var needsUpdate = false; | |
| var basisX = rotation * Vector3.right; | |
| var basisY = rotation * Vector3.up; | |
| var basisZ = rotation * Vector3.forward; | |
| Vector3[] pts = | |
| { | |
| center + basisX * halfSize.x + basisY * halfSize.y + basisZ * halfSize.z, | |
| center + basisX * halfSize.x + basisY * halfSize.y – basisZ * halfSize.z, | |
| center – basisX * halfSize.x + basisY * halfSize.y – basisZ * halfSize.z, | |
| center – basisX * halfSize.x + basisY * halfSize.y + basisZ * halfSize.z, | |
| center + basisX * halfSize.x – basisY * halfSize.y + basisZ * halfSize.z, | |
| center + basisX * halfSize.x – basisY * halfSize.y – basisZ * halfSize.z, | |
| center – basisX * halfSize.x – basisY * halfSize.y – basisZ * halfSize.z, | |
| center – basisX * halfSize.x – basisY * halfSize.y + basisZ * halfSize.z | |
| }; | |
| // Bottom | |
| needsUpdate |= Draw_Line(pts[0], pts[1], color, color, lineWidth); | |
| needsUpdate |= Draw_Line(pts[1], pts[2], color, color, lineWidth); | |
| needsUpdate |= Draw_Line(pts[2], pts[3], color, color, lineWidth); | |
| needsUpdate |= Draw_Line(pts[3], pts[0], color, color, lineWidth); | |
| // Top | |
| needsUpdate |= Draw_Line(pts[4], pts[5], color, color, lineWidth); | |
| needsUpdate |= Draw_Line(pts[5], pts[6], color, color, lineWidth); | |
| needsUpdate |= Draw_Line(pts[6], pts[7], color, color, lineWidth); | |
| needsUpdate |= Draw_Line(pts[7], pts[4], color, color, lineWidth); | |
| // Vertical lines | |
| needsUpdate |= Draw_Line(pts[0], pts[4], color, color, lineWidth); | |
| needsUpdate |= Draw_Line(pts[1], pts[5], color, color, lineWidth); | |
| needsUpdate |= Draw_Line(pts[2], pts[6], color, color, lineWidth); | |
| needsUpdate |= Draw_Line(pts[3], pts[7], color, color, lineWidth); | |
| return needsUpdate; | |
| } | |
| protected bool Draw_Line(Vector3 start, Vector3 end, Color colorStart, Color colorEnd, float lineWidth = Line.DefaultLineWidth) | |
| { | |
| // Create up a new line (unless it's already created) | |
| while (_lineData.LineIndex >= _lineData.Lines.Count) | |
| { | |
| _lineData.Lines.Add(new Line()); | |
| } | |
| // Set it | |
| var needsUpdate = _lineData.Lines[_lineData.LineIndex].Set_IfDifferent(transform.InverseTransformPoint(start), transform.InverseTransformPoint(end), colorStart, colorEnd, lineWidth); | |
| // Inc out count | |
| ++_lineData.LineIndex; | |
| return needsUpdate; | |
| } | |
| public void ClearGeometry(bool clearAll = true) | |
| { | |
| _lineBoxList = new List<AnimatedBox>(); | |
| } | |
| #endregion | |
| } |
El código completo del ejemplo se puede descargar desde aquí (link).
Saludos @ Toronto
El Bruno
References
- GitHub, HoloToolkit
- GitHub, HoloToolkit Unity
- Windows Dev Center, Case study – Expanding the spatial mapping capabilities of HoloLens
- El Bruno, How to Import the HoloToolkit Unity
- El Bruno, How to place a Hologram using AirTap and HoloToolkit
- El Bruno, Creating a menu with options with HoloToolkit
- El Bruno, Using voice commands to display a menu with HoloToolkit
- El Bruno, How to create a 3D text always visible using HoloToolkit
- El Bruno, How to create a HUD (3D text always visible without HoloToolkit)
- El Bruno, How to detect hands using HoloToolkit
- El Bruno, Windows 10, Xbox One Controller, Bluetooth and some lessons learned
- El Bruno, How to use Fire Buttons actions with an XBoxOne Controller
- El Bruno, HoloToolkit compiled packages for Unity3D in GitHub
- El Bruno, How to detect AirTap and Click actions using HoloToolkit
- El Bruno, Detect user hand interactions using #HoloToolkit
- El Bruno, Moving and rotating Holograms using an XBoxOne Controller
- El Bruno, Spatial Understanding vs Spatial Mapping, and a tutorial on how to use them
Leave a Reply