[Unity]将所有 TGA、TIFF、PSD 和 BMP(可自定义)纹理转换为 PNG,以减小项目大小,而不会在 Unity 中造成任何质量损失

如何使用

只需在"项目"窗口中创建一个名为"编辑器"的文件夹,然后在其中添加此脚本即可。然后,打开窗口-Convert Textures to PNG,配置参数并点击"Convert to PNG! "。

就我而言,它已将某些 3D 资源的总文件大小从 1.08 GB 减少到 510 MB。

只要禁用"Keep Original Files"或将项目的资源序列化模式设置为"强制文本",就会保留对转换后的纹理的引用。

csharp 复制代码
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEngine;
using Debug = UnityEngine.Debug;
using Object = UnityEngine.Object;

public class ConvertTexturesToPNG : EditorWindow
{
	private const string DUMMY_TEXTURE_PATH = "Assets/convert_dummyy_texturee.png";
	private const bool REMOVE_MATTE_FROM_PSD_BY_DEFAULT = true;

	private readonly GUIContent[] maxTextureSizeStrings = { new GUIContent( "32" ), new GUIContent( "64" ), new GUIContent( "128" ), new GUIContent( "256" ), new GUIContent( "512" ), new GUIContent( "1024" ), new GUIContent( "2048" ), new GUIContent( "4096" ), new GUIContent( "8192" ), new GUIContent( "16384" ) };
	private readonly int[] maxTextureSizeValues = { 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384 };

	private readonly GUIContent rootPathContent = new GUIContent( "Root Path:", "Textures inside this folder (recursive) will be converted" );
	private readonly GUIContent textureExtensionsContent = new GUIContent( "Textures to Convert:", "Only Textures with these extensions will be converted (';' separated)" );
	private readonly GUIContent excludedDirectoriesContent = new GUIContent( "Excluded Directories:", "Textures inside these directories won't be converted (';' separated)" );
	private readonly GUIContent keepOriginalFilesContent = new GUIContent( "Keep Original Files:", "If selected, original Texture files won't be deleted after the conversion" );
	private readonly GUIContent maxTextureSizeContent = new GUIContent( "Max Texture Size:", "Textures larger than this size will be downscaled to this size" );
	private readonly GUIContent optiPNGPathContent = new GUIContent( "OptiPNG Path (Optional):", "If 'optipng.exe' is selected, it will be used to reduce the image sizes even further (roughly 20%) but the process will take more time" );
	private readonly GUIContent optiPNGOptimizationContent = new GUIContent( "OptiPNG Optimization:", "Determines how many trials OptiPNG will do to optimize the image sizes. As this value increases, computation time will increase exponentially" );
	private readonly GUIContent optiPNGURL = new GUIContent( "...", "http://optipng.sourceforge.net/" );
	private readonly GUILayoutOption GL_WIDTH_25 = GUILayout.Width( 25f );

	private string rootPath = "";
	private string textureExtensions = ".tga;.psd;.tiff;.tif;.bmp";
	private string excludedDirectories = "";
	private bool keepOriginalFiles = false;
	private int maxTextureSize = 8192;
	private string optiPNGPath = "";
	private int optiPNGOptimization = 3;

	private Vector2 scrollPos;

	[MenuItem( "Window/Convert Textures to PNG" )]
	private static void Init()
	{
		ConvertTexturesToPNG window = GetWindow<ConvertTexturesToPNG>();
		window.titleContent = new GUIContent( "Convert to PNG" );
		window.minSize = new Vector2( 285f, 160f );
		window.Show();
	}

	private void OnEnable()
	{
		// By default, Root Path points to this project's Assets folder
		if( string.IsNullOrEmpty( rootPath ) )
			rootPath = Application.dataPath;
	}

