Serializing Skinned Mesh Renderer

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);
            }
        }
    }
}

I checked my current code. Comment out 362 to 399. Let me know how that goes.

Awesome, thank you very much for uploading!

A little strange, I got it to work as expected (including blendshapes) after I changed
“smr.sharedMesh.GetBlendShapeFrameVertices(i, 100, deltaVertices, deltaNormals, deltaTangents);” to
“smr.sharedMesh.GetBlendShapeFrameVertices(i, 0, deltaVertices, deltaNormals, deltaTangents);”
Before, this threw an “Index Out Of Bounds” error. However, I had to leave
“mesh.AddBlendShapeFrame(blendShapeName, 100, deltaVertices, deltaNormals, deltaTangents);”
as before.