Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Modelimporter - how to import as Humanoid?

Discussion in 'Animation' started by Baste, Aug 11, 2017.

  1. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,294
    I'm trying to set things up so that if our animator put a model in Character folders, the model automatically gets imported as a Humanoid, rather than as a Generic model. I'm pretty stumped.

    The problem is defining the avatar in the model. When I manually switch the model from Generic to Humanoid in the Rig tab, it generates an avatar that maps all bones correctly. When I try to supply the same bones through code (through setting the modelImporter's humanDescription), I get errors.

    Here's the hierarchy we're trying to import:

    hierarchy.png
    (I cut stuff under chest so the image wouldn't be one million pixels high :p).
    There's two different rigs defined. We're going to try for a LOD thing with the animation details.

    The stuff under unity_humanoid_rig is what we'll use for the humanoid avatar (with hips_0 as the humanoid hips bone and so on). That works when we manually change from generic to humanoid in the import settings - the correct bones are selected, and animations play correctly.

    When we try to do the same thing through asset post processing, it fails. Here's the model importer code:

    Code (csharp):
    1.  
    2. public class ModelImportSettings : AssetPostprocessor {
    3.  
    4.     private const string processedTag = "Model_preprocessed";
    5.     private const string humanoidCharacterBaseFolder = "Assets/3D Models/Characters";
    6.  
    7.     private void OnPreprocessModel() {
    8.         if (!assetPath.StartsWith(humanoidCharacterBaseFolder))
    9.             return;
    10.  
    11.         var importer = (ModelImporter) assetImporter;
    12.         if (importer.userData.Contains(processedTag))
    13.             return;
    14.  
    15.         importer.optimizeGameObjects = true;
    16.         importer.importAnimation = false;
    17.         importer.animationType = ModelImporterAnimationType.Human;
    18.  
    19.         Func<string, string, HumanBone> Bone = (humanName, boneName) => new HumanBone() {humanName = humanName, boneName = boneName};
    20.         var humanDesc = new HumanDescription {
    21.             human = new[] {
    22.                 Bone("Chest", "chest"),
    23.                 Bone("Head", "head_0"),
    24.                 Bone("Hips", "hips_0"),
    25.                 Bone("Left Index Distal", "f_index_03_L"),
    26.                 Bone("Left Index Intermediate", "f_index_02_L"),
    27.                 Bone("Left Index Proximal", "f_index_01_L"),
    28.                 Bone("Left Middle Distal", "f_middle_03_L"),
    29.                 Bone("Left Middle Intermediate", "f_middle_02_L"),
    30.                 Bone("Left Middle Proximal", "f_middle_01_L"),
    31.                 Bone("Left Ring Distal", "f_ring_03_L"),
    32.                 Bone("Left Ring Intermediate", "f_ring_02_L"),
    33.                 Bone("Left Ring Proximal", "f_ring_01_L"),
    34.                 Bone("Left Thumb Distal", "thumb_03_L"),
    35.                 Bone("Left Thumb Intermediate", "thumb_02_L"),
    36.                 Bone("Left Thumb Proximal", "thumb_01_L"),
    37.                 Bone("LeftFoot", "foot_L"),
    38.                 Bone("LeftHand", "hand_L"),
    39.                 Bone("LeftLowerArm", "forearm_L_0"),
    40.                 Bone("LeftLowerLeg", "shin_L_0"),
    41.                 Bone("LeftShoulder", "shoulder_L"),
    42.                 Bone("LeftToes", "toe_L"),
    43.                 Bone("LeftUpperArm", "upper_arm_L"),
    44.                 Bone("LeftUpperLeg", "thigh_L"),
    45.                 Bone("Neck", "neck"),
    46.                 Bone("Right Index Distal", "f_index_03_R"),
    47.                 Bone("Right Index Intermediate", "f_index_02_R"),
    48.                 Bone("Right Index Proximal", "f_index_01_R"),
    49.                 Bone("Right Middle Distal", "f_middle_03_R"),
    50.                 Bone("Right Middle Intermediate", "f_middle_02_R"),
    51.                 Bone("Right Middle Proximal", "f_middle_01_R"),
    52.                 Bone("Right Ring Distal", "f_ring_03_R"),
    53.                 Bone("Right Ring Intermediate", "f_ring_02_R"),
    54.                 Bone("Right Ring Proximal", "f_ring_01_R"),
    55.                 Bone("Right Thumb Distal", "thumb_03_R"),
    56.                 Bone("Right Thumb Intermediate", "thumb_02_R"),
    57.                 Bone("Right Thumb Proximal", "thumb_01_R"),
    58.                 Bone("RightFoot", "foot_R"),
    59.                 Bone("RightHand", "hand_R"),
    60.                 Bone("RightLowerArm", "forearm_R_0"),
    61.                 Bone("RightLowerLeg", "shin_R_0"),
    62.                 Bone("RightShoulder", "shoulder_R"),
    63.                 Bone("RightToes", "toe_R"),
    64.                 Bone("RightUpperArm", "upper_arm_R"),
    65.                 Bone("RightUpperLeg", "thigh_R"),
    66.                 Bone("Spine", "spine"),
    67.             }
    68.         };
    69.         importer.humanDescription = humanDesc;
    70.     }
    71.  
    72.     void OnPostprocessModel(GameObject model) {
    73.         if (!assetPath.StartsWith(humanoidCharacterBaseFolder))
    74.             return;
    75.  
    76.         var importer = (ModelImporter) assetImporter;
    77.         if (importer.userData.Contains(processedTag))
    78.             return;
    79.  
    80.         importer.userData = importer.userData + processedTag;
    81.  
    82.         var animator = model.EnsureComponent<Animator>();
    83.         animator.applyRootMotion = true;
    84.         animator.avatar = importer.sourceAvatar;
    85.     }
    86. }
    87.  
    When I put a .blend file in the given folder, it errors with
    "File 'NPC' avatar creation failed:
    Transform 'unity_humanoid_rig' not found in HumanDescription."

    And I get this result in the importer:

    importer.png

    Now it seems pretty clear - it expects the unity_humanoid_rig bone to be a part of the human description. The problem is that when we switch from generic to Humanoid, Unity doesn't generate any mapping for that bone, and that works fine. Here's the HumanTemplate file Unity generates for the same model:

    Code (csharp):
    1.  
    2. %YAML 1.1
    3. %TAG !u! tag:unity3d.com,2011:
    4. --- !u!1105 &110500000
    5. HumanTemplate:
    6.   m_ObjectHideFlags: 0
    7.   m_PrefabParentObject: {fileID: 0}
    8.   m_PrefabInternal: {fileID: 0}
    9.   m_Name: New Human Template
    10.   m_BoneTemplate:
    11.     Chest: chest
    12.     Head: head_0
    13.     Hips: hips_0
    14.     Left Index Distal: f_index_03_L
    15.     Left Index Intermediate: f_index_02_L
    16.     Left Index Proximal: f_index_01_L
    17.     Left Middle Distal: f_middle_03_L
    18.     Left Middle Intermediate: f_middle_02_L
    19.     Left Middle Proximal: f_middle_01_L
    20.     Left Ring Distal: f_ring_03_L
    21.     Left Ring Intermediate: f_ring_02_L
    22.     Left Ring Proximal: f_ring_01_L
    23.     Left Thumb Distal: thumb_03_L
    24.     Left Thumb Intermediate: thumb_02_L
    25.     Left Thumb Proximal: thumb_01_L
    26.     LeftFoot: foot_L
    27.     LeftHand: hand_L
    28.     LeftLowerArm: forearm_L_0
    29.     LeftLowerLeg: shin_L_0
    30.     LeftShoulder: shoulder_L
    31.     LeftToes: toe_L
    32.     LeftUpperArm: upper_arm_L
    33.     LeftUpperLeg: thigh_L
    34.     Neck: neck
    35.     Right Index Distal: f_index_03_R
    36.     Right Index Intermediate: f_index_02_R
    37.     Right Index Proximal: f_index_01_R
    38.     Right Middle Distal: f_middle_03_R
    39.     Right Middle Intermediate: f_middle_02_R
    40.     Right Middle Proximal: f_middle_01_R
    41.     Right Ring Distal: f_ring_03_R
    42.     Right Ring Intermediate: f_ring_02_R
    43.     Right Ring Proximal: f_ring_01_R
    44.     Right Thumb Distal: thumb_03_R
    45.     Right Thumb Intermediate: thumb_02_R
    46.     Right Thumb Proximal: thumb_01_R
    47.     RightFoot: foot_R
    48.     RightHand: hand_R
    49.     RightLowerArm: forearm_R_0
    50.     RightLowerLeg: shin_R_0
    51.     RightShoulder: shoulder_R
    52.     RightToes: toe_R
    53.     RightUpperArm: upper_arm_R
    54.     RightUpperLeg: thigh_R
    55.     Spine: spine
    It's exactly the same thing as I'm providing in the importer code.

    What am I doing wrong? Essentially, I want the same result as if I had clicked the "Automap" button in the avatar configuration menu (Mapping dropdown -> automap). Is there any way for me to access that?
     
    CloudyVR likes this.
  2. Mecanim-Dev

    Mecanim-Dev

    Joined:
    Nov 26, 2012
    Posts:
    1,675
    Hi baste,

    you need to provide the human mapping like you did but also all the list of transform name to include for the skeleton with the T-pose
    https://docs.unity3d.com/ScriptReference/HumanDescription.html
    https://docs.unity3d.com/ScriptReference/SkeletonBone.html

    The code below assume that your first pose in the file is a T-pose

    Code (CSharp):
    1.  
    2. Transform[] transforms = root.GetComponentsInChildren<Transform>();
    3. SkeletonBone[] skeletonBone = new SkeletonBone[transforms.Lenght];
    4. for(...)
    5. {
    6. skeletonBone[i] =  new SkeletonBone();
    7. skeletonBone[i].name = transforms[i].name;
    8. skeletonBone[i].position = transforms[i].localPosition;
    9. skeletonBone[i].rotation = transforms[i].localRotation;
    10. skeletonBone[i].scale = transforms[i].localScale;
    11. }
    12.  
     
    david-mal and Baste like this.
  3. Mecanim-Dev

    Mecanim-Dev

    Joined:
    Nov 26, 2012
    Posts:
    1,675
    Unfortunately the automap feature is not public and cannot be called by script, this is something we would like to publish at some point but there is always more important feature to do
     
  4. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,294
    Hi again!

    After implementing your suggestion, the character almost gets there. The import still fails, though.

    Looking at the difference in the Configuration menu, it seems like the automap feature runs the "enforce t-pose" option. My model errors out with a few bones that are invalid. Side-by-side:

    SideBySide.png

    Does the humanoid avatar make demands about what a valid t-pose is? It seems like the enforce t-pose command just twists the bones ever so slightly, and the animations does look right. I can do that, but then I need to know what the correct orientations are.

    Am I missing some important documentation? The information about what I'm supposed to do seems pretty sparse.
     
  5. Mecanim-Dev

    Mecanim-Dev

    Joined:
    Nov 26, 2012
    Posts:
    1,675
    Yes the auto map feature run the enforce t-pose which try different algo to do the job, because each file is exported differently depending on the DCC tools.

    https://blogs.unity3d.com/2014/05/26/mecanim-humanoids/
    Take a look at the T-pose section in this blog post, it should give you all the hints necessary for a good t-pose, the most annoying part is the thumb.
    Keep in mind that it doesn't need to be perfected aligned to work, worst case scenario you will have some offset induced by the retargeter.
    You can always go to Muscle & settings tabs and move each slider to see the full range of motion and how you skinning react
     
  6. Mecanim-Dev

    Mecanim-Dev

    Joined:
    Nov 26, 2012
    Posts:
    1,675
  7. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,294
    Okay, I'm almost there. Thanks for all the help so far.

    It seems like there's an issue internally where I can't enforce the T-pose as a part of the model importing. I'm generating the model with exactly the same humanDescription as the one Unity spits out, and I'm changing all of the bones such that they have the same position, rotation and scale as they have when Unity imports them.

    I'm still getting error messages. The error message:
    "File 'Filename' has an invalid Avatar Rig Configuration. Missing or invalid transform:
    Required human bone 'Hips' not found"

    Seems to be what the importer spits out if something is wrong no matter what the problem actually is.

    If I open the Configure menu, change nothing, and apply, the error are gone and everything works. If I right-click and re-import the model, everything works.

    I'm guessing that since I'm setting the position, rotation and scale of the bones directly in OnPostprocessModel, Unity doesn't pick those changes up and gets confused somehow. Looking through things in ILSpy, it seems like internally you're working with a SerializedObject to set those kinds of values. I can't do that since the importer doesn't expose one of those.

    Could you take a look? It's very apparent that everything's set to the correct values, it's just not getting accepted by Unity unless it runs the importer on the same data again.



    By the way - could you also add the keyword 'pinky' to AvatarAutoMapper.kLittleFingerKeywords? That's the name blender's rigify uses for the pinky, and since it's not there, it remains unmapped by the automap. It's also pretty likely that other software would use the name "pinky" for the little finger.
     
  8. Mecanim-Dev

    Mecanim-Dev

    Joined:
    Nov 26, 2012
    Posts:
    1,675
  9. Mecanim-Dev

    Mecanim-Dev

    Joined:
    Nov 26, 2012
    Posts:
    1,675
    it already there, we do have little and pinky, but the name is not the only thing that the algo take into consideration, there is probably something else messing up the final choice.

    If you want you can log a bug with you're rig that can't be mapped correctly and we will investigate
     
  10. Mecanim-Dev

    Mecanim-Dev

    Joined:
    Nov 26, 2012
    Posts:
    1,675
    No need to expose anything to work with serializedObject, they do work for all Object inheriting from the class object

    Here a small example where two model importer are serialized and modified by properties
    Code (CSharp):
    1.  
    2. SerializedObject so = new SerializedObject(new Object[] { importer, otherImporter });
    3. SerializedProperty prop = so.FindProperty("m_HumanDescription");
    4. .....
    5. so.ApplyModifiedProperties();
     
  11. gSpidy

    gSpidy

    Joined:
    Feb 1, 2017
    Posts:
    2
    It's a bit late, but if anyone still needs solution for this problem, here it is:

    Code (CSharp):
    1.  
    2. using System.Reflection;
    3.  
    4. ...
    5.  
    6. public static void MakeHumanAvatar(string avatarPath, bool reimportModel = true)
    7.     {
    8.         var aEditorType = typeof(Editor).Assembly.GetType("UnityEditor.AvatarEditor");
    9.         var editor = Editor.CreateEditor(AssetDatabase.LoadAssetAtPath<Avatar>(avatarPath), aEditorType);
    10.  
    11.         var nonPublicInstance = BindingFlags.Instance | BindingFlags.NonPublic;
    12.      
    13.         aEditorType.GetMethod("SwitchToEditMode", nonPublicInstance).Invoke(editor,null);
    14.  
    15.         var mapperField = aEditorType.GetField("m_MappingEditor", nonPublicInstance);
    16.         var mapperType = mapperField.FieldType;
    17.         var mapper = mapperField.GetValue(editor);
    18.      
    19.         mapperType.GetMethod("PerformAutoMapping",nonPublicInstance).Invoke(mapper,null);
    20.         mapperType.GetMethod("MakePoseValid", nonPublicInstance).Invoke(mapper,null);
    21.         mapperType.GetMethod("Apply", nonPublicInstance).Invoke(mapper,null);
    22.      
    23.         aEditorType.GetMethod("SwitchToAssetMode", nonPublicInstance).Invoke(editor,null);
    24.      
    25.         if(!reimportModel) return;
    26.      
    27.         var modelImporter = AssetImporter.GetAtPath(avatarPath) as ModelImporter;
    28.         if (modelImporter != null) modelImporter.SaveAndReimport();
    29.     }
    Please make shure to save your scene before calling this

    This code just calls the same private methods as when you configure humanoid avatar in editor (automap, etc). Currently, reflection is the only way, until unity exposes these methods as public
     
    Last edited: Oct 15, 2018
  12. CloudyVR

    CloudyVR

    Joined:
    Mar 26, 2017
    Posts:
    714
    how do I use the above example from within a AssetPostprocessor?

    I have been trying but I can't find the proper method to fire it,
    when in call it from OnPreprocessModel the AssetDatabase.LoadAssetAtPath can not get a reference to the .fbx and importer.sourceAvatar is always null.

    But from OnPostprocessModel I keep getting errors:
    Assertion failed: Assertion failed on expression: 'GetApplication().MayUpdate()'

    for the line:
    aEditorType.GetMethod("SwitchToEditMode", nonPublicInstance).Invoke(editor,null);


    Where should I be calling this?
     
  13. gSpidy

    gSpidy

    Joined:
    Feb 1, 2017
    Posts:
    2
    I think you can't call this from AssetPostprocessor, it's not an option, because even OnPostprocessModel is as unity scripting api says :"This function is called before the final Prefab is created and before it is written to disk"

    But we need Avatar (at least Generic) to exist at the time of MakeHumanAvatar call

    So You can call MakeHumanAvatar from any editor script, but AFTER model was imported by unity's default assetimporter
     
  14. Airmouse

    Airmouse

    Joined:
    Jan 12, 2019
    Posts:
    107
    In Unity 2019 I was getting error "TargetParameterCountException: parameters do not match signature" and had to change the line:

    From:
    Code (CSharp):
    1. aEditorType.GetMethod("SwitchToAssetMode", nonPublicInstance).Invoke(editor,null);
    To:
    Code (CSharp):
    1. aEditorType.GetMethod("SwitchToAssetMode", nonPublicInstance).Invoke(editor, new object[] {false});
     
  15. CloudyVR

    CloudyVR

    Joined:
    Mar 26, 2017
    Posts:
    714
    Unity 2019.1.0f.1 is not working with this example and the HumanDescription for the custom bone defs is ignored, even though they look correct in the bone editor!!! Unity uses the wrong bones at runtime.

    It sucks, because I see all the bones are correct (my custom overrides) in the pose editor, but at runtime it uses the wrong (default) bones.

    I have to manually go to the pose editor, hit "enforce tpose" even though it already is and nothing appears to change, then hit "apply" and finally the new bones are used.

    Unity 2019.1.0f.1 is broken, and setting HumanDescription.HumanBone is not fully supported or heavily bugged.

    So a Warning to anyone trying to automate Unity 2019 for asset imports! Even though setting custom HumanDescription.HumanBone appears to be applied in the pose editor, it is not actually applied and you must still press the "apply" button manually for the new bones to be properly selected!! Otherwise the animators use some hidden set of default bone defs and will trick you when you load your assetbundles and see your avatars looking like f#ing monsters!:mad:

    Also I have verified the exact same code example on 2018 and it worked perfectly with no discrepancies between pose editor and actual model.

    What must I do now in order to fully apply the HumanDescription.HumanBone overrides???:confused:
     
    Last edited: Apr 24, 2019
    Airmouse likes this.
  16. Mecanim-Dev

    Mecanim-Dev

    Joined:
    Nov 26, 2012
    Posts:
    1,675
    if it does work in 2018 but not in 2019 then you should log a bug, it look like a regression.

    I'm not aware of any change from the animation side that could have break this, so yes please log a bug if you can
     
  17. CloudyVR

    CloudyVR

    Joined:
    Mar 26, 2017
    Posts:
    714
    I'm not too sure now what changed (and I found another way to setup my character's using a template avatar).

    However I believe I had noticed that in the new editors you MUST absolutely set both HumanDescription.human and HumanDescription.skeleton variables otherwise the configuration will be totally ignored.

    This was not the case in previous versions. But I would not consider it to be a "bug" since all that is needed is to include the properly configured skeleton configuration.

    Again, I am no longer using reflection to activate the enforce T-Pose option (which was the most bullet proof way and I wish it was easier to use). however I was forced to use a template avatar setup instead, which gets things very very close, but not as close as they were before.

    I just wish Unity made the enforce t-pose option more accessible, would have saved me from writing 3 whole pages of code, and a whole month's worth of debugging, frustration and failure.

    Please consider making enforce t-pose option more accessible for future developers who only want to automate their editors.... Currently it's pure ridiculousness.
     
    Glader likes this.
  18. kognito1

    kognito1

    Joined:
    Apr 7, 2015
    Posts:
    331
    Somewhat related...on import we try to assign pre-existing humanoid skeletons to our fbxs. However for hand waving asset bundle reasons, we want to bake those skeletons into the fbx file (i.e. we don't want "Copy From Another Avatar" but "Create From This Model"). Essentially we are just trying to copy the metadata from a "reference skeleton file" into the importing fbx file.

    This works great except the root bone's name is not correct (it takes the name of the "reference asset" when we want it to be the name of the importing fbx file). And while we can change the names of bones, SkeletonBone does not offer a way to change a bone's parent's name. So unfortunately even if we "correct" the problem bone, child bones will still be wrong. We solved this through what I would call "creative measures", but it seemed like a glaring API omission on SkeletonBone. Is there a reason this property is not exposed?
     
    Airmouse and CloudyVR like this.
  19. errur

    errur

    Joined:
    Apr 15, 2017
    Posts:
    5
    I can't really get this to work.
    Code (csharp):
    1. mapperType.GetMethod("MakePoseValid", nonPublicInstance).Invoke(mapper, null);
    This line always crashes my unity regardless of what I set as my rig beforehand. I tried both generic and humanoid.

    Is there still no other way to do this? Currently using unity 2018.4.13f
     
  20. anishgoyal_g

    anishgoyal_g

    Joined:
    Jun 15, 2022
    Posts:
    1
    Is there any chance this has been made public since 2017?