功能介绍:

1. 压缩工具支持对图片原文件压缩(支持png/jpg),也支持使用Unity内置图片压缩批量对图片设置压缩参数。

2. 支持以文件夹或及其子文件夹为单位批量生成图集(SpriteAtlas), 支持同时生成图集变体(SpriteAtlas Variant),支持忽略像素宽高大于限定值的图片打进图集。

3. 批量给现有图集(SpriteAtlas)生成图集变体,生成图集变体后可以调整图集的缩放

4. 动画压缩,降低animation clip序列化文件的浮点型精度,保留较少的小数以降低文件大小。

工具预览:

工具完整代码参见开源框架GF_HybridCLR


图片无疑是游戏资源大户,无论数量还是文件大小占比都非常高。使用TinyPng等压缩工具压缩,近乎疯狂的压缩比,通常可以将图片文件大小降低70%左右。

对于Cocos2d-x时代的项目通常都会使用TinyPng进行图片压缩。

然而对于Unity来说,压缩图片虽然能大幅降低图片文件大小,但是最终打出的包(AssetBundle或Addressables)文件大小并不会明显降低,甚至会比压缩图片前还大。这是因为Unity针对不同平台都有对应的图片压缩模式,无论你再怎么压缩,Unity导入图片或打包时都会再次使用对应平台的压缩方式重新压缩图片,这就导致在图片分辨率不变的情况下,最终打包后的资源大小并不能有效降低。

OpenAI的问答结果

但是。。。没错,还有但是,对于庞大的项目来说,合理压缩图片原文件可以有效降低工程大小,提高打开工程的加载速度等。然后通过工具的Unity内置图片压缩批量操作可以快速方便设置图片压缩参数。

AssetBundle提供了LZ4和LZMA两种压缩方式:

LZ4: 压缩/解压较快,压缩后的文件大。适用于在线压缩/解压、网络数据等需要频繁压缩/解压的情况。

LZMA: 压缩/解压较慢,压缩后的文件小。适用于对文件大小要求高,且不频繁压缩/解压的情况。

以上两种压缩再结合GF的额外压缩,又能进一步降低包体大小。当然,也需要根据需求平衡文件加载速度和文件资源大小的取舍。

模式一:图片原文件压缩模式

一,图片原文件压缩工具功能设计:

1. 压缩算法的选择:

tinypng在线压缩 + pngquant和ImageSharp离线压缩:

tinypng压缩比极高,支持png/jpg/webp, 并且提供了包括.Net的多种编程语言API支持,适合做批处理。但是,tinypng需要上传图片到服务器,压缩完后还要下载压缩后的图片。图片较大较多时处理过程会巨慢。如果有离线压缩算法就完美了,离线压缩库使用的是pngquant和ImageSharp,都是开源压缩算法:

pngquant: 只支持png压缩,对png的压缩比接近tinypng;也可从官网可以下载命令行工具,支持windows和mac;

ImageSharp:C#实现,跨平台。对jpg的压缩比tinypng还要好。

Tinypng API : TinyPNG – API Reference

2. 添加需要压缩的文件/文件夹,并在列表中显示已经添加的文件/文件夹,支持添加/删除:

如上图,用户可以点击列表的"+"号弹出Unity自带的资源选择界面(支持选择文件夹/图片文件),

但是Unity自带选择界面仅支持单选,所以还需要做个拖拽功能以支持批量添加。

3. 压缩设置项:

对于tinypng,需要注册序列号,每个序列号可以免费压缩500张。可以一次配置多个序列号,压缩时取首行序列号。

离线压缩:对于png格式,勾选离线压缩后使用pngquant本地压缩。

覆盖原图片:勾选后压缩后的图片直接覆盖原图。

压缩质量(仅对pngquant离线压缩有效):为区间数值(min, max),当压缩质量小于min时则不对该图片压缩,其实就是为了把图片控制在一定质量范围,不至于太糊。

快压等级:等级越高,压缩处理速度越快,但压缩比随之小幅降低。一般为了极致压缩比会把快压等级调到最低。

输出路径:压缩后的图片存放路径。

备份路径:点击备份会自动把当前选择的原图备份到指定目录,以便后续还原需求。

4. 功能:

功能按钮包含压缩、备份、还原、保存当前设置。

二,功能实现:

下载tinypng压缩库:可以在Visual Studio的NuGet中搜索下载tinypng库,然后把dll放入Unity工程。

下载pngquant命令行版(有Window,Mac版本),放入Unity工程。

1. tinypng在线压缩:

/// <summary>/// 使用TinyPng在线压缩,支持png,jpg,webp/// </summary>private async Task<bool> CompressOnlineAsync(string imgFileName, string outputFileName){if (string.IsNullOrWhiteSpace(TinifyAPI.Tinify.Key)){return false;}var srcImg = TinifyAPI.Tinify.FromFile(imgFileName);await srcImg.ToFile(outputFileName);return srcImg.IsCompletedSuccessfully;}

2. pngquant和ImageSharp本地压缩:

