Combining Meshes with Bones?

So I’ve made a little script to Bake multiple meshes into one single combined mesh.

It was meant to be used on a character’s hair So instead of having a parent object holding 100 hair strands (or cards if you prefer calling like so) I’d have only a single Mesh for the entire hair.

All good, but the hair uses rigging to make It look brushed and neat in place, of course, the end result looks like the character received a strong electrical jolt as her hair is completelly straightened up, because the bones are no longer affecting It. So, now I want to know how to change this script so the final mesh still mantains the link and weights to the original rigging?

using UnityEngine;

public class MeshCombiner : MonoBehaviour
{
   public void CombineMeshes ()
   {
       Quaternion oldRot = transform.rotation;
       Vector3 oldPos = transform.position;

       transform.rotation = Quaternion.identity;
       transform.position = Vector3.zero;

       MeshFilter[] filters = GetComponentsInChildren<MeshFilter>();
       Debug.Log(name + "is Combining " + filters.Length + " Meshes");
       Mesh finalMesh = new Mesh();

       CombineInstance[] combiners = new CombineInstance[filters.Length];
       for(int a = 0; a < filters.Length; a++)
       {
           if(filters[a].transform == transform)
            continue;

           combiners[a].subMeshIndex = 0;
           combiners[a].mesh = filters[a].sharedMesh;
           combiners[a].transform = filters[a].transform.localToWorldMatrix;
       }
       finalMesh.CombineMeshes(combiners);
       GetComponent<MeshFilter>().sharedMesh = finalMesh;

       transform.rotation = oldRot;
       transform.position = oldPos;
   }
}
using UnityEngine;
using UnityEditor;

[CustomEditor (typeof (MeshCombiner))]
public class MeshCombinerEditor : Editor
{ 
    void OnSceneGUI()
    {
        MeshCombiner mc = target as MeshCombiner;
        if(Handles.Button (mc.transform.position + Vector3.up * 1,
        Quaternion.LookRotation(Vector3.up), 1, 1, Handles.CylinderCap))
        {
            mc.CombineMeshes ();
        }
    }
}

Gratefully,
Arthur

1 Like

Hey Arthur,
I did something similar some time ago. Basically I had a character skinned mesh and I would combine various components with it dynamically (such as hair, earrings, etc). Here is the script I used to do that:

using System;
using System.Collections.Generic;
using UnityEngine;

public class CharacterMesh
{
    private class MeshComponent
    {
        public Vector3[] Verts;
        public Vector3[] Normals;
        public Vector2[] Uvs;
        public int[] Triangles;
        public BoneWeight[] Weights;
    }

    public Mesh Mesh { get; }
    public SkinnedMeshRenderer Renderer { get; }

    private readonly List<MeshComponent> _components = new List<MeshComponent>(8);
    private readonly MeshComponent _baseMeshComponent;

    public CharacterMesh(SkinnedMeshRenderer renderer, Mesh baseMesh)
    {
        Renderer = renderer;

        if (baseMesh == null)
        {
            throw new ArgumentException("BaseMesh must not be null.", nameof(baseMesh));
        }

        if (renderer == null)
        {
            throw new ArgumentException("Renderer must not be null.", nameof(renderer));
        }

        Mesh = new Mesh
        {
            name = renderer.name,
            vertices = baseMesh.vertices,
            uv = baseMesh.uv,
            triangles = baseMesh.triangles,
            bindposes = baseMesh.bindposes,
            boneWeights = baseMesh.boneWeights,
            normals = baseMesh.normals,
            colors = baseMesh.colors,
            tangents = baseMesh.tangents
        };
        renderer.sharedMesh = Mesh;

        _baseMeshComponent = new MeshComponent
        {
            Verts = baseMesh.vertices,
            Normals = baseMesh.normals,
            Uvs = baseMesh.uv,
            Triangles = baseMesh.triangles,
            Weights = baseMesh.boneWeights
        };

#if UNITY_EDITOR
        Debug.Assert(Mesh.normals.Length == Mesh.vertices.Length, $"Normals and verts for {Mesh.name} do not match up.");
#endif

        //if for some reason the weights aren't set, create a placeholder
        if (_baseMeshComponent.Weights == null || _baseMeshComponent.Weights.Length == 0)
        {
#if UNITY_EDITOR
            Debug.LogWarning($"No bone weights were set for combined mesh with name {Mesh.name}.");
#endif
            _baseMeshComponent.Weights = new BoneWeight[_baseMeshComponent.Verts.Length];
        }
    }

