Skip to content

Instantly share code, notes, and snippets.

@yasirkula
Last active November 15, 2023 20:12
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save yasirkula/19905788e68d4c2bbb8e229b5670480a to your computer and use it in GitHub Desktop.
Save yasirkula/19905788e68d4c2bbb8e229b5670480a to your computer and use it in GitHub Desktop.
Find Textures with only a single color in a Unity project
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
public class SingleColorTextureDetector : EditorWindow, IHasCustomMenu
{
[Serializable]
private class SaveData
{
public List<string> paths = new List<string>();
}
private const string LOW_RES_DUMMY_TEXTURE_PATH = "Assets/low_dummyy_texturee.png";
private const string HIGH_RES_DUMMY_TEXTURE_PATH = "Assets/high_dummyy_texturee.png";
private const string SAVE_FILE_PATH = "Library/SingleColorAssetDetector.json";
private const float BUTTON_DRAG_THRESHOLD_SQR = 600f;
private readonly MethodInfo instanceIDFromGUID = typeof( AssetDatabase ).GetMethod( "GetInstanceIDFromGUID", BindingFlags.NonPublic | BindingFlags.Static );
private List<string> results = new List<string>(); // This is not readonly so that it can be serialized
private double lastClickTime;
private string lastClickedPath;
private readonly GUIContent buttonGUIContent = new GUIContent();
private Vector2 buttonPressPosition;
private Vector2 scrollPos;
[MenuItem( "Window/Single Color Texture Detector" )]
private static void Init()
{
SingleColorTextureDetector window = GetWindow<SingleColorTextureDetector>();
window.titleContent = new GUIContent( "Solid Textures" );
window.minSize = new Vector2( 200f, 150f );
window.Show();
}
private void Awake()
{
LoadSession( null );
}
// Show additional options in the window's context menu
public void AddItemsToMenu( GenericMenu menu )
{
if( results.Count > 0 )
menu.AddItem( new GUIContent( "Save To Clipboard" ), false, () => GUIUtility.systemCopyBuffer = JsonUtility.ToJson( new SaveData() { paths = results }, true ) );
else
menu.AddDisabledItem( new GUIContent( "Save To Clipboard" ) );
if( string.IsNullOrEmpty( GUIUtility.systemCopyBuffer ) )
menu.AddDisabledItem( new GUIContent( "Load From Clipboard" ) );
else
{
menu.AddItem( new GUIContent( "Load From Clipboard" ), false, () =>
{
string json = GUIUtility.systemCopyBuffer;
LoadSession( json );
SaveSession( json ); // If load succeeds, overwrite the saved session
} );
}
}
private void OnGUI()
{
Event ev = Event.current;
scrollPos = GUILayout.BeginScrollView( scrollPos );
// Calculate single color Textures
if( GUILayout.Button( "Refresh" ) )
{
try
{
double startTime = EditorApplication.timeSinceStartup;
CalculateSingleColorTextures();
SaveSession( null );
Debug.Log( "Refreshed in " + ( EditorApplication.timeSinceStartup - startTime ) + " seconds." );
}
catch( Exception e )
{
Debug.LogException( e );
}
finally
{
EditorUtility.ClearProgressBar();
if( File.Exists( LOW_RES_DUMMY_TEXTURE_PATH ) )
AssetDatabase.DeleteAsset( LOW_RES_DUMMY_TEXTURE_PATH );
if( File.Exists( HIGH_RES_DUMMY_TEXTURE_PATH ) )
AssetDatabase.DeleteAsset( HIGH_RES_DUMMY_TEXTURE_PATH );
}
GUIUtility.ExitGUI();
}
// Draw found results
if( results.Count > 0 )
{
EditorGUILayout.HelpBox( "- Double click a path to select the Texture asset\n- Right/middle click a path to hide it from the list", MessageType.Info );
for( int i = 0; i < results.Count; i++ )
{
Rect rect = EditorGUILayout.GetControlRect( false, EditorGUIUtility.singleLineHeight + 2f );
rect.xMin += 3f;
rect.xMax -= 3f;
Rect iconRect = new Rect( rect.x, rect.y, rect.height, rect.height );
rect.xMin += iconRect.width + 2f;
Texture icon = AssetDatabase.GetCachedIcon( results[i] );
if( icon )
EditorGUI.DrawTextureTransparent( iconRect, icon );
// Buttons must support 1) click and 2) drag & drop. The most reliable way is to simulate GUI.Button from scratch
buttonGUIContent.text = results[i];
int buttonControlID = GUIUtility.GetControlID( FocusType.Passive );
switch( ev.GetTypeForControl( buttonControlID ) )
{
case EventType.MouseDown:
if( rect.Contains( ev.mousePosition ) )
{
GUIUtility.hotControl = buttonControlID;
buttonPressPosition = ev.mousePosition;
}
break;
case EventType.MouseDrag:
if( GUIUtility.hotControl == buttonControlID && ev.button == 0 && ( ev.mousePosition - buttonPressPosition ).sqrMagnitude >= BUTTON_DRAG_THRESHOLD_SQR )
{
GUIUtility.hotControl = 0;
Object asset = AssetDatabase.LoadMainAssetAtPath( results[i] );
if( asset )
{
// Credit: https://forum.unity.com/threads/editor-draganddrop-bug-system-needs-to-be-initialized-by-unity.219342/#post-1464056
DragAndDrop.PrepareStartDrag();
DragAndDrop.objectReferences = new Object[] { asset };
DragAndDrop.StartDrag( "DuplicateAssetDetector" );
}
ev.Use();
}
break;
case EventType.MouseUp:
if( GUIUtility.hotControl == buttonControlID )
{
GUIUtility.hotControl = 0;
if( rect.Contains( ev.mousePosition ) )
{
if( ev.button == 0 && File.Exists( results[i] ) )
{
// Ping clicked Texture
double clickTime = EditorApplication.timeSinceStartup;
if( clickTime - lastClickTime < 0.5f && lastClickedPath == results[i] )
{
if( !ev.control && !ev.command && !ev.shift )
Selection.objects = new Object[] { AssetDatabase.LoadMainAssetAtPath( results[i] ) };
else
{
// While holding CTRL, either add clicked asset to current selection or remove it from current selection
Object asset = AssetDatabase.LoadMainAssetAtPath( results[i] );
List<Object> selection = new List<Object>( Selection.objects );
if( !selection.Remove( asset ) )
selection.Add( asset );
Selection.objects = selection.ToArray();
}
}
else if( instanceIDFromGUID != null )
EditorGUIUtility.PingObject( (int) instanceIDFromGUID.Invoke( null, new object[] { AssetDatabase.AssetPathToGUID( results[i] ) } ) );
else
EditorGUIUtility.PingObject( AssetDatabase.LoadMainAssetAtPath( results[i] ) );
lastClickTime = clickTime;
lastClickedPath = results[i];
}
else if( ev.button == 1 )
{
// Show an option to hide that Texture from the list
int _i = i;
GenericMenu menu = new GenericMenu();
menu.AddItem( new GUIContent( "Hide" ), false, () => HideTexture( _i ) );
menu.ShowAsContext();
}
else if( ev.button == 2 )
HideTexture( i );
}
}
break;
case EventType.Repaint:
EditorStyles.textField.Draw( rect, buttonGUIContent, buttonControlID );
break;
}
if( ev.isMouse && GUIUtility.hotControl == buttonControlID )
ev.Use();
}
GUILayout.Space( 1f );
}
GUILayout.EndScrollView();
}
private void CalculateSingleColorTextures()
{
// Dummy Texture is used to read Textures' pixels
CreateDummyTexture( LOW_RES_DUMMY_TEXTURE_PATH, 32 );
CreateDummyTexture( HIGH_RES_DUMMY_TEXTURE_PATH, 1024 );
results.Clear();
string[] textureGUIDs = AssetDatabase.FindAssets( "t:Texture" );
if( textureGUIDs.Length == 0 )
return;
string pathsLengthStr = "/" + textureGUIDs.Length.ToString();
float progressMultiplier = 1f / textureGUIDs.Length;
for( int i = 0; i < textureGUIDs.Length; i++ )
{
if( i % 15 == 0 && EditorUtility.DisplayCancelableProgressBar( "Please wait...", string.Concat( "Searching: ", ( i + 1 ).ToString(), pathsLengthStr ), ( i + 1 ) * progressMultiplier ) )
throw new Exception( "Search aborted" );
if( string.IsNullOrEmpty( textureGUIDs[i] ) )
continue;
string path = AssetDatabase.GUIDToAssetPath( textureGUIDs[i] );
if( string.IsNullOrEmpty( path ) || !path.StartsWith( "Assets/" ) || path == LOW_RES_DUMMY_TEXTURE_PATH || path == HIGH_RES_DUMMY_TEXTURE_PATH )
continue;
// Happens for Font Assets' Textures, for example
if( !typeof( Texture ).IsAssignableFrom( AssetDatabase.GetMainAssetTypeAtPath( path ) ) )
continue;
// First downscale the Texture to 32 pixels for performance reasons, then downscale it to 1024 pixels to verify the result
if( CheckTextureAtPath( path, LOW_RES_DUMMY_TEXTURE_PATH ) && CheckTextureAtPath( path, HIGH_RES_DUMMY_TEXTURE_PATH ) )
results.Add( path );
}
}
// Creates dummy Texture asset that will be used to read Textures' pixels
private void CreateDummyTexture( string path, int maxSize )
{
if( !File.Exists( path ) )
{
File.WriteAllBytes( path, new Texture2D( 2, 2 ).EncodeToPNG() );
AssetDatabase.ImportAsset( path, ImportAssetOptions.ForceUpdate );
}
TextureImporter textureImporter = AssetImporter.GetAtPath( path ) as TextureImporter;
textureImporter.maxTextureSize = maxSize;
textureImporter.isReadable = true;
textureImporter.filterMode = FilterMode.Point;
textureImporter.mipmapEnabled = false;
textureImporter.alphaSource = TextureImporterAlphaSource.FromInput;
textureImporter.alphaIsTransparency = true;
textureImporter.textureCompression = TextureImporterCompression.Uncompressed;
textureImporter.SaveAndReimport();
}
// Checks if downsized Texture's pixels are all same
private bool CheckTextureAtPath( string texturePath, string dummyTexturePath )
{
File.Copy( texturePath, dummyTexturePath, true );
AssetDatabase.ImportAsset( dummyTexturePath, ImportAssetOptions.ForceUpdate );
Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>( dummyTexturePath );
if( !texture ) // RenderTextures, for example, are also Textures but not Texture2Ds
return false;
Color32[] colors = texture.GetPixels32();
Color32 color = colors[0];
for( int i = 1; i < colors.Length; i++ )
{
Color32 color2 = colors[i];
if( color2.r != color.r || color2.g != color.g || color2.b != color.b || color2.a != color.a )
return false;
}
return true;
}
// Hides the Texture at the specified index from the results
private void HideTexture( int textureIndex )
{
results.RemoveAt( textureIndex );
SaveSession( null );
Repaint();
}
// Saves current session to file
private void SaveSession( string json )
{
if( string.IsNullOrEmpty( json ) )
json = JsonUtility.ToJson( new SaveData() { paths = results }, false );
File.WriteAllText( SAVE_FILE_PATH, json );
}
// Restores previous session
private void LoadSession( string json )
{
if( string.IsNullOrEmpty( json ) )
{
if( !File.Exists( SAVE_FILE_PATH ) )
return;
json = File.ReadAllText( SAVE_FILE_PATH );
}
SaveData saveData = JsonUtility.FromJson<SaveData>( json );
// Remove non-existent paths
for( int i = saveData.paths.Count - 1; i >= 0; i-- )
{
if( !File.Exists( saveData.paths[i] ) )
saveData.paths.RemoveAt( i );
}
results = saveData.paths;
Repaint();
}
}
@yasirkula
Copy link
Author

yasirkula commented Feb 2, 2021

How To

Simply create a folder called Editor inside your Project window and add this script inside it. Then, open Window-Single Color Texture Detector and hit "Refresh".

You might ask, why would I need this? Some Textures can be 1024x1024 or larger and consist of only a single color (numerous art assets on Asset Store use such Textures for their materials' Metallic/Emission/Specular/etc channels). Why would you want a 1k Texture for it; you can simply set its Max Size to 32 and it will have the same effect while optimizing the Texture's size considerably. This plugin can be used to quickly find such Textures.

SingleColorTextureDetector

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment