1269 lines
46 KiB
C#
1269 lines
46 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Globalization;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Reflection;
|
||
using System.Text;
|
||
using UnityEditor;
|
||
using UnityEngine;
|
||
|
||
[Flags]
|
||
internal enum TextureOptimizeIssue
|
||
{
|
||
None = 0,
|
||
CompressionRisk = 1 << 0,
|
||
TooLarge = 1 << 1,
|
||
TooManyPixels = 1 << 2,
|
||
TooTransparent = 1 << 3,
|
||
ReadWriteEnabled = 1 << 4,
|
||
SpriteMipmapEnabled = 1 << 5,
|
||
NoStandaloneOverride = 1 << 6,
|
||
}
|
||
|
||
public class ResourceTextureOptimizerWindow : EditorWindow
|
||
{
|
||
private enum MainTab
|
||
{
|
||
ScanRules = 0,
|
||
Optimize = 1,
|
||
Trim = 2,
|
||
Results = 3,
|
||
}
|
||
|
||
private class TextureOptimizeItem
|
||
{
|
||
public string AssetPath;
|
||
public string FileExtension;
|
||
public int SourceWidth;
|
||
public int SourceHeight;
|
||
public long FileSizeBytes;
|
||
public float FileSizeMB;
|
||
public float EstimatedRawMB;
|
||
public bool HasAlpha;
|
||
public float TransparentRatio = -1f;
|
||
public string TransparentCheckNote = string.Empty;
|
||
public TextureImporterCompression TextureCompression;
|
||
public bool IsReadable;
|
||
public bool IsSprite;
|
||
public bool MipmapEnabled;
|
||
public bool StandaloneOverridden;
|
||
public TextureImporterFormat StandaloneFormat;
|
||
public int StandaloneMaxSize;
|
||
public TextureOptimizeIssue Issues;
|
||
public string IssueText = "无";
|
||
public bool IsTrimCandidate;
|
||
public string TrimCandidateReason = string.Empty;
|
||
public bool Selected = true;
|
||
}
|
||
|
||
private const string DefaultScanFolder = "Assets/Resources";
|
||
private const string StandalonePlatformName = "Standalone";
|
||
|
||
private string _scanFolder = DefaultScanFolder;
|
||
private string _search = string.Empty;
|
||
private Vector2 _scroll;
|
||
private Vector2 _scanTabScroll;
|
||
private Vector2 _optimizeTabScroll;
|
||
private Vector2 _trimTabScroll;
|
||
private bool _isScanning;
|
||
private readonly List<TextureOptimizeItem> _items = new List<TextureOptimizeItem>();
|
||
private int _lastScannedCount;
|
||
private string _selectedAssetPath = string.Empty;
|
||
private int _pageIndex;
|
||
private int _pageSize = 80;
|
||
private bool _showOnlySelected;
|
||
private bool _showOnlyTrimCandidates;
|
||
private int _itemsVersion;
|
||
private int _cachedItemsVersion = -1;
|
||
private string _cachedFilterKey = string.Empty;
|
||
private readonly List<TextureOptimizeItem> _filteredCache = new List<TextureOptimizeItem>();
|
||
private MainTab _mainTab = MainTab.Results;
|
||
private readonly string[] _mainTabNames = { "扫描规则", "一键优化", "自动裁切", "结果列表" };
|
||
|
||
// Scan rules
|
||
private bool _scanOnlyIssueItems = true;
|
||
private bool _checkTransparentRatio = true;
|
||
private int _warnMaxDimension = 2048;
|
||
private float _warnMaxMegaPixels = 2.5f;
|
||
private float _warnTransparentRatio = 0.60f;
|
||
private float _transparentAlphaCutoff = 0.08f; // alpha <= 8% 视为透明
|
||
private int _transparentCheckMinDimension = 512;
|
||
private float _warnMinFileSizeMBForNoOverride = 1.0f;
|
||
private bool _warnReadWriteEnabled = true;
|
||
private bool _warnSpriteMipmapEnabled = true;
|
||
|
||
// Batch operations
|
||
private bool _applyStandaloneOverride = true;
|
||
private bool _applyTextureCompression = true;
|
||
private bool _applyFormatByAlpha = true;
|
||
private bool _applyMaxSize = true;
|
||
private bool _applyOnlyShrinkMaxSize = true;
|
||
private bool _applyDisableReadWrite = true;
|
||
private bool _applyDisableSpriteMipmap = true;
|
||
private bool _applyCrunch = true;
|
||
private bool _applyCompressionQuality = true;
|
||
|
||
private TextureImporterCompression _targetTextureCompression = TextureImporterCompression.CompressedHQ;
|
||
private TextureImporterFormat _targetFormatAlpha;
|
||
private TextureImporterFormat _targetFormatOpaque;
|
||
private int _targetMaxSize = 1024;
|
||
private bool _targetEnableCrunch = false;
|
||
private int _targetCompressionQuality = 80;
|
||
|
||
// Auto trim transparent border (PNG)
|
||
private bool _trimCreateBackup = true;
|
||
private bool _trimSkipSpriteMultiple = true;
|
||
private float _trimAlphaCutoff = 0.05f;
|
||
private float _trimCandidateTransparentRatio = 0.85f;
|
||
private float _trimCandidateMinRawMB = 4.0f;
|
||
|
||
[MenuItem("Tools/TH1/Resource Texture Optimizer")]
|
||
public static void ShowWindow()
|
||
{
|
||
var win = GetWindow<ResourceTextureOptimizerWindow>("Resource Texture Optimizer");
|
||
win.minSize = new Vector2(980, 620);
|
||
}
|
||
|
||
private void OnEnable()
|
||
{
|
||
_targetFormatAlpha = ResolveBestFormat(new[] { "BC7", "DXT5", "RGBA32" });
|
||
_targetFormatOpaque = ResolveBestFormat(new[] { "BC1", "DXT1", "RGB24" });
|
||
}
|
||
|
||
private void OnGUI()
|
||
{
|
||
DrawHeader();
|
||
DrawMainTabs();
|
||
|
||
switch (_mainTab)
|
||
{
|
||
case MainTab.ScanRules:
|
||
_scanTabScroll = EditorGUILayout.BeginScrollView(_scanTabScroll);
|
||
DrawScanRules();
|
||
EditorGUILayout.EndScrollView();
|
||
break;
|
||
case MainTab.Optimize:
|
||
_optimizeTabScroll = EditorGUILayout.BeginScrollView(_optimizeTabScroll);
|
||
DrawOptimizeActions();
|
||
EditorGUILayout.EndScrollView();
|
||
break;
|
||
case MainTab.Trim:
|
||
_trimTabScroll = EditorGUILayout.BeginScrollView(_trimTabScroll);
|
||
DrawTrimActions();
|
||
EditorGUILayout.EndScrollView();
|
||
break;
|
||
case MainTab.Results:
|
||
default:
|
||
DrawResultToolbar();
|
||
DrawResultList();
|
||
break;
|
||
}
|
||
}
|
||
|
||
private void DrawMainTabs()
|
||
{
|
||
EditorGUILayout.Space(4);
|
||
var newTab = (MainTab)GUILayout.Toolbar((int)_mainTab, _mainTabNames, GUILayout.Height(28));
|
||
if (newTab != _mainTab)
|
||
{
|
||
_mainTab = newTab;
|
||
GUI.FocusControl(null);
|
||
}
|
||
EditorGUILayout.Space(4);
|
||
}
|
||
|
||
private void DrawHeader()
|
||
{
|
||
EditorGUILayout.LabelField("Resources 贴图优化工具", EditorStyles.boldLabel);
|
||
EditorGUILayout.HelpBox(
|
||
"扫描并定位:压缩风险、超大图、透明区域过多、Read/Write开启、Sprite开MipMap等问题;支持一键批量修复导入设置。",
|
||
MessageType.Info);
|
||
|
||
EditorGUILayout.BeginHorizontal();
|
||
_scanFolder = EditorGUILayout.TextField("扫描目录", _scanFolder);
|
||
if (GUILayout.Button("重置", GUILayout.Width(60)))
|
||
{
|
||
_scanFolder = DefaultScanFolder;
|
||
}
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
EditorGUILayout.BeginHorizontal();
|
||
GUI.enabled = !_isScanning;
|
||
if (GUILayout.Button("扫描问题贴图", GUILayout.Height(30)))
|
||
{
|
||
ScanTextures();
|
||
}
|
||
GUI.enabled = true;
|
||
|
||
if (GUILayout.Button("导出CSV", GUILayout.Height(30), GUILayout.Width(120)))
|
||
{
|
||
ExportCsv();
|
||
}
|
||
if (GUILayout.Button("选中到Project", GUILayout.Height(30), GUILayout.Width(120)))
|
||
{
|
||
SelectCheckedInProject();
|
||
}
|
||
EditorGUILayout.EndHorizontal();
|
||
}
|
||
|
||
private void DrawScanRules()
|
||
{
|
||
EditorGUILayout.Space(6);
|
||
EditorGUILayout.LabelField("扫描规则", EditorStyles.boldLabel);
|
||
EditorGUILayout.BeginVertical("box");
|
||
|
||
_scanOnlyIssueItems = EditorGUILayout.ToggleLeft("仅保留命中问题的贴图结果", _scanOnlyIssueItems);
|
||
_checkTransparentRatio = EditorGUILayout.ToggleLeft("检测透明区域占比(较慢)", _checkTransparentRatio);
|
||
|
||
_warnMaxDimension = EditorGUILayout.IntField("超大图阈值(最长边)", _warnMaxDimension);
|
||
_warnMaxMegaPixels = EditorGUILayout.FloatField("超大图阈值(百万像素)", _warnMaxMegaPixels);
|
||
_warnTransparentRatio = EditorGUILayout.Slider("透明占比告警阈值", _warnTransparentRatio, 0.3f, 0.95f);
|
||
_transparentAlphaCutoff = EditorGUILayout.Slider("透明像素Alpha阈值", _transparentAlphaCutoff, 0.01f, 0.2f);
|
||
_transparentCheckMinDimension = EditorGUILayout.IntField("透明检测最小分辨率边长", _transparentCheckMinDimension);
|
||
_warnMinFileSizeMBForNoOverride = EditorGUILayout.FloatField("无Standalone覆盖告警最小文件(MB)", _warnMinFileSizeMBForNoOverride);
|
||
_warnReadWriteEnabled = EditorGUILayout.ToggleLeft("检查 Read/Write Enabled", _warnReadWriteEnabled);
|
||
_warnSpriteMipmapEnabled = EditorGUILayout.ToggleLeft("检查 Sprite 开启 MipMap", _warnSpriteMipmapEnabled);
|
||
|
||
EditorGUILayout.EndVertical();
|
||
}
|
||
|
||
private void DrawOptimizeActions()
|
||
{
|
||
EditorGUILayout.Space(6);
|
||
EditorGUILayout.LabelField("一键操作(对勾选结果生效)", EditorStyles.boldLabel);
|
||
EditorGUILayout.BeginVertical("box");
|
||
|
||
_applyStandaloneOverride = EditorGUILayout.ToggleLeft("强制写入 Standalone 平台设置", _applyStandaloneOverride);
|
||
_applyTextureCompression = EditorGUILayout.ToggleLeft("设置全局 Texture Compression", _applyTextureCompression);
|
||
_applyFormatByAlpha = EditorGUILayout.ToggleLeft("按透明通道设置格式(有Alpha / 无Alpha)", _applyFormatByAlpha);
|
||
_applyMaxSize = EditorGUILayout.ToggleLeft("设置 Max Texture Size", _applyMaxSize);
|
||
_applyOnlyShrinkMaxSize = EditorGUILayout.ToggleLeft("仅缩小,不放大已有 Max Size", _applyOnlyShrinkMaxSize);
|
||
_applyDisableReadWrite = EditorGUILayout.ToggleLeft("关闭 Read/Write", _applyDisableReadWrite);
|
||
_applyDisableSpriteMipmap = EditorGUILayout.ToggleLeft("关闭 Sprite 的 MipMap", _applyDisableSpriteMipmap);
|
||
_applyCrunch = EditorGUILayout.ToggleLeft("设置 Crunch 开关", _applyCrunch);
|
||
_applyCompressionQuality = EditorGUILayout.ToggleLeft("设置压缩质量", _applyCompressionQuality);
|
||
|
||
using (new EditorGUI.IndentLevelScope())
|
||
{
|
||
_targetTextureCompression = (TextureImporterCompression)EditorGUILayout.EnumPopup("Texture Compression", _targetTextureCompression);
|
||
_targetFormatAlpha = (TextureImporterFormat)EditorGUILayout.EnumPopup("有Alpha格式", _targetFormatAlpha);
|
||
_targetFormatOpaque = (TextureImporterFormat)EditorGUILayout.EnumPopup("无Alpha格式", _targetFormatOpaque);
|
||
_targetMaxSize = EditorGUILayout.IntPopup("目标Max Size", _targetMaxSize,
|
||
new[] { "256", "512", "1024", "2048", "4096" },
|
||
new[] { 256, 512, 1024, 2048, 4096 });
|
||
_targetEnableCrunch = EditorGUILayout.Toggle("Crunch", _targetEnableCrunch);
|
||
_targetCompressionQuality = EditorGUILayout.IntSlider("压缩质量", _targetCompressionQuality, 0, 100);
|
||
}
|
||
|
||
EditorGUILayout.BeginHorizontal();
|
||
if (GUILayout.Button("全选"))
|
||
{
|
||
SetSelection(true);
|
||
}
|
||
if (GUILayout.Button("全不选"))
|
||
{
|
||
SetSelection(false);
|
||
}
|
||
if (GUILayout.Button("反选"))
|
||
{
|
||
InvertSelection();
|
||
}
|
||
GUILayout.FlexibleSpace();
|
||
GUI.enabled = !_isScanning;
|
||
if (GUILayout.Button("对勾选项执行一键优化", GUILayout.Height(28), GUILayout.Width(220)))
|
||
{
|
||
ApplyOptimizationToSelected();
|
||
}
|
||
GUI.enabled = true;
|
||
EditorGUILayout.EndHorizontal();
|
||
EditorGUILayout.EndVertical();
|
||
}
|
||
|
||
private void DrawTrimActions()
|
||
{
|
||
EditorGUILayout.Space(6);
|
||
EditorGUILayout.LabelField("透明边自动裁切(PNG)", EditorStyles.boldLabel);
|
||
EditorGUILayout.BeginVertical("box");
|
||
EditorGUILayout.HelpBox(
|
||
"会直接改 PNG 文件尺寸。建议先“快速勾选候选”再执行。默认跳过 SpriteMultiple 以避免切图坐标错乱。",
|
||
MessageType.Warning);
|
||
|
||
_trimCreateBackup = EditorGUILayout.ToggleLeft("写入前创建 .trimbak 备份", _trimCreateBackup);
|
||
_trimSkipSpriteMultiple = EditorGUILayout.ToggleLeft("跳过 SpriteMultiple(推荐)", _trimSkipSpriteMultiple);
|
||
_trimAlphaCutoff = EditorGUILayout.Slider("裁切Alpha阈值", _trimAlphaCutoff, 0.01f, 0.2f);
|
||
_trimCandidateTransparentRatio = EditorGUILayout.Slider("候选透明占比阈值", _trimCandidateTransparentRatio, 0.5f, 0.98f);
|
||
_trimCandidateMinRawMB = EditorGUILayout.FloatField("候选最小估算RGBA32(MB)", _trimCandidateMinRawMB);
|
||
|
||
EditorGUILayout.BeginHorizontal();
|
||
if (GUILayout.Button("快速勾选高透明大图候选"))
|
||
{
|
||
SelectTrimCandidates();
|
||
}
|
||
GUI.enabled = !_isScanning;
|
||
if (GUILayout.Button("对勾选项执行自动裁切(PNG)", GUILayout.Height(26)))
|
||
{
|
||
TrimTransparentBordersForSelected();
|
||
}
|
||
GUI.enabled = true;
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
EditorGUILayout.EndVertical();
|
||
}
|
||
|
||
private void DrawResultToolbar()
|
||
{
|
||
EditorGUILayout.Space(6);
|
||
EditorGUILayout.BeginHorizontal();
|
||
EditorGUILayout.LabelField($"结果:{_items.Count} 项(本次扫描纹理总数:{_lastScannedCount})", EditorStyles.boldLabel);
|
||
GUILayout.FlexibleSpace();
|
||
_search = EditorGUILayout.TextField("搜索", _search, GUILayout.Width(320));
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
EditorGUILayout.BeginHorizontal();
|
||
bool newOnlySelected = EditorGUILayout.ToggleLeft("只看勾选项", _showOnlySelected, GUILayout.Width(120));
|
||
bool newOnlyTrimCandidates = EditorGUILayout.ToggleLeft("只看裁切候选", _showOnlyTrimCandidates, GUILayout.Width(140));
|
||
if (newOnlySelected != _showOnlySelected || newOnlyTrimCandidates != _showOnlyTrimCandidates)
|
||
{
|
||
_showOnlySelected = newOnlySelected;
|
||
_showOnlyTrimCandidates = newOnlyTrimCandidates;
|
||
InvalidateFilterCache();
|
||
}
|
||
|
||
int newPageSize = EditorGUILayout.IntPopup("每页", _pageSize,
|
||
new[] { "40", "80", "120", "200" },
|
||
new[] { 40, 80, 120, 200 }, GUILayout.Width(120));
|
||
if (newPageSize != _pageSize)
|
||
{
|
||
_pageSize = newPageSize;
|
||
_pageIndex = 0;
|
||
}
|
||
|
||
var filtered = GetFilteredItems();
|
||
int totalPages = Mathf.Max(1, Mathf.CeilToInt(filtered.Count / (float)Mathf.Max(1, _pageSize)));
|
||
_pageIndex = Mathf.Clamp(_pageIndex, 0, totalPages - 1);
|
||
|
||
GUI.enabled = _pageIndex > 0;
|
||
if (GUILayout.Button("上一页", GUILayout.Width(70)))
|
||
{
|
||
_pageIndex--;
|
||
}
|
||
GUI.enabled = _pageIndex < totalPages - 1;
|
||
if (GUILayout.Button("下一页", GUILayout.Width(70)))
|
||
{
|
||
_pageIndex++;
|
||
}
|
||
GUI.enabled = true;
|
||
|
||
EditorGUILayout.LabelField($"第 {_pageIndex + 1}/{totalPages} 页(筛选后 {filtered.Count} 项)");
|
||
EditorGUILayout.EndHorizontal();
|
||
}
|
||
|
||
private void DrawResultList()
|
||
{
|
||
_scroll = EditorGUILayout.BeginScrollView(_scroll);
|
||
|
||
if (_items.Count == 0)
|
||
{
|
||
EditorGUILayout.HelpBox("暂无结果。点击“扫描问题贴图”开始。", MessageType.None);
|
||
EditorGUILayout.EndScrollView();
|
||
return;
|
||
}
|
||
|
||
var filtered = GetFilteredItems();
|
||
if (filtered.Count == 0)
|
||
{
|
||
EditorGUILayout.HelpBox("当前筛选条件下无结果。", MessageType.None);
|
||
EditorGUILayout.EndScrollView();
|
||
return;
|
||
}
|
||
|
||
int start = _pageIndex * Mathf.Max(1, _pageSize);
|
||
if (start >= filtered.Count) start = 0;
|
||
int end = Mathf.Min(filtered.Count, start + Mathf.Max(1, _pageSize));
|
||
|
||
var selectedItem = GetSelectedItem();
|
||
if (selectedItem != null)
|
||
{
|
||
DrawSelectedItemDetail(selectedItem);
|
||
}
|
||
|
||
DrawListHeader();
|
||
for (int i = start; i < end; i++)
|
||
{
|
||
DrawItemCompactRow(filtered[i]);
|
||
}
|
||
|
||
EditorGUILayout.EndScrollView();
|
||
}
|
||
|
||
private void DrawListHeader()
|
||
{
|
||
EditorGUILayout.BeginHorizontal("box");
|
||
GUILayout.Label("", GUILayout.Width(22));
|
||
GUILayout.Label("", GUILayout.Width(40));
|
||
GUILayout.Label("尺寸", GUILayout.Width(90));
|
||
GUILayout.Label("源MB", GUILayout.Width(58));
|
||
GUILayout.Label("估算MB", GUILayout.Width(66));
|
||
GUILayout.Label("透明%", GUILayout.Width(58));
|
||
GUILayout.Label("裁切候选", GUILayout.Width(74));
|
||
GUILayout.Label("问题", GUILayout.Width(250));
|
||
GUILayout.Label("AssetPath");
|
||
EditorGUILayout.EndHorizontal();
|
||
}
|
||
|
||
private void DrawItemCompactRow(TextureOptimizeItem item)
|
||
{
|
||
EditorGUILayout.BeginHorizontal("box");
|
||
bool oldSelected = item.Selected;
|
||
item.Selected = EditorGUILayout.Toggle(item.Selected, GUILayout.Width(22));
|
||
if (_showOnlySelected && oldSelected != item.Selected)
|
||
{
|
||
InvalidateFilterCache();
|
||
}
|
||
|
||
if (GUILayout.Button("Ping", GUILayout.Width(40)))
|
||
{
|
||
var obj = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(item.AssetPath);
|
||
if (obj != null) EditorGUIUtility.PingObject(obj);
|
||
}
|
||
|
||
if (GUILayout.Button($"{item.SourceWidth}x{item.SourceHeight}", GUILayout.Width(90)))
|
||
{
|
||
_selectedAssetPath = item.AssetPath;
|
||
}
|
||
GUILayout.Label(item.FileSizeMB.ToString("F2"), GUILayout.Width(58));
|
||
GUILayout.Label(item.EstimatedRawMB.ToString("F2"), GUILayout.Width(66));
|
||
string transparentText = item.TransparentRatio >= 0f ? (item.TransparentRatio * 100f).ToString("F1") : "-";
|
||
GUILayout.Label(transparentText, GUILayout.Width(58));
|
||
GUILayout.Label(item.IsTrimCandidate ? "是" : "-", GUILayout.Width(74));
|
||
GUILayout.Label(item.IssueText, GUILayout.Width(250));
|
||
GUILayout.Label(item.AssetPath);
|
||
EditorGUILayout.EndHorizontal();
|
||
}
|
||
|
||
private void DrawSelectedItemDetail(TextureOptimizeItem item)
|
||
{
|
||
EditorGUILayout.BeginVertical("box");
|
||
EditorGUILayout.BeginHorizontal();
|
||
EditorGUILayout.LabelField("当前详情", EditorStyles.boldLabel);
|
||
GUILayout.FlexibleSpace();
|
||
if (GUILayout.Button("Ping", GUILayout.Width(60)))
|
||
{
|
||
var obj = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(item.AssetPath);
|
||
if (obj != null)
|
||
{
|
||
EditorGUIUtility.PingObject(obj);
|
||
}
|
||
}
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
EditorGUILayout.LabelField(item.AssetPath, EditorStyles.wordWrappedLabel);
|
||
EditorGUILayout.LabelField($"尺寸={item.SourceWidth}x{item.SourceHeight} | 源文件={item.FileSizeMB:F2}MB | 估算RGBA32={item.EstimatedRawMB:F2}MB");
|
||
EditorGUILayout.LabelField($"问题:{item.IssueText}");
|
||
EditorGUILayout.LabelField(
|
||
$"压缩={item.TextureCompression}, StandaloneOverride={item.StandaloneOverridden}, StandaloneFormat={item.StandaloneFormat}, MaxSize={item.StandaloneMaxSize}, ReadWrite={item.IsReadable}, SpriteMip={item.IsSprite && item.MipmapEnabled}");
|
||
|
||
if (item.TransparentRatio >= 0f)
|
||
{
|
||
EditorGUILayout.LabelField($"透明占比(采样)={item.TransparentRatio:P1}");
|
||
}
|
||
else if (!string.IsNullOrEmpty(item.TransparentCheckNote))
|
||
{
|
||
EditorGUILayout.LabelField($"透明检测:{item.TransparentCheckNote}");
|
||
}
|
||
if (!string.IsNullOrEmpty(item.TrimCandidateReason))
|
||
{
|
||
EditorGUILayout.LabelField($"裁切候选说明:{item.TrimCandidateReason}");
|
||
}
|
||
|
||
EditorGUILayout.EndVertical();
|
||
}
|
||
|
||
private bool FilterBySearch(TextureOptimizeItem item)
|
||
{
|
||
if (_showOnlySelected && !item.Selected)
|
||
{
|
||
return false;
|
||
}
|
||
if (_showOnlyTrimCandidates && !item.IsTrimCandidate)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(_search))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
return item.AssetPath.IndexOf(_search, StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||
item.IssueText.IndexOf(_search, StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||
item.TrimCandidateReason.IndexOf(_search, StringComparison.OrdinalIgnoreCase) >= 0;
|
||
}
|
||
|
||
private List<TextureOptimizeItem> GetFilteredItems()
|
||
{
|
||
string key = $"{_search}|{_showOnlySelected}|{_showOnlyTrimCandidates}";
|
||
if (_cachedItemsVersion == _itemsVersion && string.Equals(_cachedFilterKey, key, StringComparison.Ordinal))
|
||
{
|
||
return _filteredCache;
|
||
}
|
||
|
||
_filteredCache.Clear();
|
||
for (int i = 0; i < _items.Count; i++)
|
||
{
|
||
if (FilterBySearch(_items[i]))
|
||
{
|
||
_filteredCache.Add(_items[i]);
|
||
}
|
||
}
|
||
|
||
_cachedFilterKey = key;
|
||
_cachedItemsVersion = _itemsVersion;
|
||
return _filteredCache;
|
||
}
|
||
|
||
private void InvalidateFilterCache()
|
||
{
|
||
_cachedItemsVersion = -1;
|
||
}
|
||
|
||
private TextureOptimizeItem GetSelectedItem()
|
||
{
|
||
if (string.IsNullOrEmpty(_selectedAssetPath))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
for (int i = 0; i < _items.Count; i++)
|
||
{
|
||
if (string.Equals(_items[i].AssetPath, _selectedAssetPath, StringComparison.Ordinal))
|
||
{
|
||
return _items[i];
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private void ScanTextures()
|
||
{
|
||
if (string.IsNullOrWhiteSpace(_scanFolder) || !AssetDatabase.IsValidFolder(_scanFolder))
|
||
{
|
||
EditorUtility.DisplayDialog("扫描失败", $"无效目录:{_scanFolder}", "确定");
|
||
return;
|
||
}
|
||
|
||
_isScanning = true;
|
||
_items.Clear();
|
||
_selectedAssetPath = string.Empty;
|
||
_pageIndex = 0;
|
||
|
||
try
|
||
{
|
||
var guids = AssetDatabase.FindAssets("t:Texture2D", new[] { _scanFolder });
|
||
_lastScannedCount = guids.Length;
|
||
for (int i = 0; i < guids.Length; i++)
|
||
{
|
||
if (EditorUtility.DisplayCancelableProgressBar("扫描贴图", $"处理中 {i + 1}/{guids.Length}", (float)i / Mathf.Max(1, guids.Length)))
|
||
{
|
||
break;
|
||
}
|
||
|
||
var assetPath = AssetDatabase.GUIDToAssetPath(guids[i]);
|
||
var importer = AssetImporter.GetAtPath(assetPath) as TextureImporter;
|
||
if (importer == null)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var item = BuildItem(assetPath, importer);
|
||
if (_scanOnlyIssueItems && item.Issues == TextureOptimizeIssue.None)
|
||
{
|
||
continue;
|
||
}
|
||
_items.Add(item);
|
||
}
|
||
|
||
_items.Sort((a, b) =>
|
||
{
|
||
int issueCompare = b.Issues.CompareTo(a.Issues);
|
||
if (issueCompare != 0) return issueCompare;
|
||
return b.EstimatedRawMB.CompareTo(a.EstimatedRawMB);
|
||
});
|
||
|
||
if (_items.Count > 0)
|
||
{
|
||
_selectedAssetPath = _items[0].AssetPath;
|
||
}
|
||
_itemsVersion++;
|
||
InvalidateFilterCache();
|
||
}
|
||
finally
|
||
{
|
||
_isScanning = false;
|
||
EditorUtility.ClearProgressBar();
|
||
}
|
||
}
|
||
|
||
private TextureOptimizeItem BuildItem(string assetPath, TextureImporter importer)
|
||
{
|
||
var item = new TextureOptimizeItem();
|
||
item.AssetPath = assetPath;
|
||
item.FileExtension = Path.GetExtension(assetPath).ToLowerInvariant();
|
||
item.HasAlpha = importer.DoesSourceTextureHaveAlpha();
|
||
item.TextureCompression = importer.textureCompression;
|
||
item.IsReadable = importer.isReadable;
|
||
item.IsSprite = importer.textureType == TextureImporterType.Sprite;
|
||
item.MipmapEnabled = importer.mipmapEnabled;
|
||
|
||
TryGetSourceDimensions(importer, assetPath, out item.SourceWidth, out item.SourceHeight);
|
||
item.EstimatedRawMB = (item.SourceWidth * item.SourceHeight * 4f) / (1024f * 1024f);
|
||
|
||
var fullPath = Path.GetFullPath(assetPath);
|
||
if (File.Exists(fullPath))
|
||
{
|
||
item.FileSizeBytes = new FileInfo(fullPath).Length;
|
||
item.FileSizeMB = item.FileSizeBytes / (1024f * 1024f);
|
||
}
|
||
|
||
var standalone = importer.GetPlatformTextureSettings(StandalonePlatformName);
|
||
item.StandaloneOverridden = standalone.overridden;
|
||
item.StandaloneFormat = standalone.format;
|
||
item.StandaloneMaxSize = standalone.maxTextureSize;
|
||
|
||
var issues = TextureOptimizeIssue.None;
|
||
|
||
if (item.TextureCompression == TextureImporterCompression.Uncompressed ||
|
||
(standalone.overridden && IsUncompressedFormat(standalone.format)))
|
||
{
|
||
issues |= TextureOptimizeIssue.CompressionRisk;
|
||
}
|
||
|
||
int maxDim = Mathf.Max(item.SourceWidth, item.SourceHeight);
|
||
float megaPixels = (item.SourceWidth * item.SourceHeight) / 1000000f;
|
||
|
||
if (maxDim >= _warnMaxDimension)
|
||
{
|
||
issues |= TextureOptimizeIssue.TooLarge;
|
||
}
|
||
if (megaPixels >= _warnMaxMegaPixels)
|
||
{
|
||
issues |= TextureOptimizeIssue.TooManyPixels;
|
||
}
|
||
if (!standalone.overridden && item.FileSizeMB >= _warnMinFileSizeMBForNoOverride)
|
||
{
|
||
issues |= TextureOptimizeIssue.NoStandaloneOverride;
|
||
}
|
||
if (_warnReadWriteEnabled && item.IsReadable)
|
||
{
|
||
issues |= TextureOptimizeIssue.ReadWriteEnabled;
|
||
}
|
||
if (_warnSpriteMipmapEnabled && item.IsSprite && item.MipmapEnabled)
|
||
{
|
||
issues |= TextureOptimizeIssue.SpriteMipmapEnabled;
|
||
}
|
||
|
||
if (_checkTransparentRatio && item.HasAlpha && maxDim >= _transparentCheckMinDimension)
|
||
{
|
||
if (TryEstimateTransparentRatio(assetPath, _transparentAlphaCutoff, out float ratio, out string note))
|
||
{
|
||
item.TransparentRatio = ratio;
|
||
if (ratio >= _warnTransparentRatio)
|
||
{
|
||
issues |= TextureOptimizeIssue.TooTransparent;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
item.TransparentCheckNote = note;
|
||
}
|
||
}
|
||
|
||
item.Issues = issues;
|
||
item.IssueText = BuildIssueText(issues);
|
||
item.IsTrimCandidate = IsAutoTrimCandidate(item, out string trimReason);
|
||
item.TrimCandidateReason = trimReason;
|
||
return item;
|
||
}
|
||
|
||
private static void TryGetSourceDimensions(TextureImporter importer, string assetPath, out int width, out int height)
|
||
{
|
||
width = 0;
|
||
height = 0;
|
||
|
||
// 兼容不同 Unity 版本:反射调用 GetSourceTextureWidthAndHeight
|
||
var method = typeof(TextureImporter).GetMethod("GetSourceTextureWidthAndHeight",
|
||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||
if (method != null)
|
||
{
|
||
object[] args = { 0, 0 };
|
||
method.Invoke(importer, args);
|
||
width = (int)args[0];
|
||
height = (int)args[1];
|
||
if (width > 0 && height > 0) return;
|
||
}
|
||
|
||
var tex = AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);
|
||
if (tex != null)
|
||
{
|
||
width = tex.width;
|
||
height = tex.height;
|
||
}
|
||
}
|
||
|
||
private static bool IsUncompressedFormat(TextureImporterFormat format)
|
||
{
|
||
// 使用字符串兜底,避免不同 Unity 版本枚举不一致造成编译风险
|
||
string name = format.ToString();
|
||
return name.IndexOf("RGBA32", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||
name.IndexOf("ARGB32", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||
name.IndexOf("RGB24", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||
name.IndexOf("Alpha8", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||
string.Equals(name, "R8", StringComparison.OrdinalIgnoreCase) ||
|
||
string.Equals(name, "R16", StringComparison.OrdinalIgnoreCase) ||
|
||
string.Equals(name, "RG16", StringComparison.OrdinalIgnoreCase);
|
||
}
|
||
|
||
private bool IsAutoTrimCandidate(TextureOptimizeItem item, out string reason)
|
||
{
|
||
if (!string.Equals(item.FileExtension, ".png", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
reason = "仅支持PNG自动裁切";
|
||
return false;
|
||
}
|
||
|
||
if (item.TransparentRatio < 0f)
|
||
{
|
||
reason = "未执行透明检测";
|
||
return false;
|
||
}
|
||
|
||
if (item.TransparentRatio < _trimCandidateTransparentRatio)
|
||
{
|
||
reason = $"透明占比<{_trimCandidateTransparentRatio:P0}";
|
||
return false;
|
||
}
|
||
|
||
if (item.EstimatedRawMB < _trimCandidateMinRawMB)
|
||
{
|
||
reason = $"估算RGBA32<{_trimCandidateMinRawMB:F1}MB";
|
||
return false;
|
||
}
|
||
|
||
reason = "高透明+大纹理,可优先裁切";
|
||
return true;
|
||
}
|
||
|
||
private static TextureImporterFormat ResolveBestFormat(string[] preferredNames)
|
||
{
|
||
for (int i = 0; i < preferredNames.Length; i++)
|
||
{
|
||
if (Enum.TryParse(preferredNames[i], true, out TextureImporterFormat format))
|
||
{
|
||
return format;
|
||
}
|
||
}
|
||
|
||
// Automatic
|
||
return (TextureImporterFormat)(-1);
|
||
}
|
||
|
||
private static bool TryEstimateTransparentRatio(string assetPath, float alphaCutoff, out float ratio, out string note)
|
||
{
|
||
ratio = -1f;
|
||
note = string.Empty;
|
||
|
||
string ext = Path.GetExtension(assetPath).ToLowerInvariant();
|
||
if (ext != ".png" && ext != ".tga" && ext != ".psd" && ext != ".tif" && ext != ".tiff")
|
||
{
|
||
note = "格式不支持透明采样";
|
||
return false;
|
||
}
|
||
|
||
string fullPath = Path.GetFullPath(assetPath);
|
||
if (!File.Exists(fullPath))
|
||
{
|
||
note = "文件不存在";
|
||
return false;
|
||
}
|
||
|
||
Texture2D tmp = null;
|
||
try
|
||
{
|
||
var bytes = File.ReadAllBytes(fullPath);
|
||
tmp = new Texture2D(2, 2, TextureFormat.RGBA32, false);
|
||
if (!tmp.LoadImage(bytes, false))
|
||
{
|
||
note = "LoadImage失败";
|
||
return false;
|
||
}
|
||
|
||
var pixels = tmp.GetPixels32();
|
||
if (pixels == null || pixels.Length == 0)
|
||
{
|
||
note = "像素读取失败";
|
||
return false;
|
||
}
|
||
|
||
int step = Mathf.Max(1, pixels.Length / 200000); // 最多采样约20万像素
|
||
int sample = 0;
|
||
int transparent = 0;
|
||
byte cutoff = (byte)Mathf.Clamp(Mathf.RoundToInt(alphaCutoff * 255f), 0, 255);
|
||
for (int i = 0; i < pixels.Length; i += step)
|
||
{
|
||
sample++;
|
||
if (pixels[i].a <= cutoff) transparent++;
|
||
}
|
||
|
||
ratio = sample > 0 ? transparent / (float)sample : 0f;
|
||
return true;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
note = e.Message;
|
||
return false;
|
||
}
|
||
finally
|
||
{
|
||
if (tmp != null)
|
||
{
|
||
DestroyImmediate(tmp);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void ApplyOptimizationToSelected()
|
||
{
|
||
var selected = _items.Where(x => x.Selected).ToList();
|
||
if (selected.Count == 0)
|
||
{
|
||
EditorUtility.DisplayDialog("提示", "没有勾选任何条目。", "确定");
|
||
return;
|
||
}
|
||
|
||
int changed = 0;
|
||
try
|
||
{
|
||
for (int i = 0; i < selected.Count; i++)
|
||
{
|
||
var it = selected[i];
|
||
if (EditorUtility.DisplayCancelableProgressBar("应用贴图优化", $"处理中 {i + 1}/{selected.Count}", (float)i / selected.Count))
|
||
{
|
||
break;
|
||
}
|
||
|
||
var importer = AssetImporter.GetAtPath(it.AssetPath) as TextureImporter;
|
||
if (importer == null) continue;
|
||
|
||
bool dirty = false;
|
||
if (_applyTextureCompression && importer.textureCompression != _targetTextureCompression)
|
||
{
|
||
importer.textureCompression = _targetTextureCompression;
|
||
dirty = true;
|
||
}
|
||
|
||
if (_applyDisableReadWrite && importer.isReadable)
|
||
{
|
||
importer.isReadable = false;
|
||
dirty = true;
|
||
}
|
||
|
||
if (_applyDisableSpriteMipmap && importer.textureType == TextureImporterType.Sprite && importer.mipmapEnabled)
|
||
{
|
||
importer.mipmapEnabled = false;
|
||
dirty = true;
|
||
}
|
||
|
||
if (_applyStandaloneOverride || _applyFormatByAlpha || _applyMaxSize || _applyCrunch || _applyCompressionQuality)
|
||
{
|
||
var ps = importer.GetPlatformTextureSettings(StandalonePlatformName);
|
||
ps.name = StandalonePlatformName;
|
||
|
||
if (_applyStandaloneOverride && !ps.overridden)
|
||
{
|
||
ps.overridden = true;
|
||
dirty = true;
|
||
}
|
||
|
||
if (_applyMaxSize)
|
||
{
|
||
int newMax = _targetMaxSize;
|
||
if (_applyOnlyShrinkMaxSize && ps.maxTextureSize > 0)
|
||
{
|
||
newMax = Mathf.Min(ps.maxTextureSize, _targetMaxSize);
|
||
}
|
||
newMax = Mathf.Clamp(newMax, 32, 8192);
|
||
if (ps.maxTextureSize != newMax)
|
||
{
|
||
ps.maxTextureSize = newMax;
|
||
dirty = true;
|
||
}
|
||
}
|
||
|
||
if (_applyFormatByAlpha)
|
||
{
|
||
var target = importer.DoesSourceTextureHaveAlpha() ? _targetFormatAlpha : _targetFormatOpaque;
|
||
if (ps.format != target)
|
||
{
|
||
ps.format = target;
|
||
dirty = true;
|
||
}
|
||
}
|
||
|
||
if (_applyCrunch && ps.crunchedCompression != _targetEnableCrunch)
|
||
{
|
||
ps.crunchedCompression = _targetEnableCrunch;
|
||
dirty = true;
|
||
}
|
||
|
||
if (_applyCompressionQuality)
|
||
{
|
||
int q = Mathf.Clamp(_targetCompressionQuality, 0, 100);
|
||
if (ps.compressionQuality != q)
|
||
{
|
||
ps.compressionQuality = q;
|
||
dirty = true;
|
||
}
|
||
}
|
||
|
||
if (dirty)
|
||
{
|
||
importer.SetPlatformTextureSettings(ps);
|
||
}
|
||
}
|
||
|
||
if (dirty)
|
||
{
|
||
importer.SaveAndReimport();
|
||
changed++;
|
||
}
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
EditorUtility.ClearProgressBar();
|
||
AssetDatabase.Refresh();
|
||
}
|
||
|
||
EditorUtility.DisplayDialog("完成", $"已更新 {changed} 个贴图导入设置。", "确定");
|
||
ScanTextures();
|
||
}
|
||
|
||
private void SelectTrimCandidates()
|
||
{
|
||
int count = 0;
|
||
for (int i = 0; i < _items.Count; i++)
|
||
{
|
||
_items[i].IsTrimCandidate = IsAutoTrimCandidate(_items[i], out string reason);
|
||
_items[i].TrimCandidateReason = reason;
|
||
_items[i].Selected = _items[i].IsTrimCandidate;
|
||
if (_items[i].Selected) count++;
|
||
}
|
||
InvalidateFilterCache();
|
||
EditorUtility.DisplayDialog("完成", $"已勾选 {count} 个高透明大图候选。", "确定");
|
||
}
|
||
|
||
private void TrimTransparentBordersForSelected()
|
||
{
|
||
var selected = _items.Where(x => x.Selected).ToList();
|
||
if (selected.Count == 0)
|
||
{
|
||
EditorUtility.DisplayDialog("提示", "没有勾选任何条目。", "确定");
|
||
return;
|
||
}
|
||
|
||
if (!EditorUtility.DisplayDialog(
|
||
"确认自动裁切",
|
||
$"将尝试对 {selected.Count} 张已勾选贴图执行透明边裁切(仅PNG)。\n" +
|
||
"这会改动源图片尺寸,可能影响UI布局/锚点。\n" +
|
||
"建议先处理一小批验证。",
|
||
"继续", "取消"))
|
||
{
|
||
return;
|
||
}
|
||
|
||
int processed = 0;
|
||
int trimmed = 0;
|
||
int skipped = 0;
|
||
long savedBytes = 0;
|
||
var logs = new List<string>();
|
||
|
||
try
|
||
{
|
||
for (int i = 0; i < selected.Count; i++)
|
||
{
|
||
var item = selected[i];
|
||
if (EditorUtility.DisplayCancelableProgressBar("自动裁切透明边", $"处理中 {i + 1}/{selected.Count}", (float)i / selected.Count))
|
||
{
|
||
break;
|
||
}
|
||
|
||
processed++;
|
||
if (TryTrimPngTransparentBorder(item.AssetPath, _trimAlphaCutoff, _trimCreateBackup, _trimSkipSpriteMultiple,
|
||
out long beforeBytes, out long afterBytes, out string message))
|
||
{
|
||
trimmed++;
|
||
savedBytes += Math.Max(0, beforeBytes - afterBytes);
|
||
}
|
||
else
|
||
{
|
||
skipped++;
|
||
logs.Add($"{item.AssetPath} -> {message}");
|
||
}
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
EditorUtility.ClearProgressBar();
|
||
AssetDatabase.Refresh();
|
||
}
|
||
|
||
if (logs.Count > 0)
|
||
{
|
||
Debug.LogWarning("[ResourceTextureOptimizerWindow] 部分贴图未裁切:\n" + string.Join("\n", logs.Take(30)));
|
||
}
|
||
|
||
EditorUtility.DisplayDialog(
|
||
"裁切完成",
|
||
$"处理: {processed}\n成功裁切: {trimmed}\n跳过/失败: {skipped}\n磁盘减少约: {savedBytes / (1024f * 1024f):F2}MB",
|
||
"确定");
|
||
|
||
ScanTextures();
|
||
}
|
||
|
||
private static bool TryTrimPngTransparentBorder(
|
||
string assetPath,
|
||
float alphaCutoff,
|
||
bool createBackup,
|
||
bool skipSpriteMultiple,
|
||
out long beforeBytes,
|
||
out long afterBytes,
|
||
out string message)
|
||
{
|
||
beforeBytes = 0;
|
||
afterBytes = 0;
|
||
message = string.Empty;
|
||
|
||
if (!assetPath.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
message = "仅支持PNG";
|
||
return false;
|
||
}
|
||
|
||
var importer = AssetImporter.GetAtPath(assetPath) as TextureImporter;
|
||
if (importer == null)
|
||
{
|
||
message = "TextureImporter为空";
|
||
return false;
|
||
}
|
||
|
||
if (skipSpriteMultiple &&
|
||
importer.textureType == TextureImporterType.Sprite &&
|
||
importer.spriteImportMode == SpriteImportMode.Multiple)
|
||
{
|
||
message = "SpriteMultiple已跳过";
|
||
return false;
|
||
}
|
||
|
||
string fullPath = Path.GetFullPath(assetPath);
|
||
if (!File.Exists(fullPath))
|
||
{
|
||
message = "源文件不存在";
|
||
return false;
|
||
}
|
||
|
||
byte[] srcBytes = File.ReadAllBytes(fullPath);
|
||
beforeBytes = srcBytes.Length;
|
||
|
||
Texture2D srcTex = null;
|
||
Texture2D dstTex = null;
|
||
|
||
try
|
||
{
|
||
srcTex = new Texture2D(2, 2, TextureFormat.RGBA32, false);
|
||
if (!srcTex.LoadImage(srcBytes, false))
|
||
{
|
||
message = "LoadImage失败";
|
||
return false;
|
||
}
|
||
|
||
int width = srcTex.width;
|
||
int height = srcTex.height;
|
||
var pixels = srcTex.GetPixels32();
|
||
if (pixels == null || pixels.Length == 0)
|
||
{
|
||
message = "读取像素失败";
|
||
return false;
|
||
}
|
||
|
||
byte cutoff = (byte)Mathf.Clamp(Mathf.RoundToInt(alphaCutoff * 255f), 0, 255);
|
||
int minX = width;
|
||
int minY = height;
|
||
int maxX = -1;
|
||
int maxY = -1;
|
||
|
||
for (int y = 0; y < height; y++)
|
||
{
|
||
int row = y * width;
|
||
for (int x = 0; x < width; x++)
|
||
{
|
||
if (pixels[row + x].a > cutoff)
|
||
{
|
||
if (x < minX) minX = x;
|
||
if (y < minY) minY = y;
|
||
if (x > maxX) maxX = x;
|
||
if (y > maxY) maxY = y;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (maxX < minX || maxY < minY)
|
||
{
|
||
message = "全透明,跳过";
|
||
return false;
|
||
}
|
||
|
||
if (minX == 0 && minY == 0 && maxX == width - 1 && maxY == height - 1)
|
||
{
|
||
message = "无可裁切透明边";
|
||
return false;
|
||
}
|
||
|
||
int newWidth = maxX - minX + 1;
|
||
int newHeight = maxY - minY + 1;
|
||
var cropped = srcTex.GetPixels(minX, minY, newWidth, newHeight);
|
||
dstTex = new Texture2D(newWidth, newHeight, TextureFormat.RGBA32, false);
|
||
dstTex.SetPixels(cropped);
|
||
dstTex.Apply(false, false);
|
||
byte[] dstBytes = dstTex.EncodeToPNG();
|
||
afterBytes = dstBytes.Length;
|
||
|
||
if (createBackup)
|
||
{
|
||
string backupPath = fullPath + ".trimbak";
|
||
if (!File.Exists(backupPath))
|
||
{
|
||
File.Copy(fullPath, backupPath, false);
|
||
}
|
||
}
|
||
|
||
File.WriteAllBytes(fullPath, dstBytes);
|
||
|
||
if (importer.textureType == TextureImporterType.Sprite &&
|
||
importer.spriteImportMode == SpriteImportMode.Single)
|
||
{
|
||
Vector2 oldPivot = importer.spritePivot;
|
||
float oldPivotX = oldPivot.x * width;
|
||
float oldPivotY = oldPivot.y * height;
|
||
float newPivotX = Mathf.Clamp(oldPivotX - minX, 0, newWidth);
|
||
float newPivotY = Mathf.Clamp(oldPivotY - minY, 0, newHeight);
|
||
var tis = new TextureImporterSettings();
|
||
importer.ReadTextureSettings(tis);
|
||
tis.spriteAlignment = (int)SpriteAlignment.Custom;
|
||
importer.SetTextureSettings(tis);
|
||
importer.spritePivot = new Vector2(newPivotX / newWidth, newPivotY / newHeight);
|
||
}
|
||
|
||
importer.SaveAndReimport();
|
||
message = $"裁切成功 {width}x{height} -> {newWidth}x{newHeight}";
|
||
return true;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
message = e.Message;
|
||
return false;
|
||
}
|
||
finally
|
||
{
|
||
if (srcTex != null) DestroyImmediate(srcTex);
|
||
if (dstTex != null) DestroyImmediate(dstTex);
|
||
}
|
||
}
|
||
|
||
private static string BuildIssueText(TextureOptimizeIssue issues)
|
||
{
|
||
if (issues == TextureOptimizeIssue.None) return "无";
|
||
var list = new List<string>();
|
||
if ((issues & TextureOptimizeIssue.CompressionRisk) != 0) list.Add("压缩格式风险");
|
||
if ((issues & TextureOptimizeIssue.TooLarge) != 0) list.Add("最长边超阈值");
|
||
if ((issues & TextureOptimizeIssue.TooManyPixels) != 0) list.Add("像素总量过大");
|
||
if ((issues & TextureOptimizeIssue.TooTransparent) != 0) list.Add("透明区域过多");
|
||
if ((issues & TextureOptimizeIssue.ReadWriteEnabled) != 0) list.Add("Read/Write开启");
|
||
if ((issues & TextureOptimizeIssue.SpriteMipmapEnabled) != 0) list.Add("Sprite开MipMap");
|
||
if ((issues & TextureOptimizeIssue.NoStandaloneOverride) != 0) list.Add("未覆盖Standalone平台");
|
||
return string.Join(" | ", list);
|
||
}
|
||
|
||
private void SetSelection(bool value)
|
||
{
|
||
foreach (var item in _items) item.Selected = value;
|
||
InvalidateFilterCache();
|
||
}
|
||
|
||
private void InvertSelection()
|
||
{
|
||
foreach (var item in _items) item.Selected = !item.Selected;
|
||
InvalidateFilterCache();
|
||
}
|
||
|
||
private void SelectCheckedInProject()
|
||
{
|
||
var objs = new List<UnityEngine.Object>();
|
||
foreach (var item in _items.Where(x => x.Selected))
|
||
{
|
||
var obj = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(item.AssetPath);
|
||
if (obj != null) objs.Add(obj);
|
||
}
|
||
Selection.objects = objs.ToArray();
|
||
}
|
||
|
||
private void ExportCsv()
|
||
{
|
||
if (_items.Count == 0)
|
||
{
|
||
EditorUtility.DisplayDialog("提示", "当前没有可导出的结果。", "确定");
|
||
return;
|
||
}
|
||
|
||
string defaultName = $"TextureOptimizeReport_{DateTime.Now:yyyyMMdd_HHmmss}.csv";
|
||
string path = EditorUtility.SaveFilePanel("导出CSV", Application.dataPath, defaultName, "csv");
|
||
if (string.IsNullOrEmpty(path))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var sb = new StringBuilder();
|
||
sb.AppendLine("Selected,AssetPath,Width,Height,FileSizeMB,EstimatedRawMB,HasAlpha,TransparentRatio,TextureCompression,StandaloneOverridden,StandaloneFormat,StandaloneMaxSize,Readable,SpriteMipmap,Issues");
|
||
foreach (var item in _items)
|
||
{
|
||
string transparent = item.TransparentRatio >= 0f ? item.TransparentRatio.ToString("F4", CultureInfo.InvariantCulture) : "";
|
||
sb.Append(item.Selected ? "1" : "0").Append(",");
|
||
sb.Append(Csv(item.AssetPath)).Append(",");
|
||
sb.Append(item.SourceWidth).Append(",");
|
||
sb.Append(item.SourceHeight).Append(",");
|
||
sb.Append(item.FileSizeMB.ToString("F3", CultureInfo.InvariantCulture)).Append(",");
|
||
sb.Append(item.EstimatedRawMB.ToString("F3", CultureInfo.InvariantCulture)).Append(",");
|
||
sb.Append(item.HasAlpha ? "1" : "0").Append(",");
|
||
sb.Append(transparent).Append(",");
|
||
sb.Append(item.TextureCompression).Append(",");
|
||
sb.Append(item.StandaloneOverridden ? "1" : "0").Append(",");
|
||
sb.Append(item.StandaloneFormat).Append(",");
|
||
sb.Append(item.StandaloneMaxSize).Append(",");
|
||
sb.Append(item.IsReadable ? "1" : "0").Append(",");
|
||
sb.Append(item.IsSprite && item.MipmapEnabled ? "1" : "0").Append(",");
|
||
sb.Append(Csv(item.IssueText)).AppendLine();
|
||
}
|
||
|
||
File.WriteAllText(path, sb.ToString(), Encoding.UTF8);
|
||
Debug.Log($"[ResourceTextureOptimizerWindow] Report exported: {path}");
|
||
EditorUtility.RevealInFinder(path);
|
||
}
|
||
|
||
private static string Csv(string value)
|
||
{
|
||
if (string.IsNullOrEmpty(value)) return "\"\"";
|
||
return "\"" + value.Replace("\"", "\"\"") + "\"";
|
||
}
|
||
}
|
||
|