    public void CombineMesh(List<Mesh> meshes)
    {
        _components.Clear();

        int vertexCount = _baseMeshComponent.Verts.Length;
        int uvCount = _baseMeshComponent.Uvs.Length;
        int triangleCount = _baseMeshComponent.Triangles.Length;

        //calculate the total sizes
        for (int i = 0; i < meshes.Count; i++)
        {
            var mesh = meshes[i];
            if (mesh == null)
            {
                continue;
            }

#if UNITY_EDITOR
            Debug.Assert(mesh.normals.Length == mesh.vertices.Length, $"Normals and verts for {mesh.name} do not match up.");
#endif

#if UNITY_EDITOR
            Debug.Assert(Mesh.bindposes.Length == mesh.bindposes.Length, $"Bone structure of {mesh.name} does not match base structure of {Mesh.name}.");
#endif

            var meshComp = new MeshComponent
            {
                Verts = mesh.vertices,
                Normals = mesh.normals,
                Triangles = mesh.triangles,
                Uvs = mesh.uv,
                Weights = mesh.boneWeights
            };

            //if for some reason the weights aren't set, create a placeholder
            if (meshComp.Weights == null || meshComp.Weights.Length == 0)
            {
#if UNITY_EDITOR
                Debug.LogWarning($"No bone weights were set for combined mesh with name {mesh.name}.");
#endif
                meshComp.Weights = new BoneWeight[meshComp.Verts.Length];
            }

#if UNITY_EDITOR
            Debug.Assert(meshComp.Verts.Length == meshComp.Weights.Length, $"Bone weights must be the same length as the verts for {mesh.name}.");
#endif

            vertexCount += meshComp.Verts.Length;
            uvCount += meshComp.Uvs.Length;
            triangleCount += meshComp.Triangles.Length;

            _components.Add(meshComp);
        }

        var verts = new Vector3[vertexCount];
        var normals = new Vector3[vertexCount];
        var uvs = new Vector2[uvCount];
        var triangles = new int[triangleCount];
        var weights = new BoneWeight[vertexCount];

        //copy in base mesh
        Array.Copy(_baseMeshComponent.Verts, verts, _baseMeshComponent.Verts.Length);
        Array.Copy(_baseMeshComponent.Normals, normals, _baseMeshComponent.Normals.Length);
        Array.Copy(_baseMeshComponent.Uvs, uvs, _baseMeshComponent.Uvs.Length);
        Array.Copy(_baseMeshComponent.Triangles, triangles, _baseMeshComponent.Triangles.Length);
        Array.Copy(_baseMeshComponent.Weights, weights, _baseMeshComponent.Verts.Length);

        int vertexOffset = _baseMeshComponent.Verts.Length;
        int uvOffset = _baseMeshComponent.Uvs.Length;
        int triangleOffset = _baseMeshComponent.Triangles.Length;

        //copy in sub meshes
        for (int i = 0; i < _components.Count; i++)
        {
            var meshComp = _components[i];

            Array.Copy(meshComp.Verts, 0, verts, vertexOffset, meshComp.Verts.Length);
            Array.Copy(meshComp.Normals, 0, normals, vertexOffset, meshComp.Normals.Length);
            Array.Copy(meshComp.Uvs, 0, uvs, uvOffset, meshComp.Uvs.Length);

            //can't just copy triangles directly, need to offset
            for (int j = 0; j < meshComp.Triangles.Length; j++)
            {
                triangles[triangleOffset + j] = meshComp.Triangles[j] + vertexOffset;
            }

            Array.Copy(meshComp.Weights, 0, weights, vertexOffset, meshComp.Verts.Length);

            vertexOffset += meshComp.Verts.Length;
            uvOffset += meshComp.Uvs.Length;
            triangleOffset += meshComp.Triangles.Length;
        }

        //update the mesh components...
        Mesh.Clear();
        Mesh.vertices = verts;
        Mesh.normals = normals;
        Mesh.uv = uvs;
        Mesh.triangles = triangles;
        Mesh.boneWeights = weights;
    }
}

Note: It’s possible to have the same rig for different components in your 3D editor - but when imported into unity different bones can be optimized out or re-ordered (thanks for that - very helpful, especially since it can’t be turned off). So you can use this editor script to order the bones on the skinned meshes that are imported:

//Sourced from: https://discussions.unity.com/t/625985

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;

//sorts transform bone indexes in skinned mesh renderers so that we can swap skinned meshes at runtime
public class AssetPostProcessorReorderBones : AssetPostprocessor
{
    protected void OnPostprocessModel(GameObject go)
    {
        Process(go);
    }

