当我们解包uinity apk后会得到各种图集资源,如果保重有该图集的所有碎图.asset文件,则可以通过unity编辑器脚本,导出所有碎图,操作步骤:
1.用unity打开解包项目
你贴出的 ad.asset 很有用 — 它清楚告诉我们这个 Sprite 在大图(atlas)中的 位置/大小(m_Rect / textureRect:x:0 y:1805 width:92 height:90),以及它的 pivot / pixelsPerUnit 等信息。
你之前导出出灰色/错误图片,通常是因为:
直接用 Graphics.Blit 或自己算 UV 时,坐标系(Y 方向)或 UV 映射反了 / 计算不精确;
或纹理不可读(我们已用 RenderTexture 绕过过);
还可能是 Sprite 被打包时做了裁剪 / 旋转 / 填充(trim/rotate/packing),如果不让 Unity 的渲染管线来处理这些细节,手算很容易出错。
为了解决这些问题,我给你一个最稳健的导出方法:在编辑器里临时创建一个 SpriteRenderer 把该 Sprite 显示出来,用一个临时的正交 Camera 把它渲染到 RenderTexture,再从 RenderTexture 读回像素并保存为 PNG。因为是用 Unity 的渲染器来画,所有 packing/trim/rotation/pivot 都会被正确处理——这是对付各种 atlas 最可靠的办法(虽然比直接像素拷贝慢一点,但稳定)。
下面是完整可用的 Editor 脚本(放到 Assets/Editor/SpriteFolderExporter_Renderer.cs)——无需打开 Read/Write,对不可读或压缩贴图都适用:
这里跳过了去勾选图集的Read/Write 属性
using UnityEngine;
using UnityEditor;
using System.IO;
public class SpriteFolderExporter_Renderer
{
[MenuItem("Tools/Export Sprites From Folder (Renderer, No Read/Write)")]
static void ExportSpritesFromFolder()
{
string selectedPath = EditorUtility.OpenFolderPanel("选择包含 Sprites 的文件夹", "Assets", "");
if (string.IsNullOrEmpty(selectedPath)) return;
string projectPath = Application.dataPath.Substring(0, Application.dataPath.Length - "Assets".Length);
string relativePath = selectedPath.Replace(projectPath, "");
string exportPath = "Assets/ExportedSprites";
if (!Directory.Exists(exportPath)) Directory.CreateDirectory(exportPath);
string[] guids = AssetDatabase.FindAssets("t:Sprite", new[] { relativePath });
if (guids == null || guids.Length == 0)
{
Debug.LogWarning("没有找到 Sprite。请确认路径是否正确。");
return;
}
// 使用一个固定的临时 layer(例如 31)
int tempLayer = 31;
foreach (string guid in guids)
{
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
Sprite sprite = AssetDatabase.LoadAssetAtPath<Sprite>(assetPath);
if (sprite == null) continue;
Rect rect = sprite.rect;
int w = Mathf.Max(1, Mathf.RoundToInt(rect.width));
int h = Mathf.Max(1, Mathf.RoundToInt(rect.height));
// 创建 RenderTexture
RenderTexture rt = new RenderTexture(w, h, 24, RenderTextureFormat.ARGB32);
rt.antiAliasing = 1;
rt.Create();
// 创建临时相机
GameObject camGO = new GameObject("TempExportCam");
camGO.hideFlags = HideFlags.HideAndDontSave;
Camera cam = camGO.AddComponent<Camera>();
cam.clearFlags = CameraClearFlags.SolidColor;
cam.backgroundColor = new Color(0, 0, 0, 0); // 透明背景
cam.orthographic = true;
// 使用像素单位计算正交大小
cam.orthographicSize = (rect.height / sprite.pixelsPerUnit) / 2f;
cam.aspect = (float)w / Mathf.Max(1, h);
cam.cullingMask = 1 << tempLayer;
cam.targetTexture = rt;
// 创建临时 SpriteRenderer
GameObject spriteGO = new GameObject("TempExportSprite");
spriteGO.hideFlags = HideFlags.HideAndDontSave;
spriteGO.layer = tempLayer;
SpriteRenderer sr = spriteGO.AddComponent<SpriteRenderer>();
sr.sprite = sprite;
sr.drawMode = SpriteDrawMode.Simple;
sr.sortingOrder = 1000;
// 计算把 Sprite 的中心对齐到世界原点的偏移(考虑 pivot)
Vector2 pivot = sprite.pivot; // pivot 在像素空间内 (0..rect.width)
float px = (pivot.x - rect.width * 0.5f) / sprite.pixelsPerUnit;
float py = (pivot.y - rect.height * 0.5f) / sprite.pixelsPerUnit;
// 把 sprite 的中心放到 (0,0)
spriteGO.transform.position = new Vector3(px, py, 0f);
// 相机放在 z=-10,确保能看到 sprite(sprite 在 z=0)
cam.transform.position = new Vector3(0f, 0f, -10f);
// 渲染并读回像素
cam.Render();
RenderTexture.active = rt;
Texture2D outTex = new Texture2D(w, h, TextureFormat.ARGB32, false);
outTex.ReadPixels(new Rect(0, 0, w, h), 0, 0);
outTex.Apply();
// 保存
byte[] bytes = outTex.EncodeToPNG();
string filename = Path.Combine(exportPath, sprite.name + ".png");
File.WriteAllBytes(filename, bytes);
// 清理
RenderTexture.active = null;
cam.targetTexture = null;
rt.Release();
Object.DestroyImmediate(rt);
Object.DestroyImmediate(outTex);
Object.DestroyImmediate(spriteGO);
Object.DestroyImmediate(camGO);
}
AssetDatabase.Refresh();
Debug.Log("✅ 导出完成,保存在: " + exportPath);
}
}
📌 使用方法
- 在 Unity 工程里创建 Assets/Editor/ 文件夹(如果没有的话)。
- 把脚本存为 Assets/Editor/SpriteFolderExporter_Renderer.cs。
- 回 Unity 等脚本编译完成。
- 菜单选择:Tools -> Export Sprites From Folder (Renderer, No Read/Write),然后选择包含 .asset Sprite 的文件夹(Project 内对应的文件夹)。
- 导出结果在 Assets/ExportedSprites/(可按需改成桌面路径)。
说明与注意
这个方法把每个 Sprite 当作纯渲染对象渲染出来,因此Unity 会自动处理 packing/trim/rotation/pivot,适用于各种 atlas(包括你那类 ASTC 压缩图)。
速度比直接 GetPixels 慢(因为每个 Sprite 都会创建 Camera/RenderTexture 并渲染),如果要导出很多(数千张)可改造为重用同一个 Camera/RT 并只移动/替换 Sprite 来加速。
如果导出后仍然是“全黑/全灰/透明不正确”,请把导出的其中一个 PNG 截图发给我(或告诉我是不是全透明),我再帮你定位(例如 atlas 里有分离的 alpha texture 或特殊 shader 的情况)。
如果你希望我把脚本改成:批量导出到桌面、按图集子文件夹分组、或高性能重用 Camera 的版本,我可以直接把代码给你改好。
你把 ad.asset 发来是对的——这个脚本应当能正确导出 ad 这个 Sprite(rect 在 atlas 上 y=1805,宽 92,高 90)。把脚本跑一下告诉我结果,或者直接把导出的 PNG 发来(或描述错误),我立刻帮你继续定位和修正。
按图集分类导出资源
using UnityEngine;
using UnityEditor;
using System.IO;
public class SpriteFolderExporter_Renderer_WithAtlas
{
[MenuItem("Tools/Export Sprites From Folder With Atlas (Renderer)")]
static void ExportSpritesFromFolderWithAtlas()
{
string selectedPath = EditorUtility.OpenFolderPanel("选择包含 Sprites 的文件夹", "Assets", "");
if (string.IsNullOrEmpty(selectedPath)) return;
string projectPath = Application.dataPath.Substring(0, Application.dataPath.Length - "Assets".Length);
string relativePath = selectedPath.Replace(projectPath, "");
string exportPath = "Assets/ExportedSprites";
if (!Directory.Exists(exportPath)) Directory.CreateDirectory(exportPath);
string[] guids = AssetDatabase.FindAssets("t:Sprite", new[] { relativePath });
if (guids == null || guids.Length == 0)
{
Debug.LogWarning("没有找到 Sprite。请确认路径是否正确。");
return;
}
// 使用一个固定的临时 layer(例如 31)
int tempLayer = 31;
for (int i = 0; i < guids.Length; i++)
{
string guid = guids[i];
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
Sprite sprite = AssetDatabase.LoadAssetAtPath<Sprite>(assetPath);
if (sprite == null) continue;
// 显示进度条
EditorUtility.DisplayProgressBar("导出 Sprite", $"正在处理: {sprite.name} ({i+1}/{guids.Length})", (float)i / guids.Length);
// 获取图集名称
string atlasName = GetAtlasName(sprite, assetPath);
// 确定导出子文件夹路径
string subFolderPath = exportPath;
if (!string.IsNullOrEmpty(atlasName))
{
subFolderPath = Path.Combine(exportPath, atlasName);
if (!Directory.Exists(subFolderPath))
Directory.CreateDirectory(subFolderPath);
}
Rect rect = sprite.rect;
int w = Mathf.Max(1, Mathf.RoundToInt(rect.width));
int h = Mathf.Max(1, Mathf.RoundToInt(rect.height));
// 创建 RenderTexture
RenderTexture rt = new RenderTexture(w, h, 24, RenderTextureFormat.ARGB32);
rt.antiAliasing = 1;
rt.Create();
// 创建临时相机
GameObject camGO = new GameObject("TempExportCam");
camGO.hideFlags = HideFlags.HideAndDontSave;
Camera cam = camGO.AddComponent<Camera>();
cam.clearFlags = CameraClearFlags.SolidColor;
cam.backgroundColor = new Color(0, 0, 0, 0); // 透明背景
cam.orthographic = true;
// 使用像素单位计算正交大小
cam.orthographicSize = (rect.height / sprite.pixelsPerUnit) / 2f;
cam.aspect = (float)w / Mathf.Max(1, h);
cam.cullingMask = 1 << tempLayer;
cam.targetTexture = rt;
// 创建临时 SpriteRenderer
GameObject spriteGO = new GameObject("TempExportSprite");
spriteGO.hideFlags = HideFlags.HideAndDontSave;
spriteGO.layer = tempLayer;
SpriteRenderer sr = spriteGO.AddComponent<SpriteRenderer>();
sr.sprite = sprite;
sr.drawMode = SpriteDrawMode.Simple;
sr.sortingOrder = 1000;
// 计算把 Sprite 的中心对齐到世界原点的偏移(考虑 pivot)
Vector2 pivot = sprite.pivot; // pivot 在像素空间内 (0..rect.width)
float px = (pivot.x - rect.width * 0.5f) / sprite.pixelsPerUnit;
float py = (pivot.y - rect.height * 0.5f) / sprite.pixelsPerUnit;
// 把 sprite 的中心放到 (0,0)
spriteGO.transform.position = new Vector3(px, py, 0f);
// 相机放在 z=-10,确保能看到 sprite(sprite 在 z=0)
cam.transform.position = new Vector3(0f, 0f, -10f);
// 渲染并读回像素
cam.Render();
RenderTexture.active = rt;
Texture2D outTex = new Texture2D(w, h, TextureFormat.ARGB32, false);
outTex.ReadPixels(new Rect(0, 0, w, h), 0, 0);
outTex.Apply();
// 保存到对应的图集文件夹
string filename = Path.Combine(subFolderPath, sprite.name + ".png");
byte[] bytes = outTex.EncodeToPNG();
File.WriteAllBytes(filename, bytes);
// 清理
RenderTexture.active = null;
cam.targetTexture = null;
rt.Release();
Object.DestroyImmediate(rt);
Object.DestroyImmediate(outTex);
Object.DestroyImmediate(spriteGO);
Object.DestroyImmediate(camGO);
}
// 清除进度条
EditorUtility.ClearProgressBar();
AssetDatabase.Refresh();
Debug.Log("✅ 导出完成,保存在: " + exportPath);
}
/// <summary>
/// 获取Sprite所属的图集名称
/// </summary>
static string GetAtlasName(Sprite sprite, string assetPath)
{
// 直接通过TextureImporter获取图集信息
TextureImporter textureImporter = AssetImporter.GetAtPath(assetPath) as TextureImporter;
if (textureImporter != null)
{
// 首先尝试获取packingTag
if (!string.IsNullOrEmpty(textureImporter.spritePackingTag))
{
return CleanFileName(textureImporter.spritePackingTag);
}
// 获取sprite的图集名称(如果有多个sprite在一个纹理中)
if (textureImporter.spriteImportMode == SpriteImportMode.Multiple)
{
// 如果是多个sprite的纹理,使用纹理名称作为图集名称
Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);
if (texture != null)
{
return CleanFileName(texture.name);
}
}
}
// 方法2: 如果没有图集信息,使用纹理名称
string texturePath = AssetDatabase.GetAssetPath(sprite.texture);
if (!string.IsNullOrEmpty(texturePath))
{
Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(texturePath);
if (texture != null)
{
return CleanFileName(texture.name);
}
}
// 方法3: 如果以上都失败,使用"Uncategorized"作为默认文件夹
return "Uncategorized";
}
/// <summary>
/// 清理文件名,移除非法字符
/// </summary>
static string CleanFileName(string fileName)
{
if (string.IsNullOrEmpty(fileName))
return "Uncategorized";
// 移除路径中的非法字符
char[] invalidChars = Path.GetInvalidFileNameChars();
foreach (char c in invalidChars)
{
fileName = fileName.Replace(c, '_');
}
// 如果名称为空,使用默认名称
if (string.IsNullOrEmpty(fileName))
return "Uncategorized";
return fileName;
}
}