/// <summary>/// 使用ImageSharp压缩jpg图片/// </summary>/// <param name="imgFileName"></param>/// <param name="outputFileName"></param>/// <returns></returns>private static bool CompressJpgOffline(string imgFileName, string outputFileName){using (var img = SixLabors.ImageSharp.Image.Load(imgFileName)){var encoder = new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder(){Quality = (int)AppBuildSettings.Instance.CompressImgToolQualityLv};using (var outputStream = new FileStream(outputFileName, FileMode.Create)){img.Save(outputStream, encoder);}}return true;}/// <summary>/// 使用pngquant压缩png图片/// </summary>/// <param name="imgFileName"></param>/// <param name="outputFileName"></param>/// <returns></returns>private static bool CompressPngOffline(string imgFileName, string outputFileName){string pngquant = Path.Combine(Directory.GetParent(Application.dataPath).FullName, pngquantTool);StringBuilder strBuilder = new StringBuilder();strBuilder.AppendFormat(" --force --quality {0}-{1}", (int)AppBuildSettings.Instance.CompressImgToolQualityMinLv, (int)AppBuildSettings.Instance.CompressImgToolQualityLv);strBuilder.AppendFormat(" --speed {0}", AppBuildSettings.Instance.CompressImgToolFastLv);strBuilder.AppendFormat(" --output \"{0}\"", outputFileName);strBuilder.AppendFormat(" -- \"{0}\"", imgFileName);var proceInfo = new System.Diagnostics.ProcessStartInfo(pngquant, strBuilder.ToString());proceInfo.CreateNoWindow = true;proceInfo.UseShellExecute = false;bool success;using (var proce = System.Diagnostics.Process.Start(proceInfo)){proce.WaitForExit();success = proce.ExitCode == 0;if (!success){Debug.LogWarningFormat("离线压缩图片:{0}失败,ExitCode:{1}", imgFileName, proce.ExitCode);}}return success;}

3. 弹出Unity编辑器内置资源选择窗口:

通过反射调用Unity编辑器内置选文件窗口,需要注意反射调用不支持重载函数,所以需要把参数填写完整才能成功调用:

public class EditorUtilityExtension
{/// <summary>/// 选择相对工程路径文件夹/// </summary>/// <param name="title">标题</param>/// <param name="relativePath">默认打开的路径(相对路径)</param>/// <returns></returns>public static string OpenRelativeFolderPanel(string title, string relativePath){var rootPath = Directory.GetParent(Application.dataPath).FullName;var curFullPath = Path.Combine(rootPath, relativePath);var selectPath = EditorUtility.OpenFolderPanel(title, curFullPath, curFullPath);return string.IsNullOrWhiteSpace(selectPath) ? selectPath : Path.GetRelativePath(rootPath, selectPath);}/// <summary>/// 打开UnityEditor内置文件选择界面/// </summary>/// <param name="assetTp"></param>/// <param name="searchFilter"></param>/// <param name="onObjectSelectorClosed"></param>/// <param name="objectSelectorID"></param>/// <returns></returns>public static bool OpenAssetSelector(Type assetTp, string searchFilter = null, Action<UnityEngine.Object> onObjectSelectorClosed = null, int objectSelectorID = 0){var objSelector = Utility.Assembly.GetType("UnityEditor.ObjectSelector");var objSelectorInst = objSelector?.GetProperty("get", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public)?.GetValue(objSelector);if (objSelectorInst == null) return false;var objSelectorInstTp = objSelectorInst.GetType();var showFunc = objSelectorInstTp.GetMethod("Show", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance, null, new System.Type[] { typeof(UnityEngine.Object), typeof(Type), typeof(UnityEngine.Object), typeof(bool), typeof(List<int>), typeof(Action<UnityEngine.Object>), typeof(Action<UnityEngine.Object>) }, null);if (showFunc == null) return false;showFunc.Invoke(objSelectorInst, new object[] { null, assetTp, null, false, null, onObjectSelectorClosed, null });if (!string.IsNullOrEmpty(searchFilter)){objSelectorInstTp.GetProperty("searchFilter", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).SetValue(objSelectorInst, searchFilter);}objSelectorInstTp.GetField("objectSelectorID", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).SetValue(objSelectorInst, objectSelectorID);return true;}
}

4. 拖拽批量添加功能:

private void DrawDropArea(){var dragRect = EditorGUILayout.BeginVertical("box");{GUILayout.FlexibleSpace();EditorGUILayout.LabelField(dragAreaContent, centerLabelStyle);if (dragRect.Contains(Event.current.mousePosition)){if (Event.current.type == EventType.DragUpdated){DragAndDrop.visualMode = DragAndDropVisualMode.Generic;}else if (Event.current.type == EventType.DragExited){if (DragAndDrop.objectReferences != null && DragAndDrop.objectReferences.Length > 0){OnItemsDrop(DragAndDrop.objectReferences);}}}GUILayout.FlexibleSpace();EditorGUILayout.EndVertical();}}/// <summary>/// 拖拽松手/// </summary>/// <param name="objectReferences"></param>/// <exception cref="NotImplementedException"></exception>private void OnItemsDrop(UnityEngine.Object[] objectReferences){foreach (var item in objectReferences){if (CheckItemType(item) == ItemType.NoSupport){Debug.LogWarningFormat("添加失败! 不支持的文件格式:{0}", AssetDatabase.GetAssetPath(item));continue;}AddItem(item);}}

模式二:Unity内置压缩批处理

1. 通过编辑器脚本批量修改图片TextureImporter属性:

通过下面代码可以获取上图红色区域的设置参数:

var texSetting = new TextureImporterSettings();
texImporter.ReadTextureSettings(texSetting);

通过下面代码可以获取针对各个平台的设置参数:

var texImporter = AssetImporter.GetAtPath(assetName) as TextureImporter;
var texPlatformSetting = texImporter.GetPlatformTextureSettings(EditorUserBuildSettings.activeBuildTarget.ToString());

