Search Unity

Batch change all FBX Default Materials, Help!

Discussion in 'Editor & General Support' started by FranFndz, Feb 8, 2019.

  1. FranFndz

    FranFndz

    Joined:
    Sep 20, 2018
    Posts:
    178
    Im trying to use the ModelImporter to change all my FBX Default-Material for a Dummy Material and stoped the compile spike from skinned mesh in mobiles.

    The problem is that ModelImporter don't have that property class so i suppose I need to use SerializedObject to get the inspector material selection.

    Still, i can change mostly of the setting with this code but I'm not able to change and save the material, can someone help me?

    Already 2 Days trying.

    Code (CSharp):
    1.  [MenuItem("Assets/SetDummy")]
    2.     private static void SetDummy()
    3.     {
    4.         Object obj = Selection.activeObject;
    5.         if (obj == null)
    6.             return;
    7.         string assetPath = AssetDatabase.GetAssetPath(obj);
    8.         ModelImporter modelImporter = AssetImporter.GetAtPath(assetPath) as ModelImporter;
    9.         if (modelImporter == null)
    10.             return;
    11.         SerializedObject modelImporterObj = new SerializedObject(modelImporter);
    12.         SerializedProperty materials = modelImporterObj.FindProperty("m_Materials");
    13.         SerializedProperty m_ExternalObjects = modelImporterObj.FindProperty("m_ExternalObjects");
    14.      
    15.         var pop = modelImporterObj.GetIterator();
    16.         while(pop.NextVisible(true))
    17.             Debug.Log(pop.propertyPath);
    18.      
    19.      
    20.         for (int i = 0; i < materials.arraySize; i++)
    21.         {
    22.             SerializedProperty id = materials.GetArrayElementAtIndex(i);
    23.             var name = id.FindPropertyRelative("name").stringValue = "dummy";
    24.             var type = id.FindPropertyRelative("type").stringValue;
    25.             var assembly = id.FindPropertyRelative("assembly").stringValue;
    26.  
    27.             SerializedProperty materialProp;
    28.             Material material = null;
    29.             var propertyIdx = 0;
    30.  
    31.             for (int externalObjectIdx = 0, count = m_ExternalObjects.arraySize; externalObjectIdx < count; ++externalObjectIdx)
    32.             {
    33.                 var pair = m_ExternalObjects.GetArrayElementAtIndex(externalObjectIdx);
    34.                 var externalName = pair.FindPropertyRelative("first.name").stringValue = "dummy";
    35.                 var externalType = pair.FindPropertyRelative("first.type").stringValue;
    36.              
    37.                 if (externalName == name && externalType == type)
    38.                 {
    39.                     materialProp = pair.FindPropertyRelative("second");
    40.                  
    41.                     material = materialProp != null ? materialProp.objectReferenceValue as Material : null;
    42.                     propertyIdx = externalObjectIdx;
    43.                     break;
    44.                 }
    45.              
    46.             }
    47.         }
    48.      
    49.      
    50.         modelImporterObj.ApplyModifiedProperties();
    51.         AssetDatabase.ImportAsset(assetPath);
    52.     }
     
    Last edited: Feb 10, 2019
  2. FranFndz

    FranFndz

    Joined:
    Sep 20, 2018
    Posts:
    178
    please :(
     
  3. FranFndz

    FranFndz

    Joined:
    Sep 20, 2018
    Posts:
    178
    should I post some screenshots so Unity mods could check my issue?
     
  4. FranFndz

    FranFndz

    Joined:
    Sep 20, 2018
    Posts:
    178
    I still need help with this.

    Im using AssetPostprocessor to change all FBX materials, so all my FBX use a dummy material and a dummy shader.
    The problem is that my Cache server don't save the modifications and my build include the Unity Standard materials.
     
  5. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,332
    You're not telling the model importer to save changes, that might be the issue:

    Code (csharp):
    1. // instead of:
    2. modelImporterObj.ApplyModifiedProperties();
    3. AssetDatabase.ImportAsset(assetPath);
    4.  
    5. //try:
    6. modelImporterObj.ApplyModifiedProperties();
    7. modelImporter.SaveAndReimport();
    8.  
     
  6. FranFndz

    FranFndz

    Joined:
    Sep 20, 2018
    Posts:
    178
    Thank you, I will try again.
    Im sure my problem is this;
    https://www.jianshu.com/p/cc0410e0c31c

    https://www.jianshu.com/p/632869a87848

    He had the same issue, trying to delete FBX material and fix runtime standard compile, but as Assetimporter do no save meta or change the asset he can not commit any change, all the changes are inside Unity/Library.
    Also Cache Server do not save material changes using AssetPostProcessor.
    I copy his code but didn't work, idk if he could fix it or not. I will need a Chinese friend now ;p
     
  7. FranFndz

    FranFndz

    Joined:
    Sep 20, 2018
    Posts:
    178
    Didn't work after SaveAndReimport. it also do not work if I reimport using mouse right click.

    it must be an issue with Cache Server.

    Still waiting for unity help here :D
     
  8. FranFndz

    FranFndz

    Joined:
    Sep 20, 2018
    Posts:
    178
    Still can't make it to work.

    Going keep trying today with our jenkis build machine.
     
  9. FranFndz

    FranFndz

    Joined:
    Sep 20, 2018
    Posts:
    178
    So we give up!

    tried many code samples, another co worker also give it a try but we can t build with the changes using Material Import,

    all our 4 jenkis machine return build with the default settings and not the post material settings. We even turned off our Cache server for testing but nothing.

    Time to change all the assets by hand.
     
  10. PW_Dave

    PW_Dave

    Joined:
    Feb 9, 2017
    Posts:
    30
    I feel your pain. I've been trying to do the same thing to remove the references to the standard shader which is blowing up our ShaderLab memory usage. I'm just posting a reply in the hopes that somebody from Unity might see this.

    The problem that I'm seeing is that the external object map for the model importer is empty if the materials have not been remapped, which you can only do through the inspector (which is delightfully broken pre-2018.2.20f1, by the way).

    If the model importer could at least give us access to the FBX hierarchy, we could scrape the materials and create the entries. Or, better yet, Unity could allow us to override the default material for imported models. Otherwise, it looks like we'll be manually changing the assets by hand as well.

    I just started working on this, and I'll be kicking it around a bit more. If I find any way to workaround this issue, I'll post it here.
     
  11. PW_Dave

    PW_Dave

    Joined:
    Feb 9, 2017
    Posts:
    30
    So, I may have a workaround. I traverse the model in the OnPostprocessModel callback and find all of the materials. Then I use AssetImporter.AddRemap() for each material to remap to my default. Here's a stripped down version of my code without various material name checks and other things to make sure that I don't stomp over manually edited assets.

    Code (CSharp):
    1. public class ImporterProcessor : AssetPostprocessor
    2. {
    3.     public Material defaultMaterial;
    4.  
    5.     void OnPostprocessModel(GameObject g)
    6.     {
    7.         RemapDefaultMaterial(g.transform);
    8.     }
    9.  
    10.     void RemapDefaultMaterial(Transform t)
    11.     {
    12.         Renderer renderer = t.gameObject.GetComponent<Renderer>();
    13.  
    14.         if(defaultMaterial == null)
    15.         {
    16.             defaultMaterial = AssetDatabase.LoadAssetAtPath<Material>("Assets/Materials/Default.mat");
    17.         }
    18.  
    19.         if (renderer != null)
    20.         {
    21.             Material[] materials = renderer.sharedMaterials;
    22.  
    23.             for (int materialIndex = 0; materialIndex < materials.Length; materialIndex++)
    24.             {
    25.                 Material material = materials[materialIndex];
    26.  
    27.                 assetImporter.AddRemap(new AssetImporter.SourceAssetIdentifier(material), defaultMaterial);
    28.             }
    29.         }
    30.  
    31.         // Recurse
    32.         foreach (Transform child in t)
    33.         {
    34.             RemapDefaultMaterial(child);
    35.         }
    36.     }
    37. }
    I'm at least seeing the external object map being populated in the meta file. I've only tested this on a minimal test project, so your results may vary. After some more testing I'll unleash it on our main project and see how it goes.
     
    Last edited: Mar 12, 2019
  12. FranFndz

    FranFndz

    Joined:
    Sep 20, 2018
    Posts:
    178
    Thank you for the input. I'm pretty sure I went that path already but I will give It a try again.

    Yes I hope some Unity hero show us the light.

    Right now the only one who worked is:

    Code (CSharp):
    1. public Material OnAssignMaterialModel(Material material, Renderer renderer)
    2.     {
    3.         if (assetPath.ToLower().Contains("fbx"))
    4.         {
    5.             var materialPath = "Assets/Editor/Resources/LNC/FBX/Materials/dummy.mat";
    6.          
    7.             if(AssetDatabase.LoadAssetAtPath(materialPath, typeof(Material)))
    8.             {
    9.                 return AssetDatabase.LoadAssetAtPath(materialPath, typeof(Material)) as Material;
    10.             }
    11.             else
    12.             {
    13.                 return null;
    14.             }
    15.         }
    16.         else
    17.         {
    18.             return null;
    19.         }
    20.     }
    21.    

    It work when creating AB as well, but it doesn't work with Cache Server or using Jenkins Machine to build a game using Batch command. (never open the editor).

    Maybe in your case it can work.
     
  13. PW_Dave

    PW_Dave

    Joined:
    Feb 9, 2017
    Posts:
    30
    A couple more details that I've discovered as I continue testing this.
    - The model has to have Import Materials enabled before the remapping happens, or the materials that are found for remapping will be Default-Material. Since Default-Material is unlikely to be the material name in the FBX hierarchy, you'll end up with an invalid remapping entry in the meta file and nothing will happen.
    - You can disable the Import Materials setting after the remap, and the remap will still happen since the info exists in the meta file. It seems as though the person who wrote the inspector didn't fully understand the model importer process. Perhaps that's why it was actually broken for several versions?
    - I ended up implementing this by traversing through the AssetDatabase and remapping any models that exist already, since I didn't want to reimport all. I suspect that we'll have both methods in place if this method works. It's looking like it will take several hours to process all of our assets, so I'll be testing it tomorrow.
    - There is a new feature called the Scriptable Build Pipeline which we were informed of that may make all of this work moot. We'll be evaluating it next week. Here's the link to the docs in the package manager: https://docs.unity3d.com/Packages/com.unity.scriptablebuildpipeline@1.2/manual/index.html
     
  14. FranFndz

    FranFndz

    Joined:
    Sep 20, 2018
    Posts:
    178
    Hi!

    So I finally did it.

    It never worked using asset post process and Cache Server / And Command Line arguments to build.

    The idea is create a small script that go inside the folder and get the FBX with bad materials that are creating spike in run time.

    script step:
    Load FBX as ModelImporter
    Change the settings to the FBX
    importMaterial = True
    materialLocation = ModelImporterMaterialLocation.External
    materialName = BaseonModelNameAndMAterialName
    search = search.Local

    after this step Save and reimport

    Load FBX again as GameObject
    traverse all SkinnedMeshRenderer
    traverse all material
    check for bad material or build in shader (not in AssetBundle)
    if it find it Delete the material Asset (AssetDataBase Delete)

    get the ModelImporter again and change to
    materialLocation = inPrefab
    SaveAndReimport

    Refresh.

    Now we have a proper meta file that won't compile shaders in runtime creating frames spikes.
    The last part is important (change the model importer again)

    If you don't do the last part of the code Unity will attach unlit shader inside the building machine. (recreate meta file) and all compiles are back.

    by the way the script is very slow. (we have around 1000 fbx)
     
    FabioGaudenzi likes this.
  15. FranFndz

    FranFndz

    Joined:
    Sep 20, 2018
    Posts:
    178
    by the way this is the profile in game (android)

    before and after setting FBX material to missing. (Green line is shader compile)

     
  16. ViniciusGraciano

    ViniciusGraciano

    Joined:
    May 19, 2013
    Posts:
    13
    In Unity 2018, I found out that there is an internal property in
    ModelImporter
    that holds an array containing all the SourceAssetIdentifiers of the imported models. In this manner, we can easily remap each source material to a default material in the project.

    I am including a simple example in the code below (without error detection and caching). Note that you can use
    modelImporter.GetExternalObjectMap
    to do some more processing if needed (e.g., not remapping materials that have been manually set by the artists).
    Code (CSharp):
    1. private void OnPreprocessModel()
    2. {
    3.     var defaultMaterial = AssetDatabase.LoadAssetAtPath<Material>("Assets/Default.mat");
    4.  
    5.     var modelImporter = assetImporter as ModelImporter;
    6.     modelImporter.importMaterials = true;
    7.     modelImporter.materialLocation = ModelImporterMaterialLocation.InPrefab;
    8.     modelImporter.materialName = ModelImporterMaterialName.BasedOnModelNameAndMaterialName;
    9.     modelImporter.materialSearch = ModelImporterMaterialSearch.Local;
    10.  
    11.     var sourceMaterials = typeof(ModelImporter)
    12.             .GetProperty("sourceMaterials", BindingFlags.NonPublic | BindingFlags.Instance)?
    13.             .GetValue(modelImporter) as AssetImporter.SourceAssetIdentifier[];
    14.     foreach (var identifier in sourceMaterials ?? Enumerable.Empty<AssetImporter.SourceAssetIdentifier>())
    15.     {
    16.         modelImporter.AddRemap(identifier, defaultMaterial);
    17.     }
    18. }
    In our project it was also the case that some models had imported meshes containing zero vertices or triangles. In such scenarios, Unity creates a renderer that uses the built-in Standard material, but this information is not accessible in
    OnPreprocessModel
    . I decided to remove such meshes from our project and to prohibit the creation of a GameObject hierarchy containing any. Here's a simplified version of the code.
    Code (CSharp):
    1. private void OnPostprocessMeshHierarchy(GameObject root)
    2. {
    3.     foreach (var meshFilter in root.GetComponentsInChildren<MeshFilter>())
    4.     {
    5.         if (meshFilter.sharedMesh.vertexCount == 0 || meshFilter.sharedMesh.triangles.Length == 0)
    6.         {
    7.             Object.DestroyImmediate(meshFilter.sharedMesh);
    8.             Object.DestroyImmediate(root);
    9.             return;
    10.         }
    11.     }
    12.  
    13.     foreach (var skinnedMeshRenderer in root.GetComponentsInChildren<SkinnedMeshRenderer>())
    14.     {
    15.         if (skinnedMeshRenderer.sharedMesh.vertexCount == 0 || skinnedMeshRenderer.sharedMesh.triangles.Length == 0)
    16.         {
    17.             Object.DestroyImmediate(skinnedMeshRenderer.sharedMesh);
    18.             Object.DestroyImmediate(root);
    19.             return;
    20.         }
    21.     }
    22. }
     
    Last edited: Jun 23, 2020
  17. ViniciusGraciano

    ViniciusGraciano

    Joined:
    May 19, 2013
    Posts:
    13
    The above method won't work in Unity 2019+. They have changed the mesh import process, and the sourceMaterials array is not populated by the engine where it used to be. I will see if I am able to port the code.
     
  18. FranFndz

    FranFndz

    Joined:
    Sep 20, 2018
    Posts:
    178
    good to know that, i hope the project i worked before wont update unity
     
  19. renaudbedard

    renaudbedard

    Joined:
    Jul 2, 2012
    Posts:
    20
    I just hit the issue where the method you described @ViniciusGraciano worked well in 2018.4 but stopped working in 2019.4 :(
    If you found a workaround, I'm very interested to hear it! I'm still poking around.
     
  20. nicholasmu

    nicholasmu

    Unity Technologies

    Joined:
    Feb 27, 2017
    Posts:
    9
    Please try these codes in 2019.4.x.

    Code (CSharp):
    1. private void OnPostprocessModel(GameObject g)
    2. {
    3. var expectedMaterial = AssetDatabase.LoadAssetAtPath<Material>("Assets/custom_m.mat");
    4.  
    5.             using (var so = new SerializedObject(assetImporter))
    6.             {
    7.                 var materials = so.FindProperty("m_Materials");
    8.                 var externalObjects = so.FindProperty("m_ExternalObjects");
    9.  
    10.                 for (int materialIndex = 0; materialIndex < materials.arraySize; materialIndex ++)
    11.                 {
    12.                     var id = materials.GetArrayElementAtIndex(materialIndex);
    13.                     var name = id.FindPropertyRelative("name").stringValue;
    14.                     var type = id.FindPropertyRelative("type").stringValue;
    15.                     var assembly = id.FindPropertyRelative("assembly").stringValue;
    16.  
    17.                     SerializedProperty materialProperty = null;
    18.  
    19.                     for (int externalObjectIndex = 0; externalObjectIndex < externalObjects.arraySize; externalObjectIndex ++)
    20.                     {
    21.                         var currentSerializedProperty = externalObjects.GetArrayElementAtIndex(externalObjectIndex);
    22.                         var externalName = currentSerializedProperty.FindPropertyRelative("first.name").stringValue;
    23.                         var externalType = currentSerializedProperty.FindPropertyRelative("first.type").stringValue;
    24.  
    25.                         if (externalType == type && externalName == name)
    26.                         {
    27.                             materialProperty = currentSerializedProperty.FindPropertyRelative("second");
    28.                             break;
    29.                         }
    30.                     }
    31.  
    32.                     if (materialProperty == null)
    33.                     {
    34.                         var lastIndex = externalObjects.arraySize++;
    35.                         var currentSerializedProperty = externalObjects.GetArrayElementAtIndex(lastIndex);
    36.                         currentSerializedProperty.FindPropertyRelative("first.name").stringValue = name;
    37.                         currentSerializedProperty.FindPropertyRelative("first.type").stringValue = type;
    38.                         currentSerializedProperty.FindPropertyRelative("first.assembly").stringValue = assembly;
    39.                         currentSerializedProperty.FindPropertyRelative("second").objectReferenceValue = expectedMaterial;
    40.                     }
    41.                     else
    42.                     {
    43.                         materialProperty.objectReferenceValue = expectedMaterial;
    44.                     }
    45.                 }
    46.  
    47.                 so.ApplyModifiedPropertiesWithoutUndo();
    48.             }
    49.             assetImporter.SaveAndReimport();
    50. }
     
  21. salvolannister

    salvolannister

    Joined:
    Jan 3, 2019
    Posts:
    50
    For the ones struggling like me to make this work you need to place it in a script that extends "AssetPostprocessor" and then to test it, reimport one random model.

    I would like to use this method to change the default shader unity use for the materials (e.g. from Unlit to Sprites/default), but I am not able to figure it out how to do it.

    I know that to do it from an editor script you have to change the sharedMaterial property of the renderer, thus I tried to modify the above script in order to find the property sharedMaterials, but I had no luck because it returns me null.
    How could I do it?
    Code (CSharp):
    1.  var sharedMaterials = so.FindProperty("m_SharedMaterials");
    UPDATE
    I found out that if I use this peace of code inside OnPostprocessModel(GameObject g) I am able to change material shader as I wanted.

    Code (CSharp):
    1. Shader newShader = Shader.Find("Sprites/Default");
    2.  
    3.         Renderer renderer = g.GetComponentInChildren<MeshRenderer>();
    4.  
    5.         if (renderer != null)
    6.         {
    7.             foreach (var material in renderer.sharedMaterials)
    8.             {
    9.                 material.shader = newShader;
    10.             }
    11.         }
    12.  
    Still I don't think it is the best solution but it works
     
    Last edited: Feb 8, 2021
  22. ccllz

    ccllz

    Joined:
    Feb 25, 2021
    Posts:
    2
    Code (CSharp):
    1. public class ModelProcessor : AssetPostprocessor
    2.     {
    3.         public const string Material = "Assets/Materials/Dummy.mat";
    4.  
    5.         void OnPostprocessModel(GameObject g)
    6.         {
    7.             var expectedMaterial = AssetDatabase.LoadAssetAtPath<Material>(Material);
    8.  
    9.             var checkMaterials = new Dictionary<string, Material>();
    10.             checkMaterials.Add("Default-Material", expectedMaterial);
    11.  
    12.             var dirty = false;
    13.             var renderers = g.GetComponentsInChildren<Renderer>(true);
    14.             foreach (var renderer in renderers)
    15.             {
    16.                 dirty = Replace(renderer, checkMaterials) || dirty;
    17.             }
    18.  
    19.             if (dirty)
    20.             {
    21.                 RemapDefaultMaterial(expectedMaterial);
    22.             }
    23.         }
    24.  
    25.         void RemapDefaultMaterial(Material expectedMaterial)
    26.         {
    27.             var modelImporter = assetImporter as ModelImporter;
    28.             modelImporter.materialImportMode = ModelImporterMaterialImportMode.ImportViaMaterialDescription;
    29.             modelImporter.materialLocation = ModelImporterMaterialLocation.InPrefab;
    30.             modelImporter.materialName = ModelImporterMaterialName.BasedOnModelNameAndMaterialName;
    31.             modelImporter.materialSearch = ModelImporterMaterialSearch.Local;
    32.  
    33.             var sourceMaterials = typeof(ModelImporter)
    34.                     .GetProperty("sourceMaterials", BindingFlags.NonPublic | BindingFlags.Instance)?
    35.                     .GetValue(modelImporter) as AssetImporter.SourceAssetIdentifier[];
    36.             foreach (var identifier in sourceMaterials ?? Enumerable.Empty<AssetImporter.SourceAssetIdentifier>())
    37.             {
    38.                 modelImporter.AddRemap(identifier, expectedMaterial);
    39.             }
    40.  
    41.             modelImporter.SaveAndReimport();
    42.         }
    43.  
    44.         public static bool Replace(Renderer renderer, Dictionary<string, Material> checkMaterial)
    45.         {
    46.             var materials = renderer.sharedMaterials;
    47.             bool modified = false;
    48.  
    49.             for (int i = 0; i < materials.Length; ++i)
    50.             {
    51.                 if (materials[i] == null)
    52.                     continue;
    53.  
    54.                 var name = materials[i].name;
    55.  
    56.                 Material mat;
    57.                 if (checkMaterial.TryGetValue(name, out mat))
    58.                 {
    59.                     materials[i] = mat;
    60.                     modified = true;
    61.                 }
    62.             }
    63.  
    64.             if (modified)
    65.             {
    66.                 renderer.sharedMaterials = materials;
    67.             }
    68.  
    69.             return modified;
    70.         }
    71.     }
    I try this in 2019.4
    if I just use func 'RemapDefaultMaterial' the importer setting be changed by the script, but renderer's material do not change. I must reimport model again and get the right result. so I do another thing, repalce renderer's material first, and then change importer setting.
     
  23. Bao_Minh

    Bao_Minh

    Joined:
    Feb 22, 2021
    Posts:
    2
    Thank you so much, you save my life .
     
  24. Bao_Minh

    Bao_Minh

    Joined:
    Feb 22, 2021
    Posts:
    2
    Thank you so much, you save my life, work good in 2020.3
     
  25. noahternullo

    noahternullo

    Joined:
    May 26, 2017
    Posts:
    11
    This seems to work fine in 2021.2.0b3, and it's exactly what I needed! Thank you :)
     
  26. Wully

    Wully

    Joined:
    Mar 18, 2014
    Posts:
    15
    This no longer seems to work on Unity 2021.3.16f unfortunatly. The sourceMaterials always comes back empty