Edit: the code now works perfectly, so you can feel free to use it in your own projects.
Hi all,
I’ve made an AssetPostprocessor that indexes skinned mesh renderer bones alphabetically. The purpose of this is to create consistent bone ordering at import time since the FBX file format / exporter will shuffle these. The reason I do this is so that replacing skinned mesh renderer sharedMesh is a trivial swap during the game, where all meshes use an identical rig. This has a lot of useful purposes.
Some background information can be found here, especially @superpig 's comments, and I’ve tried to implement his approach without any success - the resultant model is a deformed mess of polygons. I’ve checked the code for a couple of days and tried everything but I can’t see what I am missing so please help out if you can - you may find it useful enough to add to the wiki too.
Here’s the code:
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.Linq;
//sorts transform bone indexes in skinned mesh renderers so that we can swap skinned meshes at runtime
public class AssetPostProcessorReorderBones : AssetPostprocessor
{
void OnPostprocessModel(GameObject g)
{
Process(g);
}
void Process(GameObject g)
{
SkinnedMeshRenderer rend = g.GetComponentInChildren<SkinnedMeshRenderer>();
if (rend == null)
{
Debug.LogWarning("Unable to find Renderer" + rend.name);
return;
}
//list of bones
List<Transform> tList = rend.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.
Dictionary<int, int> remap = new Dictionary<int, int>();
for (int i = 0; i < rend.bones.Length; i++)
{
remap[i] = tList.IndexOf(rend.bones[i]);
}
//remap bone weight indexes
BoneWeight[] bw = rend.sharedMesh.boneWeights;
for (int 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
Matrix4x4[] bp = new Matrix4x4[rend.sharedMesh.bindposes.Length];
for (int i = 0; i<bp.Length; i++)
{
bp[remap[i]] = rend.sharedMesh.bindposes[i];
}
//assign new data
rend.bones = tList.ToArray();
rend.sharedMesh.boneWeights = bw;
rend.sharedMesh.bindposes = bp;
}
private static int CompareTransform(Transform A, Transform B)
{
return A.name.CompareTo(B.name);
}
}
Please don’t hesitate to ask questions and thanks for any help you can offer.
Any skinned mesh valid model will work including Unity’s I believe, due to it just reordering it’s own bones, reimporting in the project window will allow this script to run and you can check it via the preview panel. I can’t supply this particular model due to NDA - if you don’t have one I will try and prepare one tomorrow. Thanks for replying!
I’m not not good at scripting in general but It looks like to me, you’re assuming all the vertices have 4 bones assigned.
I think it is possible to have less then 4 bones assigned to a vert.
That might cause a problem.
Yep - I get that, but when it is animated, it rotates the entire body as if there is no animation and only derives pelvis animation. Trying to figure out why. I’m using Generic if that helps try to narrow it down. I really appreciate the help so far, thank you
//remap bindposes
Matrix4x4[] bp = new Matrix4x4[rend.sharedMesh.bindposes.Length];
for (int i = 0; i<bp.Length; i++)
{
bp[remap[i]] = rend.sharedMesh.bindposes[i];
}
Swapping what’s remapped seems to fix it for the same model. I think I’m getting more and more confused at this rate.
Thanks for help everyone. I’ve amended the line in the original post so if someone wants to enjoy this code they can.
People who responded helped me think it through though, so thanks guys! And thanks to Richard Fine who helped out on slack too!
I was about to dig into this procedure, but a quick search has led me to this topic. Your script does the job pretty well, thank you very much for sharing it. This saves me a lot of time for a custom equipment system. I’ve slightly edited it to ensure it works with multiple skinned mesh renderers in the imported model’s hierarchy.
Hi, I know this is a pretty old post, but any chance you can share the changes you made? I’m having really mixed results, and struggling to tell if this script is making any difference, or if it’s purely down to altering the ‘Preserve Hierarchy’ and ‘Sort Hierarchy By Name’ options on the Model import tab.
As far as I can tell right now, I can switch the mesh, providing the root bone in the skinned mesh renderer is identical. If that differs between meshes, then the swapped in mesh isn’t visible (even if I manually change the rootbone)
e.g. of what I’m currently trying to get working
FBX01 - Includes rig, head01 mesh, body01 mesh
FBX02 - Includes rig, head02 mesh
FBX03 - Includes rig, body02 mesh
So the character prefab would be made from FBX01, and I want to be able to swap those parts for meshes in FBX02 and FBX03, but still use the original rig from FBX01.
@hippocoder If you’re able to shed any light on where I might be going wrong I’d appreciate it. Can the swap only be done at runtime, and is there more to it than simply changing the mesh reference?
The script is great! Just a note for those who are getting mixed results. Make sure your rootbone matches across the meshes, and if you have multiple meshes, the script will need to be adjusted so it finds all SMR within the asset and not just the first one listed in the FBX.
I wish I could use this. Trouble is, I wanna use this in an existing huge project and upon creation this script reworks every single .fbx in it. I’m too afraid of breaking something, not to mention it takes a huge amount of time.
Yes, the post processor has a sub-optimal implementation. It probably should just get the bone list and check if it’s already ordered and if so, just exit. Of course checking if the list is ordered also takes some time, but is a lot less taxing than the whole re-mapping. Especially since the creation of the Dictionary doesn’t make much sense since the code uses “IndexOf” in a for loop. So it’s an O(n²) loop. The point of a dictionary would be to avoid this complexity. So the dictionary should be created by mapping each “Transform” to the “old” index before the sort so you can match the new order with the old one. Also using rend.bones* in a loop is a complete performance killer. rend.bones is again a property that returns a new copy of the bones array every time. So this array should be cached, probably before the list is created.* Here’s an optimised version that does a check if the bones list is already sorted and I also removed some of the unnecessary garbage generating bits: ```csharp
*using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
//sorts transform bone indexes in skinned mesh renderers so that we can swap skinned meshes at runtime
public class AssetPostProcessorReorderBones : AssetPostprocessor
{
void OnPostprocessModel(GameObject g)
{
foreach(var rend in g.GetComponentsInChildren())
Process(rend);
}
void Process(SkinnedMeshRenderer rend)
{
Transform[] bones = rend.bones;
// already sorted? -> done here.
if (IsSorted(bones, CompareTransform))
return;
List<Transform> sortedBones = new List<Transform>(bones);
//sort alphabetically
sortedBones.Sort(CompareTransform);
// Create a lookup to match a transform instance to the new bone index
Dictionary<Transform, int> remapTrans = new Dictionary<Transform, int>();
for (int i = 0; i < sortedBones.Count; i++)
{
remapTrans[sortedBones[i]] = i;
}
// Create a lookup to match the oldbone index to the new one.
Dictionary<int, int> remap = new Dictionary<int, int>();
for (int i = 0; i < bones.Length; i++)
{
remap[i] = remapTrans[bones[i]];
}
//remap bone weight indexes
BoneWeight[] bw = rend.sharedMesh.boneWeights;
for (int 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
Matrix4x4[] oldBP = rend.sharedMesh.bindposes;
Matrix4x4[] bp = new Matrix4x4[oldBP.Length];
for (int i = 0; i < bp.Length; i++)
{
bp[remap[i]] = oldBP[i];
}
//assign new data
rend.bones = sortedBones.ToArray();
rend.sharedMesh.boneWeights = bw;
rend.sharedMesh.bindposes = bp;
}
public static bool IsSorted<T>(T[] aArray, System.Func<T, T, int> aCompare)
{
if (aArray == null || aArray.Length < 2)
return true;
T last = aArray[0];
for(int i = 1; i < aArray.Length; i++)
{
T item = aArray[i];
if(aCompare(last, item) > 0)
return false;
last = item;
}
return true;
}
private static int CompareTransform(Transform A, Transform B)
{
return A.name.CompareTo(B.name);
}
}* ``` Note that I haven’t specifically tested this script as I don’t have any skinned meshes in my test project at the moment ^^. Though it should be functionally the same as the original script, just a lot less taxing. So feel free to test this post processor.
ps: I just noticed that the actual remap could simply be an ordinary array instead of a dictionary. So you can replace the
Dictionary<int, int> remap = new Dictionary<int, int>();
with
int[] remap = new int[bones.Length];
New version
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
//sorts transform bone indexes in skinned mesh renderers so that we can swap skinned meshes at runtime
public class AssetPostProcessorReorderBones : AssetPostprocessor
{
void OnPostprocessModel(GameObject g)
{
foreach(var rend in g.GetComponentsInChildren<SkinnedMeshRenderer>())
Process(rend);
}
void Process(SkinnedMeshRenderer rend)
{
Transform[] bones = rend.bones;
// already sorted? -> done here.
if (IsSorted(bones, CompareTransform))
return;
List<Transform> sortedBones = new List<Transform>(bones);
//sort alphabetically
sortedBones.Sort(CompareTransform);
// Create a lookup to match a transform instance to the new bone index
Dictionary<Transform, int> remapTrans = new Dictionary<Transform, int>();
for (int i = 0; i < sortedBones.Count; i++)
{
remapTrans[sortedBones[i]] = i;
}
// Create a lookup to match the oldbone index to the new one.
int[] remap = new int[bones.Length];
for (int i = 0; i < bones.Length; i++)
{
remap[i] = remapTrans[bones[i]];
}
//remap bone weight indexes
BoneWeight[] bw = rend.sharedMesh.boneWeights;
for (int 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
Matrix4x4[] oldBP = rend.sharedMesh.bindposes;
Matrix4x4[] bp = new Matrix4x4[oldBP.Length];
for (int i = 0; i < bp.Length; i++)
{
bp[remap[i]] = oldBP[i];
}
//assign new data
rend.bones = sortedBones.ToArray();
rend.sharedMesh.boneWeights = bw;
rend.sharedMesh.bindposes = bp;
}
public static bool IsSorted<T>(T[] aArray, System.Func<T, T, int> aCompare)
{
if (aArray == null || aArray.Length < 2)
return true;
T last = aArray[0];
for(int i = 1; i < aArray.Length; i++)
{
T item = aArray[i];
if(aCompare(last, item) > 0)
return false;
last = item;
}
return true;
}
private static int CompareTransform(Transform A, Transform B)
{
return A.name.CompareTo(B.name);
}
}
This should even be faster with less overhead. Especially since “remap” is used quite a lot.