Search Unity

Extract material from fbx

Discussion in 'External Tools' started by F-R-O-S-T-Y, Apr 25, 2018.

  1. F-R-O-S-T-Y

    F-R-O-S-T-Y

    Joined:
    Mar 22, 2013
    Posts:
    234
    Hello,

    I would like to extract the materials of a model to a folder the same way I can do manually currently: Selecting the .fbx file, navigating to Materials tab and hitting the "Extract Materials..." button.

    I would like to be able to do that same thing with and editor script.

    My workflow currently works like this:
    1) drag-and-drop a folder containing an .FBX and .jpg, to the unity project. Fbx is the model, jpg the texture.
    2) Select the FBX and extract the materials to the same folder.
    3) Drag the jpg file onto the extracted material's albedo and adjust the color.

    I would like to automate the second step, where after I've chosen the fbx, I can use an editor script to extract the materials.

    I've done some editor scripting so navigating and finding the fbx is not a problem. It is the extracting part of the materials that I am stuck at. Doing it manually for hundreds of models is time consuming. And I need the materials extracted.

    Help is much appreciated!
    Thanks!
     
  2. F-R-O-S-T-Y

    F-R-O-S-T-Y

    Joined:
    Mar 22, 2013
    Posts:
    234
    Necrotime!

    Any ideas on how to extract materials from a model via script? Should I post this question in another thread?

    Thanks!
     
  3. fffMalzbier

    fffMalzbier

    Joined:
    Jun 14, 2011
    Posts:
    2,830
    mingingzi and F-R-O-S-T-Y like this.
  4. F-R-O-S-T-Y

    F-R-O-S-T-Y

    Joined:
    Mar 22, 2013
    Posts:
    234
    Thank you so much for the answer! It lead me into the right direction. By the way, how did you know to reference me these 2 links? How did you find them? How did you know where to look?

    ModelImporterMaterialEditor.cs uses
    PrefabUtility.ExtractMaterialsFromAsset()
    , which however seems to no longer exist. PrefabUtility does of course but the function ExtractMaterialsFromAsset() does not. So I just implemented the function on my own.

    Note that the
    target
    seen in above 2 scripts is UnityEngine.Object and in my code snippet below,
    target
    is equal to
    model
    .

    Code (CSharp):
    1. string modelPath = EditorUtility.OpenFilePanel("Select model", furnitureModelsFolder, "fbx");
    2.  
    3. if (modelPath.StartsWith(Application.dataPath))
    4. {
    5.     modelPath = "Assets" + modelPath.Substring(Application.dataPath.Length);
    6. }
    7.  
    8. Object model = AssetDatabase.LoadAssetAtPath(modelPath, typeof(Object)) as Object;
    9.  
    10. try
    11. {
    12.     AssetDatabase.StartAssetEditing();
    13.  
    14.     var assetsToReload = new HashSet<string>();
    15.  
    16.     var materials = AssetDatabase.LoadAllAssetsAtPath(modelPath).Where(x => x.GetType() == typeof(Material));
    17.  
    18.     foreach (var material in materials)
    19.     {
    20.         Material m = material as Material;
    21.         if (m != null)
    22.         {
    23.             m.color = Color.white;
    24.         }
    25.         var newAssetPath = modelPath.Substring(0, modelPath.Length - model.name.Length - 4) + material.name + ".mat"; // -4 is for .fbx
    26.  
    27.         newAssetPath = AssetDatabase.GenerateUniqueAssetPath(newAssetPath);
    28.         var error = AssetDatabase.ExtractAsset(material, newAssetPath);
    29.         if (string.IsNullOrEmpty(error))
    30.         {
    31.             assetsToReload.Add(modelPath);
    32.         }
    33.     }
    34.  
    35.     foreach (var path in assetsToReload)
    36.     {
    37.         AssetDatabase.WriteImportSettingsIfDirty(path);
    38.         AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
    39.     }
    40. }
    41. finally
    42. {
    43.     AssetDatabase.StopAssetEditing();
    44. }
     
    fherbst and mingingzi like this.
  5. fffMalzbier

    fffMalzbier

    Joined:
    Jun 14, 2011
    Posts:
    2,830
    I keep my self up to date unity funky unity stuff and i seen that unity has provided the editor C# source code.
    There where some unofficial one before that and you could dig into the editor with some disassembly tools.
    From there on i know the basic naming scene that unity uses for there inspector and editor code so its kind of easy to find.

    And thanks for sharing your solution.
     
    F-R-O-S-T-Y likes this.
  6. F-R-O-S-T-Y

    F-R-O-S-T-Y

    Joined:
    Mar 22, 2013
    Posts:
    234
    I have a problem, which follows right after extracting materials. I suppose I could just write the problem in this thread.

    Right after I export the materials, I also look for any textures in the same folder as the materials and I try to apply them. I'm just trying to fill in the albedo value.

    Code (CSharp):
    1. try
    2. {
    3.     AssetDatabase.StartAssetEditing();
    4.     List<Material> materials = new List<Material>();
    5.     foreach (string materialPath in assetsToReload)
    6.     {
    7.         Material mat = AssetDatabase.LoadAssetAtPath(materialPath, typeof(Material)) as Material;
    8.         if (mat != null) { materials.Add(mat); }
    9.     }
    10.  
    11.     string filePath = "";
    12.  
    13.     string[] acceptedExtensions = new string[2] { ".jpg", ".png" };
    14.     foreach (string f in Directory.GetFiles(newModelPath))
    15.     {
    16.         if (acceptedExtensions.Contains(Path.GetExtension(f).ToLower()))
    17.         {
    18.             filePath = f;
    19.             break;
    20.         }
    21.     }
    22.     if (string.IsNullOrEmpty(filePath)) { return; }
    23.  
    24.     Texture2D tex = null;
    25.     byte[] fileData;
    26.  
    27.     if (File.Exists(filePath))
    28.     {
    29.         fileData = File.ReadAllBytes(filePath);
    30.         tex = new Texture2D(2, 2);
    31.         tex.LoadImage(fileData);
    32.     }
    33.  
    34.     if (tex != null)
    35.     {
    36.         foreach (Material m in materials)
    37.         {
    38.             m.mainTexture = tex;
    39.             // m.SetTexture("_MainTex", tex);
    40.          }
    41.         AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
    42.     }
    43. }
    44. finally
    45. {
    46.     AssetDatabase.StopAssetEditing();
    47.     // EditorSceneManager.SaveOpenScenes();
    48. }

    The code works. Textures are found and loaded into memory. The references are also added to the materials. If I check the inspector for the material then the textures have been applied.

    Here is a link to an image of the Project and Scene view of the model, which has its materials extracted and textures applied.

    Image

    However, when I do 2 of the following things, the texture reference gets lost:
    1) Without closing the editor, I save a scene.
    2) Closing the editor and restarting it.
    If I am in an unsaved (dirty) scene, extract the materials and move to a new scene without saving the current one, the reference is Not lost.

    By losing the reference, the material loses its texture and uses the default albedo color. Also, notice in code I commented out
    / EditorSceneManager.SaveOpenScenes();
    . If I uncomment this and the scene gets saved via code, the texture reference gets lost the same way as if I was to manually save the scene.

    Do I have to save the material again after applying the texture? Do I also need to save the texture asset again? Help is much appreciated.

    EDIT: I suppose what is happening is that the Texture2D I apply to the material is created in temporary memory. After I shut down Unity or save the scene, this memory is cleared and thus my reference is lost. Does it mean then that switching the scenes without saving them does not clear the buffer? If this is the case, how can I reference not the Texture2D in memory but the actual Texture2D asset?

    Thanks!
     
    Last edited: Sep 6, 2018
  7. F-R-O-S-T-Y

    F-R-O-S-T-Y

    Joined:
    Mar 22, 2013
    Posts:
    234
    It seems my suspicions were correct. Instead of assigning a texture that is created temporarily in memory, an asset must be used instead.

    I changed this:
    Code (CSharp):
    1. Texture2D tex = null;
    2. byte[] fileData;
    3. if (File.Exists(filePath))
    4. {
    5.     fileData = File.ReadAllBytes(filePath);
    6.     tex = new Texture2D(2, 2);
    7.     tex.LoadImage(fileData);
    8. }

    to this:
    Code (CSharp):
    1. filePath = filePath.StartsWith(Application.dataPath) ? filePath.Substring(Application.dataPath.Length - "Assets".Length) : filePath;
    2.  
    3. Texture2D tex = AssetDatabase.LoadAssetAtPath<Texture2D>(filePath);

    The following link has a good example on how to do this. For setting the textures, see Step 10.
    Tutorial Link

    Cheers!
     
  8. A20T3M4

    A20T3M4

    Joined:
    Oct 4, 2016
    Posts:
    4
    @F-R-O-S-T-Y
    Where would you run this custom method if you wanted to extract materials on import?
    I'm calling this from AssetPostProcessor.OnPostprocessModel and its saying it finds 0 materials to extract (a debug that I had added.) and leaving me with an empty Materials subfolder.

    Code (CSharp):
    1. void OnPostprocessModel(GameObject g)
    2.     {
    3.         if (firstImport)
    4.             ExtractMaterials();
    5.         firstImport = false;
    6.     }
    7.  
    8.     void ExtractMaterials()
    9.     {
    10.         //Try to extract materials into a subfolder
    11.         var assetsToReload = new HashSet<string>();
    12.         var materials = AssetDatabase.LoadAllAssetsAtPath(assetPath).Where(x => x.GetType() == typeof(Material)).ToArray();
    13.         string destinationPath = Directory.CreateDirectory(Path.GetDirectoryName(assetPath) + "\\Materials").FullName;
    14.         Debug.Log(assetPath + " has " + materials.Length + " materials");
    15.         foreach (var material in materials)
    16.         {
    17.             var newAssetPath = destinationPath + material.name + ".mat";
    18.             newAssetPath = AssetDatabase.GenerateUniqueAssetPath(newAssetPath);
    19.  
    20.             var error = AssetDatabase.ExtractAsset(material, newAssetPath);
    21.             if (String.IsNullOrEmpty(error))
    22.             {
    23.                 assetsToReload.Add(assetPath);
    24.             }
    25.         }
    26.  
    27.         foreach (var path in assetsToReload)
    28.         {
    29.             AssetDatabase.WriteImportSettingsIfDirty(path);
    30.             AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
    31.         }
    32.     }
     
    Last edited: Jul 10, 2019