Unity 项目中查找仅具有单一颜色的纹理

如何使用

只需在"项目"窗口中创建一个名为**"编辑器"** 的文件夹,然后在其中添加此脚本即可。然后,打开**"窗口-单色纹理检测器"** 并点击"刷新"。

你可能会问,为什么我需要这个?某些纹理可以是 1024x1024 或更大,并且仅包含单一颜色(Asset Store 上的许多艺术资源将此类纹理用于其材质的金属/发射/镜面/等通道)。为什么你想要一个 1k 纹理?您可以简单地将其最大尺寸设置为 32,在显着优化纹理尺寸的同时,它也会产生相同的效果。该插件可用于快速找到此类纹理。

cs 复制代码
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();
	}
}
相关推荐
Sitarrrr32 分钟前
【Unity】ScriptableObject的应用和3D物体跟随鼠标移动:鼠标放置物体在场景中
3d·unity
极梦网络无忧35 分钟前
Unity中IK动画与布偶死亡动画切换的实现
unity·游戏引擎·lucene
wrx繁星点点1 小时前
状态模式(State Pattern)详解
java·开发语言·ui·设计模式·状态模式
鹏大师运维3 小时前
【功能介绍】信创终端系统上各WPS版本的授权差异
linux·wps·授权·麒麟·国产操作系统·1024程序员节·统信uos
亦枫Leonlew4 小时前
微积分复习笔记 Calculus Volume 1 - 4.7 Applied Optimization Problems
笔记·数学·微积分·1024程序员节
小肥象不是小飞象4 小时前
(六千字心得笔记)零基础C语言入门第八课——函数(上)
c语言·开发语言·笔记·1024程序员节
△曉風殘月〆7 小时前
WPF MVVM入门系列教程(二、依赖属性)
c#·wpf·mvvm
逐·風8 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#
SoraLuna10 小时前
「Mac畅玩鸿蒙与硬件28」UI互动应用篇5 - 滑动选择器实现
macos·ui·harmonyos
_oP_i10 小时前
Unity Addressables 系统处理 WebGL 打包本地资源的一种高效方式
unity·游戏引擎·webgl