In Editor - Update Material and save it to disk

It seems to be straightforward in runtime mode to update the textures of a material and save the changes to disk. However I currently try to automate setting up materials upon the import of new textures in editor mode.

Basically I am loading a material asset, update some of its textures and try to save the changes back to disk, but I don’t seem to be able to find the right command for saving the changes back to the asset.

Here’s my code:

// Asset does exist, setup maps
             Material material = (Material)AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guids[0]), typeof(Material));
             if (tex_name.Contains("_col"))
             {
                 material.SetTexture("_MainTex", tex);
             }
             if (tex_name.Contains("_nor"))
             {
                 material.SetTexture("_BumpMap", tex);
             }
             if (tex_name.Contains("_par"))
             {
                 material.EnableKeyword("_EMISSION");
                 material.SetTexture("_EmissionMap", tex);
             }
             // Save Updated Material to Disk              
             AssetDatabase.SaveAssets();

Call Undo.RecordObject on the material before applying the changes. Or, if you don’t want to save an undo state for it, use EditorUtility.SetDirty afterwards.

Thank you for your help!

Sadly neither Undo.RecordObject nor EditorUtility.SetDirty resulted in the material to be updated and saved to disk.

You sure? I just checked it myself and both of these methods worked, independently. (In one of my assets I actually use both of them at once but it seems that’s a bit redundant)

I feel like I am hitting a wall here… Would you mind showing me your working example? I tried it several times now to no effect.

What are the symptoms of it not getting updated/saved? Are the changes not getting reflected at all? Or are they not persisting past a reload? Something else?

This is the entirety of the snippet I used:

var mat = AssetDatabase.LoadAssetAtPath<Material>( "Assets/Material.mat" );

// either RecordObject here
Undo.RecordObject( mat, "Set Color" );

mat.SetColor( "_Color", Color.red );

// or SetDirty here
EditorUtility.SetDirty( mat );

Thanks for the code.
It works when setting the color, but in my case doesn’t work when setting the texture.

The texture is actually set but the when saving the asset/s the texture texture field is cleard.
I tried saving with File - Save Project and with
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();

Did you encountered this with textures? Thanks.

If you are using a texture you’re creating on the fly, you will need to save the texture as well. It is somewhat awkward to work with textures, since you need to save them to disk as an image asset, then reload the newly imported image asset (which has all its import properties reset to the default, so you also probably need to modify those and re-import), and apply that version of the texture to the material.

In other words, whatever texture you set on the material needs to exist as an asset in the project in order for the reference to be savable.

I’m open to anyone who has found a more elegant workflow when working with generated textures, it would be very nice to know about it.

1 Like

With what little I know of how Unity is architected, I can’t see any better way than this. Assets on disk can only refer to other assets on disk, full stop. :slight_smile:

1 Like

I was referring more to the fact that you need to serialize a texture to disk (as PNG, for example) in order to then load it as a new different texture, rather than just writing the texture directly to disk and immediately being able to use it as you would any other type of asset. The whole create texture → serialize to disk → import → modify import settings to match original texture → re-import → load texture workflow is garbage.

Your point still stands, though, and is a very succinct way to put the overall concept.

And I will agree, that really IS a garbage work flow!!

Thanks @Madgvox , I checked my old project and indeed I was trying to update the texture but didn’t save the changes back to disk. I just tried the serialize to disk and reimport workflow ( a bit tedious as you already said) and it works just fine! 2 years and several projects later, glad we solved this one. :slight_smile:

1 Like

@Madgvox @JDB-Artist Hey guys, please forgive me profusely for resurrecting this thread from a year ago, but I’m having the exact same issue you guys are describing. In edit mode I’m creating dynamic textures by doing screen captures and then saving them as texture assets. Then I’m ‘attempting’ to reload them and set them to a Texture2D field in a ScriptableObject.

Creating the texture asset works, but everything I’ve tried of reloading the texture back in doesn’t. It ‘appears’ to work, the Icon field seems updating and clicking it even pings the texture asset. Which makes it all the more confusing when I re-open Unity later and the reference is gone.

Would either of you mind just providing the most simple example of “The whole create texture → serialize to disk → import → modify import settings to match original texture → re-import → load texture workflow”?
I’d forever been in your debt. I have spent so much time trying to figure this out and I don’t know where else to look. All I want to do is:

  1. Create a texture in edit mode and save as texture asset
  2. Reload the saved texture and assign it to a field
  3. Have that field retain reference to newly created texture upon Unity reload

This thread has been the only place I can find any reference to this issue and I don’t know where else to turn! Any assistance is appreciated more than you could know! I am not proud enough to admit I have spent so many hours on this and just feel so dumb I’ve spent this much time and couldn’t figure out what you’d think wouldn’t be so hard.

Thanks!
Will

@lorewap3 I’m not in the position to provide an example to you at the moment, but I wanted to create a post to let you know I will get back to you on this soon. This is a standard Unity pattern for working with textures and deserves to have an online reference!

Just from your post, it sounds like you’re creating the asset ok, but not properly dirtying the asset you’re setting the texture reference on. In order to keep everything in sync, you also need to make sure the asset you’re setting the reference on also gets serialized to disk, using EditorUtility.SetDirty or Undo.RecordObject.

I made you an exemple

Link to sample project: LOREWAP3.zip - Google Drive


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//********************************************************************************
//
//********************************************************************************

public class Materials : MonoBehaviour
{
    //****************************************************************************
    //
    //****************************************************************************

    [ SerializeField ] public Material[] m_materials = new Material[ 0 ];
}
using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

#if UNITY_EDITOR

