Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Voting for the Unity Awards are OPEN! We’re looking to celebrate creators across games, industry, film, and many more categories. Cast your vote now for all categories
    Dismiss Notice
  3. Dismiss Notice

Extract material from fbx

Discussion in 'Asset Importing & Exporting' started by Wattosan, Apr 25, 2018.

  1. Wattosan

    Wattosan

    Joined:
    Mar 22, 2013
    Posts:
    450
    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. Wattosan

    Wattosan

    Joined:
    Mar 22, 2013
    Posts:
    450
    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:
    3,276
    just_gos, Deleted User and Wattosan like this.
  4. Wattosan

    Wattosan

    Joined:
    Mar 22, 2013
    Posts:
    450
    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. }
     
  5. fffMalzbier

    fffMalzbier

    Joined:
    Jun 14, 2011
    Posts:
    3,276
    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.
     
    Wattosan likes this.
  6. Wattosan

    Wattosan

    Joined:
    Mar 22, 2013
    Posts:
    450
    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. Wattosan

    Wattosan

    Joined:
    Mar 22, 2013
    Posts:
    450
    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:
    5
    @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
  9. EVariable42

    EVariable42

    Joined:
    Mar 25, 2020
    Posts:
    1
    Hiya it's because you're trying to run it on PostprocessModel but at this point the asset hasn't actually been registered as created yet, you wanna run it afterwards on PostProcessAllAssets so it has a reference to the actual mesh :) (I know this is an old post but I just went through this headache so I wanted to post a reply for any other poor soul that ends up on this thread looking for answers
     
  10. Allan_RD3

    Allan_RD3

    Joined:
    Oct 25, 2021
    Posts:
    4
    I tried man and it didn't work. How this can be done?