	private void OnGUI()
	{
		scrollPos = GUILayout.BeginScrollView( scrollPos );

		rootPath = PathField( rootPathContent, rootPath, true, "Choose target directory" );
		textureExtensions = EditorGUILayout.TextField( textureExtensionsContent, textureExtensions );
		excludedDirectories = EditorGUILayout.TextField( excludedDirectoriesContent, excludedDirectories );
		keepOriginalFiles = EditorGUILayout.Toggle( keepOriginalFilesContent, keepOriginalFiles );
		maxTextureSize = EditorGUILayout.IntPopup( maxTextureSizeContent, maxTextureSize, maxTextureSizeStrings, maxTextureSizeValues );
		optiPNGPath = PathField( optiPNGPathContent, optiPNGPath, false, "Choose optipng.exe path", optiPNGURL );
		if( !string.IsNullOrEmpty( optiPNGPath ) )
		{
			EditorGUI.indentLevel++;
			optiPNGOptimization = EditorGUILayout.IntSlider( optiPNGOptimizationContent, optiPNGOptimization, 2, 7 );
			EditorGUI.indentLevel--;
		}

		EditorGUILayout.Space();

		// Convert Textures to PNG
		if( GUILayout.Button( "Convert to PNG!" ) )
		{
			double startTime = EditorApplication.timeSinceStartup;

			List<string> convertedPaths = new List<string>( 128 );
			long originalTotalSize = 0L, convertedTotalSize = 0L, convertedTotalSizeOptiPNG = 0L;

			try
			{
				rootPath = rootPath.Trim();
				excludedDirectories = excludedDirectories.Trim();
				textureExtensions = textureExtensions.ToLowerInvariant().Replace( ".png", "" ).Trim();
				optiPNGPath = optiPNGPath.Trim();

				if( rootPath.Length == 0 )
					rootPath = Application.dataPath;

				if( optiPNGPath.Length > 0 && !File.Exists( optiPNGPath ) )
					Debug.LogWarning( "OptiPNG doesn't exist at path: " + optiPNGPath );

				string[] paths = FindTexturesToConvert();
				string pathsLengthStr = paths.Length.ToString();
				float progressMultiplier = paths.Length > 0 ? ( 1f / paths.Length ) : 1f;

				CreateDummyTexture(); // Dummy Texture is used while reading Textures' pixels

				for( int i = 0; i < paths.Length; i++ )
				{
					if( EditorUtility.DisplayCancelableProgressBar( "Please wait...", string.Concat( "Converting: ", ( i + 1 ).ToString(), "/", pathsLengthStr ), ( i + 1 ) * progressMultiplier ) )
						throw new Exception( "Conversion aborted" );

					string pngFile = Path.ChangeExtension( paths[i], ".png" );
					string pngMetaFile = pngFile + ".meta";
					string originalMetaFile = paths[i] + ".meta";

					bool isPSDImage = Path.GetExtension( paths[i] ).ToLowerInvariant() == ".psd";

					// Make sure to respect PSD assets' "Remove Matte (PSD)" option
					if( isPSDImage )
					{
						bool removeMatte = REMOVE_MATTE_FROM_PSD_BY_DEFAULT;

						if( File.Exists( originalMetaFile ) )
						{
							const string removeMatteOption = "pSDRemoveMatte: ";

							string metaContents = File.ReadAllText( originalMetaFile );
							int removeMatteIndex = metaContents.IndexOf( removeMatteOption );
							if( removeMatteIndex >= 0 )
								removeMatte = metaContents[removeMatteIndex + removeMatteOption.Length] != '0';
						}

						SerializedProperty removeMatteProp = new SerializedObject( AssetImporter.GetAtPath( DUMMY_TEXTURE_PATH ) ).FindProperty( "m_PSDRemoveMatte" );
						if( removeMatteProp != null && removeMatteProp.boolValue != removeMatte )
						{
							removeMatteProp.boolValue = removeMatte;
							removeMatteProp.serializedObject.ApplyModifiedPropertiesWithoutUndo();
						}
					}

					// Temporarily copy the image file to Assets folder to create a read-write enabled Texture from it
					File.Copy( paths[i], DUMMY_TEXTURE_PATH, true );
					AssetDatabase.ImportAsset( DUMMY_TEXTURE_PATH, ImportAssetOptions.ForceUpdate );

					// Convert the Texture to PNG and save it
					byte[] pngBytes = AssetDatabase.LoadAssetAtPath<Texture2D>( DUMMY_TEXTURE_PATH ).EncodeToPNG();
					File.WriteAllBytes( pngFile, pngBytes );

					originalTotalSize += new FileInfo( paths[i] ).Length;
					convertedTotalSize += new FileInfo( pngFile ).Length;

					// Run OptiPNG to optimize the PNG
					if( optiPNGPath.Length > 0 && File.Exists( optiPNGPath ) )
					{
						try
						{
							Process.Start( new ProcessStartInfo( optiPNGPath )
							{
								Arguments = string.Concat( "-o ", optiPNGOptimization.ToString(), " \"", pngFile, "\"" ),
								CreateNoWindow = true,
								UseShellExecute = false
							} ).WaitForExit();
						}
						catch( Exception e )
						{
							Debug.LogException( e );
						}

						convertedTotalSizeOptiPNG += new FileInfo( pngFile ).Length;
					}

					// If .meta file exists, copy it to PNG image
					if( File.Exists( originalMetaFile ) )
					{
						File.Copy( originalMetaFile, pngMetaFile, true );

						// Try changing original meta file's GUID to avoid collisions with PNG (Credit: https://gist.github.com/ZimM-LostPolygon/7e2f8a3e5a1be183ac19)
						if( keepOriginalFiles )
						{
							string metaContents = File.ReadAllText( originalMetaFile );
							int guidIndex = metaContents.IndexOf( "guid: " );
							if( guidIndex >= 0 )
							{
								string guid = metaContents.Substring( guidIndex + 6, 32 );
								string newGuid = Guid.NewGuid().ToString( "N" );
								metaContents = metaContents.Replace( guid, newGuid );
								File.WriteAllText( originalMetaFile, metaContents );
							}
						}

						// Don't show "Remote Matte (PSD)" option for converted Textures
						if( isPSDImage )
						{
							string metaContents = File.ReadAllText( pngMetaFile );
							bool modifiedMeta = false;

							if( metaContents.Contains( "pSDShowRemoveMatteOption: 1" ) )
							{
								metaContents = metaContents.Replace( "pSDShowRemoveMatteOption: 1", "pSDShowRemoveMatteOption: 0" );
								modifiedMeta = true;
							}
							if( metaContents.Contains( "pSDRemoveMatte: 1" ) )
							{
								metaContents = metaContents.Replace( "pSDRemoveMatte: 1", "pSDRemoveMatte: 0" );
								modifiedMeta = true;
							}

							if( modifiedMeta )
								File.WriteAllText( pngMetaFile, metaContents );
						}
					}

					if( !keepOriginalFiles )
					{
						File.Delete( paths[i] );

						if( File.Exists( originalMetaFile ) )
							File.Delete( originalMetaFile );
					}

					convertedPaths.Add( paths[i] );
				}
			}
			catch( Exception e )
			{
				Debug.LogException( e );
			}
			finally
			{
				EditorUtility.ClearProgressBar();

				if( File.Exists( DUMMY_TEXTURE_PATH ) )
					AssetDatabase.DeleteAsset( DUMMY_TEXTURE_PATH );

				// Force Unity to import PNG images (otherwise we'd have to minimize Unity and then maximize it)
				AssetDatabase.Refresh();

				// Print information to Console
				StringBuilder sb = new StringBuilder( 100 + convertedPaths.Count * 75 );
				sb.Append( "Converted " ).Append( convertedPaths.Count ).Append( " Texture(s) to PNG in " ).Append( ( EditorApplication.timeSinceStartup - startTime ).ToString( "F2" ) ).Append( " seconds (" ).
					Append( EditorUtility.FormatBytes( originalTotalSize ) ).Append( " -> " ).Append( EditorUtility.FormatBytes( convertedTotalSize ) );

				if( convertedTotalSizeOptiPNG > 0L )
					sb.Append( " -> " ).Append( EditorUtility.FormatBytes( convertedTotalSizeOptiPNG ) ).Append( " with OptiPNG" );

				sb.AppendLine( "):" );
				for( int i = 0; i < convertedPaths.Count; i++ )
					sb.Append( "- " ).AppendLine( convertedPaths[i] );

				Debug.Log( sb.ToString() );
			}
		}

		GUILayout.EndScrollView();
	}

