2025-12-05 19:03:45 +08:00
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Text ;
using UnityEditor ;
using UnityEngine ;
public class UnityTexturePackerEditor : EditorWindow
{
private List < Texture2D > textures = new List < Texture2D > ( ) ;
private Vector2 scrollPos ;
private string outputFolder = "Assets/TexturePackerOutput" ;
private string atlasFileName = "atlas.png" ;
private string jsonFileName = "atlas.json" ;
private int maxAtlasSize = 4096 ;
private int padding = 2 ;
private bool overwriteExisting = true ;
private string lastJson = "" ;
private string lastAtlasPath = "" ;
2026-04-17 14:21:51 +08:00
[MenuItem("AlicizaX/Extension/Texture Packer")]
2025-12-05 19:03:45 +08:00
public static void ShowWindow ( )
{
var w = GetWindow < UnityTexturePackerEditor > ( "Texture Packer" ) ;
w . minSize = new Vector2 ( 450 , 300 ) ;
}
private void OnGUI ( )
{
GUILayout . Label ( "Texture Packer (for TMP-compatible atlases)" , EditorStyles . boldLabel ) ;
DrawDragDropArea ( ) ;
GUILayout . Space ( 6 ) ;
GUILayout . Label ( $"Textures ({textures.Count})" , EditorStyles . label ) ;
scrollPos = GUILayout . BeginScrollView ( scrollPos , GUILayout . Height ( 160 ) ) ;
for ( int i = 0 ; i < textures . Count ; i + + )
{
GUILayout . BeginHorizontal ( "box" ) ;
Texture2D tex = textures [ i ] ;
Texture preview = AssetPreview . GetAssetPreview ( tex ) ? ? tex ;
GUILayout . Label ( preview , GUILayout . Width ( 64 ) , GUILayout . Height ( 64 ) ) ;
GUILayout . BeginVertical ( ) ;
GUILayout . Label ( tex . name , EditorStyles . boldLabel ) ;
string path = AssetDatabase . GetAssetPath ( tex ) ;
GUILayout . Label ( $"{tex.width}x{tex.height} — {path}" ) ;
GUILayout . EndVertical ( ) ;
GUILayout . FlexibleSpace ( ) ;
if ( GUILayout . Button ( "▲" , GUILayout . Width ( 24 ) ) ) { if ( i > 0 ) { var t = textures [ i - 1 ] ; textures [ i - 1 ] = textures [ i ] ; textures [ i ] = t ; } }
if ( GUILayout . Button ( "▼" , GUILayout . Width ( 24 ) ) ) { if ( i < textures . Count - 1 ) { var t = textures [ i + 1 ] ; textures [ i + 1 ] = textures [ i ] ; textures [ i ] = t ; } }
if ( GUILayout . Button ( "Remove" , GUILayout . Width ( 64 ) ) ) { textures . RemoveAt ( i ) ; }
GUILayout . EndHorizontal ( ) ;
}
GUILayout . EndScrollView ( ) ;
GUILayout . Space ( 8 ) ;
GUILayout . Label ( "Output Settings" , EditorStyles . boldLabel ) ;
outputFolder = EditorGUILayout . TextField ( "Output Folder" , outputFolder ) ;
GUILayout . BeginHorizontal ( ) ;
if ( GUILayout . Button ( "Choose..." ) )
{
string folder = EditorUtility . OpenFolderPanel ( "Select output folder" , "" , "" ) ;
if ( ! string . IsNullOrEmpty ( folder ) )
{
if ( folder . StartsWith ( Application . dataPath ) )
{
outputFolder = "Assets" + folder . Substring ( Application . dataPath . Length ) ;
}
else
{
EditorUtility . DisplayDialog ( "Invalid Folder" , "Please select a folder inside this Unity project (under Assets)." , "OK" ) ;
}
}
}
GUILayout . EndHorizontal ( ) ;
atlasFileName = EditorGUILayout . TextField ( "Atlas Filename" , atlasFileName ) ;
jsonFileName = EditorGUILayout . TextField ( "JSON Filename" , jsonFileName ) ;
maxAtlasSize = EditorGUILayout . IntPopup ( "Max Atlas Size" , maxAtlasSize , new string [ ] { "1024" , "2048" , "4096" } , new int [ ] { 1024 , 2048 , 4096 } ) ;
padding = EditorGUILayout . IntField ( "Padding (px)" , padding ) ;
overwriteExisting = EditorGUILayout . Toggle ( "Overwrite Existing" , overwriteExisting ) ;
GUILayout . Space ( 6 ) ;
if ( GUILayout . Button ( "Build Atlas" , GUILayout . Height ( 30 ) ) )
{
if ( textures . Count = = 0 )
{
EditorUtility . DisplayDialog ( "No textures" , "Please drag textures into the list before building." , "OK" ) ;
}
else
{
BuildAtlas ( ) ;
}
}
GUILayout . Space ( 10 ) ;
GUILayout . Label ( "Last result" , EditorStyles . boldLabel ) ;
EditorGUILayout . SelectableLabel ( lastAtlasPath , GUILayout . Height ( 16 ) ) ;
EditorGUILayout . TextArea ( lastJson , GUILayout . Height ( 120 ) ) ;
GUILayout . FlexibleSpace ( ) ;
GUILayout . Label ( "Notes: Output atlas is created as an uncompressed PNG and imported as multiple sprites so TextMeshPro can use it." , EditorStyles . wordWrappedLabel ) ;
}
private void DrawDragDropArea ( )
{
var evt = Event . current ;
Rect dropArea = GUILayoutUtility . GetRect ( 0.0f , 80.0f , GUILayout . ExpandWidth ( true ) ) ;
GUI . Box ( dropArea , "Drag textures here (Project or Explorer)" , EditorStyles . helpBox ) ;
switch ( evt . type )
{
case EventType . DragUpdated :
case EventType . DragPerform :
if ( ! dropArea . Contains ( evt . mousePosition ) ) return ;
DragAndDrop . visualMode = DragAndDropVisualMode . Copy ;
if ( evt . type = = EventType . DragPerform )
{
DragAndDrop . AcceptDrag ( ) ;
foreach ( var obj in DragAndDrop . objectReferences )
{
if ( obj is Texture2D t )
{
AddTexture ( t ) ;
}
else if ( obj is DefaultAsset )
{
string path = AssetDatabase . GetAssetPath ( obj ) ;
string [ ] guids = AssetDatabase . FindAssets ( "t:Texture2D" , new [ ] { path } ) ;
foreach ( var g in guids )
{
string p = AssetDatabase . GUIDToAssetPath ( g ) ;
var tex = AssetDatabase . LoadAssetAtPath < Texture2D > ( p ) ;
AddTexture ( tex ) ;
}
}
else
{
string p = AssetDatabase . GetAssetPath ( obj ) ;
if ( ! string . IsNullOrEmpty ( p ) )
{
var tex = AssetDatabase . LoadAssetAtPath < Texture2D > ( p ) ;
if ( tex ! = null ) AddTexture ( tex ) ;
}
}
}
}
Event . current . Use ( ) ;
break ;
}
}
private void AddTexture ( Texture2D tex )
{
if ( tex = = null ) return ;
if ( ! textures . Contains ( tex ) ) textures . Add ( tex ) ;
}
[Serializable]
private class AtlasEntry
{
public string name ;
public string sourcePath ;
public int x ;
public int y ;
public int w ;
public int h ;
public int sourceW ;
public int sourceH ;
}
private class ImporterBackup
{
public string path ;
public bool isReadable ;
public TextureImporterType type ;
public TextureImporterCompression compression ;
public int maxSize ;
}
private void BuildAtlas ( )
{
if ( ! AssetDatabase . IsValidFolder ( outputFolder ) )
{
if ( ! outputFolder . StartsWith ( "Assets" ) )
{
EditorUtility . DisplayDialog ( "Invalid output folder" , "Output folder must be inside the project's Assets folder." , "OK" ) ;
return ;
}
string [ ] parts = outputFolder . Split ( '/' ) ;
string parent = "Assets" ;
for ( int i = 1 ; i < parts . Length ; i + + )
{
string sub = string . Join ( "/" , parts , 0 , i + 1 ) ;
if ( ! AssetDatabase . IsValidFolder ( sub ) )
{
AssetDatabase . CreateFolder ( parent , parts [ i ] ) ;
}
parent = sub ;
}
}
var backups = new List < ImporterBackup > ( ) ;
var readableTextures = new List < Texture2D > ( ) ;
foreach ( var t in textures )
{
string p = AssetDatabase . GetAssetPath ( t ) ;
var ti = TextureImporter . GetAtPath ( p ) as TextureImporter ;
if ( ti = = null ) continue ;
var b = new ImporterBackup ( ) ;
b . path = p ;
b . isReadable = ti . isReadable ;
b . type = ti . textureType ;
b . compression = ti . textureCompression ;
b . maxSize = ti . maxTextureSize ;
backups . Add ( b ) ;
ti . isReadable = true ;
ti . textureType = TextureImporterType . Default ;
ti . textureCompression = TextureImporterCompression . Uncompressed ;
ti . maxTextureSize = Math . Max ( t . width , t . height ) ;
EditorUtility . SetDirty ( ti ) ;
ti . SaveAndReimport ( ) ;
var loaded = AssetDatabase . LoadAssetAtPath < Texture2D > ( p ) ;
if ( loaded ! = null ) readableTextures . Add ( loaded ) ;
}
try
{
Texture2D atlas = new Texture2D ( 2 , 2 , TextureFormat . RGBA32 , false ) ;
Rect [ ] rects = atlas . PackTextures ( readableTextures . ToArray ( ) , padding , maxAtlasSize ) ;
if ( rects = = null | | rects . Length = = 0 )
{
EditorUtility . DisplayDialog ( "Pack failed" , "Could not pack textures. Try increasing max atlas size or reduce number of textures." , "OK" ) ;
RestoreImporters ( backups ) ;
return ;
}
int usedW = atlas . width ;
int usedH = atlas . height ;
byte [ ] png = atlas . EncodeToPNG ( ) ;
string atlasPath = Path . Combine ( outputFolder , atlasFileName ) . Replace ( "\\" , "/" ) ;
if ( File . Exists ( atlasPath ) & & ! overwriteExisting )
{
if ( ! EditorUtility . DisplayDialog ( "File exists" , $"{atlasPath} already exists. Overwrite?" , "Yes" , "No" ) )
{
RestoreImporters ( backups ) ;
return ;
}
}
File . WriteAllBytes ( atlasPath , png ) ;
AssetDatabase . ImportAsset ( atlasPath ) ;
var atlasImporter = TextureImporter . GetAtPath ( atlasPath ) as TextureImporter ;
atlasImporter . textureType = TextureImporterType . Sprite ;
atlasImporter . spriteImportMode = SpriteImportMode . Multiple ;
atlasImporter . spritePixelsPerUnit = 100 ;
List < SpriteMetaData > metas = new List < SpriteMetaData > ( ) ;
List < AtlasEntry > mapping = new List < AtlasEntry > ( ) ;
for ( int i = 0 ; i < readableTextures . Count ; i + + )
{
var src = readableTextures [ i ] ;
Rect r = rects [ i ] ;
int px = Mathf . RoundToInt ( r . x * atlas . width ) ;
int py = Mathf . RoundToInt ( r . y * atlas . height ) ;
int pw = Mathf . RoundToInt ( r . width * atlas . width ) ;
int ph = Mathf . RoundToInt ( r . height * atlas . height ) ;
SpriteMetaData md = new SpriteMetaData ( ) ;
md . name = src . name ;
md . pivot = new Vector2 ( 0.5f , 0.5f ) ;
md . rect = new Rect ( px , py , pw , ph ) ;
md . alignment = ( int ) SpriteAlignment . Center ;
metas . Add ( md ) ;
AtlasEntry e = new AtlasEntry ( ) ;
e . name = src . name ;
e . sourcePath = AssetDatabase . GetAssetPath ( src ) ;
e . x = px ; e . y = py ; e . w = pw ; e . h = ph ;
e . sourceW = src . width ; e . sourceH = src . height ;
mapping . Add ( e ) ;
}
atlasImporter . spritesheet = metas . ToArray ( ) ;
EditorUtility . SetDirty ( atlasImporter ) ;
atlasImporter . SaveAndReimport ( ) ;
string jsonPath = Path . Combine ( outputFolder , jsonFileName ) . Replace ( "\\" , "/" ) ;
WriteTexturePackerJson ( jsonPath , mapping , atlas . width , atlas . height , atlasFileName ) ;
AssetDatabase . ImportAsset ( jsonPath ) ;
lastAtlasPath = atlasPath ;
lastJson = File . ReadAllText ( jsonPath ) ;
EditorUtility . DisplayDialog ( "Success" , $"Atlas created: {atlasPath}\nJSON: {jsonPath}" , "OK" ) ;
}
catch ( Exception ex )
{
Debug . LogError ( "Error while building atlas: " + ex . Message + "\n" + ex . StackTrace ) ;
EditorUtility . DisplayDialog ( "Error" , ex . Message , "OK" ) ;
}
finally
{
RestoreImporters ( backups ) ;
AssetDatabase . Refresh ( ) ;
}
}
private void WriteTexturePackerJson ( string jsonPath , List < AtlasEntry > entries , int atlasW , int atlasH , string atlasFileName )
{
var sb = new StringBuilder ( ) ;
sb . Append ( '{' ) ;
sb . Append ( "\"frames\":[" ) ;
for ( int i = 0 ; i < entries . Count ; i + + )
{
var e = entries [ i ] ;
int jsonY = atlasH - ( e . y + e . h ) ;
string filename = e . name + ".png" ;
sb . Append ( '{' ) ;
sb . AppendFormat ( "\"filename\":\"{0}\"," , filename ) ;
sb . AppendFormat ( "\"frame\":{{\"x\":{0},\"y\":{1},\"w\":{2},\"h\":{3}}}," , e . x , jsonY , e . w , e . h ) ;
sb . Append ( "\"rotated\":false," ) ;
sb . Append ( "\"trimmed\":false," ) ;
sb . AppendFormat ( "\"spriteSourceSize\":{{\"x\":0,\"y\":0,\"w\":{0},\"h\":{1}}}," , e . sourceW , e . sourceH ) ;
sb . AppendFormat ( "\"sourceSize\":{{\"w\":{0},\"h\":{1}}}," , e . sourceW , e . sourceH ) ;
sb . Append ( "\"pivot\":{\"x\":0.5,\"y\":0.5}" ) ;
sb . Append ( '}' ) ;
if ( i < entries . Count - 1 ) sb . Append ( ',' ) ;
}
sb . Append ( ']' ) ;
sb . Append ( ',' ) ;
sb . Append ( "\"meta\":{" ) ;
sb . AppendFormat ( "\"app\":\"Unity TexturePacker Export\",\"version\":\"1.0\",\"image\":\"{0}\",\"format\":\"RGBA8888\",\"size\":{{\"w\":{1},\"h\":{2}}},\"scale\":\"1\"" , atlasFileName , atlasW , atlasH ) ;
sb . Append ( '}' ) ;
sb . Append ( '}' ) ;
File . WriteAllText ( jsonPath , sb . ToString ( ) ) ;
}
private void RestoreImporters ( List < ImporterBackup > backups )
{
foreach ( var b in backups )
{
var ti = TextureImporter . GetAtPath ( b . path ) as TextureImporter ;
if ( ti = = null ) continue ;
ti . isReadable = b . isReadable ;
ti . textureType = b . type ;
ti . textureCompression = b . compression ;
ti . maxTextureSize = b . maxSize ;
EditorUtility . SetDirty ( ti ) ;
ti . SaveAndReimport ( ) ;
}
}
}