需要注意的是,不同平台支持的图片压缩方式(Format)不同,可以通过反射调用Unity内置API获取对应平台支持的所有Format类型以供下拉选择:

var getOptionsFunc = Utility.Assembly.GetType("UnityEditor.TextureImportValidFormats").GetMethod("GetPlatformTextureFormatValuesAndStrings", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public);
var paramsObjs = new object[] { TextureImporterType.Sprite, EditorUserBuildSettings.activeBuildTarget, null, null };
getOptionsFunc.Invoke(null, paramsObjs);var formatValues = paramsObjs[2] as int[];
var formatDisplayOptions = paramsObjs[3] as string[];...
//FormatEditorGUILayout.BeginHorizontal();{overrideFormat = EditorGUILayout.ToggleLeft("Format", overrideFormat, GUILayout.Width(150));EditorGUI.BeginDisabledGroup(!overrideFormat);{compressPlatformSettings.format = (TextureImporterFormat)EditorGUILayout.IntPopup((int)compressPlatformSettings.format, formatDisplayOptions, formatValues);EditorGUI.EndDisabledGroup();}EditorGUILayout.EndHorizontal();}

2. 通过EditorUtility.FormatBytes(UnityEditor.TextureUtil.GetStorageMemorySizeLong(texture))方法可以获取到对应压缩格式的文件占用大小,这样就可以通过自动比对筛选出最合适的压缩格式。此方法不是公开方法,需要通过反射调用。

3. 编辑器代码判断贴图是否符合压缩格式要求

比如ETC2要求图片像素宽高必须是4的倍数,Crunch格式要求图片宽高必须为POT(即2的N次方),对于不支持的压缩的贴图Unity还给了贴心警告,压缩失败时压缩格式会回滚到默认的通用格式,会造成贴图大小不降反升。所以需要判断贴图是否压缩成功,如果失败了就设置一个相对通用的压缩格式。

遗憾的是在Unity开源代码中并没有找到直接获取是否压缩成功的方法,但是可以通过判断是否有警告字符以判断是否压缩成功:

/// <summary>/// 检测贴图是否适用压缩格式/// </summary>/// <param name="texImporter"></param>/// <param name="warning"></param>/// <returns></returns>bool CheckTexFormatValid(TextureImporter texImporter, out string warning){var impWarningFunc = texImporter.GetType().GetMethod("GetImportWarnings", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);warning = impWarningFunc.Invoke(texImporter, null) as string;return string.IsNullOrWhiteSpace(warning);}

模式三:创建图集,图集变体

功能需求:

1. 根据用户选定的文件夹,支持以文件夹或文件夹及其子文件夹为单位批量创建图集(每个文件夹生成一个图集文件),并且支持忽略把像素宽/高大于限制大小的图片打进图集。

2. 创建AtlasVariant,AtlasVariant是用来按比例缩放SpriteAtlas的,用于资源大小优化。勾选AtlasVariant后,生成图集同时生成AtlasVariant。

3. 其他图集设置参数,同图集的Inspector设置面板。

需要注意的是SpriteAtlas目前有v1和v2两个版本,图集格式分别为spriteatlas和spriteatlasv2:

两个版本的图集创建方法不同, v1是SpriteAtlas,v2是SpriteAtlasAsset,可通过EditorSettings.spritePackerMode获取当前使用的图集版本。

使用编辑器代码创建图集(SpriteAtlas),支持v1和v2:

/// <summary>/// 创建图集/// </summary>/// <param name="atlasFilePath"></param>/// <param name="settings"></param>/// <param name="objectsForPack"></param>/// <param name="createAtlasVariant"></param>/// <param name="atlasVariantScale"></param>/// <returns></returns>public static SpriteAtlas CreateAtlas(string atlasName, AtlasSettings settings, UnityEngine.Object[] objectsForPack, bool createAtlasVariant = false, float atlasVariantScale = 1f){CreateEmptySpriteAtlas(atlasName);SpriteAtlas result;if (EditorSettings.spritePackerMode == SpritePackerMode.SpriteAtlasV2){var atlas = SpriteAtlasAsset.Load(atlasName);atlas.SetIncludeInBuild(settings.includeInBuild ?? true);atlas.Add(objectsForPack);var packSettings = atlas.GetPackingSettings();var texSettings = atlas.GetTextureSettings();var platformSettings = atlas.GetPlatformSettings(EditorUserBuildSettings.activeBuildTarget.ToString());ModifySpriteAtlasSettings(settings, ref packSettings, ref texSettings, ref platformSettings);atlas.SetPackingSettings(packSettings);atlas.SetTextureSettings(texSettings);atlas.SetPlatformSettings(platformSettings);SpriteAtlasAsset.Save(atlas, atlasName);result = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(atlasName);}else{var atlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(atlasName);atlas.SetIncludeInBuild(settings.includeInBuild ?? true);atlas.Add(objectsForPack);var packSettings = atlas.GetPackingSettings();var texSettings = atlas.GetTextureSettings();var platformSettings = atlas.GetPlatformSettings(EditorUserBuildSettings.activeBuildTarget.ToString());ModifySpriteAtlasSettings(settings, ref packSettings, ref texSettings, ref platformSettings);atlas.SetPackingSettings(packSettings);atlas.SetTextureSettings(texSettings);atlas.SetPlatformSettings(platformSettings);result = atlas;AssetDatabase.SaveAssets();}if (createAtlasVariant){var atlasVarSets = new AtlasVariantSettings(){variantScale = atlasVariantScale,readWrite = settings.readWrite,mipMaps = settings.mipMaps,sRGB = settings.sRGB,filterMode = settings.filterMode,texFormat = settings.texFormat,compressQuality = settings.compressQuality};CreateAtlasVariant(result, atlasVarSets);}return result;}

