Saving/Loading Texture2D Asset possible? AssetPreview.GetAssetPreview BUG!

I’ve spent the past two hours trying to figure out how to save and load a Texture2D asset using AssetDatabase, but haven’t been able to figure it out. Here’s what I’m doing:

foreach (Object o in Selection.GetFiltered(typeof(Object), SelectionMode.DeepAssets)) {
		var texture = AssetPreview.GetAssetPreview(o);
		texture.hideFlags = HideFlags.None;
		AssetDatabase.CreateAsset(texture, "Assets/AddressableStaging/Thumbnail.asset");
}

AssetDatabase.SaveAssets();

I know the texture is a valid texture because I saved it as a PNG before creating the asset, and I opened it up outside Unity and it’s fine. I also look at the asset file created and it’s 130KB in size, so something’s there.

Then I try to load it:

var texture = (Texture2D) AssetDatabase.LoadAssetAtPath("Assets/AddressableStaging/Thumbnail.asset", typeof(Texture2D));

Yes, I know about generics, that was how I was doing it but in deseparation I used this alternative method in the hopes that maybe this would work and there was a bug with the generic method; no dice, still doesn’t work, it returns null.

I must be doing something wrong; anyone care to enlighten me?

p.s. I’m running this from an Editor script.

UPDATE: I believe we have a bug; workaround posted in my reply to @spiney199.

When and where are you calling the load method?

There are situations where loading assets is just not possible and will always return null, such as in a static ctor or in a InitializeOnLoadMethod. Use EditorApplication.delayCall to defer the operation.

Also you cannot use AssetDatabase methods in runtime code. Meaning if you try to load this texture asset with the AssetDatabase in a MonoBehaviour during playmode, the results are undefined since you cannot use editor methods at runtime (although the editor will allow it since you’re technically in the editor).

The creation code seems correct, although I wonder about the HideFlags. This may be superfluous.
And of course what you intend to “save” afterwards. You already created these assets and did no other modifications, so there is no need to “save the entire project” at this point. :wink:
You can omit the SaveAssets part.

One thing to try: create a public Texture2D field either in a MonoBehaviour or ScriptableObject and then try to drag your preview image onto it. If this works, the type is correct.

You can then use a UIImage or Sprite to render this image either in an editor script (custom inspector or EditorWindow) or at runtime to confirm the texture is valid and what you expect it to be.

Filtering by Object will give you all assets by definition. So in all likelihood you only need to use Selection.objects.

The “deep” part will return all child objects - though I wonder if you really need that? This would generate preview images even for “empty” gameobject children that are merely used to create a folder structure. Or consider skinned meshes with their bone structure, these easily have 100+ child objects with nothing but collider and possibly rigidbody on it - hence no preview other than the GameObject default (if there is such a thing).

Thanks for replying. I should have specified that I’m running from within the editor, so this isn’t runtime code. HideFlags is necessary, otherwise it will not let me save the asset at all. I am able to save other assets, so the problem I’m having is limited to Texture2D specifically. SelectionMode.DeepAssets is not the same as SelectionMode.Deep; it simply means to do a recursive search. I know my question sounds like a beginner question, but I’ve been using assets and bundles (and now addressables) for quite some time now across many different asset types, and I’ve saved textures as part of Materials before with no problem, but I’ve only just recently had the need to save a texture all on its lonesome and I’m surprised I’m having issues with something I thought would be extremely pedestrian.

I just noticed, the extension is .asset. Not every asset uses this extension. All „external“ assets (png, fbx, mp3, etc) retain their original extension.

I think you have to save the texture not via CreateAsset (this is reserved for „internal“ assets, eg prefabs, ScriptableObjects) but rather convert the texture to PNG (or JPG). Then save it via File.WriteAllBytes and call AssetDatabase.ImportAsset afterwards.

If you save multiple it is highly recommended (10-100 times faster!) to enclose these calls in AssetDatabase.Start/StopAssetEditing (be sure to try/catch as the manual points out).

You can create texture files that are from Unity’s Texture types (Texture2D, etc), using the .asset extension.

Though not sure what’s going wrong with OP’s code.

A simple test like this works no problem:

#if UNITY_EDITOR
namespace LBG.Testing
{
	using UnityEngine;
	using UnityEditor;

