using System.Collections.Generic; using System.IO; using System.Linq; using UnityEditor; using UnityEngine; using UnityEngine.U2D; /// /// 图片 maxTextureSize 批量设置工具(按原图尺寸智能降分辨率)。 /// /// 【用途】 /// UI 图片常超采样(2048 的图实际显示只需 1200 左右),批量把 TextureImporter 的 /// maxTextureSize 降到合适档位,减小包体与内存。规则: /// 目标缩放比例控制在原图的 60%~80%: /// - 在 [60%, 80%] 区间内取 2 的幂档位(32/64/.../4096),取区间内满足 ≥60% 的最小档位(最省内存); /// - 区间内没有档位时,取 ≥60% 的最小档位(硬保清晰度下限,允许略超 80%); /// - 档位 ≥ 原图边长(缩放后无变化)→ 不修改。 /// /// 【使用】 /// 1. Project 选中一个/多个文件夹或图片(文件夹会递归) /// 2. 菜单 Tools → 图片尺寸工具 → [扫描选中对象] /// 3. 检查列表(新 maxSize、实际缩放比、预计内存节省),可逐行勾选 /// 4. [应用勾选项] 批量写入 TextureImporter 并重导入 /// /// 【注意】 /// - 已被 SpriteAtlas 收录的图会标注并默认跳过(图集的平台设置优先,单图 maxSize 无效) /// - 只处理 TextureImporter(Sprite / Default / GUI 类型),NormalMap 等自动跳过 /// public class TextureMaxSizeWindow : EditorWindow { [MenuItem("Tools/图片尺寸工具")] public static void Open() { var w = GetWindow("图片尺寸工具"); w.minSize = new Vector2(640f, 560f); } private class ItemInfo { public string path; public int w, h; public int curMax; public int newMax; public float scale; // 实际缩放比例(1=不缩) public bool inAtlas; // 已被 SpriteAtlas 收录 public bool selected = true; public long savedBytes; // 预计内存节省(RGBA32 估算) } private readonly List _items = new List(); private HashSet _atlasPaths; // 所有图集收录的资源路径 private Vector2 _scroll; private bool _skipSmall = true; // 跳过边长≤128 的小图 private bool _skipInAtlas = true; // 跳过已被图集收录的 private bool _onlyShrunk = true; // 只显示会实际缩小的 private void OnGUI() { EditorGUILayout.HelpBox( "按原图尺寸智能设置 maxTextureSize:缩放控制在原图的 60%~80%,\n" + "取区间内 ≥60% 的最小 2 的幂档位(区间无档位时取 ≥60% 最小档位,保清晰度优先)。\n" + "选中 Project 里的文件夹或图片后点扫描。", MessageType.Info); if (GUILayout.Button("扫描选中对象(文件夹/图片)", GUILayout.Height(30))) Scan(); _skipSmall = EditorGUILayout.ToggleLeft("跳过小图(最长边 ≤ 128)", _skipSmall); _skipInAtlas = EditorGUILayout.ToggleLeft("跳过已被 SpriteAtlas 收录的图(图集设置优先)", _skipInAtlas); _onlyShrunk = EditorGUILayout.ToggleLeft("只显示会实际缩小的图", _onlyShrunk); EditorGUILayout.Space(4); var visible = VisibleItems().ToList(); if (visible.Count > 0) { long totalSaved = visible.Where(i => i.selected).Sum(i => i.savedBytes); EditorGUILayout.LabelField( $"共 {visible.Count} 项 | 勾选 {visible.Count(i => i.selected)} 项 | 预计节省内存 ~{totalSaved / 1048576f:F1} MB(RGBA32 估算)", EditorStyles.boldLabel); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("全选")) visible.ForEach(i => i.selected = true); if (GUILayout.Button("全不选")) visible.ForEach(i => i.selected = false); EditorGUILayout.EndHorizontal(); _scroll = EditorGUILayout.BeginScrollView(_scroll, GUILayout.MinHeight(220)); foreach (var it in visible) { EditorGUILayout.BeginHorizontal(); it.selected = EditorGUILayout.Toggle(it.selected, GUILayout.Width(20)); string tag = it.inAtlas ? "[图集内]" : ""; string change = it.newMax != it.curMax ? $"{it.curMax} → {it.newMax}" : $"{it.curMax}(已是目标)"; GUILayout.Label($"{it.w}×{it.h} max: {change} 缩至 {it.scale:P0} {tag}", GUILayout.Width(330)); GUILayout.Label(it.path, EditorStyles.miniLabel); EditorGUILayout.EndHorizontal(); } EditorGUILayout.EndScrollView(); using (new EditorGUI.DisabledScope(!visible.Any(i => i.selected))) { if (GUILayout.Button("应用勾选项", GUILayout.Height(34))) Apply(visible.Where(i => i.selected).ToList()); } } else { EditorGUILayout.HelpBox("尚未扫描,或按当前过滤条件没有需要处理的图片", MessageType.None); } } private IEnumerable VisibleItems() { foreach (var it in _items) { if (_skipSmall && Mathf.Max(it.w, it.h) <= 128) continue; if (_skipInAtlas && it.inAtlas) continue; if (_onlyShrunk && it.newMax >= Mathf.Max(it.w, it.h)) continue; yield return it; } } /// 扫描选中对象(文件夹递归 / 单张图片),收集贴图并计算目标 maxSize private void Scan() { _items.Clear(); _atlasPaths = CollectAtlasPaths(); var paths = new HashSet(); foreach (var obj in Selection.objects) { string p = AssetDatabase.GetAssetPath(obj); if (string.IsNullOrEmpty(p)) continue; if (Directory.Exists(p)) { foreach (var guid in AssetDatabase.FindAssets("t:Texture2D", new[] { p })) paths.Add(AssetDatabase.GUIDToAssetPath(guid)); } else if (p.EndsWith(".png") || p.EndsWith(".jpg") || p.EndsWith(".jpeg") || p.EndsWith(".tga")) { paths.Add(p); } } foreach (var path in paths.OrderBy(p => p)) { var importer = AssetImporter.GetAtPath(path) as TextureImporter; if (importer == null) continue; // 只处理常规贴图类型(NormalMap 等跳过) if (importer.textureType == TextureImporterType.NormalMap) continue; var tex = AssetDatabase.LoadAssetAtPath(path); if (tex == null) continue; int side = Mathf.Max(tex.width, tex.height); int newMax = ComputeMaxSize(side); // maxSize 只在小于原边长时才实际缩放 float scale = newMax < side ? (float)newMax / side : 1f; long cur = (long)tex.width * tex.height * 4; long after = (long)(tex.width * scale) * (long)(tex.height * scale) * 4; _items.Add(new ItemInfo { path = path, w = tex.width, h = tex.height, curMax = importer.maxTextureSize, newMax = newMax, scale = scale, inAtlas = _atlasPaths.Contains(path), savedBytes = cur - after }); } ShowNotification(new GUIContent($"扫描到 {_items.Count} 张贴图")); } /// /// 计算目标 maxTextureSize: /// 取 ≥ 原边长 60% 的最小 2 的幂档位(32~4096)。 /// 若该档位落在 [60%,80%] 区间内则同时满足上限约束; /// 区间内无档位时允许略超 80%(清晰度 60% 是硬下限)。 /// private static int ComputeMaxSize(int side) { int lower = Mathf.CeilToInt(side * 0.6f); int pow = 32; while (pow < lower && pow < 4096) pow <<= 1; return Mathf.Clamp(pow, 32, 4096); } /// 收集所有 SpriteAtlas(含其 packable 文件夹展开)覆盖的资源路径。 /// 该版本 SpriteAtlas 的 packables 无公开读取 API,用 SerializedObject 读 m_EditorData.packables。 private static HashSet CollectAtlasPaths() { var set = new HashSet(); foreach (var guid in AssetDatabase.FindAssets("t:SpriteAtlas")) { var atlasPath = AssetDatabase.GUIDToAssetPath(guid); var atlas = AssetDatabase.LoadAssetAtPath(atlasPath); if (atlas == null) continue; var so = new SerializedObject(atlas); var packables = so.FindProperty("m_EditorData.packables"); if (packables == null || !packables.isArray) continue; for (int i = 0; i < packables.arraySize; i++) { var packable = packables.GetArrayElementAtIndex(i).objectReferenceValue; if (packable == null) continue; string p = AssetDatabase.GetAssetPath(packable); if (string.IsNullOrEmpty(p)) continue; if (Directory.Exists(p)) { foreach (var g in AssetDatabase.FindAssets("t:Texture2D", new[] { p })) set.Add(AssetDatabase.GUIDToAssetPath(g)); } else set.Add(p); } } return set; } /// 批量写入 maxTextureSize 并重导入 private void Apply(List targets) { try { int done = 0; foreach (var it in targets) { EditorUtility.DisplayProgressBar("应用 maxSize", it.path, (float)done / targets.Count); var importer = AssetImporter.GetAtPath(it.path) as TextureImporter; if (importer != null && importer.maxTextureSize != it.newMax) { importer.maxTextureSize = it.newMax; importer.SaveAndReimport(); } done++; } } finally { EditorUtility.ClearProgressBar(); } Debug.Log($"[图片尺寸工具] 已应用 {targets.Count} 项 maxTextureSize"); // 重新扫描刷新列表(当前值已变化) Scan(); } }