Material in the prefab scene is not updated after its replacement in the project

In short, I made an import, where at the stage of import from fbx the necessary artifacts are created. All artifacts except materials are attached to .asset, external materials in a separate folder as in the code below

modelImporter.materialLocation = ModelImporterMaterialLocation.External;
modelImporter.materialName = ModelImporterMaterialName.BasedOnMaterialName;
modelImporter.materialSearch = ModelImporterMaterialSearch.Everywhere;

then this fbx is instantiated in OnPostprocessAllAssets. And in this object the meshes are replaced with meshes from .asset, after which I create prefabs from this object:

first its visual

Visual

the second one has a visual prefab embedded in it

after that fbx is Deleted from the project

ps
if we wanted to replace fbx, then further mapping and replacement is done on name mapping. If we throw 123.fbx into the folder where 123.asset exists, the visual prefab will be recreated PrefabUtility.SaveAsPrefabAsset

Now that I’ve explained the pipeline a bit, I’ll describe the problem.

I replace the name of the material in the fbx, import it into the project where there are already prefabs based on this fbx, then in the project the materials are updated on the prefabs, but in the scene the materials are OLD. If the scene is restarted, the materials on the prefabs in the scene will be updated

Actually the question is what is happening I do not understand. In the visual prefab was write the GUID of the new material in as it should have been. But the view of the objects was not updated in the scene

I’m not sure what the problem is exactly. It sounds like a view update issue where reloading the scene fixes the display of the name of the material? But other than the name it’s working fine?

I assume you have the prefab’s Inspector open while importing. If so, does the name update if you deselect the prefab instance in the scene, then import the FBX, then select the prefab instance?

Seeing the code would help.

What do you mean by “written guid new material”?
If the material GUID changes then any references for a material with the old GUID will not update.
This may indicate that you create a new material asset. If so, then the prefab needs to be opened, edited (assign the material reference), and saved back to disk with PrefabUtility.

If you also modify the prefab instance in the scene (via script or manually) it may be flagged as modified and the “Overrides” dropdown menu will show you which changes may need to be applied back to the prefab or reverted.

Good afternoon thank you for responding, let’s try to figure it out as this bug is very critical for me.
Video of what it looks like (to replace a container, simply drag another fbx with the same name into its folder) :point_down:

Files with their materials :point_down:
Material 01

Material 02

I’ve consolidated the code into one class and minimized it to a minimum :point_down:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;

namespace Code.Editor
{
    public class FbxContainer: ScriptableObject
    {
        
    }
    
    public class AssetImportPostprocessor : AssetPostprocessor
    {
        #region Importer
        private void OnPreprocessModel()
        {
            ModelImporter modelImporter = assetImporter as ModelImporter;
        
            modelImporter.importBlendShapes = false;
            modelImporter.importCameras = false;
            modelImporter.importVisibility = false;
            modelImporter.importLights = false;
            
            modelImporter.materialImportMode = ModelImporterMaterialImportMode.ImportStandard;
            modelImporter.materialLocation = ModelImporterMaterialLocation.External;
            modelImporter.materialName = ModelImporterMaterialName.BasedOnMaterialName;
            modelImporter.materialSearch = ModelImporterMaterialSearch.Everywhere;

            modelImporter.importTangents = ModelImporterTangents.CalculateMikk;
            modelImporter.isReadable = false;
            modelImporter.animationType = ModelImporterAnimationType.None;
            modelImporter.importAnimation = false;
        }
        
        private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets,
            string[] movedFromAssetPaths)
        {
            if (importedAssets.Length == 0) return;

            foreach (var assetPath in importedAssets)
            {
                string ext = String.Empty;
                ext = Path.GetExtension(assetPath);

                switch (ext.ToLower())
                {
                    case ".fbx":
                        ConvertMesh(assetPath);
                        break;
                }
            }
        }
        
        #endregion

        #region Methods
        
