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();
}
}