Hit Enter or
TAGS:  unity3d (9)  art (6)  experience (4)  hci (4)  virtual reality (4)  vr (4)  ar (3)  computer-human-interaction (3)  digital-art (3)  generative (3)  installation (3)  interactive (3)  augmented reality (2)  business-activation (2)  graphics (2)  math (2)  p5js (2)  research (2)  shaders (2)  tangible-interfaces (2)  tutorial (2)  algorithm (1)  app (1)  architecture (1)  arduino (1)  c# (1)  c#" (1)  design (1)  development (1)  engineering (1)  git (1)  iot (1)  maths (1)  mesh (1)  nft (1)  programming (1)  sdk (1)  serial (1)  snapchat (1)  tools (1)  visualization (1)  work (1) 

Save and load mesh data in Unity

Table Of Contents

Don’t we all have a hell of a fun generating meshes in Unity? The problems are:

  • once you generated or changed an existing mesh, how do you keep it?
  • you probably found out by now that a mesh contains fields that are not marked as Serializable

The following comes handy in all situations where a generated mesh is rather complex and loading it from disk is more efficient than re-generating.

Bear in mind: we are not talking about exporting to some formats like FBX or DAE but rather about dumping our mesh data (vertices, triangles, uvs, …) to a file and read it later to become a Unity Mesh object again. We will only write the mesh data to a file in a binary format, leaving out materials and textures that may be connected to your MeshRenderer.

What data does it handle?

  • Vertices
  • Triangles
  • UV and UV2
  • Vertex Colours

Limitations:

  • Mesh must be below 65000 vertices
  • Not a fancy exporter, this only saves to and loads from a binary dump of the mesh topology.
  • The resulting data is an uncompressed binary dump

Serializable Mesh Info

To serialize out mesh data we need a sort of intermediary class that holds all data in basic types containers. In fact, in Unity and in general, non-basic types like Vector3 cannot be serialized and we have to work around it by storing 3 float values instead. This procedure is often called flattening.

The class SerializableMeshInfo will be our serializable container for Unity’s mesh data.

SerializableMeshInfo.cs

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
class SerializableMeshInfo
{
    private float[] vertices;
    private int[] triangles;
    private float[] uv;
    private float[] uv2;
    private float[] normals;
    private float[] colors;

    public SerializableMeshInfo(Mesh m) // Constructor: takes a mesh and fills out SerializableMeshInfo data structure which basically mirrors Mesh object's parts.
    {
        vertices = new float[m.vertexCount * 3]; // initialize flattened vertices array.
        for (int i = 0; i < m.vertexCount; i++)
        {
            vertices[i * 3] = m.vertices[i].x;
            vertices[i * 3 + 1] = m.vertices[i].y;
            vertices[i * 3 + 2] = m.vertices[i].z;
        }
        triangles = new int[m.triangles.Length]; // initialize triangles array (1-dimensional so no need for flattening)
        for (int i = 0; i < m.triangles.Length; i++)
        {
            triangles[i] = m.triangles[i];
        }
        uv = new float[m.uv.Length * 2]; // initialize flattened uvs array
        for (int i = 0; i < m.uv.Length; i++)
        {
            uv[i * 2] = m.uv[i].x;
            uv[i * 2 + 1] = m.uv[i].y;
        }
        uv2 = new float[m.uv2.Length * 2]; // uv2
        for (int i = 0; i < m.uv2.Length; i++)
        {
            uv2[i * 2] = m.uv2[i].x;
            uv2[i * 2 + 1] = m.uv2[i].y;
        }
        normals = new float[m.normals.Length * 3]; // initialize flattened normals array
        for (int i = 0; i < m.normals.Length; i++)
        {
            normals[i * 3] = m.normals[i].x;
            normals[i * 3 + 1] = m.normals[i].y;
            normals[i * 3 + 2] = m.normals[i].z;
        }
        
        colors = new float[m.colors.Length * 4];
        for (int i = 0; i < m.colors.Length; i++)
        {
            colors[i * 4] = m.colors[i].r;
            colors[i * 4 + 1] = m.colors[i].g;
            colors[i * 4 + 2] = m.colors[i].b;
            colors[i * 4 + 3] = m.colors[i].a;
        }
    }

