Mesh to string conversion taking too long, hope to get some optimization suggestions

Hi all,

I believe this is an interesting problem that was solved in a probably not very efficient way, and I was hoping to get some advice on optimization.

My HoloLens app needs to retrieve the mesh of the surroundings within a certain radius and send it to the server as a string. I know strings aren’t very efficient but this is just how TCP/IP communication was built and I can’t change it now.

HoloLens creates the mesh of the surroundings in hundreds of small pieces with about 500 faces each. Within Unity, I retrieve all these small pieces and create 4 strings (representing vertices, triangles, and their total numbers) formatted to follow the structure of a .ply file format. Strings are sent to the server which adds a header, saves the .ply (one large mesh created from hundreds of small pieces) and passes it to other functions.

The way I wrote this works, but is quite slow and can take more than 6 seconds with about 50 000 faces. I tried gathering all vertices and triangles of these small pieces before converting them to a string, but lists are very slow and I can’t use arrays as I don’t know the final size.

I was hoping to get some optimization suggestions! Thanks in advance

The relevant code is below:

//Strings to hold vertices, triangle (faces) and their numbers. To be sent to the server.
private string verticesString;
private string trianglesString;
private string verticesNumString;
private string trianglesNumString;

GameObject boundary;

private void Start()
{
    boundary = GameObject.FindGameObjectWithTag("Boundary");
}

public void RetrieveAndSerializeMesh()
{
    var observer = CoreServices.GetSpatialAwarenessSystemDataProvider<IMixedRealitySpatialAwarenessMeshObserver>();

    //As one large mesh is created from dozens of small pieces, vertex indices that form triangles must be corrected
    int vertexIndexCorrection = 0;

    int verticesNum = 0;
    int trianglesNum = 0;

    //Only mesh pieces within this boundary are retrieved
    Bounds boundaryBounds = boundary.GetComponent<MeshRenderer>().bounds;

    foreach (SpatialAwarenessMeshObject meshObject in observer.Meshes.Values)
    {
        Mesh mesh = meshObject.Filter.mesh;

        if (boundaryBounds.Contains(mesh.bounds.center))
        {
            //Transform of the GO is needed for conversion from local to world coordinates of mesh vertices.
            Transform tf = meshObject.Filter.GetComponent<Transform>();
          
            verticesString += SerializeVector3Array(mesh.vertices, tf);
            trianglesString += SerializeIntArray(mesh.triangles, vertexIndexCorrection);

            vertexIndexCorrection += mesh.vertexCount;

            verticesNum += mesh.vertexCount;
            trianglesNum += (mesh.triangles.Length / 3);
        }
    }
}

//Modified from https://answers.unity.com/questions/1030082/convert-vector-3-to-string-and-reverse-c.html
//Convert a Vector3 array to string.
public static string SerializeVector3Array(Vector3[] aVectors, Transform tf)
{
    StringBuilder sb = new StringBuilder();

    foreach (Vector3 v in aVectors)
    {
        Vector3 vt = tf.TransformPoint(v);
        sb.Append(vt.x).Append(" ").Append(vt.y).Append(" ").Append(vt.z).Append("\n");
    }

    return sb.ToString();
}

//Convert an int array of mesh triangles to a string in the format of .ply.
public static string SerializeIntArray(int[] ints, int indexCorrection)
{
    StringBuilder sb = new StringBuilder();
    int len = ints.Length;
    int i = 0;

    while (i < len)
    {
        sb.Append("3 ").Append(ints[i] + indexCorrection).Append(" ").Append(ints[i + 1] + indexCorrection).Append(" ").Append(ints[i + 2] + indexCorrection).Append("\n");
        i += 3;
    }

    return sb.ToString();
}

EDIT: Code after implementing suggestions from @Bunny83

//Strings to hold vertices, triangle (faces) and their numbers. To be sent to the server.
private string verticesString;
private string trianglesString;
private string verticesNumString;
private string trianglesNumString;

private StringBuilder sbVertices = new StringBuilder();
private StringBuilder sbTriangles = new StringBuilder();

private List<Vector3> listVertices = new List<Vector3>();
private List<int> listTriangles = new List<int>();

GameObject boundary;

private void Start()
{
    boundary = GameObject.FindGameObjectWithTag("Boundary");
}

public void RetrieveAndSerializeMesh()
{
    var observer = CoreServices.GetSpatialAwarenessSystemDataProvider<IMixedRealitySpatialAwarenessMeshObserver>();

    int verticesNum = 0;
    int trianglesNum = 0;

    //Only mesh pieces within this boundary are retrieved
    Bounds boundaryBounds = boundary.GetComponent<MeshRenderer>().bounds;

    foreach (SpatialAwarenessMeshObject meshObject in observer.Meshes.Values)
    {
        Mesh mesh = meshObject.Filter.mesh;

        if (boundaryBounds.Contains(mesh.bounds.center))
        {
            //Transform of the GO is needed for conversion from local to world coordinates of mesh vertices.
            Transform tf = meshObject.Filter.GetComponent<Transform>();

            listVertices.Clear();
            listTriangles.Clear();

            mesh.GetVertices(listVertices);
            mesh.GetTriangles(listTriangles, 0);

            SerializeVector3Array(sbVertices, listVertices, tf);
            SerializeIntArray(sbTriangles, listTriangles, verticesNum);

            verticesNum += listVertices.Count;
            trianglesNum += (listTriangles.Count / 3);
        }
    }
   
    //After the string streams are built convert them to strings to be sent to the server.
    verticesString = sbVertices.ToString();
    trianglesString = sbTriangles.ToString();

    verticesNumString = verticesNum.ToString();
    trianglesNumString = trianglesNum.ToString();
}

