当前位置:   article > 正文

【Unity编辑器扩展】包体优化神器,图片压缩,批量生成图集/图集变体,动画压缩_sprite atlas v1 v2

sprite atlas v1 v2

功能介绍:

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在线压缩 + pngquantImageSharp离线压缩:

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

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在线压缩:

  1. /// <summary>
  2. /// 使用TinyPng在线压缩,支持png,jpg,webp
  3. /// </summary>
  4. private async Task<bool> CompressOnlineAsync(string imgFileName, string outputFileName)
  5. {
  6. if (string.IsNullOrWhiteSpace(TinifyAPI.Tinify.Key))
  7. {
  8. return false;
  9. }
  10. var srcImg = TinifyAPI.Tinify.FromFile(imgFileName);
  11. await srcImg.ToFile(outputFileName);
  12. return srcImg.IsCompletedSuccessfully;
  13. }

 2. pngquant和ImageSharp本地压缩:

  1. /// <summary>
  2. /// 使用ImageSharp压缩jpg图片
  3. /// </summary>
  4. /// <param name="imgFileName"></param>
  5. /// <param name="outputFileName"></param>
  6. /// <returns></returns>
  7. private static bool CompressJpgOffline(string imgFileName, string outputFileName)
  8. {
  9. using (var img = SixLabors.ImageSharp.Image.Load(imgFileName))
  10. {
  11. var encoder = new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder()
  12. {
  13. Quality = (int)AppBuildSettings.Instance.CompressImgToolQualityLv
  14. };
  15. using (var outputStream = new FileStream(outputFileName, FileMode.Create))
  16. {
  17. img.Save(outputStream, encoder);
  18. }
  19. }
  20. return true;
  21. }
  22. /// <summary>
  23. /// 使用pngquant压缩png图片
  24. /// </summary>
  25. /// <param name="imgFileName"></param>
  26. /// <param name="outputFileName"></param>
  27. /// <returns></returns>
  28. private static bool CompressPngOffline(string imgFileName, string outputFileName)
  29. {
  30. string pngquant = Path.Combine(Directory.GetParent(Application.dataPath).FullName, pngquantTool);
  31. StringBuilder strBuilder = new StringBuilder();
  32. strBuilder.AppendFormat(" --force --quality {0}-{1}", (int)AppBuildSettings.Instance.CompressImgToolQualityMinLv, (int)AppBuildSettings.Instance.CompressImgToolQualityLv);
  33. strBuilder.AppendFormat(" --speed {0}", AppBuildSettings.Instance.CompressImgToolFastLv);
  34. strBuilder.AppendFormat(" --output \"{0}\"", outputFileName);
  35. strBuilder.AppendFormat(" -- \"{0}\"", imgFileName);
  36. var proceInfo = new System.Diagnostics.ProcessStartInfo(pngquant, strBuilder.ToString());
  37. proceInfo.CreateNoWindow = true;
  38. proceInfo.UseShellExecute = false;
  39. bool success;
  40. using (var proce = System.Diagnostics.Process.Start(proceInfo))
  41. {
  42. proce.WaitForExit();
  43. success = proce.ExitCode == 0;
  44. if (!success)
  45. {
  46. Debug.LogWarningFormat("离线压缩图片:{0}失败,ExitCode:{1}", imgFileName, proce.ExitCode);
  47. }
  48. }
  49. return success;
  50. }

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

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

  1. public class EditorUtilityExtension
  2. {
  3. /// <summary>
  4. /// 选择相对工程路径文件夹
  5. /// </summary>
  6. /// <param name="title">标题</param>
  7. /// <param name="relativePath">默认打开的路径(相对路径)</param>
  8. /// <returns></returns>
  9. public static string OpenRelativeFolderPanel(string title, string relativePath)
  10. {
  11. var rootPath = Directory.GetParent(Application.dataPath).FullName;
  12. var curFullPath = Path.Combine(rootPath, relativePath);
  13. var selectPath = EditorUtility.OpenFolderPanel(title, curFullPath, curFullPath);
  14. return string.IsNullOrWhiteSpace(selectPath) ? selectPath : Path.GetRelativePath(rootPath, selectPath);
  15. }
  16. /// <summary>
  17. /// 打开UnityEditor内置文件选择界面
  18. /// </summary>
  19. /// <param name="assetTp"></param>
  20. /// <param name="searchFilter"></param>
  21. /// <param name="onObjectSelectorClosed"></param>
  22. /// <param name="objectSelectorID"></param>
  23. /// <returns></returns>
  24. public static bool OpenAssetSelector(Type assetTp, string searchFilter = null, Action<UnityEngine.Object> onObjectSelectorClosed = null, int objectSelectorID = 0)
  25. {
  26. var objSelector = Utility.Assembly.GetType("UnityEditor.ObjectSelector");
  27. var objSelectorInst = objSelector?.GetProperty("get", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public)?.GetValue(objSelector);
  28. if (objSelectorInst == null) return false;
  29. var objSelectorInstTp = objSelectorInst.GetType();
  30. 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);
  31. if (showFunc == null) return false;
  32. showFunc.Invoke(objSelectorInst, new object[] { null, assetTp, null, false, null, onObjectSelectorClosed, null });
  33. if (!string.IsNullOrEmpty(searchFilter))
  34. {
  35. objSelectorInstTp.GetProperty("searchFilter", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).SetValue(objSelectorInst, searchFilter);
  36. }
  37. objSelectorInstTp.GetField("objectSelectorID", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).SetValue(objSelectorInst, objectSelectorID);
  38. return true;
  39. }
  40. }