using UnityEditor;
using UnityEditor.SceneManagement;

//********************************************************************************
//
//********************************************************************************

public class CreateSomeMatsAndTextures
{
    //****************************************************************************
    //
    //****************************************************************************

    private const string DEFAULT_DIRECTORY = "Assets/MatsAndTextures";
                                           
    private const string DEFAULT_SHADER    = "Standard";

    //****************************************************************************
    //
    //****************************************************************************

    static private Texture2D CreateSomeTexture( int w, int h, Color[] pix )
    {
        Texture2D tex = new Texture2D( w, h, TextureFormat.RGBA32, false, true );

        tex.SetPixels( pix );

        return tex;
    }

    //****************************************************************************
    //
    //****************************************************************************

    static private Material CreateSomeMaterial( Shader shd, Texture tex )
    {
        Material mat = new Material( shd );

        mat.mainTexture = tex;

        return mat;
    }

    //****************************************************************************
    //
    //****************************************************************************

    static private Tuple< Material[], Texture2D[] > CreateMatsAndTextures( int nb )
    {
        if( nb <= 0 ) return null;

        Material[]  mats = new Material [ nb ];

        Texture2D[] texs = new Texture2D[ nb ];

        Shader      shdr = Shader.Find( DEFAULT_SHADER );


        int       w   = 64;
                     
        int       h   = 64;

        Color[]   pix = new Color[ w * h ];

        for( int x = 0; x < w; ++x )
        {
            for( int y = 0; y < h; ++y )
            {
                pix[ x + ( y * w ) ] = Color.HSVToRGB( ( float )y / ( float )h, ( float )x / ( float )w, 1.0f );
            }
        }


        try
        {
            AssetDatabase.DisallowAutoRefresh();
       
            for( int t = 0; t < nb; ++t )
            {
                Texture2D tex_tmp  = CreateSomeTexture( w, h, pix );

                string    tex_path = AssetDatabase.GenerateUniqueAssetPath( DEFAULT_DIRECTORY + "/SomeTexture.jpg" );

                File.WriteAllBytes   ( tex_path, tex_tmp.EncodeToJPG() );

                AssetDatabase.Refresh();

               
               
                Texture2D tex = AssetDatabase.LoadAssetAtPath< Texture2D >( tex_path );

                Material  mat = CreateSomeMaterial( shdr, tex );

                AssetDatabase.CreateAsset( mat, AssetDatabase.GenerateUniqueAssetPath( DEFAULT_DIRECTORY + "/SomeMaterial.mat" ) );

                EditorUtility.SetDirty   ( tex );

                EditorUtility.SetDirty   ( mat );

                texs[ t ] = tex;

                mats[ t ] = mat;
            }
        }

        finally
        {
            AssetDatabase.SaveAssets();

            AssetDatabase.Refresh   ();

            AssetDatabase.AllowAutoRefresh();
        }

        return new Tuple< Material[], Texture2D[] >( mats, texs );
    }

    //****************************************************************************
    //
    //****************************************************************************

    static private void CreateObjectMatsAndTextures( GameObject obj, int nb )
    {
        Tuple< Material[], Texture2D[] > resources = CreateMatsAndTextures( 4 );

        if( resources != null )
        {
            Materials mats_comp = ( obj != null ) ? obj.GetComponent< Materials >() : null;

            if( mats_comp != null )
            {
                mats_comp.m_materials = resources.Item1;

                EditorSceneManager.MarkSceneDirty( obj.scene );

                EditorSceneManager.SaveOpenScenes();
            }
        }
    }

    //****************************************************************************
    //
    //****************************************************************************

    [ MenuItem( "GameObject/CREATE LOREWAP3 TEXTURES", false, 0 ) ]

    [ MenuItem( "Assets/CREATE LOREWAP3 TEXTURES",     false, 0 ) ] static private void Generate( MenuCommand cmd )
    {
        GameObject obj = ( ( cmd.context != null ) && ( cmd.context is GameObject ) )? cmd.context as GameObject : Selection.activeGameObject;

        CreateObjectMatsAndTextures( obj, 4 );
    }
}

#endif
1 Like

Thank you @Yseron ! This is exactly what I was looking for!

I had been using EditorUtility.SetDirty on the texture, but no idea if the ordering was correct. And I didn’t even know about AssetDatabase.GenerateUniqueAssetPath or Disallow/Allow AutoRefresh. This makes alot of sense!

I’ve been using AssetDatabase.Refresh(AssetImportOptions.ForceUpdate / ForceSynchronousImport) to try and force the refresh but using DisallowAutoRefresh certainly makes more sense. I’ve read that AssetDatabase.Refresh() is asynchronous. I’m guessing the DisallowAutoRefresh also causes Refresh to be synchronous? I kept having issues with the texture not necessarily being available by the time that next line happens to load it back in. I’d been using weird coroutines that waited a few seconds to see if it had been imported yet. Really hokey…

Thanks to you too @Madgvox for the quick reply! It’s always refreshing when people are happy to help even on a super old thread.

Thanks again guys this helps me so much!
Will

1 Like

I have to same issue, but set mainTexture is working like a charm.

If I use:

if (propertyName.Equals("_MainTex"))
    foundMaterial.mainTexture = texture; //working code
else
    foundMaterial.SetTexture(propertyName, texture); //??? not working, why?? when propertyName is
//_BaseMap or _NormalTex or _BumpMap or _OcclusionMap or _MainTex

As you see the

material.SetTexture(“_MainTex”) not working only the
material.mainTexture=texture;

I want to update the normal texture slots and other texture slots, not only the mainTex slot.

Thanks