	private string PathField( GUIContent label, string path, bool isDirectory, string title, GUIContent downloadURL = null )
	{
		GUILayout.BeginHorizontal();
		path = EditorGUILayout.TextField( label, path );
		if( GUILayout.Button( "o", GL_WIDTH_25 ) )
		{
			string selectedPath = isDirectory ? EditorUtility.OpenFolderPanel( title, "", "" ) : EditorUtility.OpenFilePanel( title, "", "exe" );
			if( !string.IsNullOrEmpty( selectedPath ) )
				path = selectedPath;

			GUIUtility.keyboardControl = 0; // Remove focus from active text field
		}
		if( downloadURL != null && GUILayout.Button( downloadURL, GL_WIDTH_25 ) )
			Application.OpenURL( downloadURL.tooltip );
		GUILayout.EndHorizontal();

		return path;
	}

	private string[] FindTexturesToConvert()
	{
		HashSet<string> texturePaths = new HashSet<string>();
		HashSet<string> targetExtensions = new HashSet<string>( textureExtensions.Split( ';' ) );

		// Get directories to exclude
		string[] excludedPaths = excludedDirectories.Split( ';' );
		for( int i = 0; i < excludedPaths.Length; i++ )
		{
			excludedPaths[i] = excludedPaths[i].Trim();
			if( excludedPaths[i].Length == 0 )
				excludedPaths[i] = "NULL/";
			else
			{
				excludedPaths[i] = Path.GetFullPath( excludedPaths[i] );

				// Make sure excluded directory paths end with directory separator char
				if( Directory.Exists( excludedPaths[i] ) && !excludedPaths[i].EndsWith( Path.DirectorySeparatorChar.ToString() ) )
					excludedPaths[i] += Path.DirectorySeparatorChar;
			}
		}

		// Iterate through all files in Root Path
		string[] allFiles = Directory.GetFiles( rootPath, "*.*", SearchOption.AllDirectories );
		for( int i = 0; i < allFiles.Length; i++ )
		{
			// Only process filtered image files
			if( targetExtensions.Contains( Path.GetExtension( allFiles[i] ).ToLowerInvariant() ) )
			{
				bool isExcluded = false;
				if( excludedPaths.Length > 0 )
				{
					// Make sure the image file isn't part of an excluded directory
					string fileFullPath = Path.GetFullPath( allFiles[i] );
					for( int j = 0; j < excludedPaths.Length; j++ )
					{
						if( fileFullPath.StartsWith( excludedPaths[j] ) )
						{
							isExcluded = true;
							break;
						}
					}
				}

				if( !isExcluded )
					texturePaths.Add( allFiles[i] );
			}
		}

		string[] result = new string[texturePaths.Count];
		texturePaths.CopyTo( result );

		return result;
	}

	// Creates dummy Texture asset that will be used to read Textures' pixels
	private void CreateDummyTexture()
	{
		if( !File.Exists( DUMMY_TEXTURE_PATH ) )
		{
			File.WriteAllBytes( DUMMY_TEXTURE_PATH, new Texture2D( 2, 2 ).EncodeToPNG() );
			AssetDatabase.ImportAsset( DUMMY_TEXTURE_PATH, ImportAssetOptions.ForceUpdate );
		}

		TextureImporter textureImporter = AssetImporter.GetAtPath( DUMMY_TEXTURE_PATH ) as TextureImporter;
		textureImporter.maxTextureSize = maxTextureSize;
		textureImporter.isReadable = true;
		textureImporter.filterMode = FilterMode.Point;
		textureImporter.mipmapEnabled = false;
		textureImporter.alphaSource = TextureImporterAlphaSource.FromInput;
		textureImporter.npotScale = TextureImporterNPOTScale.None;
		textureImporter.textureCompression = TextureImporterCompression.Uncompressed;
		textureImporter.SaveAndReimport();
	}
}
相关推荐
Autumn_ing7 小时前
Design.md生成UI怎么验收?举例运营后台的颜色、状态和数据核对
前端·ui·ai·设计系统·paico·ai生成ui·design.md
RobinDevNotes8 小时前
一套代码发布到十几个平台:Cocos入门速览
typescript·c#·游戏开发
阿松爱学习10 小时前
【Unity开发】监听Scene场景事件
unity·c#
特立独行的猫a12 小时前
仓颉语言UI开发实战:我用仓颉写了个桌面版力扣
算法·leetcode·ui·tauri·仓颉·cj-tarui
Wang's Blog13 小时前
Java 项目实战: 交友项目 - JWT 概述:为什么要用 Token 换掉 Session
java·ui·交友
骑士雄师17 小时前
食堂的节点和提示词的关系
ui·节点和提示词
xiaogai_gai17 小时前
BOM 物料清单同步实战:金蝶云星空 → 四化智造 MES 修改场景深度拆解
unity·游戏引擎
天天代码码天天19 小时前
屏幕翻拍满是彩色条纹?用 C# + Vulkan 做了一个去摩尔纹工具,支持单图与文件夹批量
c#·去摩尔纹
2501_9307077819 小时前
使用C#代码在 Word 文档中添加、插入或删除页面
开发语言·c#·word
兰亭妙微UI设计公司20 小时前
兰亭妙微交互设计公司:从4个方面聊聊UI设计规范
ui·设计规范