使用编辑器代码为指定图集创建图集变体:

/// <summary>/// 根据图集对象生成图集变体/// </summary>/// <param name="atlas"></param>/// <param name="settings"></param>/// <returns></returns>public static SpriteAtlas CreateAtlasVariant(SpriteAtlas atlasMaster, AtlasVariantSettings settings){if (atlasMaster == null || atlasMaster.isVariant) return atlasMaster;var atlasFileName = AssetDatabase.GetAssetPath(atlasMaster);if (string.IsNullOrEmpty(atlasFileName)){Debug.LogError($"atlas '{atlasMaster.name}' is not a asset file.");return null;}var atlasVariantName = UtilityBuiltin.ResPath.GetCombinePath(Path.GetDirectoryName(atlasFileName), $"{Path.GetFileNameWithoutExtension(atlasFileName)}_Variant{Path.GetExtension(atlasFileName)}");SpriteAtlas varAtlas;if (EditorSettings.spritePackerMode == SpritePackerMode.SpriteAtlasV2){var atlas = SpriteAtlasAsset.Load(atlasFileName);atlas.SetIncludeInBuild(false);var packSettings = atlas.GetPackingSettings();var texSettings = atlas.GetTextureSettings();var platformSettings = atlas.GetPlatformSettings(EditorUserBuildSettings.activeBuildTarget.ToString());ModifySpriteAtlasSettings(settings, ref packSettings, ref texSettings, ref platformSettings);atlas.SetPackingSettings(packSettings);atlas.SetTextureSettings(texSettings);atlas.SetPlatformSettings(platformSettings);SpriteAtlasAsset.Save(atlas, atlasFileName);CreateEmptySpriteAtlas(atlasVariantName);var tmpVarAtlas = SpriteAtlasAsset.Load(atlasVariantName);tmpVarAtlas.SetIncludeInBuild(true);tmpVarAtlas.SetIsVariant(true);packSettings = tmpVarAtlas.GetPackingSettings();texSettings = tmpVarAtlas.GetTextureSettings();platformSettings = tmpVarAtlas.GetPlatformSettings(EditorUserBuildSettings.activeBuildTarget.ToString());ModifySpriteAtlasSettings(settings, ref packSettings, ref texSettings, ref platformSettings);tmpVarAtlas.SetPackingSettings(packSettings);tmpVarAtlas.SetTextureSettings(texSettings);tmpVarAtlas.SetPlatformSettings(platformSettings);tmpVarAtlas.SetMasterAtlas(atlasMaster);tmpVarAtlas.SetVariantScale(settings.variantScale);SpriteAtlasAsset.Save(tmpVarAtlas, atlasVariantName);varAtlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(atlasVariantName);}else{var atlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(atlasFileName);atlas.SetIncludeInBuild(false);var packSettings = atlas.GetPackingSettings();var texSettings = atlas.GetTextureSettings();var platformSettings = atlas.GetPlatformSettings(EditorUserBuildSettings.activeBuildTarget.ToString());ModifySpriteAtlasSettings(settings, ref packSettings, ref texSettings, ref platformSettings);atlas.SetPackingSettings(packSettings);atlas.SetTextureSettings(texSettings);atlas.SetPlatformSettings(platformSettings);CreateEmptySpriteAtlas(atlasVariantName);var tmpVarAtlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(atlasVariantName);tmpVarAtlas.SetIncludeInBuild(true);tmpVarAtlas.SetIsVariant(true);packSettings = tmpVarAtlas.GetPackingSettings();texSettings = tmpVarAtlas.GetTextureSettings();platformSettings = tmpVarAtlas.GetPlatformSettings(EditorUserBuildSettings.activeBuildTarget.ToString());ModifySpriteAtlasSettings(settings, ref packSettings, ref texSettings, ref platformSettings);tmpVarAtlas.SetPackingSettings(packSettings);tmpVarAtlas.SetTextureSettings(texSettings);tmpVarAtlas.SetPlatformSettings(platformSettings);tmpVarAtlas.SetMasterAtlas(atlasMaster);tmpVarAtlas.SetVariantScale(settings.variantScale);AssetDatabase.SaveAssets();varAtlas = tmpVarAtlas;}return varAtlas;}

模式四:Animation Clip动画文件大小优化

原理非常简单,动画文件的位置、旋转、缩放等数据以浮点型保存在动画文件,默认精度太高,保留了一大串小数点后的数字,实际上不需要精度过大,保留3位小数即可。降低浮点型精度可以降低动画文件大小以减少打包后包体大小。

我这里直接偷懒使用正则匹配动画文件里的小数并降低小数的精度(注意,此方式只适用于Asset Serialization位Force Text模式,不支持Force Binary,Unity工程默认是Force Text模式):

public static void OptimizeAnimationClips(List<string> list, int precision){string pattern = $"(\\d+\\.[\\d]{{{precision},}})";int totalCount = list.Count;int finishCount = 0;foreach (var itmName in list){if (File.GetAttributes(itmName) != FileAttributes.ReadOnly){if (Path.GetExtension(itmName).ToLower().CompareTo(".anim") == 0){finishCount++;if (EditorUtility.DisplayCancelableProgressBar(string.Format("压缩浮点精度({0}/{1})", finishCount, totalCount), itmName, finishCount / (float)totalCount)){break;}var allTxt = File.ReadAllText(itmName);// 将匹配到的浮点型数字替换为精确到3位小数的浮点型数字string outputString = Regex.Replace(allTxt, pattern, match =>float.Parse(match.Value).ToString($"F{precision}"));File.WriteAllText(itmName, outputString);Debug.LogFormat("----->压缩动画浮点精度:{0}", itmName);}}}EditorUtility.ClearProgressBar();AssetDatabase.Refresh();}

工具的功能代码:

using UnityEditor.U2D;
using UnityEditor;
using UnityEngine;
using UnityEngine.U2D;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using TinifyAPI;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp;
using GameFramework;
using System.Collections.Generic;
using System.Text.RegularExpressions;namespace UGF.EditorTools
{public class AtlasSettings : IReference{public bool? includeInBuild = null;public bool? allowRotation = null;public bool? tightPacking = null;public bool? alphaDilation = null;public int? padding = null;public bool? readWrite = null;public bool? mipMaps = null;public bool? sRGB = null;public FilterMode? filterMode = null;public int? maxTexSize = null;public TextureImporterFormat? texFormat = null;public int? compressQuality = null;public virtual void Clear(){includeInBuild = null;allowRotation = null;tightPacking = null;alphaDilation = null;padding = null;readWrite = null;mipMaps = null;sRGB = null;filterMode = null;maxTexSize = null;texFormat = null;compressQuality = null;}}public class AtlasVariantSettings : AtlasSettings{public float variantScale = 0.5f;public override void Clear(){base.Clear();variantScale = 0.5f;}public static AtlasVariantSettings CreateFrom(AtlasSettings atlasSettings, float scale = 1f){var settings = ReferencePool.Acquire<AtlasVariantSettings>();settings.includeInBuild = atlasSettings.includeInBuild;settings.allowRotation = atlasSettings.allowRotation;settings.tightPacking = atlasSettings.tightPacking;settings.alphaDilation = atlasSettings.alphaDilation;settings.padding = atlasSettings.padding;settings.readWrite = atlasSettings.readWrite;settings.mipMaps = atlasSettings.mipMaps;settings.sRGB = atlasSettings.sRGB;settings.filterMode = atlasSettings.filterMode;settings.maxTexSize = atlasSettings.maxTexSize;settings.texFormat = atlasSettings.texFormat;settings.compressQuality = atlasSettings.compressQuality;settings.variantScale = scale;return settings;}}public class CompressTool{
#if UNITY_EDITOR_WINconst string pngquantTool = "Tools/CompressImageTools/pngquant_win/pngquant.exe";
#elif UNITY_EDITOR_OSXconst string pngquantTool = "Tools/CompressImageTools/pngquant_mac/pngquant";
#endif/// <summary>/// 使用TinyPng在线压缩,支持png,jpg,webp/// </summary>public static async Task<bool> CompressOnlineAsync(string imgFileName, string outputFileName, string tinypngKey){if (string.IsNullOrWhiteSpace(tinypngKey)){return false;}Tinify.Key = tinypngKey;var srcImg = TinifyAPI.Tinify.FromFile(imgFileName);await srcImg.ToFile(outputFileName);return srcImg.IsCompletedSuccessfully;}/// <summary>/// 使用pngquant离线压缩,只支持png/// </summary>public static bool CompressImageOffline(string imgFileName, string outputFileName){var fileExt = Path.GetExtension(imgFileName).ToLower();switch (fileExt){case ".png":return CompressPngOffline(imgFileName, outputFileName);case ".jpg":return CompressJpgOffline(imgFileName, outputFileName);}return false;}/// <summary>/// 按比例缩放图片尺寸/// </summary>/// <param name="imgFileName"></param>/// <param name="outputFileName"></param>/// <param name="scale"></param>/// <returns></returns>public static bool ResizeImage(string imgFileName, string outputFileName, float scale){using (var img = SixLabors.ImageSharp.Image.Load(imgFileName)){int scaleWidth = (int)(img.Width * scale);int scaleHeight = (int)(img.Height * scale);img.Mutate(x => x.Resize(scaleWidth, scaleHeight));img.Save(outputFileName);}return true;}/// <summary>/// 设置图片尺寸/// </summary>/// <param name="imgFileName"></param>/// <param name="outputFileName"></param>/// <param name="width"></param>/// <param name="height"></param>/// <returns></returns>public static bool ResizeImage(string imgFileName, string outputFileName, int width, int height){using (var img = SixLabors.ImageSharp.Image.Load(imgFileName)){img.Mutate(x => x.Resize(width, height));img.Save(outputFileName);}return true;}/// <summary>/// 使用ImageSharp压缩jpg图片/// </summary>/// <param name="imgFileName"></param>/// <param name="outputFileName"></param>/// <returns></returns>private static bool CompressJpgOffline(string imgFileName, string outputFileName){using (var img = SixLabors.ImageSharp.Image.Load(imgFileName)){var encoder = new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder(){Quality = (int)EditorToolSettings.Instance.CompressImgToolQualityLv};using (var outputStream = new FileStream(outputFileName, FileMode.Create)){img.Save(outputStream, encoder);}}return true;}/// <summary>/// 使用pngquant压缩png图片/// </summary>/// <param name="imgFileName"></param>/// <param name="outputFileName"></param>/// <returns></returns>private static bool CompressPngOffline(string imgFileName, string outputFileName){string pngquant = Path.Combine(Directory.GetParent(Application.dataPath).FullName, pngquantTool);StringBuilder strBuilder = new StringBuilder();strBuilder.AppendFormat(" --force --quality {0}-{1}", (int)EditorToolSettings.Instance.CompressImgToolQualityMinLv, (int)EditorToolSettings.Instance.CompressImgToolQualityLv);strBuilder.AppendFormat(" --speed {0}", EditorToolSettings.Instance.CompressImgToolFastLv);strBuilder.AppendFormat(" --output \"{0}\"", outputFileName);strBuilder.AppendFormat(" -- \"{0}\"", imgFileName);var proceInfo = new System.Diagnostics.ProcessStartInfo(pngquant, strBuilder.ToString());proceInfo.CreateNoWindow = true;proceInfo.UseShellExecute = false;bool success;using (var proce = System.Diagnostics.Process.Start(proceInfo)){proce.WaitForExit();success = proce.ExitCode == 0;if (!success){Debug.LogWarningFormat("离线压缩图片:{0}失败,ExitCode:{1}", imgFileName, proce.ExitCode);}}return success;}/// <summary>/// 创建图集/// </summary>/// <param name="atlasFilePath"></param>/// <param name="settings"></param>/// <param name="objectsForPack"></param>/// <param name="createAtlasVariant"></param>/// <param name="atlasVariantScale"></param>/// <returns></returns>public static SpriteAtlas CreateAtlas(string atlasName, AtlasSettings settings, UnityEngine.Object[] objectsForPack, bool createAtlasVariant = false, float atlasVariantScale = 1f){CreateEmptySpriteAtlas(atlasName);SpriteAtlas result;if (EditorSettings.spritePackerMode == SpritePackerMode.SpriteAtlasV2){var atlas = SpriteAtlasAsset.Load(atlasName);atlas.SetIncludeInBuild(settings.includeInBuild ?? true);atlas.Add(objectsForPack);var packSettings = atlas.GetPackingSettings();var texSettings = atlas.GetTextureSettings();var platformSettings = atlas.GetPlatformSettings(EditorUserBuildSettings.activeBuildTarget.ToString());ModifySpriteAtlasSettings(settings, ref packSettings, ref texSettings, ref platformSettings);atlas.SetPackingSettings(packSettings);atlas.SetTextureSettings(texSettings);atlas.SetPlatformSettings(platformSettings);SpriteAtlasAsset.Save(atlas, atlasName);result = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(atlasName);}else{var atlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(atlasName);atlas.SetIncludeInBuild(settings.includeInBuild ?? true);atlas.Add(objectsForPack);var packSettings = atlas.GetPackingSettings();var texSettings = atlas.GetTextureSettings();var platformSettings = atlas.GetPlatformSettings(EditorUserBuildSettings.activeBuildTarget.ToString());ModifySpriteAtlasSettings(settings, ref packSettings, ref texSettings, ref platformSettings);atlas.SetPackingSettings(packSettings);atlas.SetTextureSettings(texSettings);atlas.SetPlatformSettings(platformSettings);result = atlas;AssetDatabase.SaveAssets();}if (createAtlasVariant){var atlasVarSets = new AtlasVariantSettings(){variantScale = atlasVariantScale,readWrite = settings.readWrite,mipMaps = settings.mipMaps,sRGB = settings.sRGB,filterMode = settings.filterMode,texFormat = settings.texFormat,compressQuality = settings.compressQuality};CreateAtlasVariant(result, atlasVarSets);}return result;}private static void ModifySpriteAtlasSettings(AtlasSettings input, ref SpriteAtlasPackingSettings packSets, ref SpriteAtlasTextureSettings texSets, ref TextureImporterPlatformSettings platSets){packSets.enableRotation = input.allowRotation ?? packSets.enableRotation;packSets.enableTightPacking = input.tightPacking ?? packSets.enableTightPacking;packSets.enableAlphaDilation = input.alphaDilation ?? packSets.enableAlphaDilation;packSets.padding = input.padding ?? packSets.padding;texSets.readable = input.readWrite ?? texSets.readable;texSets.generateMipMaps = input.mipMaps ?? texSets.generateMipMaps;texSets.sRGB = input.sRGB ?? texSets.sRGB;texSets.filterMode = input.filterMode ?? texSets.filterMode;platSets.overridden = null != input.maxTexSize || null != input.texFormat || null != input.compressQuality;platSets.maxTextureSize = input.maxTexSize ?? platSets.maxTextureSize;platSets.format = input.texFormat ?? platSets.format;platSets.compressionQuality = input.compressQuality ?? platSets.compressionQuality;}/// <summary>/// 根据文件夹名字返回一个图集名/// </summary>/// <param name="folder"></param>/// <returns></returns>public static string GetAtlasExtensionV1V2(){return EditorSettings.spritePackerMode == SpritePackerMode.SpriteAtlasV2 ? ".spriteatlasv2" : ".spriteatlas";}public static void CreateEmptySpriteAtlas(string atlasAssetName){if (EditorSettings.spritePackerMode == SpritePackerMode.SpriteAtlasV2){SpriteAtlasAsset.Save(new SpriteAtlasAsset(), atlasAssetName);}else{AssetDatabase.CreateAsset(new SpriteAtlas(), atlasAssetName);}AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);}/// <summary>/// 根据图集对象生成图集变体/// </summary>/// <param name="atlas"></param>/// <param name="settings"></param>/// <returns></returns>public static SpriteAtlas CreateAtlasVariant(SpriteAtlas atlasMaster, AtlasVariantSettings settings){if (atlasMaster == null || atlasMaster.isVariant) return atlasMaster;var atlasFileName = AssetDatabase.GetAssetPath(atlasMaster);if (string.IsNullOrEmpty(atlasFileName)){Debug.LogError($"atlas '{atlasMaster.name}' is not a asset file.");return null;}var atlasVariantName = UtilityBuiltin.ResPath.GetCombinePath(Path.GetDirectoryName(atlasFileName), $"{Path.GetFileNameWithoutExtension(atlasFileName)}_Variant{Path.GetExtension(atlasFileName)}");SpriteAtlas varAtlas;if (EditorSettings.spritePackerMode == SpritePackerMode.SpriteAtlasV2){var atlas = SpriteAtlasAsset.Load(atlasFileName);atlas.SetIncludeInBuild(false);var packSettings = atlas.GetPackingSettings();var texSettings = atlas.GetTextureSettings();var platformSettings = atlas.GetPlatformSettings(EditorUserBuildSettings.activeBuildTarget.ToString());ModifySpriteAtlasSettings(settings, ref packSettings, ref texSettings, ref platformSettings);atlas.SetPackingSettings(packSettings);atlas.SetTextureSettings(texSettings);atlas.SetPlatformSettings(platformSettings);SpriteAtlasAsset.Save(atlas, atlasFileName);CreateEmptySpriteAtlas(atlasVariantName);var tmpVarAtlas = SpriteAtlasAsset.Load(atlasVariantName);tmpVarAtlas.SetIncludeInBuild(true);tmpVarAtlas.SetIsVariant(true);packSettings = tmpVarAtlas.GetPackingSettings();texSettings = tmpVarAtlas.GetTextureSettings();platformSettings = tmpVarAtlas.GetPlatformSettings(EditorUserBuildSettings.activeBuildTarget.ToString());ModifySpriteAtlasSettings(settings, ref packSettings, ref texSettings, ref platformSettings);tmpVarAtlas.SetPackingSettings(packSettings);tmpVarAtlas.SetTextureSettings(texSettings);tmpVarAtlas.SetPlatformSettings(platformSettings);tmpVarAtlas.SetMasterAtlas(atlasMaster);tmpVarAtlas.SetVariantScale(settings.variantScale);SpriteAtlasAsset.Save(tmpVarAtlas, atlasVariantName);varAtlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(atlasVariantName);}else{var atlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(atlasFileName);atlas.SetIncludeInBuild(false);var packSettings = atlas.GetPackingSettings();var texSettings = atlas.GetTextureSettings();var platformSettings = atlas.GetPlatformSettings(EditorUserBuildSettings.activeBuildTarget.ToString());ModifySpriteAtlasSettings(settings, ref packSettings, ref texSettings, ref platformSettings);atlas.SetPackingSettings(packSettings);atlas.SetTextureSettings(texSettings);atlas.SetPlatformSettings(platformSettings);CreateEmptySpriteAtlas(atlasVariantName);var tmpVarAtlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(atlasVariantName);tmpVarAtlas.SetIncludeInBuild(true);tmpVarAtlas.SetIsVariant(true);packSettings = tmpVarAtlas.GetPackingSettings();texSettings = tmpVarAtlas.GetTextureSettings();platformSettings = tmpVarAtlas.GetPlatformSettings(EditorUserBuildSettings.activeBuildTarget.ToString());ModifySpriteAtlasSettings(settings, ref packSettings, ref texSettings, ref platformSettings);tmpVarAtlas.SetPackingSettings(packSettings);tmpVarAtlas.SetTextureSettings(texSettings);tmpVarAtlas.SetPlatformSettings(platformSettings);tmpVarAtlas.SetMasterAtlas(atlasMaster);tmpVarAtlas.SetVariantScale(settings.variantScale);AssetDatabase.SaveAssets();varAtlas = tmpVarAtlas;}return varAtlas;}/// <summary>/// 根据Atlas文件名为Atlas生成Atlas变体(Atlas Variant)/// </summary>/// <param name="atlasFile"></param>/// <param name="settings"></param>/// <returns></returns>public static SpriteAtlas CreateAtlasVariant(string atlasFile, AtlasVariantSettings settings){var atlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(atlasFile);return CreateAtlasVariant(atlas, settings);}/// <summary>/// 批量重新打包图集/// </summary>/// <param name="spriteAtlas"></param>public static void PackAtlases(SpriteAtlas[] spriteAtlas){SpriteAtlasUtility.PackAtlases(spriteAtlas, EditorUserBuildSettings.activeBuildTarget);}public static void OptimizeAnimationClips(List<string> list, int precision){string pattern = $"(\\d+\\.[\\d]{{{precision},}})";int totalCount = list.Count;int finishCount = 0;foreach (var itmName in list){if (File.GetAttributes(itmName) != FileAttributes.ReadOnly){if (Path.GetExtension(itmName).ToLower().CompareTo(".anim") == 0){finishCount++;if (EditorUtility.DisplayCancelableProgressBar(string.Format("压缩浮点精度({0}/{1})", finishCount, totalCount), itmName, finishCount / (float)totalCount)){break;}var allTxt = File.ReadAllText(itmName);// 将匹配到的浮点型数字替换为精确到3位小数的浮点型数字string outputString = Regex.Replace(allTxt, pattern, match =>float.Parse(match.Value).ToString($"F{precision}"));File.WriteAllText(itmName, outputString);Debug.LogFormat("----->压缩动画浮点精度:{0}", itmName);}}}EditorUtility.ClearProgressBar();AssetDatabase.Refresh();}}
}

【Unity编辑器扩展】包体优化神器,图片压缩,批量生成图集/图集变体,动画压缩相关推荐

  1. 【Unity编辑器扩展】(三)PSD转UGUI Prefab, 一键拼UI解放美术/程序(完结)

    工具效果: 第一步,把psd图层转换为可编辑的节点树,并自动解析UI类型.自动绑定UI子元素: 第二步, 点击"生成UIForm"按钮生成UI预制体 (若有UI类型遗漏可在下拉菜单 ...

  2. 【Unity编辑器扩展实践】、查找所有引用该图片的预制体

    上一篇Unity编辑器扩展实践二.通过代码查找所有预制中已经查到到所有的预制体了. 然后我们就可以用这些预制体做一些其他的操作了,比如查找该预制的资源引用.可以直接遍历预制,找到预制里面的所有Imag ...

  3. Unity编辑器扩展: 程序化打图集工具

    开始前的声明:该案例中图集所使用图片资源均来源于网络,仅限于学习使用 一.前言 关于编辑器扩展相关的知识,在前面的两篇内容中做了详细的描述,链接地址: 第一篇 :Unity编辑器扩展 UI控件篇 第二 ...

  4. 【Unity编辑器扩展】(二)PSD转UGUI Prefab, 图层解析和碎图导出

    书接上回:[Unity编辑器扩展](一)PSD转UGUI Prefab, Aspose.PSD和Harmony库的使用_TopGames的博客-CSDN博客 工具使用预览: 工具目标: 1. 实现将p ...

  5. 【Unity编辑器扩展】(一)PSD转UGUI Prefab, Aspose.PSD和Harmony库的使用

    [Unity编辑器扩展](二)PSD转UGUI Prefab, 图层解析和碎图导出_psd导入unity_TopGames的博客-CSDN博客 [Unity编辑器扩展](三)PSD转UGUI Pref ...

  6. Unity编辑器扩展之EditorWindow

    Unity编辑器扩展之EditorWindow 继承这个类的编辑器脚本可以用来创建一个编辑器窗口,类似Inspector窗口 若要在这个类中添加一些控件,可以使用GUI和GUILayout控件,还可以 ...

  7. Unity 编辑器扩展菜单

    Unity 编辑器扩展菜单 目录 Unity 编辑器扩展菜单 一.MenuItem 特性 菜单栏扩展 1.创建多级菜单 2.创建可使用快捷键的菜单项 3.创建可被勾选的菜单项 4.检查菜单是否使用 5 ...

  8. Unity编辑器扩展 UI控件篇

    前摇 :认识编辑器扩展的必要性 由于各种各样的原因,无论是移动端亦或是主机/PC端,进几年的发行的游戏体量是越来越大.通常来说大体量的游戏开发需要一套很成熟完善的工作流,亦或说有很强的工业化的能力,像 ...

  9. Photoshop根据图片模板批量生成动态图片

    需求:给定图片模板,在图片上面维护相关数据,把Excel表中的信息一个一个作到图中. 就技术角度而言,这样的操作并没有多少技术含量,然而却需要耗费大量的时间.如果提供的数据是上百条甚至上千条,你是否仍 ...

最新文章

  1. springmvc获取request对象
  2. MYSQL的一些常用函数
  3. MongoDB数据库(8.Python中使用mongodb数据库以及pymongo模块用法)
  4. C++数组与指针的区别
  5. 给array添加元素_前囧(06篇)Array 方法详解
  6. C语言基于GTK+Libvlc实现的简易视频播放器(二)
  7. 对话阿里云Alex Chen:下一代存储应如何面对云转型?
  8. 如何识别哭泣csdn_如何让敏感的孩子,不再那么敏感?不是溺爱,而是懂得这些方法...
  9. fastjson JSONObject.toJSONString 出现 $ref: $.的解决办法(重复引用)
  10. 2021富途校招后台C++
  11. 测试平台的发展与未来趋势
  12. Spring Boot 2.X 如何优雅的解决跨域问题?
  13. 《CATIA V5 从入门到精通(第二版)》——2.3 操作草图(Operation)
  14. Mac OS下phonegap开发环境的建立
  15. Node.js学习笔记(五)——WebSocket
  16. 计算机管理设置透明,教你把win10任务栏透明设置
  17. 超详细|开关电源电路图及原理讲解
  18. 戴尔新电脑笔记本桌面没有计算机,自主日常维修,更换戴尔灵越15屏幕过程记录...
  19. 合影效果java_【蚂蚁课堂】四十三:如何拍酷炫集体照?
  20. STM32CubeMX学习笔记(38)——FSMC接口使用(TFT-LCD屏显示)

热门文章

  1. 侠诺QVM-108高清视频、图片无线切换系统功能介绍
  2. php hs256加密,JWT签名算法中HS256和RS256有什么区别
  3. 规则引擎之Drools
  4. DP——2008 APAC local onsites C Millionaire
  5. MathType公式编辑器右边选项变灰
  6. 物联网学习笔记——京东云擎初体验 部署REST服务
  7. 道路车辆 盲区监测(bsd)系统性能要求及试验方法_驾驶辅助系统硬件在环仿真技术发展现状...
  8. 测绘地图资源不够用?教你个万能图源制作方法
  9. iOS 高级开发 KVC(一),ios开发kvc
  10. thinkphp5基础三