    // GetMesh gets a Mesh object from the current data in this SerializableMeshInfo object.
    // Sequential values are deserialized to Mesh original data types like Vector3 for vertices.
    public Mesh GetMesh()
    {
        Mesh m = new Mesh();
        List<Vector3> verticesList = new List<Vector3>();
        for (int i = 0; i < vertices.Length / 3; i++)
        {
            verticesList.Add(new Vector3(
                    vertices[i * 3], vertices[i * 3 + 1], vertices[i * 3 + 2]
                ));
        }
        m.SetVertices(verticesList);
        m.triangles = triangles;
        List<Vector2> uvList = new List<Vector2>();
        for (int i = 0; i < uv.Length / 2; i++)
        {
            uvList.Add(new Vector2(
                    uv[i * 2], uv[i * 2 + 1]
                ));
        }
        m.SetUVs(0, uvList);
        List<Vector2> uv2List = new List<Vector2>();
        for (int i = 0; i < uv2.Length / 2; i++)
        {
            uv2List.Add(new Vector2(
                    uv2[i * 2], uv2[i * 2 + 1]
                ));
        }
        m.SetUVs(1, uv2List);
        List<Vector3> normalsList = new List<Vector3>();
        for (int i = 0; i < normals.Length / 3; i++)
        {
            normalsList.Add(new Vector3(
                    normals[i * 3], normals[i * 3 + 1], normals[i * 3 + 2]
                ));
        }
        m.SetNormals(normalsList);

        List<Color> colorsList = new List<Color>();
        for (int i = 0; i < colors.Length / 4; i++)
        {
            colorsList.Add(new Color(
                    colors[i * 4],
                    colors[i * 4 + 1],
                    colors[i * 4 + 2],
                    colors[i * 4 + 3]
                ));
        }
        m.SetColors(colorsList);

        return m;
    }

}

Side note: since version 5.5.2, Unity introduced more methods like non-allocating accessors to Mesh (GetBindposes, GetBoneWeights, GetColors, GetIndices, GetNormals, GetTangents, GetTriangles, GetVertices), that return mesh data into a given user-specified List. These can be used to access the Mesh data instead of accessing its arrays as above.

Generic Binary Save/Load Methods

To have save and load functionalities we can create a simple, generic implementation in a static class.
These are not bound to our example, they can save and load any data that’s serializable.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

public static class DataSerializer
{
    public static void Save<T>(T data, string filePath)
    {
        try
        {
            using (FileStream fileStream = new FileStream(filePath, FileMode.Create))
            {
                IFormatter formatter = new BinaryFormatter();
                formatter.Serialize(fileStream, data);
            }
        }
        catch
        {
            // Forward exceptions
            throw;
        }
    }

    public static T Load<T>(string filePath)
    {
        try
        {
            using (FileStream fileStream = new FileStream(filePath, FileMode.Open))
            {
                IFormatter formatter = new BinaryFormatter();
                return (T)formatter.Deserialize(fileStream);
            }
        }
        catch
        {
           // Forward exceptions
           throw;
           // return default;
        }
    }
}

Save and Load Example

Now that we have a completely serializable data structure for our mesh and a way to store and load it, we can write a quick test script that saves a mesh and then loads it at start. To test:

  • Attach the following script to a GameObject
  • Set the mesh slot with a mesh of your choice (leave the mesh2 slot empty)
  • Hit Play
  • Check that the mesh has been loaded into the mesh2 slot
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using UnityEngine;

public class SerializableMeshInfoTest : MonoBehaviour
{
    [SerializeField] Mesh _mesh, _mesh2;
    [SerializeField] string _fileName = "mesh.dat";

    private void Start()
    {
        string path = $"{Application.dataPath}/{_fileName}";
        SaveMesh(_mesh, path);
        _mesh2 = LoadMesh(path);
    }

    /// <summary>
    /// Saves the mesh binary blob to the specified path.
    /// </summary>
    /// <param name="savepath">File path to save to. Tip: use `Application.dataPath` to make it relative to the application path (Assets folder when run from Unity Editor).</param>
    public static void SaveMesh(Mesh mesh, string savepath)
    {
        DataSerializer.Save<SerializableMeshInfo>(new SerializableMeshInfo(mesh), savepath);
    }

    /// <summary>
    /// Loads the mesh binary blob from the specified path.
    /// </summary>
    /// <param name="loadpath">File path to load from. Tip: use `Application.dataPath` to make it relative to the application path (Assets folder when run from Unity Editor).</param>
    Mesh LoadMesh(string loadpath)
    {
        if (!System.IO.File.Exists(loadpath))
        {
            Debug.LogError($"{loadpath} file does not exist.");
        }
        return DataSerializer.Load<SerializableMeshInfo>(loadpath).GetMesh();
    }
}

I hope this was helpful. Peace!