4. 拖拽批量添加功能:

  1. private void DrawDropArea()
  2. {
  3. var dragRect = EditorGUILayout.BeginVertical("box");
  4. {
  5. GUILayout.FlexibleSpace();
  6. EditorGUILayout.LabelField(dragAreaContent, centerLabelStyle);
  7. if (dragRect.Contains(Event.current.mousePosition))
  8. {
  9. if (Event.current.type == EventType.DragUpdated)
  10. {
  11. DragAndDrop.visualMode = DragAndDropVisualMode.Generic;
  12. }
  13. else if (Event.current.type == EventType.DragExited)
  14. {
  15. if (DragAndDrop.objectReferences != null && DragAndDrop.objectReferences.Length > 0)
  16. {
  17. OnItemsDrop(DragAndDrop.objectReferences);
  18. }
  19. }
  20. }
  21. GUILayout.FlexibleSpace();
  22. EditorGUILayout.EndVertical();
  23. }
  24. }
  25. /// <summary>
  26. /// 拖拽松手
  27. /// </summary>
  28. /// <param name="objectReferences"></param>
  29. /// <exception cref="NotImplementedException"></exception>
  30. private void OnItemsDrop(UnityEngine.Object[] objectReferences)
  31. {
  32. foreach (var item in objectReferences)
  33. {
  34. if (CheckItemType(item) == ItemType.NoSupport)
  35. {
  36. Debug.LogWarningFormat("添加失败! 不支持的文件格式:{0}", AssetDatabase.GetAssetPath(item));
  37. continue;
  38. }
  39. AddItem(item);
  40. }
  41. }

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

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

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

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

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

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

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

  1. var getOptionsFunc = Utility.Assembly.GetType("UnityEditor.TextureImportValidFormats").GetMethod("GetPlatformTextureFormatValuesAndStrings", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public);
  2. var paramsObjs = new object[] { TextureImporterType.Sprite, EditorUserBuildSettings.activeBuildTarget, null, null };
  3. getOptionsFunc.Invoke(null, paramsObjs);
  4. var formatValues = paramsObjs[2] as int[];
  5. var formatDisplayOptions = paramsObjs[3] as string[];
  6. ...
  7. //Format
  8. EditorGUILayout.BeginHorizontal();
  9. {
  10. overrideFormat = EditorGUILayout.ToggleLeft("Format", overrideFormat, GUILayout.Width(150));
  11. EditorGUI.BeginDisabledGroup(!overrideFormat);
  12. {
  13. compressPlatformSettings.format = (TextureImporterFormat)EditorGUILayout.IntPopup((int)compressPlatformSettings.format, formatDisplayOptions, formatValues);
  14. EditorGUI.EndDisabledGroup();
  15. }
  16. EditorGUILayout.EndHorizontal();
  17. }

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

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

 

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

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

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

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

 功能需求:

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

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

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

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

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

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

  1. /// <summary>
  2. /// 创建图集
  3. /// </summary>
  4. /// <param name="atlasFilePath"></param>
  5. /// <param name="settings"></param>
  6. /// <param name="objectsForPack"></param>
  7. /// <param name="createAtlasVariant"></param>
  8. /// <param name="atlasVariantScale"></param>
  9. /// <returns></returns>
  10. public static SpriteAtlas CreateAtlas(string atlasName, AtlasSettings settings, UnityEngine.Object[] objectsForPack, bool createAtlasVariant = false, float atlasVariantScale = 1f)
  11. {
  12. CreateEmptySpriteAtlas(atlasName);
  13. SpriteAtlas result;
  14. if (EditorSettings.spritePackerMode == SpritePackerMode.SpriteAtlasV2)
  15. {
  16. var atlas = SpriteAtlasAsset.Load(atlasName);
  17. atlas.SetIncludeInBuild(settings.includeInBuild ?? true);
  18. atlas.Add(objectsForPack);
  19. var packSettings = atlas.GetPackingSettings();
  20. var texSettings = atlas.GetTextureSettings();
  21. var platformSettings = atlas.GetPlatformSettings(EditorUserBuildSettings.activeBuildTarget.ToString());
  22. ModifySpriteAtlasSettings(settings, ref packSettings, ref texSettings, ref platformSettings);
  23. atlas.SetPackingSettings(packSettings);
  24. atlas.SetTextureSettings(texSettings);
  25. atlas.SetPlatformSettings(platformSettings);
  26. SpriteAtlasAsset.Save(atlas, atlasName);
  27. result = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(atlasName);
  28. }
  29. else
  30. {
  31. var atlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(atlasName);
  32. atlas.SetIncludeInBuild(settings.includeInBuild ?? true);
  33. atlas.Add(objectsForPack);
  34. var packSettings = atlas.GetPackingSettings();
  35. var texSettings = atlas.GetTextureSettings();
  36. var platformSettings = atlas.GetPlatformSettings(EditorUserBuildSettings.activeBuildTarget.ToString());
  37. ModifySpriteAtlasSettings(settings, ref packSettings, ref texSettings, ref platformSettings);
  38. atlas.SetPackingSettings(packSettings);
  39. atlas.SetTextureSettings(texSettings);
  40. atlas.SetPlatformSettings(platformSettings);
  41. result = atlas;
  42. AssetDatabase.SaveAssets();
  43. }
  44. if (createAtlasVariant)
  45. {
  46. var atlasVarSets = new AtlasVariantSettings()
  47. {
  48. variantScale = atlasVariantScale,
  49. readWrite = settings.readWrite,
  50. mipMaps = settings.mipMaps,
  51. sRGB = settings.sRGB,
  52. filterMode = settings.filterMode,
  53. texFormat = settings.texFormat,
  54. compressQuality = settings.compressQuality
  55. };
  56. CreateAtlasVariant(result, atlasVarSets);
  57. }
  58. return result;
  59. }

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

  1. /// <summary>
  2. /// 根据图集对象生成图集变体
  3. /// </summary>
  4. /// <param name="atlas"></param>
  5. /// <param name="settings"></param>
  6. /// <returns></returns>
  7. public static SpriteAtlas CreateAtlasVariant(SpriteAtlas atlasMaster, AtlasVariantSettings settings)
  8. {
  9. if (atlasMaster == null || atlasMaster.isVariant) return atlasMaster;
  10. var atlasFileName = AssetDatabase.GetAssetPath(atlasMaster);
  11. if (string.IsNullOrEmpty(atlasFileName))
  12. {
  13. Debug.LogError($"atlas '{atlasMaster.name}' is not a asset file.");
  14. return null;
  15. }
  16. var atlasVariantName = UtilityBuiltin.ResPath.GetCombinePath(Path.GetDirectoryName(atlasFileName), $"{Path.GetFileNameWithoutExtension(atlasFileName)}_Variant{Path.GetExtension(atlasFileName)}");
  17. SpriteAtlas varAtlas;
  18. if (EditorSettings.spritePackerMode == SpritePackerMode.SpriteAtlasV2)
  19. {
  20. var atlas = SpriteAtlasAsset.Load(atlasFileName);
  21. atlas.SetIncludeInBuild(false);
  22. var packSettings = atlas.GetPackingSettings();
  23. var texSettings = atlas.GetTextureSettings();
  24. var platformSettings = atlas.GetPlatformSettings(EditorUserBuildSettings.activeBuildTarget.ToString());
  25. ModifySpriteAtlasSettings(settings, ref packSettings, ref texSettings, ref platformSettings);
  26. atlas.SetPackingSettings(packSettings);
  27. atlas.SetTextureSettings(texSettings);
  28. atlas.SetPlatformSettings(platformSettings);
  29. SpriteAtlasAsset.Save(atlas, atlasFileName);
  30. CreateEmptySpriteAtlas(atlasVariantName);
  31. var tmpVarAtlas = SpriteAtlasAsset.Load(atlasVariantName);
  32. tmpVarAtlas.SetIncludeInBuild(true);
  33. tmpVarAtlas.SetIsVariant(true);
  34. packSettings = tmpVarAtlas.GetPackingSettings();
  35. texSettings = tmpVarAtlas.GetTextureSettings();
  36. platformSettings = tmpVarAtlas.GetPlatformSettings(EditorUserBuildSettings.activeBuildTarget.ToString());
  37. ModifySpriteAtlasSettings(settings, ref packSettings, ref texSettings, ref platformSettings);
  38. tmpVarAtlas.SetPackingSettings(packSettings);
  39. tmpVarAtlas.SetTextureSettings(texSettings);
  40. tmpVarAtlas.SetPlatformSettings(platformSettings);
  41. tmpVarAtlas.SetMasterAtlas(atlasMaster);
  42. tmpVarAtlas.SetVariantScale(settings.variantScale);
  43. SpriteAtlasAsset.Save(tmpVarAtlas, atlasVariantName);
  44. varAtlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(atlasVariantName);
  45. }
  46. else
  47. {
  48. var atlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(atlasFileName);
  49. atlas.SetIncludeInBuild(false);
  50. var packSettings = atlas.GetPackingSettings();
  51. var texSettings = atlas.GetTextureSettings();
  52. var platformSettings = atlas.GetPlatformSettings(EditorUserBuildSettings.activeBuildTarget.ToString());
  53. ModifySpriteAtlasSettings(settings, ref packSettings, ref texSettings, ref platformSettings);
  54. atlas.SetPackingSettings(packSettings);
  55. atlas.SetTextureSettings(texSettings);
  56. atlas.SetPlatformSettings(platformSettings);
  57. CreateEmptySpriteAtlas(atlasVariantName);
  58. var tmpVarAtlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(atlasVariantName);
  59. tmpVarAtlas.SetIncludeInBuild(true);
  60. tmpVarAtlas.SetIsVariant(true);
  61. packSettings = tmpVarAtlas.GetPackingSettings();
  62. texSettings = tmpVarAtlas.GetTextureSettings();
  63. platformSettings = tmpVarAtlas.GetPlatformSettings(EditorUserBuildSettings.activeBuildTarget.ToString());
  64. ModifySpriteAtlasSettings(settings, ref packSettings, ref texSettings, ref platformSettings);
  65. tmpVarAtlas.SetPackingSettings(packSettings);
  66. tmpVarAtlas.SetTextureSettings(texSettings);
  67. tmpVarAtlas.SetPlatformSettings(platformSettings);
  68. tmpVarAtlas.SetMasterAtlas(atlasMaster);
  69. tmpVarAtlas.SetVariantScale(settings.variantScale);
  70. AssetDatabase.SaveAssets();
  71. varAtlas = tmpVarAtlas;
  72. }
  73. return varAtlas;
  74. }

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

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

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

  1. public static void OptimizeAnimationClips(List<string> list, int precision)
  2. {
  3. string pattern = $"(\\d+\\.[\\d]{{{precision},}})";
  4. int totalCount = list.Count;
  5. int finishCount = 0;
  6. foreach (var itmName in list)
  7. {
  8. if (File.GetAttributes(itmName) != FileAttributes.ReadOnly)
  9. {
  10. if (Path.GetExtension(itmName).ToLower().CompareTo(".anim") == 0)
  11. {
  12. finishCount++;
  13. if (EditorUtility.DisplayCancelableProgressBar(string.Format("压缩浮点精度({0}/{1})", finishCount, totalCount), itmName, finishCount / (float)totalCount))
  14. {
  15. break;
  16. }
  17. var allTxt = File.ReadAllText(itmName);
  18. // 将匹配到的浮点型数字替换为精确到3位小数的浮点型数字
  19. string outputString = Regex.Replace(allTxt, pattern, match =>
  20. float.Parse(match.Value).ToString($"F{precision}"));
  21. File.WriteAllText(itmName, outputString);
  22. Debug.LogFormat("----->压缩动画浮点精度:{0}", itmName);
  23. }
  24. }
  25. }
  26. EditorUtility.ClearProgressBar();
  27. AssetDatabase.Refresh();
  28. }

 工具的功能代码:

  1. using UnityEditor.U2D;
  2. using UnityEditor;
  3. using UnityEngine;
  4. using UnityEngine.U2D;
  5. using System.IO;
  6. using System.Text;
  7. using System.Threading.Tasks;
  8. using TinifyAPI;
  9. using SixLabors.ImageSharp.Processing;
  10. using SixLabors.ImageSharp;
  11. using GameFramework;
  12. using System.Collections.Generic;
  13. using System.Text.RegularExpressions;
  14. namespace UGF.EditorTools
  15. {
  16. public class AtlasSettings : IReference
  17. {
  18. public bool? includeInBuild = null;
  19. public bool? allowRotation = null;
  20. public bool? tightPacking = null;
  21. public bool? alphaDilation = null;
  22. public int? padding = null;
  23. public bool? readWrite = null;
  24. public bool? mipMaps = null;
  25. public bool? sRGB = null;
  26. public FilterMode? filterMode = null;
  27. public int? maxTexSize = null;
  28. public TextureImporterFormat? texFormat = null;
  29. public int? compressQuality = null;
  30. public virtual void Clear()
  31. {
  32. includeInBuild = null;
  33. allowRotation = null;
  34. tightPacking = null;
  35. alphaDilation = null;
  36. padding = null;
  37. readWrite = null;
  38. mipMaps = null;
  39. sRGB = null;
  40. filterMode = null;
  41. maxTexSize = null;
  42. texFormat = null;
  43. compressQuality = null;
  44. }
  45. }
  46. public class AtlasVariantSettings : AtlasSettings
  47. {
  48. public float variantScale = 0.5f;
  49. public override void Clear()
  50. {
  51. base.Clear();
  52. variantScale = 0.5f;
  53. }
  54. public static AtlasVariantSettings CreateFrom(AtlasSettings atlasSettings, float scale = 1f)
  55. {
  56. var settings = ReferencePool.Acquire<AtlasVariantSettings>();
  57. settings.includeInBuild = atlasSettings.includeInBuild;
  58. settings.allowRotation = atlasSettings.allowRotation;
  59. settings.tightPacking = atlasSettings.tightPacking;
  60. settings.alphaDilation = atlasSettings.alphaDilation;
  61. settings.padding = atlasSettings.padding;
  62. settings.readWrite = atlasSettings.readWrite;
  63. settings.mipMaps = atlasSettings.mipMaps;
  64. settings.sRGB = atlasSettings.sRGB;
  65. settings.filterMode = atlasSettings.filterMode;
  66. settings.maxTexSize = atlasSettings.maxTexSize;
  67. settings.texFormat = atlasSettings.texFormat;
  68. settings.compressQuality = atlasSettings.compressQuality;
  69. settings.variantScale = scale;
  70. return settings;
  71. }
  72. }
  73. public class CompressTool
  74. {
  75. #if UNITY_EDITOR_WIN
  76. const string pngquantTool = "Tools/CompressImageTools/pngquant_win/pngquant.exe";
  77. #elif UNITY_EDITOR_OSX
  78. const string pngquantTool = "Tools/CompressImageTools/pngquant_mac/pngquant";
  79. #endif
  80. /// <summary>
  81. /// 使用TinyPng在线压缩,支持png,jpg,webp
  82. /// </summary>
  83. public static async Task<bool> CompressOnlineAsync(string imgFileName, string outputFileName, string tinypngKey)
  84. {
  85. if (string.IsNullOrWhiteSpace(tinypngKey))
  86. {
  87. return false;
  88. }
  89. Tinify.Key = tinypngKey;
  90. var srcImg = TinifyAPI.Tinify.FromFile(imgFileName);
  91. await srcImg.ToFile(outputFileName);
  92. return srcImg.IsCompletedSuccessfully;
  93. }
  94. /// <summary>
  95. /// 使用pngquant离线压缩,只支持png
  96. /// </summary>
  97. public static bool CompressImageOffline(string imgFileName, string outputFileName)
  98. {
  99. var fileExt = Path.GetExtension(imgFileName).ToLower();
  100. switch (fileExt)
  101. {
  102. case ".png":
  103. return CompressPngOffline(imgFileName, outputFileName);
  104. case ".jpg":
  105. return CompressJpgOffline(imgFileName, outputFileName);
  106. }
  107. return false;
  108. }
  109. /// <summary>
  110. /// 按比例缩放图片尺寸
  111. /// </summary>
  112. /// <param name="imgFileName"></param>
  113. /// <param name="outputFileName"></param>
  114. /// <param name="scale"></param>
  115. /// <returns></returns>
  116. public static bool ResizeImage(string imgFileName, string outputFileName, float scale)
  117. {
  118. using (var img = SixLabors.ImageSharp.Image.Load(imgFileName))
  119. {
  120. int scaleWidth = (int)(img.Width * scale);
  121. int scaleHeight = (int)(img.Height * scale);
  122. img.Mutate(x => x.Resize(scaleWidth, scaleHeight));
  123. img.Save(outputFileName);
  124. }
  125. return true;
  126. }
  127. /// <summary>
  128. /// 设置图片尺寸
  129. /// </summary>
  130. /// <param name="imgFileName"></param>
  131. /// <param name="outputFileName"></param>
  132. /// <param name="width"></param>
  133. /// <param name="height"></param>
  134. /// <returns></returns>
  135. public static bool ResizeImage(string imgFileName, string outputFileName, int width, int height)
  136. {
  137. using (var img = SixLabors.ImageSharp.Image.Load(imgFileName))
  138. {
  139. img.Mutate(x => x.Resize(width, height));
  140. img.Save(outputFileName);
  141. }
  142. return true;
  143. }
  144. /// <summary>
  145. /// 使用ImageSharp压缩jpg图片
  146. /// </summary>
  147. /// <param name="imgFileName"></param>
  148. /// <param name="outputFileName"></param>
  149. /// <returns></returns>
  150. private static bool CompressJpgOffline(string imgFileName, string outputFileName)
  151. {
  152. using (var img = SixLabors.ImageSharp.Image.Load(imgFileName))
  153. {
  154. var encoder = new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder()
  155. {
  156. Quality = (int)EditorToolSettings.Instance.CompressImgToolQualityLv
  157. };
  158. using (var outputStream = new FileStream(outputFileName, FileMode.Create))
  159. {
  160. img.Save(outputStream, encoder);
  161. }
  162. }
  163. return true;
  164. }
  165. /// <summary>
  166. /// 使用pngquant压缩png图片
  167. /// </summary>
  168. /// <param name="imgFileName"></param>
  169. /// <param name="outputFileName"></param>
  170. /// <returns></returns>
  171. private static bool CompressPngOffline(string imgFileName, string outputFileName)
  172. {
  173. string pngquant = Path.Combine(Directory.GetParent(Application.dataPath).FullName, pngquantTool);
  174. StringBuilder strBuilder = new StringBuilder();
  175. strBuilder.AppendFormat(" --force --quality {0}-{1}", (int)EditorToolSettings.Instance.CompressImgToolQualityMinLv, (int)EditorToolSettings.Instance.CompressImgToolQualityLv);
  176. strBuilder.AppendFormat(" --speed {0}", EditorToolSettings.Instance.CompressImgToolFastLv);
  177. strBuilder.AppendFormat(" --output \"{0}\"", outputFileName);
  178. strBuilder.AppendFormat(" -- \"{0}\"", imgFileName);
  179. var proceInfo = new System.Diagnostics.ProcessStartInfo(pngquant, strBuilder.ToString());
  180. proceInfo.CreateNoWindow = true;
  181. proceInfo.UseShellExecute = false;
  182. bool success;
  183. using (var proce = System.Diagnostics.Process.Start(proceInfo))
  184. {
  185. proce.WaitForExit();
  186. success = proce.ExitCode == 0;
  187. if (!success)
  188. {
  189. Debug.LogWarningFormat("离线压缩图片:{0}失败,ExitCode:{1}", imgFileName, proce.ExitCode);
  190. }
  191. }
  192. return success;
  193. }
  194. /// <summary>
  195. /// 创建图集
  196. /// </summary>
  197. /// <param name="atlasFilePath"></param>
  198. /// <param name="settings"></param>
  199. /// <param name="objectsForPack"></param>
  200. /// <param name="createAtlasVariant"></param>
  201. /// <param name="atlasVariantScale"></param>
  202. /// <returns></returns>
  203. public static SpriteAtlas CreateAtlas(string atlasName, AtlasSettings settings, UnityEngine.Object[] objectsForPack, bool createAtlasVariant = false, float atlasVariantScale = 1f)
  204. {
  205. CreateEmptySpriteAtlas(atlasName);
  206. SpriteAtlas result;
  207. if (EditorSettings.spritePackerMode == SpritePackerMode.SpriteAtlasV2)
  208. {
  209. var atlas = SpriteAtlasAsset.Load(atlasName);
  210. atlas.SetIncludeInBuild(settings.includeInBuild ?? true);
  211. atlas.Add(objectsForPack);
  212. var packSettings = atlas.GetPackingSettings();
  213. var texSettings = atlas.GetTextureSettings();
  214. var platformSettings = atlas.GetPlatformSettings(EditorUserBuildSettings.activeBuildTarget.ToString());
  215. ModifySpriteAtlasSettings(settings, ref packSettings, ref texSettings, ref platformSettings);
  216. atlas.SetPackingSettings(packSettings);
  217. atlas.SetTextureSettings(texSettings);
  218. atlas.SetPlatformSettings(platformSettings);
  219. SpriteAtlasAsset.Save(atlas, atlasName);
  220. result = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(atlasName);
  221. }
  222. else
  223. {
  224. var atlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(atlasName);
  225. atlas.SetIncludeInBuild(settings.includeInBuild ?? true);
  226. atlas.Add(objectsForPack);
  227. var packSettings = atlas.GetPackingSettings();
  228. var texSettings = atlas.GetTextureSettings();
  229. var platformSettings = atlas.GetPlatformSettings(EditorUserBuildSettings.activeBuildTarget.ToString());
  230. ModifySpriteAtlasSettings(settings, ref packSettings, ref texSettings, ref platformSettings);
  231. atlas.SetPackingSettings(packSettings);
  232. atlas.SetTextureSettings(texSettings);
  233. atlas.SetPlatformSettings(platformSettings);
  234. result = atlas;
  235. AssetDatabase.SaveAssets();
  236. }
  237. if (createAtlasVariant)
  238. {
  239. var atlasVarSets = new AtlasVariantSettings()
  240. {
  241. variantScale = atlasVariantScale,
  242. readWrite = settings.readWrite,
  243. mipMaps = settings.mipMaps,
  244. sRGB = settings.sRGB,
  245. filterMode = settings.filterMode,
  246. texFormat = settings.texFormat,
  247. compressQuality = settings.compressQuality
  248. };
  249. CreateAtlasVariant(result, atlasVarSets);
  250. }
  251. return result;
  252. }
  253. private static void ModifySpriteAtlasSettings(AtlasSettings input, ref SpriteAtlasPackingSettings packSets, ref SpriteAtlasTextureSettings texSets, ref TextureImporterPlatformSettings platSets)
  254. {
  255. packSets.enableRotation = input.allowRotation ?? packSets.enableRotation;
  256. packSets.enableTightPacking = input.tightPacking ?? packSets.enableTightPacking;
  257. packSets.enableAlphaDilation = input.alphaDilation ?? packSets.enableAlphaDilation;
  258. packSets.padding = input.padding ?? packSets.padding;
  259. texSets.readable = input.readWrite ?? texSets.readable;
  260. texSets.generateMipMaps = input.mipMaps ?? texSets.generateMipMaps;
  261. texSets.sRGB = input.sRGB ?? texSets.sRGB;
  262. texSets.filterMode = input.filterMode ?? texSets.filterMode;
  263. platSets.overridden = null != input.maxTexSize || null != input.texFormat || null != input.compressQuality;
  264. platSets.maxTextureSize = input.maxTexSize ?? platSets.maxTextureSize;
  265. platSets.format = input.texFormat ?? platSets.format;
  266. platSets.compressionQuality = input.compressQuality ?? platSets.compressionQuality;
  267. }
  268. /// <summary>
  269. /// 根据文件夹名字返回一个图集名
  270. /// </summary>
  271. /// <param name="folder"></param>
  272. /// <returns></returns>
  273. public static string GetAtlasExtensionV1V2()
  274. {
  275. return EditorSettings.spritePackerMode == SpritePackerMode.SpriteAtlasV2 ? ".spriteatlasv2" : ".spriteatlas";
  276. }
  277. public static void CreateEmptySpriteAtlas(string atlasAssetName)
  278. {
  279. if (EditorSettings.spritePackerMode == SpritePackerMode.SpriteAtlasV2)
  280. {
  281. SpriteAtlasAsset.Save(new SpriteAtlasAsset(), atlasAssetName);
  282. }
  283. else
  284. {
  285. AssetDatabase.CreateAsset(new SpriteAtlas(), atlasAssetName);
  286. }
  287. AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
  288. }
  289. /// <summary>
  290. /// 根据图集对象生成图集变体
  291. /// </summary>
  292. /// <param name="atlas"></param>
  293. /// <param name="settings"></param>
  294. /// <returns></returns>
  295. public static SpriteAtlas CreateAtlasVariant(SpriteAtlas atlasMaster, AtlasVariantSettings settings)
  296. {
  297. if (atlasMaster == null || atlasMaster.isVariant) return atlasMaster;
  298. var atlasFileName = AssetDatabase.GetAssetPath(atlasMaster);
  299. if (string.IsNullOrEmpty(atlasFileName))
  300. {
  301. Debug.LogError($"atlas '{atlasMaster.name}' is not a asset file.");
  302. return null;
  303. }
  304. var atlasVariantName = UtilityBuiltin.ResPath.GetCombinePath(Path.GetDirectoryName(atlasFileName), $"{Path.GetFileNameWithoutExtension(atlasFileName)}_Variant{Path.GetExtension(atlasFileName)}");
  305. SpriteAtlas varAtlas;
  306. if (EditorSettings.spritePackerMode == SpritePackerMode.SpriteAtlasV2)
  307. {
  308. var atlas = SpriteAtlasAsset.Load(atlasFileName);
  309. atlas.SetIncludeInBuild(false);
  310. var packSettings = atlas.GetPackingSettings();
  311. var texSettings = atlas.GetTextureSettings();
  312. var platformSettings = atlas.GetPlatformSettings(EditorUserBuildSettings.activeBuildTarget.ToString());
  313. ModifySpriteAtlasSettings(settings, ref packSettings, ref texSettings, ref platformSettings);
  314. atlas.SetPackingSettings(packSettings);
  315. atlas.SetTextureSettings(texSettings);
  316. atlas.SetPlatformSettings(platformSettings);
  317. SpriteAtlasAsset.Save(atlas, atlasFileName);
  318. CreateEmptySpriteAtlas(atlasVariantName);
  319. var tmpVarAtlas = SpriteAtlasAsset.Load(atlasVariantName);
  320. tmpVarAtlas.SetIncludeInBuild(true);
  321. tmpVarAtlas.SetIsVariant(true);
  322. packSettings = tmpVarAtlas.GetPackingSettings();
  323. texSettings = tmpVarAtlas.GetTextureSettings();
  324. platformSettings = tmpVarAtlas.GetPlatformSettings(EditorUserBuildSettings.activeBuildTarget.ToString());
  325. ModifySpriteAtlasSettings(settings, ref packSettings, ref texSettings, ref platformSettings);
  326. tmpVarAtlas.SetPackingSettings(packSettings);
  327. tmpVarAtlas.SetTextureSettings(texSettings);
  328. tmpVarAtlas.SetPlatformSettings(platformSettings);
  329. tmpVarAtlas.SetMasterAtlas(atlasMaster);
  330. tmpVarAtlas.SetVariantScale(settings.variantScale);
  331. SpriteAtlasAsset.Save(tmpVarAtlas, atlasVariantName);
  332. varAtlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(atlasVariantName);
  333. }
  334. else
  335. {
  336. var atlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(atlasFileName);
  337. atlas.SetIncludeInBuild(false);
  338. var packSettings = atlas.GetPackingSettings();
  339. var texSettings = atlas.GetTextureSettings();
  340. var platformSettings = atlas.GetPlatformSettings(EditorUserBuildSettings.activeBuildTarget.ToString());
  341. ModifySpriteAtlasSettings(settings, ref packSettings, ref texSettings, ref platformSettings);
  342. atlas.SetPackingSettings(packSettings);
  343. atlas.SetTextureSettings(texSettings);
  344. atlas.SetPlatformSettings(platformSettings);
  345. CreateEmptySpriteAtlas(atlasVariantName);
  346. var tmpVarAtlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(atlasVariantName);
  347. tmpVarAtlas.SetIncludeInBuild(true);
  348. tmpVarAtlas.SetIsVariant(true);
  349. packSettings = tmpVarAtlas.GetPackingSettings();
  350. texSettings = tmpVarAtlas.GetTextureSettings();
  351. platformSettings = tmpVarAtlas.GetPlatformSettings(EditorUserBuildSettings.activeBuildTarget.ToString());
  352. ModifySpriteAtlasSettings(settings, ref packSettings, ref texSettings, ref platformSettings);
  353. tmpVarAtlas.SetPackingSettings(packSettings);
  354. tmpVarAtlas.SetTextureSettings(texSettings);
  355. tmpVarAtlas.SetPlatformSettings(platformSettings);
  356. tmpVarAtlas.SetMasterAtlas(atlasMaster);
  357. tmpVarAtlas.SetVariantScale(settings.variantScale);
  358. AssetDatabase.SaveAssets();
  359. varAtlas = tmpVarAtlas;
  360. }
  361. return varAtlas;
  362. }
  363. /// <summary>
  364. /// 根据Atlas文件名为Atlas生成Atlas变体(Atlas Variant)
  365. /// </summary>
  366. /// <param name="atlasFile"></param>
  367. /// <param name="settings"></param>
  368. /// <returns></returns>
  369. public static SpriteAtlas CreateAtlasVariant(string atlasFile, AtlasVariantSettings settings)
  370. {
  371. var atlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(atlasFile);
  372. return CreateAtlasVariant(atlas, settings);
  373. }
  374. /// <summary>
  375. /// 批量重新打包图集
  376. /// </summary>
  377. /// <param name="spriteAtlas"></param>
  378. public static void PackAtlases(SpriteAtlas[] spriteAtlas)
  379. {
  380. SpriteAtlasUtility.PackAtlases(spriteAtlas, EditorUserBuildSettings.activeBuildTarget);
  381. }
  382. public static void OptimizeAnimationClips(List<string> list, int precision)
  383. {
  384. string pattern = $"(\\d+\\.[\\d]{{{precision},}})";
  385. int totalCount = list.Count;
  386. int finishCount = 0;
  387. foreach (var itmName in list)
  388. {
  389. if (File.GetAttributes(itmName) != FileAttributes.ReadOnly)
  390. {
  391. if (Path.GetExtension(itmName).ToLower().CompareTo(".anim") == 0)
  392. {
  393. finishCount++;
  394. if (EditorUtility.DisplayCancelableProgressBar(string.Format("压缩浮点精度({0}/{1})", finishCount, totalCount), itmName, finishCount / (float)totalCount))
  395. {
  396. break;
  397. }
  398. var allTxt = File.ReadAllText(itmName);
  399. // 将匹配到的浮点型数字替换为精确到3位小数的浮点型数字
  400. string outputString = Regex.Replace(allTxt, pattern, match =>
  401. float.Parse(match.Value).ToString($"F{precision}"));
  402. File.WriteAllText(itmName, outputString);
  403. Debug.LogFormat("----->压缩动画浮点精度:{0}", itmName);
  404. }
  405. }
  406. }
  407. EditorUtility.ClearProgressBar();
  408. AssetDatabase.Refresh();
  409. }
  410. }
  411. }
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/636906
推荐阅读
相关标签
  

闽ICP备14008679号