-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How to Save Understanding Mesh #188
Comments
@jbienzms I can share the code no problem. I am triggering the save after mesh finalization as shown in the example. I then just use a voice command "Save room". This calls the function saveRoom() in my RoomSaver class here. using HoloToolkit.Unity;
using UnityEngine;
using UnityEngine.VR.WSA;
using UnityEngine.VR.WSA.Persistence;
using System.Collections;
using System.Collections.Generic;
public class RoomSaver : MonoBehaviour {
public string fileName; // name of file to store meshes
public string anchorStoreName; // name of world anchor to store for room
List<MeshFilter> roomMeshFilters;
WorldAnchorStore anchorStore;
int meshCount = 0;
// Use this for initialization
void Start()
{
WorldAnchorStore.GetAsync(AnchorStoreReady);
}
void AnchorStoreReady(WorldAnchorStore store)
{
anchorStore = store;
}
public void saveRoom()
{
// if the anchor store is not ready then we cannot save the room mesh
if (anchorStore == null)
return;
// delete old relevant anchors
string[] anchorIds = anchorStore.GetAllIds();
for (int i = 0; i < anchorIds.Length; i++)
{
if (anchorIds[i].Contains(anchorStoreName))
{
anchorStore.Delete(anchorIds[i]);
}
}
Debug.Log("Old anchors deleted...");
// get all mesh filters used for spatial mapping meshes
roomMeshFilters = SpatialUnderstanding.Instance.UnderstandingCustomMesh.GetMeshFilters() as List<MeshFilter>;
Debug.Log("Mesh filters fetched...");
// create new list of room meshes for serialization
List<Mesh> roomMeshes = new List<Mesh>();
// cycle through all room mesh filters
foreach (MeshFilter filter in roomMeshFilters)
{
// increase count of meshes in room
meshCount++;
// make mesh name = anchor name + mesh count
string meshName = anchorStoreName + meshCount.ToString();
filter.mesh.name = meshName;
Debug.Log("Mesh " + filter.mesh.name + ": " + filter.transform.position + "\n--- rotation " + filter.transform.localRotation + "\n--- scale: " + filter.transform.localScale);
// add mesh to room meshes for serialization
roomMeshes.Add(filter.mesh);
// save world anchor
WorldAnchor attachingAnchor = filter.gameObject.GetComponent<WorldAnchor>();
if (attachingAnchor == null)
{
attachingAnchor = filter.gameObject.AddComponent<WorldAnchor>();
Debug.Log("" + filter.mesh.name + ": Using new anchor...");
}
else
{
Debug.Log("" + filter.mesh.name + ": Deleting existing anchor...");
DestroyImmediate(attachingAnchor);
Debug.Log("" + filter.mesh.name + ": Creating new anchor...");
attachingAnchor = filter.gameObject.AddComponent<WorldAnchor>();
}
if (attachingAnchor.isLocated)
{
if (!anchorStore.Save(meshName, attachingAnchor))
Debug.Log("" + meshName + ": Anchor save failed...");
else
Debug.Log("" + meshName + ": Anchor SAVED...");
}
else
{
attachingAnchor.OnTrackingChanged += AttachingAnchor_OnTrackingChanged;
}
}
// serialize and save meshes
MeshSaver.Save(fileName, roomMeshes);
}
private void AttachingAnchor_OnTrackingChanged(WorldAnchor self, bool located)
{
if (located)
{
string meshName = self.gameObject.GetComponent<MeshFilter>().mesh.name;
if (!anchorStore.Save(meshName, self))
Debug.Log("" + meshName + ": Anchor save failed...");
else
Debug.Log("" + meshName + ": Anchor SAVED...");
self.OnTrackingChanged -= AttachingAnchor_OnTrackingChanged;
}
}
} Then I load it with my RoomLoader class that I attach to my GameManager. This requires a prefab with the basic components (MeshFilter, MeshRenderer, MeshCollider). using HoloToolkit.Unity;
using UnityEngine;
using UnityEngine.VR.WSA.Persistence;
using System.Collections;
using System.Collections.Generic;
public class RoomLoader : MonoBehaviour {
public GameObject surfaceObject; // prefab for surface mesh objects
public string fileName; // name of file used to store mesh
public string anchorStoreName; // name of world anchor for room
List<Mesh> roomMeshes; // list of meshes saved from spatial mapping
WorldAnchorStore anchorStore; // store of world anchors
// Use this for initialization
void Start () {
// get instance of WorldAnchorStore
WorldAnchorStore.GetAsync(AnchorStoreReady);
}
// Update is called once per frame
void Update () {
}
void AnchorStoreReady(WorldAnchorStore store)
{
// save instance
anchorStore = store;
// load room meshes
roomMeshes = MeshSaver.Load(fileName) as List<Mesh>;
foreach (Mesh surface in roomMeshes)
{
GameObject obj = Instantiate(surfaceObject) as GameObject;
obj.GetComponent<MeshFilter>().mesh = surface;
obj.GetComponent<MeshCollider>().sharedMesh = surface;
if (!anchorStore.Load(surface.name, obj))
Debug.Log("WorldAnchor load failed...");
Debug.Log("Mesh " + surface.name + " Position: " + obj.transform.position + "\n--- Rotation: " + obj.transform.localRotation + "\n--- Scale: " + obj.transform.localScale);
}
GameObject.Find("GameManager").GetComponent<GameManager>().RoomLoaded();
}
} In both classes, the anchor store name needs to be set to the same value. One is for saving the world anchors, the other is for loading them. Also, it is important to note that I am using the Mesh saver but I extended the SimpleMeshSerializer.cs to support saving mesh names. This is critical to associating the saved mesh with the right world anchor. Changed code is as follows: // Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
using System.Collections.Generic;
using SysDiag = System.Diagnostics;
using System.IO;
using UnityEngine;
namespace HoloToolkit.Unity
{
///
/// SimpleMeshSerializer converts a UnityEngine.Mesh object to and from an array of bytes.
/// This class saves minimal mesh data (vertices and triangle indices) in the following format:
/// File header: vertex count (32 bit integer), triangle count (32 bit integer)
/// Vertex list: vertex.x, vertex.y, vertex.z (all 32 bit float)
/// Triangle index list: 32 bit integers
///
public static class SimpleMeshSerializer
{
///
/// The mesh header consists of two 32 bit integers.
///
private static int HeaderSize = sizeof(int) * 2;
/// <summary>
/// Serializes a list of Mesh objects into a byte array.
/// </summary>
/// <param name="meshes">List of Mesh objects to be serialized.</param>
/// <returns>Binary representation of the Mesh objects.</returns>
public static byte[] Serialize(IEnumerable<Mesh> meshes)
{
byte[] data = null;
using (MemoryStream stream = new MemoryStream())
{
using (BinaryWriter writer = new BinaryWriter(stream))
{
foreach (Mesh mesh in meshes)
{
WriteMesh(writer, mesh);
}
stream.Position = 0;
data = new byte[stream.Length];
stream.Read(data, 0, data.Length);
}
}
return data;
}
/// <summary>
/// Deserializes a list of Mesh objects from the provided byte array.
/// </summary>
/// <param name="data">Binary data to be deserialized into a list of Mesh objects.</param>
/// <returns>List of Mesh objects.</returns>
public static IEnumerable<Mesh> Deserialize(byte[] data)
{
List<Mesh> meshes = new List<Mesh>();
using (MemoryStream stream = new MemoryStream(data))
{
using (BinaryReader reader = new BinaryReader(stream))
{
while (reader.BaseStream.Length - reader.BaseStream.Position >= HeaderSize)
{
meshes.Add(ReadMesh(reader));
}
}
}
return meshes;
}
/// <summary>
/// Writes a Mesh object to the data stream.
/// </summary>
/// <param name="writer">BinaryWriter representing the data stream.</param>
/// <param name="mesh">The Mesh object to be written.</param>
private static void WriteMesh(BinaryWriter writer, Mesh mesh)
{
SysDiag.Debug.Assert(writer != null);
// Write the mesh data.
WriteMeshHeader(writer, mesh.name, mesh.vertexCount, mesh.triangles.Length);
WriteVertices(writer, mesh.vertices);
WriteTriangleIndicies(writer, mesh.triangles);
}
/// <summary>
/// Reads a single Mesh object from the data stream.
/// </summary>
/// <param name="reader">BinaryReader representing the data stream.</param>
/// <returns>Mesh object read from the stream.</returns>
private static Mesh ReadMesh(BinaryReader reader)
{
SysDiag.Debug.Assert(reader != null);
int vertexCount = 0;
int triangleIndexCount = 0;
string name;
// Read the mesh data.
ReadMeshHeader(reader, out name, out vertexCount, out triangleIndexCount);
Vector3[] vertices = ReadVertices(reader, vertexCount);
int[] triangleIndices = ReadTriangleIndicies(reader, triangleIndexCount);
// Create the mesh.
Mesh mesh = new Mesh();
mesh.name = name;
mesh.vertices = vertices;
mesh.triangles = triangleIndices;
// Reconstruct the normals from the vertices and triangles.
mesh.RecalculateNormals();
return mesh;
}
/// <summary>
/// Writes a mesh header to the data stream.
/// </summary>
/// <param name="writer">BinaryWriter representing the data stream.</param>
/// <param name="vertexCount">Count of vertices in the mesh.</param>
/// <param name="triangleIndexCount">Count of triangle indices in the mesh.</param>
private static void WriteMeshHeader(BinaryWriter writer, string name, int vertexCount, int triangleIndexCount)
{
SysDiag.Debug.Assert(writer != null);
writer.Write(name);
writer.Write(vertexCount);
writer.Write(triangleIndexCount);
}
/// <summary>
/// Reads a mesh header from the data stream.
/// </summary>
/// <param name="reader">BinaryReader representing the data stream.</param>
/// <param name="vertexCount">Count of vertices in the mesh.</param>
/// <param name="triangleIndexCount">Count of triangle indices in the mesh.</param>
private static void ReadMeshHeader(BinaryReader reader, out string name, out int vertexCount, out int triangleIndexCount)
{
SysDiag.Debug.Assert(reader != null);
name = reader.ReadString();
vertexCount = reader.ReadInt32();
triangleIndexCount = reader.ReadInt32();
}
/// <summary>
/// Writes a mesh's vertices to the data stream.
/// </summary>
/// <param name="reader">BinaryReader representing the data stream.</param>
/// <param name="vertices">Array of Vector3 structures representing each vertex.</param>
private static void WriteVertices(BinaryWriter writer, Vector3[] vertices)
{
SysDiag.Debug.Assert(writer != null);
foreach (Vector3 vertex in vertices)
{
writer.Write(vertex.x);
writer.Write(vertex.y);
writer.Write(vertex.z);
}
}
/// <summary>
/// Reads a mesh's vertices from the data stream.
/// </summary>
/// <param name="reader">BinaryReader representing the data stream.</param>
/// <param name="vertexCount">Count of vertices to read.</param>
/// <returns>Array of Vector3 structures representing the mesh's vertices.</returns>
private static Vector3[] ReadVertices(BinaryReader reader, int vertexCount)
{
SysDiag.Debug.Assert(reader != null);
Vector3[] vertices = new Vector3[vertexCount];
for (int i = 0; i < vertices.Length; i++)
{
vertices[i] = new Vector3(reader.ReadSingle(),
reader.ReadSingle(),
reader.ReadSingle());
}
return vertices;
}
/// <summary>
/// Writes the vertex indices that represent a mesh's triangles to the data stream
/// </summary>
/// <param name="writer">BinaryWriter representing the data stream.</param>
/// <param name="triangleIndices">Array of integers that describe how the vertex indices form triangles.</param>
private static void WriteTriangleIndicies(BinaryWriter writer, int[] triangleIndices)
{
SysDiag.Debug.Assert(writer != null);
foreach (int index in triangleIndices)
{
writer.Write(index);
}
}
/// <summary>
/// Reads the vertex indices that represent a mesh's triangles from the data stream
/// </summary>
/// <param name="reader">BinaryReader representing the data stream.</param>
/// <param name="triangleIndexCount">Count of indices to read.</param>
/// <returns>Array of integers that describe how the vertex indices form triangles.</returns>
private static int[] ReadTriangleIndicies(BinaryReader reader, int triangleIndexCount)
{
SysDiag.Debug.Assert(reader != null);
int[] triangleIndices = new int[triangleIndexCount];
for (int i = 0; i < triangleIndices.Length; i++)
{
triangleIndices[i] = reader.ReadInt32();
}
return triangleIndices;
}
}
} Enjoy. |
Hey @JeffPack, your post needs a formatting correction pass |
Thank you again for sharing this @JeffPack. I was hoping to be able to use the Surface Understanding mesh at design time in Unity. If I'm reading this correctly, it looks like this only works at runtime. Is that right? The new ObjectSurfaceObserver lets you specify a prefab that can be used at design time as the Surface Mesh. I was hoping to grab the Surface Understanding mesh and use it in concert with this. However, I see you're using MeshSaver which (unfortunately) saves the mesh off in a custom format that Unity doesn't understand. This is why people usually end up going to the Device Portal and saving the room mesh as a .obj file and then bringing that in and using with ObjectSurfaceObserver. One of the last pieces I still haven't figured out in all of this is how the SpatialMappingSource gets set. ObjectSurfaceObserver, FileSurfaceObserver, SpatialMappingObserver, RemoteMeshTarget and SpatialUnderstandingCustomMesh all inherit from SpatialMappingSource. And I see that SpatialMappingManager has a method called SetSpatialMappingSource that allows you to set the source. But I'm still trying to understand which one gets selected if the scene includes more than one (for example an ObjectSurfaceObserver and a SpatialUnderstandingCustomMesh). I'm not clear if I have to do something to detect when I'm running in the editor and enable / disable them or if that's handled automatically. |
Ah! I just answered this for myself. ObjectSurfaceObserver's Start method only contains code when UNITY_EDITOR is defined. Conversely, SpatialUnderstanding.AllowSpatialUnderstanding is only true when UNITY_EDITOR is NOT defined. So they can both be in the scene at the same time. SpatialMappingObserver MUST be in the scene because SpatialMappingManager requires it through a RequireComponent attribute. It's also set as the default source for the SpatialMappingManager. ObjectSurfaceObserver overwrites the default in its Start handler, but interestingly SpatialUnderstanding doesn't do this. In fact, I can't find anywhere in the sample at all where SpatialUnderstandingCustomMesh gets set as the source for SpatialMappingManager. This seems to be a bug. I will bring this up in the pull request thread. |
Nope, I take that back. SpatialMappingManager.Source becomes the source for the processing done by the DLL and eventually stored back into SpatialUnderstandingCustomMesh. Both would not be visible at the same time, but it does appear both are needed. |
@jbienzms Sorry I misinterpreted what you were saying. If, by design time, you mean runtime within the Unity editor, then yes you can still use mesh saver, because calling Mesh.Load() returns a list of Mesh components which you can then assign to prefabs like I do in RoomLoader.cs Though the trick here is that you would have to extend SimpleMeshSerializer.cs again so that it could save the position and orientation of that meshes gameObject so as not to depend on the WorldAnchor system, which as I am sure you know, is not available from within the Unity editor. Once scanned and saved you would need to get them from your hololens to your computer. If, by design time, you mean when Unity is NOT running your application, then you would probably need to grab the meshes after finalization and use something like ObjExporter (found here) on them, although this could be slightly complicated to extend as there is more than one mesh involved. Again, you would then need to get the obj files from your hololens to your computer. |
@riverar Sorry! It was such a quick copy and paste job. It should be fixed now. |
Possible action: There might be enough community interest (I just saw a forum question about this topic) to warrant creating a Spatial Understanding Save/Load component for the HoloToolkit. |
I'd love to see it added directly to the understanding module as base functionality. |
@jevertt @NeerajW @angelaHillier Cool! Maybe we can make a new Issue outlining the desired change? |
This would be a wonderful enhancement. Do you want me to change the description or do you want to close this one and open another? The primary purpose would be to get a much cleaner mesh than what we currently get out of the Device Portal. If we could have the ultimate request I'd love to see an option to get the Spatial Understanding mesh from the Device Portal if an app is running and SU has been initialized. I'm not sure if that's possible though. |
I think a new issue makes sense, and you can just reference this one. It'll focus the conversation to reboot I think. |
Is there a way to get SpatialUnderstanding "understand" the mesh of the RoomModel property of the ObjectSurfaceObserver? It would be great to have this working, because it would allow us to use Unity Editor to check how a particular mesh stored in OBJ format could be interpreted by the Spatial Understanding module. |
My work does the first step of saving either the Spatial Mapping or Spatial Understanding meshes. There's still work that will need to be done to enable the "understanding" portion to run on a saved mesh within the Unity Editor. |
@mrbobbybobberson, did #432 also address this issue? |
I believe it addressed it only in a minor way, but there’s a lot more that can be done for #188.
…Sent from my Windows 10 phone
From: Stephen Hodgson
Sent: Wednesday, February 15, 2017 7:19 AM
To: Microsoft/HoloToolkit-Unity
Cc: Robert El-Soudani; Mention
Subject: Re: [Microsoft/HoloToolkit-Unity] How to Save Understanding Mesh(#188)
@mrbobbybobberson, did #290 also address this?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or mute the thread.
|
@matmuze has some great ideas on that.
…Sent from my Windows 10 phone
From: mrbobbybobberson@gmail.com
Sent: Wednesday, February 15, 2017 12:38 PM
To: Microsoft/HoloToolkit-Unity; Microsoft/HoloToolkit-Unity
Cc: Mention
Subject: RE: [Microsoft/HoloToolkit-Unity] How to Save UnderstandingMesh(#188)
I believe it addressed it only in a minor way, but there’s a lot more that can be done for #188.
Sent from my Windows 10 phone
From: Stephen Hodgson
Sent: Wednesday, February 15, 2017 7:19 AM
To: Microsoft/HoloToolkit-Unity
Cc: Robert El-Soudani; Mention
Subject: Re: [Microsoft/HoloToolkit-Unity] How to Save Understanding Mesh(#188)
@mrbobbybobberson, did #290 also address this?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or mute the thread.
|
Hi, @jbienzms, I can not run your example. I create roomLoader and roomSaver and modify SimpleMeshSerializer. I save and load correcty the room, but I want to use the SpatialUndestandingDLL query to place the gameobject on wall o sittable, for ex. |
That code above is actually from @JeffPack. Jeff, could you weigh in on @Claudio69's question above? Thanks! |
This code doesn't save any of the topological information (the understanding part of the module). It just saves the simplified mesh which is much much cleaner than the native one. Capturing the topological information would require a more in depth method. Perhaps you can pre-query this information and save it under a separate file with it's own world anchors? Not sure... |
Hi @JeffPack , I am trying to save the meshes created from the SpatialUnderstanding scan on HoloLens as .obj files. I retrieved the meshes as you did using
Then I updated the MeshSaver.cs to save .obj file as below:
I used the ObjExporter.cs with some updates as I do not need the material information. And here is how I call it after calling SpatialUnderstanding.Instance.RequestFinishScan();
Now what happens is that it saves a very small part of the scene but not the whole scene scanned. From your comment here https://github.com/Microsoft/HoloToolkit-Unity/pull/187 I think I am calling it before the mesh finalization. So I wanted to know where do you actually call the save function? Where are the meshes finalized? |
@AliaAlaaElDinAdly Have you been able to find a solution for that ? I have been stuck in a similar situation. My goal is to save the whole mesh as one obj file. I have been using starting scene as example scene in Holotoolkit for Saving spatial mesh. I also used objExporter.cs MeshToString function so that I can convert a mesh into a stream of strings and then POST it as any other text to the cloud. Till now, I have been able to extract individual sub-meshes of scanned surface and put them to cloud. For example My house consists of 42 sub-meshes. Somebody please help to identify where in the code can I access the whole mesh as once. Once I get it, I shall be able to directly export it as single obj file. I read somewhere worldanchors can help but I am not sure. |
Going to close this. It's probably been archived by google by now. |
Has the issue been resolved? Can we add a link here? |
Actually upon second review probably not. I saw a few PR merges that addressed in some way. |
I have setup start and stop spatial understanding through voice commands but got stuck after that. I tried to save the mesh but then instead of creating a different class for Load mesh on a prefab, Can we load the saved .obj of mesh on the ObjectSurfaceObserver in runtime after we finished scanning and saving it? if not, what are the potential issues with it? |
Hello, Any advice ? Additionally, I can't find the function to load so how (or maybe when) does it really load the stored mesh please ? EDIT :Okay, as I understand the tools we have I should use the MeshSaver script (thanks for your work @krishnan-unni btw 👍). But I do not see the difference between "SaveSpatialUnderstanding" and "SaveSpatialMapping" regarding this script. Obviously SpatialUnderstanding is present in the relevant example but MeshSaver is placed in SpatialMapping, so I tend to understand that it will save mapping mesh and not understanding mesh like I wish. |
Hello to everyone ,
I wish, this common problem "no have idea how to save this" will be solved soon by our community |
@AliaAlaaElDinAdly came very close to solving the Save Mesh as OBJ issue.
The reason for only seeing "part of the scene" is that the faces are sequential for all objects. If faces for Mesh 1 start from 1 and end at 100 then for Mesh 2, the must start from 101. A quick fix for this will be adding a static field, faceCount and increment:
|
So unfortunately I do not develop for the Hololens anymore so I can't help resolve this issue. My apologies! I hope it all comes together for you soon. |
- Using @JeffPack microsoft#188 solution
- Using @JeffPack microsoft#188 solution
I'd like to understand how to save the custom mesh built by the new Spatial Understanding contributed by @jevertt. A sample was supplied by @JeffPack inside the pull request but hopefully we can get it moved here.
The text was updated successfully, but these errors were encountered: