简介:这份资源面向 Unity 开发者与游戏美术,聚焦「将一张大图集切割成多张小图并批量导出」这一常见需求,适合需要处理 UI 图集、精灵表或序列帧的初中级开发者参考。压缩包内仅 1 个 docx 文档,约 133KB,以图文笔记形式梳理了从图像导入、纹理类型与 Sprite Mode 设置、精灵编辑器切片,到 C# 脚本批量导出的完整思路,并附有可直接参考的 TestSaveSprite 脚本代码。内容覆盖图像读写属性配置、Resources.LoadAll 加载、Texture2D 逐块拷贝与 EncodeToPNG 写盘等关键环节,还涉及异步处理与资源路径管理,能帮助读者理解图集切割与导出的底层逻辑,并据此改造出适配自身项目的导出工具。目前已有 4291 人学习,适合想快速掌握 Unity 图集拆分与自动化导出流程的开发者查阅。
1. 一张 8K 贴图把项目卡成 PPT,问题真不在显卡
美术丢过来一张 8192×8192 的图集,说“就一张图,你导进去就行”。结果 Unity 编辑器里点一下这张图,Inspector 预览转圈十几秒,打包后运行时内存直接飙到几百兆,低端机还没进场景就闪退。这不是显卡不行,是单张纹理尺寸超过了目标设备的硬上限,同时 Unity 默认会把整张图按平台压缩格式完整解压进显存,你根本用不到的区域也在占坑。
“Unity 如何将一张大图切割成几张小图导出”这件事,本质是两件事叠在一起:一是把大图按规则切成若干张尺寸合规的小图,二是把这些小图作为独立资源导出到磁盘,而不是只留在 Unity 里当 Sprite 用。它解决的是纹理尺寸超限、图集过大、内存峰值过高、以及需要把切好的图交给外部工具或另一个引擎使用的问题。适合谁?做 2D 图集优化的、做 UI 资源拆分的、需要把 Unity 里的图导出给服务端或原生层用的,还有被一张巨图反复折磨的客户端开发。下面按我实际做过的路径,从规则设计一路讲到导出脚本和踩过的坑。
2. 切割规则先定死:网格切、按 Alpha 切还是按标记切
2.1 三种切法分别适合什么图
网格切(Grid)是最省事的一种,你给一个行列数或者单块像素尺寸,脚本按固定步长把大图裁成 N×M 张小图。它适合本身就规整排布的图集,比如角色帧动画、图标矩阵、瓦片地图。缺点是它不关心内容,空白区域也会被切出来,所以切完往往还要按透明度过滤一遍。
按 Alpha 切(Trim/Alpha Bounds)是先扫描整张图的非透明连通区域,把每一块内容的外接矩形算出来再裁。它适合图标散落、间距不一的图,切出来没有废边。代价是连通域算法在 8K 图上跑一次不便宜,而且相邻元素如果像素挨着,会被当成一块,需要先做膨胀或腐蚀处理。
按标记切(Marker/Slice)是美术在源文件里用纯色块或命名图层标出每块的位置,脚本读标记坐标再裁。它最可控,适合 UI 九宫格、需要精确 padding 的场景,但依赖美术配合,标记颜色和容差要提前约定。
我一般先问一句:这张图后续还要不要重新排版?要,就网格切加透明过滤;不要,且元素零散,就 Alpha 切;对边距有严格要求,就上标记切。选错切法,后面导出命名和导入设置全要返工。
2.2 用 Texture2D 读像素前必须处理的导入设置
不管哪种切法,第一步都是把源图读成可操作的像素数据。这里有个高频翻车点:Unity 默认的纹理导入设置会改像素。如果这张图在 Inspector 里勾了 sRGB、压缩格式是 ASTC 或 DXT、还有 Mipmap,你GetPixels拿到的根本不是原图数据,切出来的边缘会发灰、颜色偏移。
正确做法是给源图单独建一个“切割专用”的导入配置:Texture Type 设为 Default,Read/Write Enabled 勾上,Compression 设 None,Generate Mip Maps 关掉,sRGB 按原图色彩空间决定。这些设置可以用 AssetPostprocessor 按路径自动套,避免手动漏改。
// 放在 Editor 目录下,按路径前缀自动套用切割专用导入设置 using UnityEditor; using UnityEngine; public class SliceTexturePostprocessor : AssetPostprocessor { void OnPreprocessTexture() { // 只对放在 Assets/RawForSlice/ 下的源图生效 if (!assetPath.Replace('\\', '/').Contains("/RawForSlice/")) return; var importer = (TextureImporter)assetImporter; importer.textureType = TextureImporterType.Default; importer.isReadable = true; // 必须开,否则 GetPixels 报错 importer.textureCompression = TextureImporterCompression.Uncompressed; importer.mipmapEnabled = false; // 关 Mipmap,避免尺寸被改写 importer.sRGBTexture = true; // 按原图色彩空间,线性图改 false importer.maxTextureSize = 8192; // 别让导入时被压到 2048 } }逻辑说明:OnPreprocessTexture在资源导入前触发,assetPath是当前资源路径,用路径前缀做白名单,避免影响项目里其他贴图。isReadable不开,运行时GetPixels会直接抛异常;textureCompression设 Uncompressed 是为了拿到原始像素;maxTextureSize如果小于源图尺寸,Unity 会在导入阶段就把图缩掉,你后面切出来的坐标全对不上。参数上,sRGBTexture要跟源图一致,UI 图一般是 sRGB,法线、遮罩这类数据图要设 false。
2.3 网格切的最小实现与坐标换算
网格切的核心就一个双重循环,但坐标换算容易写反。Unity 的Texture2D像素原点在左下角,而Rect的 y 也是从下往上,所以裁块时y要从底部往上推,别按屏幕坐标的习惯从顶部算。
// 把一张大图按固定块尺寸切成若干小图,返回每块的像素矩形 using System.Collections.Generic; using UnityEngine; public static class GridSlicer { public static List<RectInt> Slice(Texture2D src, int blockW, int blockH) { var result = new List<RectInt>(); int cols = Mathf.CeilToInt((float)src.width / blockW); int rows = Mathf.CeilToInt((float)src.height / blockH); for (int row = 0; row < rows; row++) { for (int col = 0; col < cols; col++) { int x = col * blockW; // 从底部往上算,保证 RectInt 的 y 与像素原点一致 int y = row * blockH; int w = Mathf.Min(blockW, src.width - x); // 边缘块可能不足一块 int h = Mathf.Min(blockH, src.height - y); if (w <= 0 || h <= 0) continue; result.Add(new RectInt(x, y, w, h)); } } return result; } }逻辑说明:cols、rows用CeilToInt是因为源图尺寸不一定能被块尺寸整除,最后一行一列会剩边角。Mathf.Min处理边缘块,避免RectInt超出源图范围导致GetPixels越界。参数上,blockW、blockH建议取目标平台上限的约数,比如目标上限 2048,就用 1024 或 2048,别用 1500 这种,切出来边缘块尺寸参差,后续图集打包反而更碎。如果你要的是固定行列数而不是固定块尺寸,把blockW换成src.width / cols即可,但同样要处理不能整除的余数。
3. 导出成 PNG:从像素块到磁盘文件的完整链路
3.1 用 RenderTexture 还是直接 EncodeToPNG
拿到RectInt之后,有两种方式生成小图。一种是Texture2D.GetPixels取出像素数组,新建一张Texture2D塞进去再EncodeToPNG;另一种是走Graphics.Blit配合RenderTexture,再ReadPixels回读。前者代码直观,适合块数不多、尺寸不大的场景;后者在 GPU 上做缩放和格式转换更顺,但RenderTexture的格式、sRGB 开关、回读时机任何一个没对齐,颜色就会偏。
我一般切图导出用前者,因为切割本身是 CPU 侧的像素搬运,没必要绕 GPU。只有需要顺便做缩放或格式转换时才上RenderTexture。下面给的是GetPixels路径,注意GetPixels返回的是Color[],顺序是从左下往右上,跟RectInt的坐标一致,直接塞给新纹理即可。
// 按 RectInt 列表逐块导出 PNG 到磁盘 using System.IO; using UnityEditor; using UnityEngine; public static class TextureExporter { public static void ExportBlocks(Texture2D src, System.Collections.Generic.List<RectInt> blocks, string outDir, string prefix) { if (!Directory.Exists(outDir)) Directory.CreateDirectory(outDir); for (int i = 0; i < blocks.Count; i++) { var r = blocks[i]; // 取出该块的像素,注意 GetPixels 的起点是左下角 Color[] pixels = src.GetPixels(r.x, r.y, r.width, r.height); var small = new Texture2D(r.width, r.height, TextureFormat.RGBA32, false); small.SetPixels(pixels); small.Apply(); byte[] png = small.EncodeToPNG(); string fileName = $"{prefix}_{i:D3}_{r.x}_{r.y}.png"; File.WriteAllBytes(Path.Combine(outDir, fileName), png); Object.DestroyImmediate(small); // 编辑器下及时释放,避免内存堆积 } AssetDatabase.Refresh(); } }逻辑说明:GetPixels(x, y, w, h)的x、y是块左下角在源图中的像素坐标,跟第 2 章RectInt的算法对齐。新建的Texture2D用RGBA32且mipChain设 false,保证导出的是原始像素,不被 Mipmap 改写。文件名里带上i、x、y三个信息,是为了后续排查时能一眼看出这块图在原图里的位置,比纯序号好用。DestroyImmediate在编辑器脚本里必须调,否则每导一块就多占一份显存,切 8K 图时很容易把编辑器拖爆。参数上,outDir建议放在Assets之外的目录,比如工程根目录的SliceOutput,避免 Unity 立刻导入这些中间产物再触发一轮压缩。
3.2 导出目录、命名和 AssetDatabase 刷新的顺序
导出顺序有个细节:先写文件,最后统一AssetDatabase.Refresh()。如果你每写一张就刷新一次,Unity 会为每张小图触发一次导入流程,切 64 块就是 64 次导入,编辑器直接卡死。正确做法是全部写完再刷新一次,或者干脆导到Assets外,完全不触发导入。
命名上我习惯前缀_序号_原图x_原图y.png,序号补零到三位,保证文件管理器里排序稳定。如果这些小图后续要重新导入 Unity 当 Sprite,那命名还要带上行列信息,方便写图集打包规则。别用中文名和空格,跨平台工具链容易出乱码。
提示:导出目录如果放在
Assets内,务必在导出完成后才Refresh,并且给这些中间图单独设一个不压缩的导入预设,否则 Unity 会按默认设置再压一遍,你切图的尺寸优势会被二次压缩抵消。
3.3 批量导出时的内存控制
8K 图GetPixels一次返回的是 8192×8192 的Color[],一个Color是 4 个 float,算下来 1GB 量级。如果你在循环里反复GetPixels整图再裁,内存峰值会非常难看。正确做法是整图只GetPixels一次,缓存成一个大数组,然后按RectInt从大数组里做下标搬运,而不是每块都调一次GetPixels。
// 整图只读一次像素,按块做下标搬运,降低内存峰值 Color[] all = src.GetPixels(); // 只调一次 int srcW = src.width; for (int i = 0; i < blocks.Count; i++) { var r = blocks[i]; Color[] block = new Color[r.width * r.height]; for (int row = 0; row < r.height; row++) { // 大数组里第 row 行的起始下标 int srcIndex = (r.y + row) * srcW + r.x; System.Array.Copy(all, srcIndex, block, row * r.width, r.width); } // 后续 SetPixels + EncodeToPNG 同上 }逻辑说明:all是整图像素,srcIndex用(r.y + row) * srcW + r.x定位到该块每一行在大数组里的起点,Array.Copy按行搬r.width个像素。这样峰值内存只有一份整图数组加一块小图数组,比每块都GetPixels省一个数量级。参数上,srcW要提前取出来,循环里别反复访问src.width,虽然属性访问不贵,但放在内层循环里没必要。
4. 避坑与排查:切图导出最容易翻车的 5 个点
4.1 切出来的图边缘发灰、颜色变淡
现象:导出的小图跟原图比,边缘一圈发灰,半透明区域颜色变淡。原因:源图导入时勾了 sRGB 且压缩格式不是 None,GetPixels拿到的是经过色彩空间转换和压缩的像素。解决:按 2.2 的导入设置把源图改成 Uncompressed、Read/Write Enabled,并且确认sRGBTexture跟原图一致。如果原图是线性空间的数据图,sRGBTexture要设 false,否则会被反向转换一次。
4.2 块坐标对不上,切出来的内容错位
现象:切出来的小图内容跟预期位置差半块或者上下颠倒。原因:RectInt的 y 按屏幕坐标从顶部算了,而GetPixels的原点在左下角。解决:统一用左下角原点,y = row * blockH从底部往上推。如果源图本身是顶部原点导出的,先在图像工具里翻转一次,别在代码里混着算。
4.3 导出几十块后编辑器内存爆掉
现象:切到一半 Unity 编辑器卡死或崩溃。原因:每块新建的Texture2D没释放,或者每块都调了一次整图GetPixels。解决:按 3.3 整图只读一次,循环里DestroyImmediate及时释放临时纹理。如果块数超过几百,考虑分批导出,每批之间手动Resources.UnloadUnusedAssets。
4.4 导出目录在 Assets 内导致重复导入卡死
现象:导出过程中编辑器越来越卡,最后无响应。原因:每写一张图就触发一次资源导入,导入又触发压缩,循环放大。解决:导出目录放到Assets外,或者全部写完再AssetDatabase.Refresh()。如果必须放Assets内,给导出目录加一个.asmdef之外的忽略规则,或者用AssetDatabase.StartAssetEditing/StopAssetEditing包住整个导出过程。
4.5 边缘块尺寸不足导致后续图集打包失败
现象:切出来的最后一行一列尺寸比标准块小,重新导入当 Sprite 时图集打包报尺寸不一致。原因:源图尺寸不能被块尺寸整除,边缘块被Mathf.Min裁短了。解决:两种办法,一是切之前先把源图补边到块尺寸的整数倍,补透明像素;二是边缘块单独命名并单独设导入规则,别跟标准块混在一个图集里。我一般选补边,因为后续处理最省心。
5. 进阶:把切割参数做成可配置的 ScriptableObject
切图这件事做一次不难,难的是不同项目、不同美术规范反复来。我后来的习惯是把块尺寸、切法、输出目录、命名前缀、是否补边这些参数抽成一个ScriptableObject,切割逻辑读配置执行,换项目只换配置不改代码。
// 切割配置,放在 Assets 下,Inspector 里填参数 using UnityEngine; [CreateAssetMenu(fileName = "SliceConfig", menuName = "Tools/Slice Config")] public class SliceConfig : ScriptableObject { public enum SliceMode { Grid, Alpha, Marker } public SliceMode mode = SliceMode.Grid; public int blockW = 1024; public int blockH = 1024; public bool padToBlock = true; // 是否补边到整数倍 public string outputDir = "SliceOutput"; public string prefix = "slice"; [Range(0f, 1f)] public float alphaThreshold = 0.01f; // Alpha 切法的透明阈值 }逻辑说明:CreateAssetMenu让这个配置能在 Project 右键菜单里创建,mode决定走哪条切割分支,padToBlock控制是否补边,alphaThreshold给 Alpha 切法用,低于这个值的像素算透明。参数上,blockW、blockH默认 1024 是个稳妥值,目标平台上限 2048 时留了一倍余量;alphaThreshold别设 0,浮点误差会让本该透明的边缘被算进连通域,0.01 到 0.05 之间比较稳。
验证切图结果对不对,我一般做两件事。一是把导出的小图按文件名里的x、y重新拼回一张大图,跟原图做逐像素比对,差异超过阈值就说明坐标或色彩空间有问题。二是统计每张小图的非透明像素占比,如果某张图占比接近 0,说明切到了空白区,网格切的参数需要调。这两个检查写成编辑器菜单项,每次切完点一下,比肉眼一张张看靠谱得多。
血泪经验是:别在运行时做切图导出。EncodeToPNG和文件 IO 在移动端又慢又容易触发权限问题,切图导出是资源管线的事,放在编辑器脚本里,导出结果作为构建输入。运行时需要小图就直接加载导出好的资源,别现场切。另外,源图和大图导出目录都别提交到版本库的常规资源路径里,单独放一个RawAssets目录并配好忽略规则,不然仓库体积会被这些中间产物撑爆。希望帮到你。
本文还有配套的精品资源,点击获取