Files
5.24BuYu/Assets/MyGame/Framework/Editor/TextureMaxSizeWindow.cs
T
2026-10-20 18:30:03 +08:00

252 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
}
}