//Modified from https://answers.unity.com/questions/1030082/convert-vector-3-to-string-and-reverse-c.html
//Convert a Vector3 array to string.
public static void SerializeVector3Array(StringBuilder sb, List<Vector3> aVectors, Transform tf)
{       
    foreach (Vector3 v in aVectors)
    {
        Vector3 vt = tf.TransformPoint(v);
        sb.Append(vt.x).Append(" ").Append(vt.y).Append(" ").Append(vt.z).Append("\n");
    }
}

//Convert an int array of mesh triangles to a string in the format of .ply. 
public static void SerializeIntArray(StringBuilder sb, List<int> ints, int indexCorrection)
{
    int len = ints.Count;
    int i = 0;
    while (i < len)
    {
        sb.Append("3 ").Append(ints[i] + indexCorrection).Append(" ").Append(ints[i + 1] + indexCorrection).Append(" ").Append(ints[i + 2] + indexCorrection).Append("\n");
        i += 3;
    }
}

Serializing to a string is unnecessary and wasteful. You would be better off using a compact binary format here, which will save on both network bandwidth and parsing/encoding time.

You really need to change this. It’s the main thing that’s causing the issue.

Another small optimization would be to cache the mesh.triangles array, which you are reading twice, and reuse it. Every time you call mesh.triangles it copies an entirely fresh copy of the triangles array into managed memory.

I would highly recommend to use the newer GetVertices and GetTriangles methods that can take a pre-allocated List instead of returning a new array each time. This may be the core reason why it’s slow. You’re allocating tons of arrays. Second since you said about 500 vertices per piece that means about 100 pieces for 50k. When you’re constructing large strings, always use a StringBuilder, also a single one. You have so many pointless ToString conversions. In your main loop you’re again concating your long strings with +=. This will eat up tons of memory and processing power.

So you should refactor your “SerializeVector3Array” and “SerializeIntArray” method to take a List instead of an array and pre allocate that list once. Just call Clear for the next piece. Second instead of having those two methods create a new stringbuilder each time and then return a string at the end, just pass the stringbuilder in as parameter and don’t return anything. Create one stringbuilder outside that actually acculmulates all the data in one builder (Or two in your current layout, one for the vertices and one for the triangles).

I would also highly recommend to use a binary format for such data. Note even when your protocol is text based, you can sill embed the binary end result base64 encoded. However it’s not clear if you actually need to do any processing / parsing serverside. When you send something to a server there has to be a reason why you do that and what you actually want to do with that data. Just store it for later retrieve? Or do you have to parse it serverside?

Anyways, I highly recommend you pull up the profiler in Unity, enable deep profiling and watch one of your serialization run carefully and see how much memory you’re allocating at the moment. With the optimisation I mentioned it should be cut down by an order of magnitude. Note if you can estimate how large the final strings in the stringbuilder may get, you should initialize it with that estimate. This reduces the internal re-allocation of the buffer.

1 Like

Hi, thank you so much for the help, you’re a legend.

I implemented all of your suggestions except for the one in the last paragraph with the profiler and memory allocation, as I don’t really know how to do it.

The result is great in the editor - it cuts the time for about 70%. However, on HoloLens there is basically no difference. I edited the main post and put the new code in, could you please have a quick look to see if I messed something up?

I’ll try to explain why I’m even sending the mesh to the server. I’m a PhD student proposing a novel type of localization for augmented reality - essentially, I’m trying to place holograms in correct places within the environment. I’m doing that by running a deep neural network on a GPU-based linux server, which after receiving the mesh from HL proceeds with 3D to 3D model registration.

Networking was very challenging for me to set up - I get by but I’m not a professional coder. I managed to set up a C++ TCP server on linux and establish a communication with HL using strings, not knowing how slow it’s gonna be. Now, everything is built already and the localization prototype works. It’s slow and I’d like to speed it up, but changing from a string to a binary format for me at this stage would probably take too long.

Thanks again!

Agree with previous replies, your main issue here is a bandwidth problem and limiting yourself to only sending strings across the network is going to be heavily limiting in terms of processing power to convert data and overall bandwidth.

Do you even need the mesh data?
Do you need surface normals or just a way to reconstruct world position?

Can you perhaps render that mesh data in a different camera as a depth texture then send that along with the camera projection matrix to reconstruct position from that?

If you need 360 degrees of depth you could have multiple cameras and render to a cube map.

Depending on your actual use case you might be able to get away with rendering depth at a fairly low resolution and smartly sampling and interpolating the depth data (using some history to exclude obvious discontinuous samples) etc.

I need the world position of the mesh, 360 degrees, in as high a resolution as possible. I don’t need the normals and don’t send them across at all.

Hmm, I wouldn’t even know where to start with the depth texture and rendering to a cube map. Would you be so good as to provide more detail?

Another idea might be (but it will only work with simple mesh and lose some details for sure), is to deconstruct your meshes vertices and get their coordinates info, and compose them by using a separator (|) into one string. After doing that you can start to reconstruct it in unity by some quick algorithm (for eg. MIConvexHull) that will returns you a convex mesh by reading your string points.

Thanks, I think I ended up doing something like this (I don’t really remember at this point as it’s been a couple of years)