2025-11-05 17:34:40 +08:00

736 lines
26 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using TexturePacker;
using Unity.Mathematics;
using UnityEditor;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering;
using UnityEngine.UIElements;
public sealed class TexturePackerEditor : EditorWindow
{
[SerializeField]
private VisualTreeAsset m_VisualTreeAsset = default;
private TextureMapInfo textureMapInfo;
private Texture2D textureAtlas;
private Texture2DArray textureArray;
private IMGUIContainer imageListContainer;
private List<Texture2D> selectedTexs = new();
private List<Rect> texRects = new();
private Vector2 cellSize = new Vector2(100, 100);
private const int MaxArraySize = 255;
private int selectIndex = -1;
private int selectIndexForMove = -1;
private Vector2 toolbarOffset = new Vector2(0, 35);
private int atlasPadding = 0;
private static Func<UnityEngine.Object, bool, EditorWindow> fnOpenPropertyEditor;
private int textureSize = 512;
private EditorWindow atlasPropertiesWindow = null;
private EditorWindow arrayPropertiesWindow = null;
private Label imgCntLabel = null;
private Label infoLabel = null;
private GraphicsFormat graphicsFormat = GraphicsFormat.R32G32B32A32_SFloat;
public enum ETextureSize
{
_128 = 1 << 7,
_256 = 1 << 8,
_512 = 1 << 9,
_1024 = 1 << 10,
}
static TexturePackerEditor()
{
var editorCore = AppDomain.CurrentDomain.GetAssemblies().Where(a => a.GetName().Name.Equals("UnityEditor.CoreModule")).FirstOrDefault();
var type = editorCore.GetType("UnityEditor.PropertyEditor");
var methodInfo = type.GetMethod("OpenPropertyEditor", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance, null, new Type[] { typeof(UnityEngine.Object), typeof(bool) }, null);
fnOpenPropertyEditor = methodInfo.CreateDelegate(typeof(Func<UnityEngine.Object, bool, EditorWindow>)) as Func<UnityEngine.Object, bool, EditorWindow>;
}
[MenuItem("Tools/Performance/TexturePackerEditor")]
public static void OpenTexturePackerEditor()
{
TexturePackerEditor wnd = GetWindow<TexturePackerEditor>();
wnd.titleContent = new GUIContent("TexturePackerEditor");
}
[UnityEditor.Callbacks.OnOpenAsset(0)]
private static bool OnOpenTextureMapInfo(int instanceID, int line)
{
var textureMapInfo = EditorUtility.InstanceIDToObject(instanceID) as TextureMapInfo;
if(!textureMapInfo)
{
return false;
}
TexturePackerEditor wnd = GetWindow<TexturePackerEditor>();
wnd.titleContent = new GUIContent("TexturePackerEditor");
wnd.InitWithTextureMapInfo(textureMapInfo);
EditorApplication.delayCall += () =>
{
wnd.UpdateResult();
};
return false;
}
private static readonly Color btNormalColor = ColorUtils.ToRGBA(0xFF585858);
private static readonly Color btClikedColor = ColorUtils.ToRGBA(0xFF46607C);
public void CreateGUI()
{
// Each editor window contains a root VisualElement object
VisualElement root = rootVisualElement;
// Instantiate UXML
VisualElement labelFromUXML = m_VisualTreeAsset.Instantiate();
root.Add(labelFromUXML);
var btArray = root.Q<Button>("btArray");
var btAtlas = root.Q<Button>("btAtlas");
var btSave = root.Q<Button>("btSave");
infoLabel = root.Q<Label>("infoLabel");
var slAtlasPadding = root.Q<SliderInt>("slAtlasPadding");
var textureSizeEnum = root.Q<EnumField>("textureSizeEnum");
imgCntLabel = root.Q<Label>("imgCntLabel");
root.Q<Toggle>("tgIsNormal").RegisterValueChangedCallback(b =>
{
textureMapInfo.IsNormalMap = b.newValue;
SetNormalOriginTextureFormat(!textureMapInfo.IsNormalMap);
UpdateResult();
});
textureSizeEnum.RegisterCallback<ChangeEvent<Enum>>((e) =>
{
var newTextureSize = (int)(ETextureSize)e.newValue;
if(textureSize != newTextureSize)
{
textureSize = newTextureSize;
UpdateResult();
}
});
btArray.clicked += () =>
{
btArray.style.backgroundColor = btClikedColor;
btAtlas.style.backgroundColor = btNormalColor;
textureMapInfo.PackType = TextureMapInfo.EPackType.Array;
slAtlasPadding.visible = false;
UpdateResult();
};
btAtlas.clicked += () =>
{
btAtlas.style.backgroundColor = btClikedColor;
btArray.style.backgroundColor = btNormalColor;
textureMapInfo.PackType = TextureMapInfo.EPackType.Atlas;
slAtlasPadding.visible = true;
UpdateResult();
};
btSave.clicked += Save;
btAtlas.style.backgroundColor = btClikedColor;
slAtlasPadding.RegisterCallback<ChangeEvent<int>>((i) =>
{
atlasPadding = i.newValue;
UpdateResult();
});
imageListContainer = root.Q<IMGUIContainer>("imgList");
imageListContainer.onGUIHandler = OnImageListGUI;
imageListContainer.RegisterCallback<MouseDownEvent>((e) =>
{
var mousePos = Event.current.mousePosition - toolbarOffset;
for (int i = 0; i < texRects.Count; i++)
{
if (texRects[i].Contains(mousePos))
{
selectIndexForMove = i;
break;
}
}
});
imageListContainer.RegisterCallback<MouseUpEvent>((e) =>
{
var swpaIndex = -1;
var mousePos = Event.current.mousePosition - toolbarOffset;
for (int i = 0; i < texRects.Count; i++)
{
if (texRects[i].Contains(mousePos))
{
swpaIndex = i;
break;
}
}
if (swpaIndex != -1 && selectIndexForMove != -1)
{
if (selectIndexForMove != swpaIndex)
{
(selectedTexs[selectIndexForMove], selectedTexs[swpaIndex]) = (selectedTexs[swpaIndex], selectedTexs[selectIndexForMove]);
(texRects[selectIndexForMove], texRects[swpaIndex]) = (texRects[swpaIndex], texRects[selectIndexForMove]);
UpdateResult();
}
}
selectIndexForMove = -1;
});
}
private void SetNormalOriginTextureFormat(bool toNormal)
{
foreach (var item in selectedTexs)
{
var ti = TextureImporter.GetAtPath(AssetDatabase.GetAssetPath(item)) as TextureImporter;
if(toNormal)
{
ti.textureType = TextureImporterType.NormalMap;
}
else if (ti.textureType == TextureImporterType.NormalMap)
{
ti.textureType = TextureImporterType.Default;
ti.sRGBTexture = false;
}
ti.SaveAndReimport();
}
}
private void OnImageListGUI()
{
float windowWidth = position.width;
int dynamicColumns = Mathf.FloorToInt(windowWidth / (cellSize.x + 5));
int count = selectedTexs.Count;
int currentColumn = 0;
int currentRow = 0;
var bkColor = GUI.backgroundColor;
for (int i = 0; i < count; i++)
{
if (currentColumn == 0)
{
GUILayout.BeginHorizontal();
}
var rect = EditorGUILayout.GetControlRect(GUILayout.Width(cellSize.x), GUILayout.Height(cellSize.y));
texRects[i] = rect;
if (i == selectIndex || selectIndexForMove != -1 && rect.Contains(Event.current.mousePosition - toolbarOffset))
{
GUI.backgroundColor = Color.blue;
}
else
{
GUI.backgroundColor = bkColor;
}
// insert ?
//EditorGUI.DrawRect
if (GUI.Button(rect, selectedTexs[i]))
{
if (selectIndex == i)
{
EditorGUIUtility.PingObject(selectedTexs[i]);
}
selectIndex = i;
}
currentColumn++;
if (currentColumn >= dynamicColumns)
{
GUILayout.EndHorizontal();
currentRow++;
currentColumn = 0;
}
}
GUI.backgroundColor = bkColor;
if (selectIndexForMove != -1)
{
GUI.Button(new Rect(Event.current.mousePosition, cellSize), selectedTexs[selectIndexForMove]);
}
if (selectIndex != -1)
{
infoLabel.visible = true;
var tex = selectedTexs[selectIndex];
var tinfo = textureMapInfo.OriginTexture2TextureInfo[tex];
if (textureMapInfo.PackType == TextureMapInfo.EPackType.Array)
{
infoLabel.text = $"{tex.name} array index:{tinfo.ArrayIndex}";
}
else
{
infoLabel.text = $"{tex.name} atlas offset:{tinfo.Offset}";
}
}
else
{
infoLabel.visible = false;
}
if (currentColumn != 0)
{
GUILayout.EndHorizontal();
}
}
private void InitWithTextureMapInfo(TextureMapInfo textureMapInfo)
{
this.textureMapInfo = textureMapInfo;
VisualElement root = rootVisualElement;
var btArray = root.Q<Button>("btArray");
var btAtlas = root.Q<Button>("btAtlas");
var slAtlasPadding = root.Q<SliderInt>("slAtlasPadding");
if (textureMapInfo.PackType == TextureMapInfo.EPackType.Atlas)
{
btArray.style.backgroundColor = btNormalColor;
btAtlas.style.backgroundColor = btClikedColor;
slAtlasPadding.visible = true;
}
else
{
btArray.style.backgroundColor = btClikedColor;
btAtlas.style.backgroundColor = btNormalColor;
slAtlasPadding.visible = false;
}
if(textureMapInfo.IsNormalMap)
{
root.Q<Toggle>("tgIsNormal").value = true;
SetNormalOriginTextureFormat(!textureMapInfo.IsNormalMap);
}
selectedTexs = textureMapInfo.Textures;
texRects = new List<Rect>(new Rect[selectedTexs.Count]);
}
private void OnGUI()
{
if (textureMapInfo == null)
{
textureMapInfo = ScriptableObject.CreateInstance<TextureMapInfo>();
}
switch (Event.current.type)
{
case EventType.MouseDrag:
break;
case EventType.MouseDown:
selectIndex = -1;
Repaint();
break;
case EventType.KeyDown:
break;
case EventType.KeyUp:
if (Event.current.modifiers == EventModifiers.Control && Event.current.keyCode == KeyCode.S)
{
Save();
}
if (Event.current.keyCode == KeyCode.Delete)
{
if (selectIndex != -1)
{
RmTexture(selectedTexs[selectIndex]);
selectIndex = -1;
}
}
break;
case EventType.DragUpdated:
{
for (var i = 0; i < DragAndDrop.objectReferences.Length; i++)
{
if (DragAndDrop.objectReferences[i] is Texture)
{
DragAndDrop.visualMode = DragAndDropVisualMode.Generic;
break;
}
}
break;
}
case EventType.DragExited:
{
for (var i = 0; i < DragAndDrop.objectReferences.Length; i++)
{
var tex = DragAndDrop.objectReferences[i] as Texture2D;
if (tex)
{
AddTexture(tex);
}
}
}
break;
case EventType.ContextClick:
break;
case EventType.MouseEnterWindow:
break;
case EventType.MouseLeaveWindow:
break;
default:
break;
}
}
private void UpdateResult(Texture2D result, int cellCnt)
{
result.Apply();
var scale = (float)textureSize / result.width;
for (int i = 0; i < selectedTexs.Count; i++)
{
var tex = selectedTexs[i];
if (tex.width != textureSize)
{
Debug.LogWarning($"{tex.name} 尺寸错误: {tex.width} 正确值: {textureSize}");
}
int cellx = i % cellCnt;
int celly = i / cellCnt;
var mip = 0;
if (tex.width > textureSize)
{
mip = (int)Mathf.Log(2, tex.width / textureSize);
}
{
var temp = new Texture2D(textureSize, textureSize, graphicsFormat, 0, TextureCreationFlags.None);
Graphics.ConvertTexture(tex, temp);
var offsetx = cellx * (textureSize + atlasPadding);
var offsety = celly * (textureSize + atlasPadding);
Graphics.CopyTexture(temp, 0, 0, 0, 0, textureSize, textureSize, result, 0, 0, offsetx, offsety);
GameObject.DestroyImmediate(temp);
textureMapInfo.AtlasScale = scale;
textureMapInfo.OriginTexture2TextureInfo[tex] = new TextureMapInfo.TextureInfo()
{
OriginTexture = tex,
Offset = new float2((float)offsetx / result.width, (float)offsety / result.height),
};
}
}
}
private void UpdateResult(Texture2DArray result)
{
for (int i = 0; i < selectedTexs.Count; i++)
{
var tex = selectedTexs[i];
if (tex.width != textureSize)
{
Debug.LogWarning($"{tex.name} 尺寸错误: {tex.width} 正确值: {textureSize}");
}
Graphics.ConvertTexture(tex, 0, result, i);
textureMapInfo.OriginTexture2TextureInfo[tex] = new TextureMapInfo.TextureInfo()
{
OriginTexture = tex,
ArrayIndex = i,
};
}
}
private void UpdateResult()
{
if(selectedTexs.Count < 2)
{
return;
}
if (textureMapInfo.PackType == TextureMapInfo.EPackType.Atlas)
{
arrayPropertiesWindow?.Close();
arrayPropertiesWindow = null;
var cellCnt = Mathf.CeilToInt(Mathf.Sqrt(selectedTexs.Count));
var atlasPaddedSize = cellCnt * textureSize + (cellCnt - 1) * atlasPadding;
if (textureAtlas == null || textureAtlas.width != atlasPaddedSize)
{
if (textureAtlas != null)
{
GameObject.DestroyImmediate(textureAtlas, false);
}
textureAtlas = new Texture2D(atlasPaddedSize, atlasPaddedSize,
graphicsFormat, (int)Mathf.Log(2, atlasPaddedSize) + 1, TextureCreationFlags.None)
{
wrapMode = TextureWrapMode.Clamp,
};
atlasPropertiesWindow?.Close();
atlasPropertiesWindow = null;
}
UpdateResult(textureAtlas, cellCnt);
if (!atlasPropertiesWindow)
{
atlasPropertiesWindow = fnOpenPropertyEditor(textureAtlas, true);
try
{
this.Dock(atlasPropertiesWindow, Docker.DockPosition.Right);
}
catch
{
}
}
atlasPropertiesWindow.Repaint();
}
else
{
atlasPropertiesWindow?.Close();
atlasPropertiesWindow = null;
if (textureArray == null || textureArray.depth != selectedTexs.Count || textureArray.width != textureSize)
{
if (textureArray != null)
{
GameObject.DestroyImmediate(textureArray, false);
}
textureArray = new Texture2DArray(textureSize,
textureSize, selectedTexs.Count, graphicsFormat, TextureCreationFlags.None, 12)
{
wrapMode = TextureWrapMode.Clamp,
};
arrayPropertiesWindow?.Close();
arrayPropertiesWindow = null;
}
UpdateResult(textureArray);
if (!arrayPropertiesWindow)
{
arrayPropertiesWindow = fnOpenPropertyEditor(textureArray, true);
this.Dock(arrayPropertiesWindow, Docker.DockPosition.Right);
}
arrayPropertiesWindow.Repaint();
}
}
private void AddTexture(Texture2D texture2D)
{
if(TextureMapIndex.Instance.ResultGuid2TextureMap.TryGetValue(AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(texture2D)), out var mapInfo))
{
selectedTexs.Clear();
textureMapInfo = mapInfo;
InitWithTextureMapInfo(textureMapInfo);
}
if (texture2D.width % textureSize != 0)
{
EditorUtility.DisplayDialog("提示", $"贴图 {texture2D.name} 尺寸错误", "ok");
return;
}
if(selectedTexs.Count == MaxArraySize)
{
EditorUtility.DisplayDialog("提示", $"贴图数量最多 {MaxArraySize}", "ok");
return;
}
if (!selectedTexs.Contains(texture2D))
{
selectedTexs.Add(texture2D);
texRects.Add(new Rect());
}
else
{
ToastManager.ShowNotification(new Toast.ToastArgs()
{
Title = "Info",
Message = "当前图片已存在",
LifeTime = 2f,
Severity = Toast.ToastSeverity.Info,
ToastPosition = Toast.ToastPosition.TopCenter
});
return;
}
imgCntLabel.text = selectedTexs.Count.ToString();
UpdateResult();
}
private void RmTexture(Texture2D texture2D)
{
var idx = selectedTexs.IndexOf(texture2D);
if (idx != -1)
{
texRects.RemoveAt(idx);
selectedTexs.RemoveAt(idx);
}
imgCntLabel.text = selectedTexs.Count.ToString();
UpdateResult();
}
private void UpdateMapinfoBeforeSave()
{
textureMapInfo.Textures = selectedTexs;
var hasAlpha = GraphicsFormatUtility.HasAlphaChannel(selectedTexs[0].graphicsFormat);
for (int i = 1; i < selectedTexs.Count; i++)
{
var tex = selectedTexs[i];
if (hasAlpha != GraphicsFormatUtility.HasAlphaChannel(tex.graphicsFormat))
{
if(!EditorUtility.DisplayDialog("警告", $"源贴图 alpha 不一致 :{tex.name}", "继续", "取消"))
{
return;
}
}
}
var rmLs = textureMapInfo.OriginTexture2TextureInfo.Keys.Where(t => !selectedTexs.Contains(t)).ToList();
foreach (var rm in rmLs)
{
textureMapInfo.OriginTexture2TextureInfo.Remove(rm);
}
if (textureMapInfo.PackType == TextureMapInfo.EPackType.Atlas)
{
GameObject.DestroyImmediate(textureArray);
textureArray = null;
textureMapInfo.TextureArray = null;
if (string.IsNullOrEmpty(textureMapInfo.TextureAtlasPath))
{
var savePath = EditorUtility.SaveFilePanelInProject("保存", "", "png", "");
if (string.IsNullOrEmpty(savePath))
{
return;
}
textureMapInfo.TextureAtlasPath = savePath;
}
AsyncGPUReadback.Request(textureAtlas, 0, (req) =>
{
if (!req.hasError)
{
{
using var dataArray = ImageConversion.EncodeNativeArrayToPNG(req.GetData<byte>(), textureAtlas.graphicsFormat, (uint)textureAtlas.width, (uint)textureAtlas.height);
using var fr = File.OpenWrite(textureMapInfo.TextureAtlasPath);
fr.Write(dataArray);
}
AssetDatabase.ImportAsset(textureMapInfo.TextureAtlasPath);
if(TextureImporter.GetAtPath(textureMapInfo.TextureAtlasPath) is TextureImporter ti)
{
ti.SetTextureSettings(new TextureImporterSettings()
{
textureShape = TextureImporterShape.Texture2D,
flipbookColumns = 1,
flipbookRows = 1,
sRGBTexture = false,
readable = false,
mipmapEnabled = true,
wrapMode = TextureWrapMode.Clamp,
});
ti.SaveAndReimport();
}
EditorApplication.delayCall += () =>
{
textureMapInfo.TextureAtlas = AssetDatabase.LoadAssetAtPath<Texture2D>(textureMapInfo.TextureAtlasPath);
var guid = AssetDatabase.AssetPathToGUID(textureMapInfo.TextureAtlasPath);
TextureMapIndex.Instance.ResultGuid2TextureMap[guid] = textureMapInfo;
EditorGUIUtility.PingObject(textureMapInfo.TextureAtlas);
};
}
});
}
else
{
GameObject.DestroyImmediate(textureAtlas);
textureAtlas = null;
textureMapInfo.TextureAtlas = null;
if (string.IsNullOrEmpty(textureMapInfo.TextureArrayPath))
{
var savePath = EditorUtility.SaveFilePanelInProject("保存", "", "png", "");
if (string.IsNullOrEmpty(savePath))
{
return;
}
textureMapInfo.TextureArrayPath = savePath;
}
AsyncGPUReadback.Request(textureArray, 0, 0, textureArray.width, 0, textureArray.height, 0, textureArray.depth, (req) =>
{
if (!req.hasError)
{
{
using var fr = File.Open(textureMapInfo.TextureArrayPath, FileMode.OpenOrCreate);
using MemoryStream memoryStream = new MemoryStream();
for (int i = textureArray.depth - 1; i >=0 ; --i)
{
var array = req.GetData<byte>(i);
memoryStream.Write(array);
}
memoryStream.Seek(0, SeekOrigin.Begin);
var dataArray = ImageConversion.EncodeArrayToPNG(memoryStream.GetBuffer(), textureArray.graphicsFormat, (uint)textureArray.width, (uint)textureArray.height * (uint)textureArray.depth);
fr.Write(dataArray);
}
AssetDatabase.ImportAsset(textureMapInfo.TextureArrayPath);
if (TextureImporter.GetAtPath(textureMapInfo.TextureArrayPath) is TextureImporter ti)
{
ti.SetTextureSettings(new TextureImporterSettings()
{
textureShape = TextureImporterShape.Texture2DArray,
flipbookColumns = 1,
flipbookRows = textureArray.depth,
sRGBTexture = false,
readable = false,
mipmapEnabled = true,
wrapMode = TextureWrapMode.Clamp,
});
ti.SaveAndReimport();
}
textureMapInfo.TextureArray = AssetDatabase.LoadAssetAtPath<Texture2DArray>(textureMapInfo.TextureArrayPath);
var guid = AssetDatabase.AssetPathToGUID(textureMapInfo.TextureArrayPath);
TextureMapIndex.Instance.ResultGuid2TextureMap[guid] = textureMapInfo;
EditorGUIUtility.PingObject(textureMapInfo.TextureArray);
}
});
}
AssetDatabase.SaveAssetIfDirty(TextureMapIndex.Instance);
}
private void Save()
{
if (AssetDatabase.IsMainAsset(textureMapInfo))
{
UpdateMapinfoBeforeSave();
AssetDatabase.SaveAssetIfDirty(textureMapInfo);
}
else
{
var path = EditorUtility.SaveFilePanelInProject("保存映射关系", DateTime.Now.ToString("yy_MM_dd_hhmmss"), "asset", "保存源贴图到 atlas 对应关系", TextureMapInfo.TextureMapDir);
if(string.IsNullOrEmpty(path))
{
return;
}
AssetDatabase.CreateAsset(textureMapInfo, path);
UpdateMapinfoBeforeSave();
AssetDatabase.SaveAssetIfDirty(textureMapInfo);
}
base.SaveChanges();
AssetDatabase.Refresh();
}
private void OnDestroy()
{
SetNormalOriginTextureFormat(textureMapInfo.IsNormalMap);
AssetDatabase.SaveAssetIfDirty(textureMapInfo);
}
}