    private void Process(GameObject go)
    {
        var renderers = go.GetComponentsInChildren<SkinnedMeshRenderer>(true);

        foreach (var renderer in renderers)
        {
            if (renderer == null)
            {
                continue;
            }

            //list of bones
            var tList = renderer.bones.ToList();

            //sort alphabetically
            tList.Sort(CompareTransform);

            //record bone index mappings (richardf advice)
            //build a Dictionary<int, int> that records the old bone index => new bone index mappings,
            //then run through every vertex and just do boneIndexN = dict[boneIndexN] for each weight on each vertex.
            var remap = new Dictionary<int, int>();
            for (var i = 0; i < renderer.bones.Length; i++)
            {
                remap[i] = tList.IndexOf(renderer.bones[i]);
            }

            //remap bone weight indexes
            var bw = renderer.sharedMesh.boneWeights;
            for (var i = 0; i < bw.Length; i++)
            {
                bw[i].boneIndex0 = remap[bw[i].boneIndex0];
                bw[i].boneIndex1 = remap[bw[i].boneIndex1];
                bw[i].boneIndex2 = remap[bw[i].boneIndex2];
                bw[i].boneIndex3 = remap[bw[i].boneIndex3];
            }

            //remap bindposes
            var bp = new Matrix4x4[renderer.sharedMesh.bindposes.Length];
            for (var i = 0; i < bp.Length; i++)
            {
                bp[remap[i]] = renderer.sharedMesh.bindposes[i];
            }

            //assign new data
            renderer.bones = tList.ToArray();
            renderer.sharedMesh.boneWeights = bw;
            renderer.sharedMesh.bindposes = bp;
        }
    }

    private static int CompareTransform(Transform a, Transform b)
    {
        return string.Compare(a.name, b.name, StringComparison.Ordinal);
    }
}

Finally, I would suggest creating a .asset from the skinned mesh objects to avoid bones going missing / other issues (Unity won’t optimize the .assets) so you can copy out the bones from the mesh that has all the bones such as your player character into the other meshes (such as hair) using this wizard:

using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;

public class SkinnedMeshComponentWizard : ScriptableWizard
{
    public SkinnedMeshRenderer BaseMesh;
    public List<SkinnedMeshRenderer> Components;

    [MenuItem("Tools/Mesh/Generate Skinned Mesh Component")]
    protected static void CreateWizard()
    {
        DisplayWizard<SkinnedMeshComponentWizard>("Skinned Mesh Component Generation", "Generate");
    }

    protected void OnWizardCreate()
    {
        var path = EditorUtility.SaveFolderPanel("Export Meshes", string.Empty, string.Empty);
        string relativePath = path.Substring(path.IndexOf("Assets/", StringComparison.Ordinal));
        foreach (var component in Components)
        {
            var boneMapping = new Dictionary<int, int>();
            for (int i = 0; i < component.bones.Length; i++)
            {
                var compBone = component.bones[i];
                for (int j = 0; j < BaseMesh.bones.Length; j++)
                {
                    var baseBone = BaseMesh.bones[j];
                    if (compBone.name == baseBone.name)
                    {
                        boneMapping.Add(i, j);
                        break;
                    }
                }
            }

            //remap the bones
            var weights = component.sharedMesh.boneWeights;
            for (int i = 0; i < weights.Length; i++)
            {
                var weight = weights[i];
                weights[i].boneIndex0 = boneMapping[weight.boneIndex0];
                weights[i].boneIndex1 = boneMapping[weight.boneIndex1];
                weights[i].boneIndex2 = boneMapping[weight.boneIndex2];
                weights[i].boneIndex3 = boneMapping[weight.boneIndex3];
            }

            var verticies = component.sharedMesh.vertices;
            var scale = component.transform.lossyScale;
            for (var i = 0; i < verticies.Length; i++)
            {
                var vertex = verticies[i];
                vertex.x = vertex.x * scale.x;
                vertex.y = vertex.y * scale.y;
                vertex.z = vertex.z * scale.z;
                verticies[i] = vertex;
            }

            var mesh = new Mesh
            {
                name = component.sharedMesh.name,
                vertices = verticies,
                uv = component.sharedMesh.uv,
                triangles = component.sharedMesh.triangles,
                bindposes = BaseMesh.sharedMesh.bindposes,
                boneWeights = weights,
                normals = component.sharedMesh.normals,
                colors = component.sharedMesh.colors,
                tangents = component.sharedMesh.tangents
            };
            AssetDatabase.CreateAsset(mesh, Path.Combine(relativePath, $"{mesh.name}.asset"));
        }
    }

    protected void OnWizardUpdate()
    {
        helpString = "Creates a new mesh based off the bone structure of a base mesh and ensures everything is in the correct order in the new mesh.";
        isValid = (BaseMesh != null) && (Components != null && Components.Count > 0);
    }
}

Thank You Very Much!

Hello, I’m not so much of a programmer as I’m an artist, and I do not think I have enough the knowledge to fully comprehend what is going on on these scripts. Can you explain to me how to use It? What components do I need my separate meshes to have (for example, my scripts demands every separate object I want to combine have a Mesh Filter)? Where and wich script do I apply? where will my “baked” combined mesh will appear, this kind of stuff?

Gratefully,
Arthur