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