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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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