        public static void ConvertMesh(string sourceAssetPath)
        {
            var sourceModel = AssetDatabase.LoadAssetAtPath<GameObject>(sourceAssetPath);
            var sourceModelCopy = Object.Instantiate(sourceModel);
            sourceModelCopy.name = sourceModelCopy.name.Replace("(Clone)", "");
             
            var sourceMeshFilters = sourceModelCopy.GetComponentsInChildren<MeshFilter>();
            var sourceSkinnedMeshRenderer = sourceModelCopy.GetComponentsInChildren<SkinnedMeshRenderer>();
            var sourceAvatar = AssetDatabase.LoadAssetAtPath<Avatar>(sourceAssetPath);
            
            // PRODUCE CONTAINER
            string containerPath = GetFullPathWithoutExtension(sourceAssetPath) + ".asset";
            FbxContainer container = AssetDatabase.LoadAssetAtPath<FbxContainer>(containerPath);
            
            // if there is no container, create one and fill it
            if (!container)
            {
                ScriptableObject containerObj = ScriptableObject.CreateInstance<FbxContainer>();
                AssetDatabase.CreateAsset(containerObj, containerPath);

                foreach (var smf in sourceMeshFilters)
                {
                    var sourceSubAsset = smf.sharedMesh;
                    var artifactSubAsset = AttachSubAsset(sourceSubAsset, containerPath);
                    // assign a mesh to the source model meshFilter so that when the model is saved to the prefab, the link will already be thrown through
                    smf.sharedMesh = artifactSubAsset;
                }
            }
            // if the container exists
            else
            {
                var existingContainerSubAssets = AssetDatabase.LoadAllAssetRepresentationsAtPath(containerPath).OfType<Mesh>().ToList();
                
                foreach (var smf in sourceMeshFilters)
                {
                    var sourceSubAsset = smf.sharedMesh;
                    // check if a particular subAsset needs to be updated
                    var artifactSubAsset = UpdateSubAsset(existingContainerSubAssets, sourceSubAsset, containerPath);
                    smf.sharedMesh = artifactSubAsset;
                }
                
                // destroy because the Artist removed them in the original model
                foreach (var subAsset in existingContainerSubAssets)
                {
                    Object.DestroyImmediate(subAsset, true);
                }
            }
            
            AssetDatabase.SaveAssets();
            
            // PRODUCE PREFAB, VISUAL PREFAB, MECHANIC PREFABS
            GameObject tempPrefab = null;
            
            // get paths
            var modelPath = sourceAssetPath.Substring(0, sourceAssetPath.LastIndexOf("/"));
            var assetsRootPath = modelPath.Substring(0, modelPath.LastIndexOf("/"));
            
            // simulate path
            var pathVisualPrefabsFolder = assetsRootPath + "/Prefabs/VisualPrefabs";
            var pathPrefabsFolder = assetsRootPath + "/Prefabs";
            
            var pathToSaveVisualPrefab = pathVisualPrefabsFolder + "/" + sourceModelCopy.name + "_!VisualPrefab.prefab";
            var pathToSaveMechanicPrefab = pathPrefabsFolder + "/" + sourceModelCopy.name + ".prefab";
            
            // VISUAL PREFAB
            // recursively create folders
            Directory.CreateDirectory(pathVisualPrefabsFolder);

            tempPrefab = StaticVisualPrefabCreate(sourceModelCopy, pathToSaveVisualPrefab);
            
            // MECHANIC PREFAB
            if (!AssetDatabase.AssetPathExists(pathToSaveMechanicPrefab))
            {
                if (tempPrefab)
                {
                    StaticMechanicPrefabCreate(tempPrefab, pathToSaveMechanicPrefab);
                }
            }
            
            Object.DestroyImmediate(sourceModelCopy, true);
            DeleteAsset(sourceAssetPath);
        }
        
        private static Mesh AttachSubAsset(Mesh sourceSubAsset, string containerPath)
        {
            Mesh artifactSubAsset = Object.Instantiate(sourceSubAsset);
            artifactSubAsset.name = sourceSubAsset.name + "_!Artifact";
                    
            AssetDatabase.AddObjectToAsset(artifactSubAsset, containerPath);

            return artifactSubAsset;
        }
        