	public static class CreateTextureTest
	{
		[MenuItem("Testing/Create and Load Texture")]
		private static void CreateAndLoadTexture()
		{
			int width = 512;
			int height = 512;

			var texture = new Texture2D(width, height);

			Color32 black = Color.black;
			Color32[] colors = new Color32[width * height];
			int count = colors.Length;
			for (int i = 0; i < count; i++)
			{
				colors[i] = black;
			}

			texture.SetPixels32(colors);
			AssetDatabase.CreateAsset(texture, "Assets/texture.asset");

			var loadedTexture = AssetDatabase.LoadAssetAtPath<Texture2D>("Assets/texture.asset");
			Debug.Log("Loaded Texture Successfully: " + loadedTexture != null);
		}
	}
}
#endif

After you create the asset, can you see in your project files, and view its properties in the inspector?

1 Like

The .asset extension should work just fine. Also, I do use the Start and Stop AssetEditing calls, but I had commented it out because erroring out between Start and Stop caused it to sever the connection between Unity and Visual Studio, and I stripped the code I posted to only the relevant parts. I already have png’s generated as my workaround for all this was to use EncodeToPNG and save to file, but I don’t like mysteries, so I’m trying to figure out why I’m having issues the CreateAsset route.

Hey @spiney199!

Thanks for showing me a working example, it helped me figure out what’s going on. Apparently it’s a bug with AssetPreview.GetAssetPreview.

I have a folder of prefabs, so I select all of the prefabs and run my editor script. Here’s the full code:

		AssetDatabase.StartAssetEditing();

		var items = Selection.GetFiltered(typeof(Object), SelectionMode.DeepAssets);
		for (var i=0; i<items.Length; i++) {
			var o = (GameObject) items[i];
			var name = o.name.Replace("_Static", "");

			Texture2D texture;
			do {
				// GetAssetPreview is an async call, so we need to wait for texture to get populated
				texture = AssetPreview.GetAssetPreview(o);

				// waits while asset is loading...
				while (AssetPreview.IsLoadingAssetPreview(o.GetInstanceID())) {
					Thread.Sleep(1);
				}
				
			// despite IsLoadingAssetPreview returing false, I found that texture may still return null
			// at times, so this outer loop ensures that it refetches in case of null
			// this is a red flag that something is amiss...
			} while (texture == null);

			// hideFlags needed here because the texture generated has protections that doesn't allow
			// it to be saved.
			texture.hideFlags = HideFlags.None;
			AssetDatabase.CreateAsset(texture, "Assets/AddressableStaging/Thumbnails2/" + name + ".asset");
		}

		AssetDatabase.StopAssetEditing();
		AssetDatabase.Refresh();

I find that about 90% of the assets created this way are valid, while the rest are not. I select the first asset generated and can see the preview in the inspector just fine, and I just go down the list one by one and every now and then an asset will display nothing in the inspector, and this happens for about 10% of the assets generated. This tells me that somewhere between the IsLoadingAssetPreview returning false and the CreateAsset call, something is destroying the texture behind the scenes. During testing, I found that when I select a single prefab and run the code, it seems to generate fine, but when I selected two, it failed a lot on the first one. This has me thinking perhaps the item selection itself may be interfering with this process somehow, perhaps because it does its own preview of the highlighted items in the inspector.

To sum this all up, I select more than a hundred prefabs and run an editor script to generate previews of the items I selected, but Unity also runs previews of its own on the selected items and displays them in the inspector, and I think that this may be interfering with GetAssetPreview calls and causing some of the generated textures to get destroyed while you’re trying to save them. I’m able to save them just fine via saving texture.EncodeToPNG() to file though, so my guess is that since texture.EncodeToPNG() is a method that texture knows is running, it doesn’t allow any destroy logic to run while it’s executing, but that’s just a guess.

Any Unity devs out there in the know?

update: Interesting… when I show my generated assets in list view (text only), when I select a good asset I see the inspector show the preview just fine, but when I select a bad asset the inspector is completely blank. But when I change the view to display icons instead, I can actually see the icon of the bad asset just fine, but selecting it still shows an empty inspector.

workaround update: I got it working, but I had to use Graphics.CopyTexture to copy it before calling CreateAsset, and there’s probably still a chance of some race condition happening.

