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:
drag-and-drop a folder containing an .FBX and .jpg, to the unity project. Fbx is the model, jpg the texture.
Select the FBX and extract the materials to the same folder.
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.
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 snippet
string modelPath = EditorUtility.OpenFilePanel("Select model", furnitureModelsFolder, "fbx");
if (modelPath.StartsWith(Application.dataPath))
{
modelPath = "Assets" + modelPath.Substring(Application.dataPath.Length);
}
Object model = AssetDatabase.LoadAssetAtPath(modelPath, typeof(Object)) as Object;
try
{
AssetDatabase.StartAssetEditing();
var assetsToReload = new HashSet<string>();
var materials = AssetDatabase.LoadAllAssetsAtPath(modelPath).Where(x => x.GetType() == typeof(Material));
foreach (var material in materials)
{
Material m = material as Material;
if (m != null)
{
m.color = Color.white;
}
var newAssetPath = modelPath.Substring(0, modelPath.Length - model.name.Length - 4) + material.name + ".mat"; // -4 is for .fbx
newAssetPath = AssetDatabase.GenerateUniqueAssetPath(newAssetPath);
var error = AssetDatabase.ExtractAsset(material, newAssetPath);
if (string.IsNullOrEmpty(error))
{
assetsToReload.Add(modelPath);
}
}
foreach (var path in assetsToReload)
{
AssetDatabase.WriteImportSettingsIfDirty(path);
AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
}
}
finally
{
AssetDatabase.StopAssetEditing();
}
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.
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 snippet
try
{
AssetDatabase.StartAssetEditing();
List<Material> materials = new List<Material>();
foreach (string materialPath in assetsToReload)
{
Material mat = AssetDatabase.LoadAssetAtPath(materialPath, typeof(Material)) as Material;
if (mat != null) { materials.Add(mat); }
}
string filePath = "";
string[] acceptedExtensions = new string[2] { ".jpg", ".png" };
foreach (string f in Directory.GetFiles(newModelPath))
{
if (acceptedExtensions.Contains(Path.GetExtension(f).ToLower()))
{
filePath = f;
break;
}
}
if (string.IsNullOrEmpty(filePath)) { return; }
Texture2D tex = null;
byte[] fileData;
if (File.Exists(filePath))
{
fileData = File.ReadAllBytes(filePath);
tex = new Texture2D(2, 2);
tex.LoadImage(fileData);
}
if (tex != null)
{
foreach (Material m in materials)
{
m.mainTexture = tex;
// m.SetTexture("_MainTex", tex);
}
AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
}
}
finally
{
AssetDatabase.StopAssetEditing();
// EditorSceneManager.SaveOpenScenes();
}
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.
However, when I do 2 of the following things, the texture reference gets lost:
Without closing the editor, I save a scene.
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?
@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.
void OnPostprocessModel(GameObject g)
{
if (firstImport)
ExtractMaterials();
firstImport = false;
}
void ExtractMaterials()
{
//Try to extract materials into a subfolder
var assetsToReload = new HashSet<string>();
var materials = AssetDatabase.LoadAllAssetsAtPath(assetPath).Where(x => x.GetType() == typeof(Material)).ToArray();
string destinationPath = Directory.CreateDirectory(Path.GetDirectoryName(assetPath) + "\\Materials").FullName;
Debug.Log(assetPath + " has " + materials.Length + " materials");
foreach (var material in materials)
{
var newAssetPath = destinationPath + material.name + ".mat";
newAssetPath = AssetDatabase.GenerateUniqueAssetPath(newAssetPath);
var error = AssetDatabase.ExtractAsset(material, newAssetPath);
if (String.IsNullOrEmpty(error))
{
assetsToReload.Add(assetPath);
}
}
foreach (var path in assetsToReload)
{
AssetDatabase.WriteImportSettingsIfDirty(path);
AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
}
}
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
Hey guys, I used a much simpler solution, if you change the material location to external, all materials and textures are extracted to folders automatically
var modelPaths = Directory.GetFiles(sourceModelPath, "*.*", SearchOption.AllDirectories)
.Where(path => path.EndsWith(".fbx"));
foreach (var path in modelPaths)
{
var importer = AssetImporter.GetAtPath(path) as ModelImporter;
if (importer is not null)
{
importer.materialLocation = ModelImporterMaterialLocation.External;
AssetDatabase.WriteImportSettingsIfDirty(path);
AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
}
}