        private static Mesh UpdateSubAsset(List<Mesh> existingContainerSubAssets, Mesh sourceSubAsset, string containerPath)
        {
            // are there any subAssets that in theory need to be updated?
            var artifactToUpdate = existingContainerSubAssets.Find(x => x.name.Equals(sourceSubAsset.name + "_!Artifact"));
            if (artifactToUpdate)
            {
                Vector3[] normals = sourceSubAsset.normals;
                
                sourceSubAsset.name += "_!Artifact";
                EditorUtility.CopySerialized(sourceSubAsset, artifactToUpdate);
                
                artifactToUpdate.normals = normals;
                
                // all this recalculation crap doesn't seem to be necessary
                // artifactToUpdate.RecalculateBounds();
                // RecalculateNormals always leaves a UV seam trail
                // artifactToUpdate.RecalculateNormals(); 
                // artifactToUpdate.RecalculateTangents();
                        
                // remove from the list the subAssets that have been processed, the remaining subAssets in
                // the list are candidates for destroy because the Artist removed them in the original model
                existingContainerSubAssets.Remove(artifactToUpdate);
                            
                // assign a mesh to the source model meshFilter so that when the model is saved to the prefab, the link will already be thrown through
                return artifactToUpdate;
            }
            
            // if a subAsset is present in the source model, but it is not present in the container subAssets,
            // then this subAsset must be created and attached to the container 
            var artifactSubAsset = AttachSubAsset(sourceSubAsset, containerPath);
            // assign a mesh to the source model meshFilter so that when the model is saved to the prefab, the link will already be thrown through
            return artifactSubAsset;
        }
        
        private static GameObject StaticVisualPrefabCreate(GameObject sourceModel, string pathToSaveVisualPrefab)
        {
            sourceModel.name += "_!Visual";
            sourceModel.isStatic = true;
            
            var visualPrefab = PrefabUtility.SaveAsPrefabAsset(sourceModel, pathToSaveVisualPrefab);
            
            return visualPrefab;
        }
        
        private static void StaticMechanicPrefabCreate(GameObject visualPrefab, string pathToSavePrefab)
        {
            GameObject rootObj = new GameObject();

            GameObject visualRootObj = new GameObject();
            visualRootObj.transform.parent = rootObj.transform;
            visualRootObj.name = "Visual";
            
            rootObj.name = visualPrefab.name.Replace("_!Visual", "");

            GameObject visualPrefabInst = PrefabUtility.InstantiatePrefab(visualPrefab) as GameObject;
            visualPrefabInst.transform.parent = visualRootObj.transform;
            
            PrefabUtility.SaveAsPrefabAsset(rootObj, pathToSavePrefab);

            Object.DestroyImmediate(visualPrefabInst);
            Object.DestroyImmediate(rootObj);
            
            AssetDatabase.SaveAssets();
        }
        
        #endregion

        #region Helpers
        
        private static String GetFullPathWithoutExtension(String path)
        {
            return Path.Combine(Path.GetDirectoryName(path), Path.GetFileNameWithoutExtension(path));
        }
        private static void DeleteAsset(string path)
        {
            AssetDatabase.DeleteAsset(path);
        }
        
        #endregion
    }
}

I see. Now I wonder: does this also affect how the 3d model looks like in the scene view or when entering playmode, or is it only an Inspector glitch? If it’s the latter you could just ignore that since it’s temporary.

This check is unnecessary. The foreach will be skipped if the collection is empty.

You should defer CreateAsset. First, instantiate. Then, modify the object. Then create an asset for this object. This avoids having to a) first write the asset to disk to create it in its default state and then b) write the asset to disk again in order to save the modifications.

This also avoids the common issue where scripted modifications to ScriptableObject instances don’t dirty the object, thus not saving the changes unless you call EditorUtility.SetDirty() before saving.

Also: SaveAssets will save more than the assets you modify, and you call it in multiple locations. Prefer to use SaveAssetIfDirty instead. Doing so will ensure that your code doesn’t simply save something by chance (but may not save under some circumstances) but rather you intentionally save specifically what’s modified. It does help to prevent issues by being specific, if only to prevent unnecessary disk operations (doesn’t matter for a single file but eventually you may import many or a more complex one).

The manual has an interesting bit to say about this method in particular when you overwrite. Might be the issue you run into:

If you are saving over an existing Prefab, Unity tries to preserve references to the Prefab itself and the individual parts of the Prefab such as child GameObjects and Components. To do this, it matches the names of GameObjects between the new saved Prefab and the existing Prefab.

Note: Because this matching is done by name only, if there are multiple GameObjects with the same name in the Prefab’s hierarchy, you cannot predict which will be matched. Therefore if you need to ensure your references are preserved when saving over an existing prefab, you must ensure all GameObjects within the Prefab have unique names.

Also note: You may encounter a similar problem in the case of preserving references to existing Components when you save over an existing Prefab, if a single GameObject within the Prefab has more than one of the same Component type attached. In this case you cannot predict which of them will be matched to the existing references.

It’s hardly an inspector or stage problem. I see that when renaming/replacing FBX material in the standard piplane, everything happens as it should.

Video :point_down:

It looks like after:
fbx deletions
|| material reference violations
|| object installation

some links are broken and the scene does not receive a call to redraw, although the material was replaced in the prefab.

to test this, I used the following code:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;

namespace Code.Editor
{
    public class FbxContainer: ScriptableObject
    {
        
    }
    
    public class AssetImportPostprocessor : AssetPostprocessor
    {
        #region Importer
        private void OnPreprocessModel()
        {
            ModelImporter modelImporter = assetImporter as ModelImporter;
        
            modelImporter.importBlendShapes = false;
            modelImporter.importCameras = false;
            modelImporter.importVisibility = false;
            modelImporter.importLights = false;
            
            modelImporter.materialImportMode = ModelImporterMaterialImportMode.ImportStandard;
            modelImporter.materialLocation = ModelImporterMaterialLocation.External;
            modelImporter.materialName = ModelImporterMaterialName.BasedOnMaterialName;
            modelImporter.materialSearch = ModelImporterMaterialSearch.Everywhere;

            modelImporter.importTangents = ModelImporterTangents.CalculateMikk;
            modelImporter.isReadable = false;
            modelImporter.animationType = ModelImporterAnimationType.None;
            modelImporter.importAnimation = false;
        }
        
        private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets,
            string[] movedFromAssetPaths)
        {
            Debug.Log("Log");
            
            foreach (var assetPath in importedAssets)
            {
                string ext = String.Empty;
                ext = Path.GetExtension(assetPath);
            
                switch (ext.ToLower())
                {
                    case ".fbx":
                        CreateSimplePrefab(assetPath);
                        // ConvertMesh(assetPath);
                        break;
                }
            }
        }
        
        #endregion

        #region Methods

        public static void CreateSimplePrefab(string sourceAssetPath)
        {
            var sourceModel = AssetDatabase.LoadAssetAtPath<GameObject>(sourceAssetPath);
           
            // get paths
            var modelPath = sourceAssetPath.Substring(0, sourceAssetPath.LastIndexOf("/"));
            var assetsRootPath = modelPath.Substring(0, modelPath.LastIndexOf("/"));
            
            // simulate path
            var pathVisualPrefabsFolder = assetsRootPath + "/Prefabs/VisualPrefabs";
            var pathPrefabsFolder = assetsRootPath + "/Prefabs";
            
            // VISUAL PREFAB
            // recursively create folders
            Directory.CreateDirectory(pathVisualPrefabsFolder);
            
            var pathToSaveVisualPrefab = pathVisualPrefabsFolder + "/" + sourceModel.name + "_!VisualPrefab.prefab";
            
            sourceModel.name += "_!Visual";
            sourceModel.isStatic = true;
            
            PrefabUtility.SaveAsPrefabAsset(sourceModel, pathToSaveVisualPrefab);
        }
        
