☰
Unity大图切割指南:用Texture2D.GetPixels批量导出PNG与Sprite
2026/9/29 12:59:33 网站建设 项目流程

简介:一份面向 Unity 开发者与初学者的图像切割与导出教程资源,解决将 Sprite 图集按规则切成多张小图并批量保存为 PNG 的实际需求。内容以单个 docx 文档封装,仅 133KB,轻量易读,重点梳理了 Unity 图像导入、纹理类型与 Sprite Mode 设置、精灵编辑器 Slice 切片、Read/Write Enabled 读写配置、C# 脚本自动导出,以及异步纹理处理与资源管理等关键环节。文档采用图文步骤说明,覆盖从 PNG/TGA 图集导入、Sprite Mode 与 Format 设置、Sprite Editor 自动切片,到 Advanced 读写配置和 C# 菜单导出的完整流程,并附有完整 TestSaveSprite 脚本示例;选中图集后可通过 Tools 菜单一键加载所有 Sprite,自动创建导出目录并逐张写入 PNG,代码思路清晰,可直接迁移到项目中使用。脚本注释清晰,读者可按需修改导出路径和格式。已有 4290 人学习下载,适合需要在 Unity 中快速拆分图集或希望理解 Resources.LoadAll 与 Texture2D 导出流程的读者参考。

1. Unity大图切割:把一张纹理批量导出小图的场景与常见误区

把一张 4096×4096 的大图按 N×M 切成几十张小图导出,是Unity开发里出现频率相当高的需求。美术交付一张序列帧、一张 UI 九宫格、一张图集,程序侧要的是可直接引用的 PNG 或 Sprite,手动开 PS 一张张切不仅慢,而且美术改一版你就要重切一版。Unity 大图切割本质上做两件事:按矩形区域取像素、把像素编码成图片文件,难点不在 API,而在坐标方向、纹理格式和内存峰值。本文将拆一套编辑器内直接用的切割工具,从最小脚本到带面板的可复用扩展,再到图集坐标导出与验证方式,适合做工具链的Unity开发者、独立游戏开发者,也适合刚接触 Unity 编辑器扩展的新手。

2. 切割前的选型:为什么我推荐用 Texture2D.GetPixels 而不是截图方案

2.1 三种切割路线的取舍:GetPixels / ReadPixels / Graphics.Blip

在写任何代码之前,先确定用哪条技术路线。Unity 里把大图切成小图,常见做法有三种:Texture2D.GetPixels、ReadPixels从 RenderTexture 读回、以及Graphics.Blip直接拷贝。

GetPixels(x, y, blockWidth, blockHeight)是三个方案里最直观的,它按像素坐标从源纹理上取一块矩形区域,返回Color[]数组,再塞进新建的Texture2D里编码导出。这条路适合规则网格切割,代码量最小,我也最常用。

ReadPixels则需要先把源纹理绘制到 RenderTexture 上,再从 RenderTexture 读回像素。它多了一次 GPU 到 CPU 的拷贝,性能比GetPixels慢一些,但它的真正用途是读取相机渲染结果、后处理特效画面这类“不在常规纹理里的像素”。如果只是切一张已有的美术图,用ReadPixels属于绕路。

Graphics.Blip可以把纹理的矩形区域直接绘制到目标纹理上,整个过程走 GPU,不需要把大图整张读到 CPU 内存里,对大图场景反而最稳。但它只负责拷贝,目标纹理后续还是得走一次EncodeToPNG,而且两张纹理之间进行像素级比对时不如GetPixels方便。

方案像素走向内存峰值适用场景
Texture2D.GetPixels源纹理 → Color[] → 新纹理高(整块数组在托管堆)规则网格切割、像素校验
RenderTexture.ReadPixels源纹理 → RT → 新纹理中(多一次RT)相机画面、后处理结果导出
Graphics.Blip源纹理 → 目标纹理 GPU 直拷低大尺寸批量切割、跑图集

