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