如何把精灵图拆分成一张张 PNG
精灵图把一段动画的每一帧塞进同一张图里。引擎喜欢这样; 你通常不喜欢,至少在改图的时候不喜欢。下面讲怎么把帧干净地取出来, 以及要检查什么才能让每一刀落在对的像素上。
1. 动刀之前先把网格找准
大多数精灵图就是一个规整的网格:每一帧占一个尺寸相同的格子, 从左到右、从上到下排列。只要知道列数和行数,单元格尺寸就能从整张图的尺寸里算出来:
单元格宽 = 整图宽 / 列数单元格高 = 整图高 / 行数
一张 256×128、装了八帧分两行的图,格子就是 64×64。 如果除不尽,那就是你的列数填错了——或者这张图带了留白,那是下一节的事。
反过来算也一样常见。你知道美术本来就是 32×32,因为那是你画的, 你想知道能塞下多少帧。那就用整图尺寸去除以单元格尺寸, 并使用工具的固定单元格尺寸模式。
2. 小心边距和间距
导出器经常会在整张图周围加一圈外边距、在格子之间加空隙,或者两者都加—— 通常是为了防止 GPU 采样贴图时相邻的帧渗到彼此身上。 这些留白会让上面那个朴素的除法失效。真正的公式是:
单元格宽 = (整图宽 − 左边距 − 间距X × (列数 − 1)) / 列数
实际操作时你不用手算:填上边距和间距,看叠加层。 当那些线正好压在帧的边界上、看不到隔壁帧露出的哪怕一条细边时,数字就对了。 差一个像素这种事,放大到 4 倍一眼就能看见,在「适应宽度」下则完全看不出来, 所以下结论之前先放大。
3. 网格还是自动识别?
图来自动画软件时,用网格模式。 每一帧共用一个格子,轴心点是一致的,按格子边界切能保住这份一致—— 而这正是你的引擎里动画播放器所期待的。
图是紧凑图集时,用自动识别: 帧大小不一,按尽量少浪费空间的方式排布。这种图没有网格可找, 所以工具会用漫水填充找出不透明像素连成的独立区域,取每一块的外接矩形。
自动识别有一个设置值得理解:合并间隙。 一个把剑伸得老远的角色,可能是两块互不相连的像素区域。 把合并间隙设成几个像素,靠得近的区域就会被当成同一帧。 如果单帧被拆开了就调大它;如果相邻的帧被粘在了一起就调小它。
4. 裁边,还是不裁
裁边会把每一帧收缩到可见像素的外接矩形。 这样得到的文件最小,也是图集打包器会做的事, 但它改变了每一帧的尺寸——以及随之而来的,帧原点的位置。
如果你接下来要把这些帧重新打包成新的图集,或者引擎会为每一帧存一个偏移量, 那就裁。如果这些帧是喂给一个「假设每帧都一样大」的简单动画播放器, 那就别裁;否则播放时精灵看上去会抖,因为每一帧的居中方式都不一样。
与之相关的设置是透明度阈值。低于或等于它的像素算作空白。 严格来说该是 0,但导出时带了一层很淡的背景色、或者经过有损压缩的图, 整张图上的透明度值可能是 1–3,这会让裁边和空白帧检测同时失效。 把阈值抬到 8 左右能修好这些,又不至于吃掉真正的柔和边缘。
5. 给帧起一个能正确排序的名字
序号要补零。frame_2.png 和 frame_10.png 在几乎所有文件浏览器和相当一部分素材导入器里排序都是错的;frame_02.png 和 frame_10.png 则不会。 切割工具的 {i0} 占位符会自动按最大序号的位数补零。
如果一张图里装了好几段动画——第 0 行是走路循环、第 1 行是攻击—— 用 {row} 和 {col} 按网格位置命名,能让每一段动画各自聚在一起。
6. 把帧导入你的引擎
Godot:把 PNG 拖进项目,然后作为 AnimatedSprite2D 上 SpriteFrames 资源的帧加进去。 在导入面板里把过滤设为 Nearest—— 默认的线性过滤会在精灵一被缩放时就把像素画弄糊。
Unity:Texture Type 设成 Sprite (2D and UI), Filter Mode 设成 Point (no filter), 像素画的 Compression 设成 None。 选中这些帧一起拖到场景里,会自动生成一个动画片段。
Phaser:单独的帧用 this.load.image() 加载。 如果你更想保留那一张整图,切割工具还会导出一份图集格式的 frames.json 清单,this.load.atlas() 可以直接读。
常见故障
每一帧的某条边上都露出隔壁帧的一条细边
单元格尺寸大了一点点,或者这张图有你没算进去的间距。 把间距设成 1–2px,然后放大到 4 倍再检查一遍。
最后一行或最后一列被切掉了
行数或列数多填了一个,或者某个边距把网格顶出了图的边界。
自动识别返回了几百个碎帧
抗锯齿或者抖动的像素被当成了独立区域。把最小帧尺寸调大,同时把合并间隙调大。
导出的帧看着发糊
糊是在导入时加上的,不是导出时——这里输出的 PNG 是无损的。 把引擎的贴图过滤设成邻近采样。