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!