        public static void ConvertMesh(string sourceAssetPath)
        {
            var sourceModel = AssetDatabase.LoadAssetAtPath<GameObject>(sourceAssetPath);
            var sourceModelCopy = Object.Instantiate(sourceModel);
            sourceModelCopy.name = sourceModelCopy.name.Replace("(Clone)", "");
             
            var sourceMeshFilters = sourceModelCopy.GetComponentsInChildren<MeshFilter>();
            var sourceSkinnedMeshRenderer = sourceModelCopy.GetComponentsInChildren<SkinnedMeshRenderer>();
            var sourceAvatar = AssetDatabase.LoadAssetAtPath<Avatar>(sourceAssetPath);
            
            // PRODUCE CONTAINER
            string containerPath = GetFullPathWithoutExtension(sourceAssetPath) + ".asset";
            FbxContainer container = AssetDatabase.LoadAssetAtPath<FbxContainer>(containerPath);
            
            // if there is no container, create one and fill it
            if (!container)
            {
                ScriptableObject containerObj = ScriptableObject.CreateInstance<FbxContainer>();
                AssetDatabase.CreateAsset(containerObj, containerPath);

                foreach (var smf in sourceMeshFilters)
                {
                    var sourceSubAsset = smf.sharedMesh;
                    var artifactSubAsset = AttachSubAsset(sourceSubAsset, containerPath);
                    // assign a mesh to the source model meshFilter so that when the model is saved to the prefab, the link will already be thrown through
                    smf.sharedMesh = artifactSubAsset;
                }
            }
            // if the container exists
            else
            {
                var existingContainerSubAssets = AssetDatabase.LoadAllAssetRepresentationsAtPath(containerPath).OfType<Mesh>().ToList();
                
                foreach (var smf in sourceMeshFilters)
                {
                    var sourceSubAsset = smf.sharedMesh;
                    // check if a particular subAsset needs to be updated
                    var artifactSubAsset = UpdateSubAsset(existingContainerSubAssets, sourceSubAsset, containerPath);
                    smf.sharedMesh = artifactSubAsset;
                }
                
                // destroy because the Artist removed them in the original model
                foreach (var subAsset in existingContainerSubAssets)
                {
                    Object.DestroyImmediate(subAsset, true);
                }
            }
            
            AssetDatabase.SaveAssets();
            
            // PRODUCE PREFAB, VISUAL PREFAB, MECHANIC PREFABS
            GameObject tempPrefab = null;
            
            // get paths
            var modelPath = sourceAssetPath.Substring(0, sourceAssetPath.LastIndexOf("/"));
            var assetsRootPath = modelPath.Substring(0, modelPath.LastIndexOf("/"));
            
            // simulate path
            var pathVisualPrefabsFolder = assetsRootPath + "/Prefabs/VisualPrefabs";
            var pathPrefabsFolder = assetsRootPath + "/Prefabs";
            
            var pathToSaveVisualPrefab = pathVisualPrefabsFolder + "/" + sourceModelCopy.name + "_!VisualPrefab.prefab";
            var pathToSaveMechanicPrefab = pathPrefabsFolder + "/" + sourceModelCopy.name + ".prefab";
            
            // VISUAL PREFAB
            // recursively create folders
            Directory.CreateDirectory(pathVisualPrefabsFolder);

            tempPrefab = StaticVisualPrefabCreate(sourceModelCopy, pathToSaveVisualPrefab);
            
            // MECHANIC PREFAB
            if (!AssetDatabase.AssetPathExists(pathToSaveMechanicPrefab))
            {
                if (tempPrefab)
                {
                    StaticMechanicPrefabCreate(tempPrefab, pathToSaveMechanicPrefab);
                }
            }
            
            Object.DestroyImmediate(sourceModelCopy, true);
            DeleteAsset(sourceAssetPath);
        }
        
        private static Mesh AttachSubAsset(Mesh sourceSubAsset, string containerPath)
        {
            Mesh artifactSubAsset = Object.Instantiate(sourceSubAsset);
            artifactSubAsset.name = sourceSubAsset.name + "_!Artifact";
                    
            AssetDatabase.AddObjectToAsset(artifactSubAsset, containerPath);

            return artifactSubAsset;
        }
        
