Here is some code i wrote recently for serializing/deserializing a skinned mesh renderer. It ignores the materials, so that would be a task for a material serializer. But it does handle sub meshes, vertices, normals, tangets, uvs, bindposes, bone transforms, bone weights and blendshapes.
////////////////////////////////////////////////////////////////////////////////
// VGS Common
//
// VGS_SkinnedMeshUtility.cs
//
// Skinned Mesh Renderer Serializer To Byte Array
//
// Author Mark Day. 2021. (Free To Use)
////////////////////////////////////////////////////////////////////////////////
using System.Collections.Generic;
using System.IO;
using UnityEngine;
namespace VGS.Common
{
public class VGS_SkinnedMeshUtility
{
public class BoneResult
{
public string rootBoneName;
public string[] boneNames;
public BoneResult(string rootBoneName, string[] boneNames)
{
this.rootBoneName = rootBoneName;
this.boneNames = boneNames;
}
}
public class SkeletonGO
{
public string name;
public string parentName;
public Transform transform;
public SkeletonGO(string name, string parentName, Transform transform)
{
this.name = name;
this.parentName = parentName;
this.transform = transform;
}
}
/// <summary>
/// Serialize the skinned mesh renderer into a byte array
/// </summary>
/// <param name="smr">Skinned mesh renderer to serialize</param>
/// <param name="skeletonRoot">The root transform of the skeleton containing the bones. Accepts null for no skeleton.</param>
/// <param name="saveNormals">Save the vertex normals</param>
/// <param name="saveTangents">Save the vertex tangents</param>
/// <param name="saveBlendshapes">Save the blendshape frame data</param>
/// <returns>Byte array</returns>
public static byte[] Serialize(SkinnedMeshRenderer smr, Transform skeletonRoot, bool saveNormals, bool saveTangents, bool saveBlendshapes)
{
var stream = new MemoryStream();
var buf = new BinaryWriter(stream);
// Write game object local transform
WriteLocalTransform(smr.gameObject.transform, buf);
// Write header
buf.Write(saveNormals);
buf.Write(saveTangents);
buf.Write(skeletonRoot != null);
buf.Write(saveBlendshapes);
// Write vertex count
buf.Write(smr.sharedMesh.vertices.Length);
// Write vertex data
WriteVector3Array(smr.sharedMesh.vertices, buf);
// Write normal data
if (saveNormals)
{
WriteVector3Array(smr.sharedMesh.normals, buf);
}
// Write tangent data
if (saveTangents)
{
WriteVector4Array(smr.sharedMesh.tangents, buf);
}
// Write uv data
// uv
buf.Write(smr.sharedMesh.uv != null ? smr.sharedMesh.uv.Length : 0);
WriteVector2Array(smr.sharedMesh.uv, buf);
// uv2
buf.Write(smr.sharedMesh.uv2 != null ? smr.sharedMesh.uv2.Length : 0);
WriteVector2Array(smr.sharedMesh.uv2, buf);
// uv3
buf.Write(smr.sharedMesh.uv3 != null ? smr.sharedMesh.uv3.Length : 0);
WriteVector2Array(smr.sharedMesh.uv3, buf);
// uv4
buf.Write(smr.sharedMesh.uv4 != null ? smr.sharedMesh.uv4.Length : 0);
WriteVector2Array(smr.sharedMesh.uv4, buf);
// uv5
buf.Write(smr.sharedMesh.uv5 != null ? smr.sharedMesh.uv5.Length : 0);
WriteVector2Array(smr.sharedMesh.uv5, buf);
// uv6
buf.Write(smr.sharedMesh.uv6 != null ? smr.sharedMesh.uv6.Length : 0);
WriteVector2Array(smr.sharedMesh.uv6, buf);
// uv7
buf.Write(smr.sharedMesh.uv7 != null ? smr.sharedMesh.uv7.Length : 0);
WriteVector2Array(smr.sharedMesh.uv7, buf);
// uv8
buf.Write(smr.sharedMesh.uv8 != null ? smr.sharedMesh.uv8.Length : 0);
WriteVector2Array(smr.sharedMesh.uv8, buf);
// Write sub mesh count
buf.Write(smr.sharedMesh.subMeshCount);
// Write triangle indices per sub mesh
for (int i = 0; i < smr.sharedMesh.subMeshCount; i++)
{
var tris = smr.sharedMesh.GetTriangles(i);
buf.Write(tris.Length);
foreach (var idx in tris)
buf.Write(idx);
}
// Write the root bone name
buf.Write(smr.rootBone != null ? smr.rootBone.name : "");
// Write the bone count
buf.Write(smr.bones.Length);
// Write bone transform names
foreach (Transform t in smr.bones)
{
buf.Write(t.name);
}
// Write the skeleton
if (skeletonRoot != null)
{
Transform[] skeleton = skeletonRoot.GetComponentsInChildren<Transform>(true);
buf.Write(skeleton.Length);
Dictionary<string, SkeletonGO> skeletonMap = new Dictionary<string, SkeletonGO>();
foreach (Transform t in skeleton)
{
skeletonMap.Add(t.name, new SkeletonGO(t.name, t.Equals(skeletonRoot) ? string.Empty : skeletonMap[t.parent.name].name, t));
buf.Write(skeletonMap[t.name].name);
WriteLocalTransform(skeletonMap[t.name].transform, buf);
buf.Write(skeletonMap[t.name].parentName);
}
}
// Write bone weights
buf.Write(smr.sharedMesh.boneWeights.Length);
foreach (BoneWeight boneWeight in smr.sharedMesh.boneWeights)
{
buf.Write(boneWeight.boneIndex0);
buf.Write(boneWeight.boneIndex1);
buf.Write(boneWeight.boneIndex2);
buf.Write(boneWeight.boneIndex3);
buf.Write(boneWeight.weight0);
buf.Write(boneWeight.weight1);
buf.Write(boneWeight.weight2);
buf.Write(boneWeight.weight3);
}
// Write bind poses
buf.Write(smr.sharedMesh.bindposes.Length);
foreach (Matrix4x4 bindpose in smr.sharedMesh.bindposes)
{
buf.Write(bindpose.m00);
buf.Write(bindpose.m01);
buf.Write(bindpose.m02);
buf.Write(bindpose.m03);
buf.Write(bindpose.m10);
buf.Write(bindpose.m11);
buf.Write(bindpose.m12);
buf.Write(bindpose.m13);
buf.Write(bindpose.m20);
buf.Write(bindpose.m21);
buf.Write(bindpose.m22);
buf.Write(bindpose.m23);
buf.Write(bindpose.m30);
buf.Write(bindpose.m31);
buf.Write(bindpose.m32);
buf.Write(bindpose.m33);
}
// Write blendshapes
if (saveBlendshapes)
{
buf.Write(smr.sharedMesh.blendShapeCount);
for (int i = 0; i < smr.sharedMesh.blendShapeCount; i++)
{
Vector3[] deltaVertices = new Vector3[smr.sharedMesh.vertices.Length];
Vector3[] deltaNormals = new Vector3[smr.sharedMesh.vertices.Length];
Vector3[] deltaTangents = new Vector3[smr.sharedMesh.vertices.Length];
smr.sharedMesh.GetBlendShapeFrameVertices(i, 100, deltaVertices, deltaNormals, deltaTangents);
// Write blendshape name
buf.Write(smr.sharedMesh.GetBlendShapeName(i));
// Write blendshape frame data
WriteVector3Array(deltaVertices, buf);
WriteVector3Array(deltaNormals, buf);
WriteVector3Array(deltaTangents, buf);
}
}
// Write mesh bounds
buf.Write(smr.sharedMesh.bounds.size.x);
buf.Write(smr.sharedMesh.bounds.size.y);
buf.Write(smr.sharedMesh.bounds.size.z);
buf.Write(smr.sharedMesh.bounds.center.x);
buf.Write(smr.sharedMesh.bounds.center.y);
buf.Write(smr.sharedMesh.bounds.center.z);
buf.Write(smr.sharedMesh.bounds.extents.x);
buf.Write(smr.sharedMesh.bounds.extents.y);
buf.Write(smr.sharedMesh.bounds.extents.z);
// Write smr local bounds
buf.Write(smr.localBounds.size.x);
buf.Write(smr.localBounds.size.y);
buf.Write(smr.localBounds.size.z);
buf.Write(smr.localBounds.center.x);
buf.Write(smr.localBounds.center.y);
buf.Write(smr.localBounds.center.z);
buf.Write(smr.localBounds.extents.x);
buf.Write(smr.localBounds.extents.y);
buf.Write(smr.localBounds.extents.z);
buf.Close();
stream.Close();
return stream.ToArray();
}
/// <summary>
/// Builds the skinned mesh renderer from a byte array
/// </summary>
/// <param name="parent">Parent of the skinned mesh renderer game object and skeleton root game object</param>
/// <param name="smr">Skinned Mesh Renderer To Update</param>
/// <param name="bytes">Skinned Mesh Renderer Byte Array</param>
/// <param name="createSkeleton">Create the skeleton with the names and transform values</param>
/// <returns>Return BoneResult</returns>
public static BoneResult Deserialize(GameObject parent, SkinnedMeshRenderer smr, byte[] bytes, bool createSkeleton)
{
MemoryStream memStream = new MemoryStream(bytes);
BinaryReader buf = new BinaryReader(memStream);
Mesh mesh = new Mesh();
// Read game object local transform
ReadLocalTransform(smr.gameObject.transform, buf);
bool saveNormals = buf.ReadBoolean();
bool saveTangents = buf.ReadBoolean();
bool saveSkeleton = buf.ReadBoolean() && createSkeleton;
bool saveBlendshapes = buf.ReadBoolean();
int vertCount = buf.ReadInt32();
// Read vertex data
Vector3[] vertices = new Vector3[vertCount];
ReadVector3Array(vertices, buf);
mesh.vertices = vertices;
// Read normal data
if (saveNormals)
{
Vector3[] normals = new Vector3[vertCount];
ReadVector3Array(normals, buf);
mesh.normals = normals;
}
// Read tangent data
if (saveTangents)
{
Vector4[] tangents = new Vector4[vertCount];
ReadVector4Array(tangents, buf);
mesh.tangents = tangents;
}
// Read uv data
Vector2[] uvs = new Vector2[buf.ReadInt32()];
ReadVector2Array(uvs, buf);
mesh.uv = uvs;
Vector2[] uv2 = new Vector2[buf.ReadInt32()];
ReadVector2Array(uv2, buf);
mesh.uv2 = uv2;
Vector2[] uv3 = new Vector2[buf.ReadInt32()];
ReadVector2Array(uv3, buf);
mesh.uv3 = uv3;
Vector2[] uv4 = new Vector2[buf.ReadInt32()];
ReadVector2Array(uv4, buf);
mesh.uv4 = uv4;
Vector2[] uv5 = new Vector2[buf.ReadInt32()];
ReadVector2Array(uv5, buf);
mesh.uv5 = uv5;
Vector2[] uv6 = new Vector2[buf.ReadInt32()];
ReadVector2Array(uv6, buf);
mesh.uv6 = uv6;
Vector2[] uv7 = new Vector2[buf.ReadInt32()];
ReadVector2Array(uv7, buf);
mesh.uv7 = uv7;
Vector2[] uv8 = new Vector2[buf.ReadInt32()];
ReadVector2Array(uv8, buf);
mesh.uv8 = uv8;
// Read sub mesh count
mesh.subMeshCount = buf.ReadInt32();
// Read triangle indexes into sub meshes
for (int i = 0; i < mesh.subMeshCount; i++)
{
int[] subMeshTriIndex = new int[buf.ReadInt32()];
for (int j = 0; j < subMeshTriIndex.Length; j++)
{
subMeshTriIndex[j] = buf.ReadInt32();
}
mesh.SetTriangles(subMeshTriIndex, i);
}
// Read the root bone name
string rootBoneName = buf.ReadString();
// Read the bone names ( rootbone + bone names )
int boneCount = buf.ReadInt32();
List<string> boneNames = new List<string>();
for (int i = 0; i < boneCount; i++)
{
boneNames.Add(buf.ReadString());
}
// Read the skeleton
if (saveSkeleton)
{
Transform[] bones = new Transform[boneCount];
Dictionary<string, SkeletonGO> skeletonMap = new Dictionary<string, SkeletonGO>();
int skeletonLength = buf.ReadInt32();
// Read the skeleton data into the map
for (int i = 0; i < skeletonLength; i++)
{
GameObject go = new GameObject(buf.ReadString());
ReadLocalTransform(go.transform, buf);
string parentName = buf.ReadString();
skeletonMap.Add(go.name, new SkeletonGO(go.name, parentName, go.transform));
go.transform.SetParent(parentName == string.Empty ? parent.transform : skeletonMap[parentName].transform, false);
// Assign the bone transform to the smr bone array
int boneIndex = boneNames.FindIndex(x => x == go.name);
if (boneIndex >= 0)
{
bones[boneIndex] = go.transform;
// Assign root bone
if (go.name == rootBoneName)
{
smr.rootBone = go.transform;
}
}
}
smr.bones = bones;
}
// Read bone weights
BoneWeight[] boneWeights = new BoneWeight[buf.ReadInt32()];
for (int i = 0; i < boneWeights.Length; i++)
{
boneWeights[i].boneIndex0 = buf.ReadInt32();
boneWeights[i].boneIndex1 = buf.ReadInt32();
boneWeights[i].boneIndex2 = buf.ReadInt32();
boneWeights[i].boneIndex3 = buf.ReadInt32();
boneWeights[i].weight0 = buf.ReadSingle();
boneWeights[i].weight1 = buf.ReadSingle();
boneWeights[i].weight2 = buf.ReadSingle();
boneWeights[i].weight3 = buf.ReadSingle();
}
mesh.boneWeights = boneWeights;
// Read bind poses
Matrix4x4[] bindposes = new Matrix4x4[buf.ReadInt32()];
for (int i = 0; i < bindposes.Length; i++)
{
bindposes[i].m00 = buf.ReadSingle();
bindposes[i].m01 = buf.ReadSingle();
bindposes[i].m02 = buf.ReadSingle();
bindposes[i].m03 = buf.ReadSingle();
bindposes[i].m10 = buf.ReadSingle();
bindposes[i].m11 = buf.ReadSingle();
bindposes[i].m12 = buf.ReadSingle();
bindposes[i].m13 = buf.ReadSingle();
bindposes[i].m20 = buf.ReadSingle();
bindposes[i].m21 = buf.ReadSingle();
bindposes[i].m22 = buf.ReadSingle();
bindposes[i].m23 = buf.ReadSingle();
bindposes[i].m30 = buf.ReadSingle();
bindposes[i].m31 = buf.ReadSingle();
bindposes[i].m32 = buf.ReadSingle();
bindposes[i].m33 = buf.ReadSingle();
}
mesh.bindposes = bindposes;
// Read blendshapes
if (saveBlendshapes)
{
int blendShapeCount = buf.ReadInt32();
for (int i = 0; i < blendShapeCount; i++)
{
Vector3[] deltaVertices = new Vector3[vertCount];
Vector3[] deltaNormals = new Vector3[vertCount];
Vector3[] deltaTangents = new Vector3[vertCount];
// Read blendshape name
string blendShapeName = buf.ReadString();
// Read blendshape data
ReadVector3Array(deltaVertices, buf);
ReadVector3Array(deltaNormals, buf);
ReadVector3Array(deltaTangents, buf);
mesh.AddBlendShapeFrame(blendShapeName, 100, deltaVertices, deltaNormals, deltaTangents);
}
}
// Read mesh bounds
Bounds bounds = new Bounds();
bounds.size = new Vector3(buf.ReadSingle(), buf.ReadSingle(), buf.ReadSingle());
bounds.center = new Vector3(buf.ReadSingle(), buf.ReadSingle(), buf.ReadSingle());
bounds.extents = new Vector3(buf.ReadSingle(), buf.ReadSingle(), buf.ReadSingle());
mesh.bounds = bounds;
// Read smr local bounds
bounds = new Bounds();
bounds.size = new Vector3(buf.ReadSingle(), buf.ReadSingle(), buf.ReadSingle());
bounds.center = new Vector3(buf.ReadSingle(), buf.ReadSingle(), buf.ReadSingle());
bounds.extents = new Vector3(buf.ReadSingle(), buf.ReadSingle(), buf.ReadSingle());
smr.localBounds = bounds;
if (!saveNormals)
mesh.RecalculateNormals();
if (!saveTangents)
mesh.RecalculateTangents();
//mesh.RecalculateBounds();
buf.Close();
memStream.Close();
smr.sharedMesh = mesh;
return new BoneResult(rootBoneName, boneNames.ToArray());
}
// Private Methods
private static void ReadLocalTransform(Transform t, BinaryReader buf)
{
t.localPosition = new Vector3(buf.ReadSingle(), buf.ReadSingle(), buf.ReadSingle());
t.localRotation = new Quaternion(buf.ReadSingle(), buf.ReadSingle(), buf.ReadSingle(), buf.ReadSingle());
t.localScale = new Vector3(buf.ReadSingle(), buf.ReadSingle(), buf.ReadSingle());
}
private static void WriteLocalTransform(Transform t, BinaryWriter buf)
{
buf.Write(t.localPosition.x);
buf.Write(t.localPosition.y);
buf.Write(t.localPosition.z);
buf.Write(t.localRotation.x);
buf.Write(t.localRotation.y);
buf.Write(t.localRotation.z);
buf.Write(t.localRotation.w);
buf.Write(t.localScale.x);
buf.Write(t.localScale.y);
buf.Write(t.localScale.z);
}
private static void ReadVector3Array(Vector3[] arr, BinaryReader buf)
{
if (arr == null)
return;
for (var i = 0; i < arr.Length; ++i)
{
arr[i] = new Vector3(buf.ReadSingle(), buf.ReadSingle(), buf.ReadSingle());
}
}
private static void WriteVector3Array(Vector3[] arr, BinaryWriter buf)
{
if (arr == null)
return;
foreach (var v in arr)
{
buf.Write(v.x);
buf.Write(v.y);
buf.Write(v.z);
}
}
private static void ReadVector2Array(Vector2[] arr, BinaryReader buf)
{
if (arr == null)
return;
for (var i = 0; i < arr.Length; ++i)
{
arr[i] = new Vector2(buf.ReadSingle(), buf.ReadSingle());
}
}
private static void WriteVector2Array(Vector2[] arr, BinaryWriter buf)
{
if (arr == null)
return;
foreach (var v in arr)
{
buf.Write(v.x);
buf.Write(v.y);
}
}
private static void ReadVector4Array(Vector4[] arr, BinaryReader buf)
{
if (arr == null)
return;
for (var i = 0; i < arr.Length; ++i)
{
arr[i] = new Vector4(buf.ReadSingle(), buf.ReadSingle(), buf.ReadSingle(), buf.ReadSingle());
}
}
private static void WriteVector4Array(Vector4[] arr, BinaryWriter buf)
{
if (arr == null)
return;
foreach (var v in arr)
{
buf.Write(v.x);
buf.Write(v.y);
buf.Write(v.z);
buf.Write(v.w);
}
}
}
}