选型结论:做这个标题下的“大图切割导出”功能,默认走GetPixels。内存不是瓶颈时,它最直白、最好排错。切 4096×4096 这种量级时再考虑Graphics.Blip或分块读取。

2.2 最小可跑通的Editor脚本:从选中纹理到输出九张PNG

先给一个最简版本。在编辑器下选中一张纹理,点击菜单,按 3×3 切成九张 PNG 输出到 Assets 目录。这段脚本可以直接粘到Editor文件夹下运。

using System.IO; using UnityEditor; using UnityEngine; public static class TextureSlicer { [MenuItem("Tools/Texture Slicer/Export 3x3 PNGs")] static void Export3x3() { Texture2D source = Selection.activeObject as Texture2D; if (source == null) { Debug.LogError("请先选中一张 Texture2D"); return; } int rows = 3; int cols = 3; int blockW = source.width / cols; int blockH = source.height / rows; // 输出到 Assets 下,方便 AssetDatabase 管理 string dir = Path.Combine(Application.dataPath, "TextureSlices", source.name); Directory.CreateDirectory(dir); for (int r = 0; r < rows; r++) { for (int c = 0; c < cols; c++) { int x = c * blockW; // 纹理坐标原点在左下,按左上角习惯要翻转 y int y = source.height - (r + 1) * blockH; Color[] pixels = source.GetPixels(x, y, blockW, blockH); Texture2D outTex = new Texture2D(blockW, blockH, TextureFormat.RGBA32, false); outTex.SetPixels(pixels); File.WriteAllBytes(Path.Combine(dir, $"r{r}_c{c}.png"), outTex.EncodeToPNG()); Object.DestroyImmediate(outTex); } } AssetDatabase.Refresh(); Debug.Log($"切割完成,输出目录:{dir}"); } }

逻辑说明:GetPixels(x, y, blockWidth, blockHeight)的x、y是源纹理上的像素起点,blockWidth、blockHeight是取块尺寸。代码里x = c * blockW按列递增,y用了source.height - (r + 1) * blockH,因为GetPixels的 y 轴以纹理左下角为 0,美术习惯的九宫格是从左上角开始数,这里不翻转,切出来的顺序会上下颠倒,后面避坑章节还会详细说。

参数说明:TextureFormat.RGBA32是导出 PNG 最不容易出问题的格式,带透明通道且编码无损;new Texture2D(..., false)的最后一个参数是mipChain,切割导出用不到 mipmap,关掉可以省内存。每次写完文件后立即DestroyImmediate(outTex),否则循环次数一多,临时纹理堆积会把编辑器内存吃满。最后一定调AssetDatabase.Refresh(),否则文件在磁盘上存在,但 Unity 的资源数据库不会立刻识别。

2.3 脚本里的关键参数说明:宽高、行列、偏移量与文件命名

上面这段代码直接用source.width / cols决定块尺寸,意味着原图尺寸必须能被行列数整除。实际项目中大图很少刚好整除,比如 4096×4096 切成 3×3,最后一块的宽就应该是4096 - 2 * 1365 = 1366,如果还按 1365 取,右侧会丢一列像素。

边界处理常见做法是:前 N-1 块用固定blockW,最后一块用余数补齐。写成代码就是int actualW = (c == cols - 1) ? source.width - c * blockW : blockW;高同理。这个逻辑要写进工具,否则切出来的图集尺寸对不上,UI 拼接时会出现错位。

另一个关键参数是 overlap,也就是重叠像素。图集在运行时用 UV 采样时,边缘经常出现渗色,美术会要求在切割时每块向外多取 1~2 像素。实现上把取块起点改成c * blockW - overlap,块宽改成blockW + overlap * 2。注意首尾两块不能越界,x最小为 0,x + width最大不超过源图宽度。

文件命名也要在工具设计时确定。常见做法是{原图名}_{行}_{列}.png,比如hero_0_0.png。只用r0_c0.PNG这种命名遇到两张不同大图切出来同名文件时会互相覆盖。输出目录我一般固定成Assets/TextureSlices/{原图名}/,每个源图一个子目录,互不干扰。

3. 把切割工具做成可复用的编辑器窗口:行数、列数、重叠量与文件格式都交给面板

3.1 用 EditorWindow 封装面板:输入参数与校验逻辑

菜单项适合自己临时用,团队里其他人也要用,就得做成带输入面板的编辑器窗口。用EditorWindow.GetWindow创建窗口,在OnGUI里画出行数、列数、重叠量、输出目录等字段,点按钮再执行切割。这样美术同学不用改代码,直接在面板里填数字就能导出一套新图。

using System.IO; using UnityEditor; using UnityEngine; public class TextureSlicerWindow : EditorWindow { private Texture2D source; private int rows = 4; private int cols = 4; private int overlap = 1; private string outputDir = "Assets/TextureSlices"; [MenuItem("Tools/Texture Slicer/Open Window")] static void OpenWindow() { TextureSlicerWindow window = GetWindow<TextureSlicerWindow>("Texture Slicer"); window.Show(); } private void OnGUI() { source = (Texture2D)EditorGUILayout.ObjectField("源纹理", source, typeof(Texture2D), false); rows = EditorGUILayout.IntField("行数", rows); cols = EditorGUILayout.IntField("列数", cols); overlap = EditorGUILayout.IntField("重叠像素", overlap); outputDir = EditorGUILayout.TextField("输出目录", outputDir); if (GUILayout.Button("切割并导出")) { if (source == null) { EditorUtility.DisplayDialog("提示", "请先指定源纹理", "OK"); return; } if (rows <= 0 || cols <= 0) { EditorUtility.DisplayDialog("提示", "行列数必须大于 0", "OK"); return; } ExecuteSlice(); } } private void ExecuteSlice() { int blockW = source.width / cols; int blockH = source.height / rows; string fullDir = Path.Combine(Application.dataPath, outputDir.Replace("Assets/", ""), source.name); Directory.CreateDirectory(fullDir); EditorUtility.DisplayProgressBar("切割纹理", "正在处理...", 0f); try { for (int r = 0; r < rows; r++) { for (int c = 0; c < cols; c++) { int x = Mathf.Max(0, c * blockW - overlap); int y = Mathf.Max(0, source.height - (r + 1) * blockH - overlap); int w = (c == cols - 1) ? source.width - c * blockW : blockW; int h = (r == rows - 1) ? source.height - r * blockH : blockH; w = Mathf.Min(w + overlap * 2, source.width - x); h = Mathf.Min(h + overlap * 2, source.height - y); Color[] pixels = source.GetPixels(x, y, w, h); Texture2D outTex = new Texture2D(w, h, TextureFormat.RGBA32, false); outTex.SetPixels(pixels); File.WriteAllBytes(Path.Combine(fullDir, $"{source.name}_{r}_{c}.png"), outTex.EncodeToPNG()); Object.DestroyImmediate(outTex); } EditorUtility.DisplayProgressBar("切割纹理", $"第 {r + 1}/{rows} 行", (float)r / rows); } } finally { EditorUtility.ClearProgressBar(); } AssetDatabase.Refresh(); EditorUtility.DisplayDialog("完成", "切割完成", "OK"); } }

逻辑说明:ExecuteSlice把边界余数、重叠量、进度条都整合进了循环里。x和y做了Mathf.Max(0, ...)防越界,最后一块的宽高用Mathf.Min(w + overlap * 2, source.width - x)保证不超过源图范围。DisplayProgressBar必须配finally里的ClearProgressBar,否则中途报错后进度条会一直卡在编辑器上。

参数说明:overlap 设 0 就是紧密切割;设 1 会让每张图向外扩展一圈,适合运行时做 UV 边缘抗渗色。这个窗口版本已经能覆盖九宫格、序列帧、图集三种常见切割需求,唯一还没解决的是纹理可读性,也就是下一节要讲的格式问题。

3.2 纹理格式与像素读取的对应关系:什么时候必须临时转 RGBA32

GetPixels不是一个无条件可用的接口。纹理导入设置里没有勾选 Read/Write Enabled,或者纹理是 ASTC、ETC、PVRTC 这类压缩格式时,编辑器下直接调GetPixels会抛异常,常见报错是Texture '' is not readable。美术给的图经常是压缩格式,所以工具里要做一层格式兜底。

常见做法是把源纹理先转成 RGBA32 的临时副本,再对副本做切割。转换方式是用 RenderTexture 中转,代码如下。

private Texture2D ToReadableRGBA32(Texture2D src) { RenderTexture rt = RenderTexture.GetTemporary( src.width, src.height, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB); Graphics.Blit(src, rt); Texture2D copy = new Texture2D(src.width, src.height, TextureFormat.RGBA32, false); RenderTexture.active = rt; copy.ReadPixels(new Rect(0, 0, src.width, src.height), 0, 0); RenderTexture.active = null; copy.Apply(); RenderTexture.ReleaseTemporary(rt); return copy; }

逻辑说明:Graphics.Blit(src, rt)把源纹理绘制到临时 RenderTexture 上,这一步把压缩格式解压成可供读回的 ARGB32;ReadPixels从当前激活的 RT 里读像素;RenderTexture.active = null必须写,否则后续其它渲染操作会读到残留的 RT;最后Apply()把读回的像素真正提交到copy纹理上。

参数说明:RenderTextureFormat.ARGB32对应 RGBA32 的 RT 版本,带 Alpha 通道;RenderTextureReadWrite.sRGB是颜色空间相关的关键参数,在 Linear 项目里如果不显式传 sRGB,读回的结果会偏灰,这一条在避坑章节还会展开。转出来的copy用完记得DestroyImmediate,RT 用ReleaseTemporary归还,否则连续处理多张大图,编辑器显存会涨得很难看。

3.3 导出命名规则与目录结构:避免同名覆盖和路径中文坑

命名规则和时间戳是个容易被忽略的细节。两个人同时用同一张图导出一份同名文件是很常见的事,建议格式里带源图名加行列号,例如hero_sheet_2_3.png,另外在工具面板里加一个“导出时覆盖同名文件”的勾选项,默认开启,关闭后若目标文件已存在,则自动追加_1后缀,这种直观的后悔药比弹窗确认要好用。

路径方面,建议输出目录统一放Assets下。放在项目外虽然也能写文件,但AssetDatabase.Refresh()刷不到外部目录,Unity 资源数据库不认,运行时Resources.Load或 AssetBundle 打包都查不到,坑比较大。第二个坑是路径里的中文字符,Windows 编辑器下中文目录通常没问,但打包到部分安卓渠道或跨平台协作时,有概率遇到编码不一致导致找不到文件,我一般默认英文目录,中文只出现在文件名的展示部分。

路径拼接统一用Path.Combine,不要手写"/" + outputDir + source.name这种字符串,不同平台目录分隔符不一样。拼接时注意Application.dataPath的末尾不带斜杠,outputDir里如果有Assets/前缀要先去掉,否则会拼出Assets/Assets/TextureSlices这种重复路径。

4. Unity大图切割避坑:4 个让工具跑偏的典型问题

4.1 现象:切割结果发灰、偏色

用 ReadPixels 方案导出 PNG 后,在外部看图软件里打开颜色整体发灰发暗,但原图在 Unity 里看着是正常的。

原因:项目 Color Space 设置为 Linear 时,RenderTexture.GetTemporary默认的读写颜色空间是线性,ReadPixels读回来的像素没有经过 sRGB 到线性、再线性到 sRGB 的转换链路,保存成 PNG 时颜色通道就对不上,表现就是灰度整体被压缩。

解决:创建临时 RT 时显式指定RenderTextureReadWrite.sRGB(即第 3.2 节代码里的写法),或者在导出后统一调用Texture2D.Apply前做一次LinearToGammaSpace。GetPixels方案不受影响,因为它读的是纹理原始存储值,不经过 RT 的颜色空间转换。遇到偏色,先排查你走的是哪条像素读取路线,再检查 RT 创建参数的第三个参数。

4.2 现象:ReadPixels 得到的图片上下颠倒

切割结果每张单独看没问题,但按行列拼回原图时发现上下方向反了,第一行的图实际是原图最后一行。

原因:Unity 纹理坐标原点在左下角,ReadPixels读取 RT 时同样以左下角为原点,而流程中如果直接从Rect(0, 0, src.width, src.height)读一整张再按行切,写文件顺序和视觉顺序是相反的。GetPixels也有同样问题,它返回的数组第一行对应纹理最底下一行。

解决:用GetPixels时按source.height - (r + 1) * blockH翻转 y 起点;用ReadPixels时把读取 Rect 的 y 改成source.height - blockH - r * blockH,也就是从顶部往下读。判断方向是否正确的土办法:切一张 2×2 的图,四角分别填充红绿蓝白,切完看输出文件的位置对应关系,一眼就能看出翻转方向。

4.3 现象:菜单点击后什么都没发生,也不报错

点击菜单项后编辑器毫无反应,没有输出文件,也没有日志。再点一次还是没有,像黑匣子一样。

原因:最常见是根本没选中纹理,Selection.activeObject as Texture2D返回 null,但代码里 return 之前没有日志;或者输出目录拼错了位置,写到了项目根目录之外的地方,Directory.CreateDirectory能创建但 Unity 资源数据库感知不到;还有一种情况是保存路径与现有资源同名被忽略。

解决:加校验日志是第一步,if (source == null) Debug.LogError(...),别静默返回。路径层面,在ExecuteSlice开头用Debug.Log(fullDir)把最终绝对路径打出来,看看是否落在项目内。输出后如果文件存在但 Unity 没刷新,手动补一句AssetDatabase.Refresh()。我给编辑器工具加日志的习惯是:菜单函数里能打印路径的地方都打印,工具对使用者来说是黑匣子,日志就是唯一的探针。

4.4 现象:大图切割时编辑器卡死,内存暴涨

切 4096×4096 的图,进度条走到一半 Unity 界面卡住,任务管理器里内存占用冲到几个 GB。

原因:GetPixels一次调用会把指定矩形区域的所有像素拷贝到Color[],一张 4096×4096 的图读一次就有 6400 万个 float 类型颜色值,占 1GB 以上托管内存。如果循环里同时持有源纹理、临时 Texture2D、Color[],并且没及时释放,GC 来不及回收,内存峰值就会非常难看。

解决:逐块处理并及时释放。每块切完立刻DestroyImmediate(outTex),Color[]用完不再引用它;如果单块尺寸也很大,比如一次切 2048×2048,那就再细化行列数。不想走 CPU 数组时切到Graphics.Blip,直接 GPU 拷贝,内存峰值能降一个量级。另外把 3.1 里的进度条加上,至少能直观看到卡在哪一步,而不是一脸懵地等编辑器恢复。

5. 进阶:从“切图”到“图集坐标”:让 UGUI 直接用切割结果

5.1 批量切割图集并输出 Sprite:按行列写入 TextureImporter 的 spriteMetaData

切完 PNG 只是最基础的需求,实际项目里 UI 拼大图、做图集时,程序侧更希望能直接拿到带Sprite的图集,而不是导入一堆零散 PNG 再自己拼 Rect。Unity 的TextureImporter支持把一张纹理标记为 Multiple 模式,并配置多个SpriteMetaData,这样大图本体还保留在项目里,UI 直接引用子 Sprite 即可。

常见做法是把大图的各项SpriteMetaData.rect计算好,写入导入设置后重新导入。核心代码分成三部分:读取当前导入器、计算子矩形、写回并触发导入。

using UnityEditor; using UnityEngine; private static void ConfigureSpriteSlices(Texture2D source, int rows, int cols) { string assetPath = AssetDatabase.GetAssetPath(source); TextureImporter importer = AssetImporter.GetAtPath(assetPath) as TextureImporter; if (importer == null) return; importer.textureType = TextureImporterType.Sprite; importer.spriteImportMode = SpriteImportMode.Multiple; importer.spritePixelsPerUnit = 100; int blockW = source.width / cols; int blockH = source.height / rows; SpriteMetaData[] metas = new SpriteMetaData[rows * cols]; for (int r = 0; r < rows; r++) { for (int c = 0; c < cols; c++) { int index = r * cols + c; int x = c * blockW; int y = source.height - (r + 1) * blockH; int w = (c == cols - 1) ? source.width - c * blockW : blockW; int h = (r == rows - 1) ? source.height - r * blockH : blockH; metas[index] = new SpriteMetaData { name = $"{source.name}_{r}_{c}", rect = new Rect(x, y, w, h), alignment = (int)SpriteAlignment.Center, pivot = new Vector2(0.5f, 0.5f) }; } } importer.spritesheet = metas; EditorUtility.SetDirty(importer); AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate); }

逻辑说明:spriteImportMode = SpriteImportMode.Multiple是开启多 Sprite 的前提;spritePixelsPerUnit决定 Sprite 的物理大小,UI 图一般 100,序列帧一般按美术给定的值填;SpriteMetaData.rect的坐标原点是纹理左下角(单位为像素),所以y的计算方式和GetPixels保持一致,用source.height - (r + 1) * blockH。

参数说明:alignment = (int)SpriteAlignment.Center是整个 Sprite 的默认锚点,也可以在pivot里单独指定。ImportAssetOptions.ForceUpdate是必须的,只调SetDirty不触发重新导入,Sprite 列表不会更新,这一点很反直觉,我最早在这上面翻过车。

5.2 导出坐标清单:用 JSON 记录每张小图的 Rect 与 Pivot

图集切好、Sprite 也配好后,程序侧希望运行时动态加载切割坐标,用于纹理打包或在逻辑层重建 UI。这时候可以额外导出一份 JSON 清单,记录每张小图的rect、pivot、原始尺寸,程序读取这份配置来做运行时纹理映射。

using System; using System.Collections.Generic; using System.IO; using UnityEngine; [Serializable] public class SliceInfo { public string name; public int x; public int y; public int width; public int height; public float pivotX; public float pivotY; } [Serializable] public class SliceManifest { public string sourceName; public int textureWidth; public int textureHeight; public List<SliceInfo> slices = new List<SliceInfo>(); } public static void ExportManifest(Texture2D source, string outputDir, SpriteMetaData[] metas) { SliceManifest manifest = new SliceManifest { sourceName = source.name, textureWidth = source.width, textureHeight = source.height }; foreach (SpriteMetaData meta in metas) { manifest.slices.Add(new SliceInfo { name = meta.name, x = (int)meta.rect.x, y = (int)meta.rect.y, width = (int)meta.rect.width, height = (int)meta.rect.height, pivotX = meta.pivot.x, pivotY = meta.pivot.y }); } string json = JsonUtility.ToJson(manifest, true); string path = Path.Combine(outputDir, $"{source.name}_manifest.json"); File.WriteAllText(path, json); AssetDatabase.Refresh(); }

逻辑说明:JsonUtility是 Unity 内置的序列化工具,需要给类标记[Serializable]并且字段是公开字段才能正常读写。这里把SpriteMetaData的像素坐标和锚点全部转成普通结构体,是为了和运行时逻辑解耦,游戏运行时不需要引UnityEditor命名空间。缩进参数true让 JSON 可读性好一些,便于美术和策划手工检查。

参数说明:rect的坐标是纹理像素坐标,pivot是归一化值,0~1 范围。运行时要还原 Sprite 在世界空间的位置,需要结合spritePixelsPerUnit换算,所以 manifest 里最好再记录一次pixelsPerUnit,否则只知道像素坐标不知道物理尺寸,拼图时会错位。

5.3 让工具自动运行:AssetPostprocessor 里监听导入时机

团队使用流程里,手动点工具面板总会有忘记的时候。更省事的做法是让切割逻辑挂在资源导入流程上:美术把大图拖进项目,图片导入的一瞬间自动按预设行列切分并配好 Sprite。这个用AssetPostprocessor.OnPostprocessTexture实现。

using UnityEditor; using UnityEngine; public class TextureSlicePostprocessor : AssetPostprocessor { private void OnPostprocessTexture(Texture2D texture) { // 只处理文件名带 _sliced 标记的图,避免影响普通纹理 if (!assetPath.Contains("_sliced")) return; TextureImporter importer = assetImporter as TextureImporter; if (importer == null) return; importer.textureType = TextureImporterType.Sprite; importer.spriteImportMode = SpriteImportMode.Multiple; int rows = 4; int cols = 4; // 行列数也可以约定写在文件名里,比如 hero_4x4_sliced string fileName = System.IO.Path.GetFileNameWithoutExtension(assetPath); string[] parts = fileName.Split('_'); if (parts.Length >= 3) { int.TryParse(parts[parts.Length - 2], out rows); int.TryParse(parts[parts.Length - 1], out cols); } int blockW = texture.width / cols; int blockH = texture.height / rows; SpriteMetaData[] metas = new SpriteMetaData[rows * cols]; for (int r = 0; r < rows; r++) { for (int c = 0; c < cols; c++) { int index = r * cols + c; metas[index] = new SpriteMetaData { name = $"{fileName}_{r}_{c}", rect = new Rect( c * blockW, texture.height - (r + 1) * blockH, (c == cols - 1) ? texture.width - c * blockW : blockW, (r == rows - 1) ? texture.height - r * blockH : blockH), alignment = (int)SpriteAlignment.Center, pivot = new Vector2(0.5f, 0.5f) }; } } importer.spritesheet = metas; } }

逻辑说明:OnPostprocessTexture是纹理导入管线里的回调,纹理数据已经生成但导入设置尚未完全落盘时触发,在回调里改assetImporter的spritesheet可以影响最终导入结果。文件名规约用_sliced做标记,避免项目里所有图片都被自动切分。文件名里附带行列数,比如hero_4x4_sliced.png,工具就能自动解析行列配置,不用每次都改代码。

这一步完成后,整个工具链闭环了:美术往项目里拖图,导入即切分,路径下自动生成 Sprite 切片和 JSON 清单。第 5.1 节的菜单工具反而变成了临时修补手段,平时都用不到。

6. 切割结果的验证与反悔药:像素对比、尺寸核对与一键回滚

切割工具的验证不看出图效果,要看像素数据。写一个校验函数,遍历每张小图,用GetPixels分别读取小图和原图对应区域的颜色值,逐一对比,RGBA32 导出是无损的,只要坐标没算错,两组像素应该完全一致。任何一个像素有差异,说明坐标映射有偏差,直接定位到是哪一块、哪一行。

尺寸核对用数学关系就够了:常规块的宽 × 列数小于等于原图宽,最后一列块宽大于等于常规块宽。我用这个规则让工具在校验失败时自动弹窗列出异常切片编号,不再往下执行。这个校验也是给后续接手的程序留的“证据”,不然切完图出了问题,谁都不愿意承认是自己参数填错了。

回滚方案我一般做两层。第一层轻量,输出目录固定为独立文件夹,生产环境引用路径不指向这里,确认无误后再手动覆盖到正式目录;第二层是切割前自动把原图文件复制到Assets/TextureSlices/{源图名}/backup/下,改坏了之后能从 backup 拖回原图,相当于一颗后悔药。这个习惯帮我救回不少美术资源,尤其是源文件只交付一次、本地没存档的情况。

最终工具里我保留了三个入口:菜单项给程序快速处理,EditorWindow 面板给美术填参数,AssetPostprocessor 自动流程给常规批量导入。每次写完切割相关代码,我都会拿一张四角颜色不同的 2×2 小图先跑一遍方向校验,确认无误再上大图。这条习惯帮我避开了大部分坐标翻转和颜色空间问题,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询