        private static Mesh UpdateSubAsset(List<Mesh> existingContainerSubAssets, Mesh sourceSubAsset, string containerPath)
        {
            // are there any subAssets that in theory need to be updated?
            var artifactToUpdate = existingContainerSubAssets.Find(x => x.name.Equals(sourceSubAsset.name + "_!Artifact"));
            if (artifactToUpdate)
            {
                Vector3[] normals = sourceSubAsset.normals;
                
                sourceSubAsset.name += "_!Artifact";
                EditorUtility.CopySerialized(sourceSubAsset, artifactToUpdate);
                
                artifactToUpdate.normals = normals;
                
                // all this recalculation crap doesn't seem to be necessary
                // artifactToUpdate.RecalculateBounds();
                // RecalculateNormals always leaves a UV seam trail
                // artifactToUpdate.RecalculateNormals(); 
                // artifactToUpdate.RecalculateTangents();
                        
                // remove from the list the subAssets that have been processed, the remaining subAssets in
                // the list are candidates for destroy because the Artist removed them in the original model
                existingContainerSubAssets.Remove(artifactToUpdate);
                            
                // assign a mesh to the source model meshFilter so that when the model is saved to the prefab, the link will already be thrown through
                return artifactToUpdate;
            }
            
            // if a subAsset is present in the source model, but it is not present in the container subAssets,
            // then this subAsset must be created and attached to the container 
            var artifactSubAsset = AttachSubAsset(sourceSubAsset, containerPath);
            // assign a mesh to the source model meshFilter so that when the model is saved to the prefab, the link will already be thrown through
            return artifactSubAsset;
        }
        
        private static GameObject StaticVisualPrefabCreate(GameObject sourceModel, string pathToSaveVisualPrefab)
        {
            sourceModel.name += "_!Visual";
            sourceModel.isStatic = true;
            
            var visualPrefab = PrefabUtility.SaveAsPrefabAsset(sourceModel, pathToSaveVisualPrefab);
            
            return visualPrefab;
        }
        
        private static void StaticMechanicPrefabCreate(GameObject visualPrefab, string pathToSavePrefab)
        {
            GameObject rootObj = new GameObject();

            GameObject visualRootObj = new GameObject();
            visualRootObj.transform.parent = rootObj.transform;
            visualRootObj.name = "Visual";
            
            rootObj.name = visualPrefab.name.Replace("_!Visual", "");

            GameObject visualPrefabInst = PrefabUtility.InstantiatePrefab(visualPrefab) as GameObject;
            visualPrefabInst.transform.parent = visualRootObj.transform;
            
            PrefabUtility.SaveAsPrefabAsset(rootObj, pathToSavePrefab);

            Object.DestroyImmediate(visualPrefabInst);
            Object.DestroyImmediate(rootObj);
            
            AssetDatabase.SaveAssets();
        }
        
        #endregion

        #region Helpers
        
        private static String GetFullPathWithoutExtension(String path)
        {
            return Path.Combine(Path.GetDirectoryName(path), Path.GetFileNameWithoutExtension(path));
        }
        private static void DeleteAsset(string path)
        {
            AssetDatabase.DeleteAsset(path);
        }
        
        #endregion
    }
}

commenting deleting the source fixes the situation, but the source requires deletion and so it can’t be left like that it’s just a symptom

// MECHANIC PREFAB
            if (!AssetDatabase.AssetPathExists(pathToSaveMechanicPrefab))
            {
                if (tempPrefab)
                {
                    StaticMechanicPrefabCreate(tempPrefab, pathToSaveMechanicPrefab);
                }
            }
            
            // DeleteAsset(sourceAssetPath);

Adding AssetDatabase.ForceReserializeAssets(); helps to fix the problem, but you can’t do it for all assets, you need to know what to reserialize at least. Also in this variant the impector blinks when the material is changed, but only the material field should be redrawn.

// MECHANIC PREFAB
            if (!AssetDatabase.AssetPathExists(pathToSaveMechanicPrefab))
            {
                if (tempPrefab)
                {
                    StaticMechanicPrefabCreate(tempPrefab, pathToSaveMechanicPrefab);
                }
            }
            
            AssetDatabase.ForceReserializeAssets();
            DeleteAsset(sourceAssetPath);

That’s meant to upgrade asset versions when you upgrade editor versions. That way you can ensure all asset and .meta file changes the new editor versions are committed to source control all at once rather than polluting individual commits.

I wrote about this here:

For other use cases it is functionally no different than flagging all assets as dirty and then calling SaveAssets(). So if it does make a difference, I suppose the problem is simply that perhaps something else needs saving that isn’t dirtied - in your case possibly the scene with the instance of the prefab.

Give that a try: dirty the currently active scene and save it with EditorSceneManager.