252 lines
10 KiB
C#
252 lines
10 KiB
C#
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using UnityEditor;
|
||
using UnityEngine;
|
||
using UnityEngine.U2D;
|
||
|
||
/// <summary>
|
||
/// 图片 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 等自动跳过
|
||
/// </summary>
|
||
public class TextureMaxSizeWindow : EditorWindow
|
||
{
|
||
[MenuItem("Tools/图片尺寸工具")]
|
||
public static void Open()
|
||
{
|
||
var w = GetWindow<TextureMaxSizeWindow>("图片尺寸工具");
|
||
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<ItemInfo> _items = new List<ItemInfo>();
|
||
private HashSet<string> _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<ItemInfo> 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;
|
||
}
|
||
}
|
||
|
||
/// <summary>扫描选中对象(文件夹递归 / 单张图片),收集贴图并计算目标 maxSize</summary>
|
||
private void Scan()
|
||
{
|
||
_items.Clear();
|
||
_atlasPaths = CollectAtlasPaths();
|
||
var paths = new HashSet<string>();
|
||
|
||
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<Texture2D>(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} 张贴图"));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算目标 maxTextureSize:
|
||
/// 取 ≥ 原边长 60% 的最小 2 的幂档位(32~4096)。
|
||
/// 若该档位落在 [60%,80%] 区间内则同时满足上限约束;
|
||
/// 区间内无档位时允许略超 80%(清晰度 60% 是硬下限)。
|
||
/// </summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>收集所有 SpriteAtlas(含其 packable 文件夹展开)覆盖的资源路径。
|
||
/// 该版本 SpriteAtlas 的 packables 无公开读取 API,用 SerializedObject 读 m_EditorData.packables。</summary>
|
||
private static HashSet<string> CollectAtlasPaths()
|
||
{
|
||
var set = new HashSet<string>();
|
||
foreach (var guid in AssetDatabase.FindAssets("t:SpriteAtlas"))
|
||
{
|
||
var atlasPath = AssetDatabase.GUIDToAssetPath(guid);
|
||
var atlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(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;
|
||
}
|
||
|
||
/// <summary>批量写入 maxTextureSize 并重导入</summary>
|
||
private void Apply(List<ItemInfo> 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();
|
||
}
|
||
} |