Search Unity

AssetPostprocessor skinned mesh renderer bone index reorder [solved]

Discussion in 'Scripting' started by hippocoder, May 3, 2016.

  1. hippocoder

    hippocoder

    Digital Ape

    Joined:
    Apr 11, 2010
    Posts:
    29,723
    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:

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5.  
    6. //sorts transform bone indexes in skinned mesh renderers so that we can swap skinned meshes at runtime
    7. public class AssetPostProcessorReorderBones : AssetPostprocessor
    8. {
    9.     void OnPostprocessModel(GameObject g)
    10.     {
    11.         Process(g);
    12.     }
    13.  
    14.     void Process(GameObject g)
    15.     {
    16.         SkinnedMeshRenderer rend = g.GetComponentInChildren<SkinnedMeshRenderer>();
    17.         if (rend == null)
    18.         {
    19.             Debug.LogWarning("Unable to find Renderer" + rend.name);
    20.             return;
    21.         }
    22.  
    23.         //list of bones
    24.         List<Transform> tList = rend.bones.ToList();
    25.  
    26.         //sort alphabetically
    27.         tList.Sort(CompareTransform);
    28.  
    29.         //record bone index mappings (richardf advice)
    30.         //build a Dictionary<int, int> that records the old bone index => new bone index mappings,
    31.         //then run through every vertex and just do boneIndexN = dict[boneIndexN] for each weight on each vertex.
    32.         Dictionary<int, int> remap = new Dictionary<int, int>();
    33.         for (int i = 0; i < rend.bones.Length; i++)
    34.         {
    35.             remap[i] = tList.IndexOf(rend.bones[i]);
    36.         }
    37.  
    38.         //remap bone weight indexes
    39.         BoneWeight[] bw = rend.sharedMesh.boneWeights;
    40.         for (int i = 0; i < bw.Length; i++)
    41.         {
    42.             bw[i].boneIndex0 = remap[bw[i].boneIndex0];
    43.             bw[i].boneIndex1 = remap[bw[i].boneIndex1];
    44.             bw[i].boneIndex2 = remap[bw[i].boneIndex2];
    45.             bw[i].boneIndex3 = remap[bw[i].boneIndex3];
    46.         }
    47.  
    48.         //remap bindposes
    49.         Matrix4x4[] bp = new Matrix4x4[rend.sharedMesh.bindposes.Length];
    50.         for (int i = 0; i<bp.Length; i++)
    51.         {
    52.             bp[remap[i]] = rend.sharedMesh.bindposes[i];
    53.         }
    54.  
    55.         //assign new data
    56.         rend.bones = tList.ToArray();
    57.         rend.sharedMesh.boneWeights = bw;
    58.         rend.sharedMesh.bindposes = bp;
    59.     }
    60.  
    61.     private static int CompareTransform(Transform A, Transform B)
    62.     {
    63.         return A.name.CompareTo(B.name);
    64.     }
    65. }
    66.  
    Please don't hesitate to ask questions and thanks for any help you can offer.
     
    Vesnican, Bunny83, JonasMumm and 3 others like this.
  2. Voxel-Busters

    Voxel-Busters

    Joined:
    Feb 25, 2015
    Posts:
    1,963
    Could you please share the model to test out?
     
  3. hippocoder

    hippocoder

    Digital Ape

    Joined:
    Apr 11, 2010
    Posts:
    29,723
    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!
     
    Bunny83 likes this.
  4. Voxel-Busters

    Voxel-Busters

    Joined:
    Feb 25, 2015
    Posts:
    1,963
    I tried recalculating the bind poses and it seems to work. Not sure why it gave the issue, still finding it out.

    Code (CSharp):
    1. bp[i] = tList[i].worldToLocalMatrix * g.transform.localToWorldMatrix;
     
  5. gurayg

    gurayg

    Joined:
    Nov 28, 2013
    Posts:
    269
    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.
     
  6. hippocoder

    hippocoder

    Digital Ape

    Joined:
    Apr 11, 2010
    Posts:
    29,723
    Does it animate correctly? When I tried this, it was misaligned or did not animate...

    They will sum to 1 from FBX import as far as I know (hope!)
     
  7. gurayg

    gurayg

    Joined:
    Nov 28, 2013
    Posts:
    269
    this might help to see comparing before and after mesh data.
     
  8. Voxel-Busters

    Voxel-Busters

    Joined:
    Feb 25, 2015
    Posts:
    1,963
    @hippocoder Yes, it worked fine.
    I used unity's third person controller character to test (ethan.fbx)
     
  9. hippocoder

    hippocoder

    Digital Ape

    Joined:
    Apr 11, 2010
    Posts:
    29,723
    I get this result from Ethan.fbx:
    upload_2016-5-3_15-54-38.png

    With code modification to bindposes, I find there is no animation and it is of a massive scale, lying on it's side :/
     
  10. Voxel-Busters

    Voxel-Busters

    Joined:
    Feb 25, 2015
    Posts:
    1,963
    Thats very strange then!
    Have a look at the attached gif. On Unity 5.3.3
     
  11. hippocoder

    hippocoder

    Digital Ape

    Joined:
    Apr 11, 2010
    Posts:
    29,723
    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 :)
     
  12. hippocoder

    hippocoder

    Digital Ape

    Joined:
    Apr 11, 2010
    Posts:
    29,723
    Code (CSharp):
    1.         //remap bindposes
    2.         Matrix4x4[] bp = new Matrix4x4[rend.sharedMesh.bindposes.Length];
    3.         for (int i = 0; i<bp.Length; i++)
    4.         {
    5.             bp[remap[i]] = rend.sharedMesh.bindposes[i];
    6.         }
    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!
     
    Last edited: May 3, 2016
    Malbers and theANMATOR2b like this.
  13. SidarVasco

    SidarVasco

    Joined:
    Feb 9, 2015
    Posts:
    163
    I just tried the script but it deforms the mesh completely. Any reason why this might happen?

    They are all humanoid setups.
     
    Last edited: Sep 14, 2016
  14. GlitchyKitty

    GlitchyKitty

    Joined:
    Sep 29, 2013
    Posts:
    1
    Awesome. This really saved us a lot of time. thank you!

    I guess Unity should do that by default...
     
  15. RobinBlancOSG

    RobinBlancOSG

    Joined:
    Mar 25, 2016
    Posts:
    5
    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.
     
    hippocoder likes this.
  16. Monophobe

    Monophobe

    Joined:
    Jul 28, 2020
    Posts:
    1
    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?
     
    Last edited: Oct 14, 2021
    Sphax84 likes this.
  17. Andrea_B

    Andrea_B

    Joined:
    May 21, 2015
    Posts:
    1
    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.
     
    StickyKevin likes this.
  18. BuzzKirill

    BuzzKirill

    Joined:
    Nov 14, 2017
    Posts:
    48
    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. :(
     
  19. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,992
    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[i]
    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:

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3. using System.Collections.Generic;
    4.  
    5. //sorts transform bone indexes in skinned mesh renderers so that we can swap skinned meshes at runtime
    6. public class AssetPostProcessorReorderBones : AssetPostprocessor
    7. {
    8.     void OnPostprocessModel(GameObject g)
    9.     {
    10.         foreach(var rend in g.GetComponentsInChildren<SkinnedMeshRenderer>())
    11.             Process(rend);
    12.     }
    13.  
    14.     void Process(SkinnedMeshRenderer rend)
    15.     {
    16.         Transform[] bones = rend.bones;
    17.  
    18.         // already sorted? -> done here.
    19.         if (IsSorted(bones, CompareTransform))
    20.             return;
    21.  
    22.         List<Transform> sortedBones = new List<Transform>(bones);
    23.  
    24.         //sort alphabetically
    25.         sortedBones.Sort(CompareTransform);
    26.  
    27.         // Create a lookup to match a transform instance to the new bone index
    28.         Dictionary<Transform, int> remapTrans = new Dictionary<Transform, int>();
    29.         for (int i = 0; i < sortedBones.Count; i++)
    30.         {
    31.             remapTrans[sortedBones[i]] = i;
    32.         }
    33.         // Create a lookup to match the oldbone index to the new one.
    34.         Dictionary<int, int> remap = new Dictionary<int, int>();
    35.         for (int i = 0; i < bones.Length; i++)
    36.         {
    37.             remap[i] = remapTrans[bones[i]];
    38.         }
    39.  
    40.  
    41.         //remap bone weight indexes
    42.         BoneWeight[] bw = rend.sharedMesh.boneWeights;
    43.         for (int i = 0; i < bw.Length; i++)
    44.         {
    45.             bw[i].boneIndex0 = remap[bw[i].boneIndex0];
    46.             bw[i].boneIndex1 = remap[bw[i].boneIndex1];
    47.             bw[i].boneIndex2 = remap[bw[i].boneIndex2];
    48.             bw[i].boneIndex3 = remap[bw[i].boneIndex3];
    49.         }
    50.  
    51.         //remap bindposes
    52.         Matrix4x4[] oldBP = rend.sharedMesh.bindposes;
    53.         Matrix4x4[] bp = new Matrix4x4[oldBP.Length];
    54.         for (int i = 0; i < bp.Length; i++)
    55.         {
    56.             bp[remap[i]] = oldBP[i];
    57.         }
    58.  
    59.         //assign new data
    60.         rend.bones = sortedBones.ToArray();
    61.         rend.sharedMesh.boneWeights = bw;
    62.         rend.sharedMesh.bindposes = bp;
    63.     }
    64.  
    65.     public static bool IsSorted<T>(T[] aArray, System.Func<T, T, int> aCompare)
    66.     {
    67.         if (aArray == null || aArray.Length < 2)
    68.             return true;
    69.         T last = aArray[0];
    70.         for(int i = 1; i < aArray.Length; i++)
    71.         {
    72.             T item = aArray[i];
    73.             if(aCompare(last, item) > 0)
    74.                 return false;
    75.             last = item;
    76.         }
    77.         return true;
    78.     }
    79.  
    80.     private static int CompareTransform(Transform A, Transform B)
    81.     {
    82.         return A.name.CompareTo(B.name);
    83.     }
    84. }
    85.  
    86.  
    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.
     
  20. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,992
    ps: I just noticed that the actual remap could simply be an ordinary array instead of a dictionary. So you can replace the

    Code (CSharp):
    1. Dictionary<int, int> remap = new Dictionary<int, int>();
    with
    Code (CSharp):
    1. int[] remap = new int[bones.Length];
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3. using System.Collections.Generic;
    4. //sorts transform bone indexes in skinned mesh renderers so that we can swap skinned meshes at runtime
    5. public class AssetPostProcessorReorderBones : AssetPostprocessor
    6. {
    7.     void OnPostprocessModel(GameObject g)
    8.     {
    9.         foreach(var rend in g.GetComponentsInChildren<SkinnedMeshRenderer>())
    10.             Process(rend);
    11.     }
    12.     void Process(SkinnedMeshRenderer rend)
    13.     {
    14.         Transform[] bones = rend.bones;
    15.         // already sorted? -> done here.
    16.         if (IsSorted(bones, CompareTransform))
    17.             return;
    18.         List<Transform> sortedBones = new List<Transform>(bones);
    19.         //sort alphabetically
    20.         sortedBones.Sort(CompareTransform);
    21.         // Create a lookup to match a transform instance to the new bone index
    22.         Dictionary<Transform, int> remapTrans = new Dictionary<Transform, int>();
    23.         for (int i = 0; i < sortedBones.Count; i++)
    24.         {
    25.             remapTrans[sortedBones[i]] = i;
    26.         }
    27.         // Create a lookup to match the oldbone index to the new one.
    28.         int[] remap = new int[bones.Length];
    29.         for (int i = 0; i < bones.Length; i++)
    30.         {
    31.             remap[i] = remapTrans[bones[i]];
    32.         }
    33.         //remap bone weight indexes
    34.         BoneWeight[] bw = rend.sharedMesh.boneWeights;
    35.         for (int i = 0; i < bw.Length; i++)
    36.         {
    37.             bw[i].boneIndex0 = remap[bw[i].boneIndex0];
    38.             bw[i].boneIndex1 = remap[bw[i].boneIndex1];
    39.             bw[i].boneIndex2 = remap[bw[i].boneIndex2];
    40.             bw[i].boneIndex3 = remap[bw[i].boneIndex3];
    41.         }
    42.         //remap bindposes
    43.         Matrix4x4[] oldBP = rend.sharedMesh.bindposes;
    44.         Matrix4x4[] bp = new Matrix4x4[oldBP.Length];
    45.         for (int i = 0; i < bp.Length; i++)
    46.         {
    47.             bp[remap[i]] = oldBP[i];
    48.         }
    49.         //assign new data
    50.         rend.bones = sortedBones.ToArray();
    51.         rend.sharedMesh.boneWeights = bw;
    52.         rend.sharedMesh.bindposes = bp;
    53.     }
    54.     public static bool IsSorted<T>(T[] aArray, System.Func<T, T, int> aCompare)
    55.     {
    56.         if (aArray == null || aArray.Length < 2)
    57.             return true;
    58.         T last = aArray[0];
    59.         for(int i = 1; i < aArray.Length; i++)
    60.         {
    61.             T item = aArray[i];
    62.             if(aCompare(last, item) > 0)
    63.                 return false;
    64.             last = item;
    65.         }
    66.         return true;
    67.     }
    68.     private static int CompareTransform(Transform A, Transform B)
    69.     {
    70.         return A.name.CompareTo(B.name);
    71.     }
    72. }

    This should even be faster with less overhead. Especially since "remap" is used quite a lot.
     
    Ryiah likes this.