2 Likes

Getting somewhere, that’s good!

Honestly, I would refactor the code to work with lists of assets. Likely more efficient if you use IsLoadingAssetPreviews (plural).

My guess is that Thread.Sleep(1) is killing it. Isn’t the time in milliseconds?
Nevertheless, it would help to first issue all the asset previews. Then continue to check their completion in a EditorCoroutine (there’s a package you can install). Sleeping the thread sounds fishy and Unity might not like that.

As preview images become available, store them in a list. Only when they’re all done, create the assets for all of them to keep the Start/StopEditing block as short as possible.
Note that any asset operations within that Start/StopEditing block may fail! Eg creating an asset, then loading it within the block will return null. Could also be why you still have some odd issues.

You may have to build another list, this time of the assets you created, if you need to perform any postprocessing after the fact. But if possible, modify the texture before creating the asset to avoid writing the asset twice.

And finally, the Refresh() at the end is unnecessary. :wink:
It’s implicit by Start/StopAssetEditing, which collects a list of changed assets and then processes them all at once while keeping the AssetDatabase state up to date.

I was working with a list of assets using the plural IsLoadingAssetPreviews() to check that everything was done when I ran into this issue, so I modified my script to work with it on a one-by-one basis to try to narrow down the problem as best as I can. I know you’re trying to be helpful with these optimization tips, but when I’m trying to debug something I try to dumb it down to its simplest form and get rid of any optimization; it was only by doing this that I was able to narrow the problem down and figure out a workaround.

As for Refresh() at the end, that’s absolutely needed for when I save to File since Unity is unaware of the underlying changes to the assets; Start/Stop AssetEditing is only for changes you make via the AssetDatabase calls, it will not refresh if you’re updating the underlying file system via external calls such as File.WriteAllBytes :

// 				var bytes = texture.EncodeToPNG();
// 				File.WriteAllBytes(Application.dataPath + "/AddressableStaging/Thumbnails/" + name + ".png", bytes);

I disable/enable these two lines whenever I want actual png’s of my assets, but I leave the Refresh() method uncommented because it does no harm. There’s a lot of commented/uncommented code or irrelevant code that I leave out for brevity sake, and I’m looking for help on the actual problem/bug at the moment and not looking for ways to optimize the code.

p.s. I was unaware of the Editor coroutine you mentioned; that’s good to know, I’ll look it up!

@CodeSmile FYI, thanks to you I now know of the Editor coroutine library, so I switched the code over to use it, eliminating the Thread.Sleep(1) line and removing the workaround, but alas the issue is still there.

I understand you’re primary looking for a solution, at least you found a workaround now. But much like EditorCoroutine I continue to educate if you don’t mind since AssetDatabase is a bit of a pet peeve of mine. :wink:

The refresh is nevertheless unnecessary because it just works as a “catch-all” method. The proper way of getting the System IO changes registered is to do this:

var path = Application.dataPath + "/AddressableStaging/Thumbnails/" + name + ".png";
File.WriteAllBytes(path, bytes);
AssetDatabase.ImportAsset(path);

You may have noticed that you’ve never had any use for ImportAsset. Now you know why. :wink:

IMO Refresh ought to have been named ImportAllAssetsAndUnloadUnusedResources to indicate it isn’t a minor operation.

It’s the latter part I warn everyone about because it’s not “free” and the effect of it increases with the complexity of the project.

Imagine you have some editor tooling that relies on some form of LoadAsset to do some processing. Since the objects remain in memory repeated execution is fast. However, add a single Refresh anywhere, and all the in-memory assets are unloaded. Thus repeated execution of that script would read them in from disk. This can lead to an “all of a sudden” slowness in some editor tooling, and typically it gets blamed on Unity.

This even affects selection in the Project view - every time you click through assets, they are loaded. Select them again, they are still loaded and incur no performance penalty. Call a Refresh in between, and the selection will once again load the asset from disk. Call a Refresh in some innocuous place like “OnSelectionChanged” and you can easily trash editor performance.

But also “Refresh” has to scan all the files and folders for any changed, deleted or new files. That operation is also not free and scales with the number of assets.

Calling ImportAsset on the one file that you know you just changed and getting rid of unnecessary Refresh calls alleviates both (potential future) issues. :wink: