Toolkitly

精灵图生成器

把一张张 PNG 帧合并成一张精灵图——网格、动画横条,或者紧凑排布的图集—— 并附上配套的 JSON 清单。免费、无需注册,每一个像素都留在你的浏览器里。

不会上传任何文件——所有帧都在你的浏览器里解码和打包。

怎么做一张精灵图

从一个装着帧的文件夹,到一张引擎能按序号索引的图,四步。

  1. 1一次把所有帧加进来拖进来,或者在文件对话框里选。它们会按名称做数字排序,所以就算没补零,frame_2 也排在 frame_10 前面。
  2. 2顺序要紧的话,就调一下拖动任意缩略图就能移动它。每一帧下面的序号就是它在图集里的位置, 也正是你的动画播放器会用到的那个数字。
  3. 3选排布方式动画用网格——每个格子一样大,按序号索引。 经典的动画横条用单行。 帧大小不一、又没人按位置索引时,用紧凑排布
  4. 4导出图集和清单下载 PNG,再下载旁边那份 JSON。选 TexturePacker(JSON Array),Phaser 或 PixiJS 的加载器可以直接读; 要自己写加载器就选普通列表。紧凑排布的图集必须带清单,网格图集带上则是一份有用的存档。

单元格尺寸,以及为什么由最大的那一帧说了算

在网格图集里,每个格子都一样大,而这个尺寸取自这批帧里最大的那一张。 比它小的帧就待在自己的格子里,周围留一圈透明。 这不是为了浪费而浪费:正是它让这张图可以被索引。 动画播放器算一帧的位置用的是 列号 × 单元格宽度—— 这个算术只有在格子统一时才成立。

真正值得想一想的,是小帧在格子里摆在哪。居中是安全的默认值。底部居中才是角色该用的:走路循环各帧高度不一时, 居中会让精灵在播放时上下抖,而对齐到底部能让脚落在同一条线上。

如果这些帧导出时带了很多透明边,打开裁边。 每一帧在摆放前会先被量到可见像素范围,这会缩小单元格尺寸, 通常整张图也会小一大截——代价是每一帧的原点位置变了。

间距、渗色,以及精灵边上那条杂线

如果一个精灵的某条边上出现了一像素宽的、来自隔壁帧的线,那就是贴图渗色。 它出现在 GPU 以小数坐标采样这张图的时候——被缩放的精灵、被旋转的精灵、 停在非整数像素上的摄像机——采样点正好落在两帧之间。

解法是帧间距:格子之间留一两个透明像素, 这样采歪了也只会采到透明,而不是隔壁的精灵。 它花掉一点图集面积,把问题彻底解决。如果整张图集本身还会被缩放, 那再加上外边距,让最外圈的帧也得到同样的保护。

网格、横条,还是紧凑排布

网格

统一的格子,按序号索引。Godot 的 AnimatedSprite2D、 Unity 的切片精灵模式、Phaser 的 load.spritesheet() 要的都是这个。

单行

同样的东西,只有一帧高。肉眼好检查、切起来也简单,多数教程默认的就是这种格式。

紧凑排布

把大小不一的帧拼在一起,尽量少浪费空间。没有网格可供索引, 所以 JSON 清单不是可选项。

什么时候用得上

从逐帧动画软件导出之后

动画软件给你的是一堆编号 PNG,引擎要的是一张贴图。这一步就夹在中间。

改完之后重新打包

精灵图切割工具把图集拆开,改好其中一帧,再用同样的设置打包回去。

减少 draw call

一张装了五十个精灵的贴图,合批之后的 draw call 远少于五十张贴图。 这就是图集存在的首要理由。

发布 UI 图集

各种尺寸的图标、按钮、面板碎片打进一个文件,配一份加载器启动时读的清单。

在引擎里加载这张图

每个引擎都要知道单元格尺寸——工具会报给你,清单里也记着。

Godot

把 PNG 指给一个 SpriteFrames 资源,用 Select Frames 填入报出来的列数和行数。像素画记得把导入过滤设为 Nearest

Unity

Sprite Mode 选 Multiple,然后 Sprite Editor → Slice → Grid By Cell Size,填预览上方显示的单元格尺寸。Filter Mode 设成 Point (no filter)

Phaser

网格图集用 this.load.spritesheet(key, url, { frameWidth, frameHeight }) 加载。紧凑排布的图集用 this.load.atlas()—— 清单导出成 TexturePacker(JSON Array),作为第三个参数传进去。

清单格式

图集只是导出的一半。另一半负责说明每一帧在哪,而各家引擎对这份说明该长什么样意见不一。

TexturePacker(JSON Array)

默认值,也是最接近通用交换格式的一种。Phaser 3 通过 load.atlas() 读它,多数照着 TexturePacker 输出写的加载器也一样。帧是一个列表, 每一项带 filename

TexturePacker(JSON Hash)

同样的数据,改成以文件名为键的对象。PixiJS 的图集解析器要的是这个, Phaser 也接受。如果你的加载器是按名字查帧而不是遍历,就选它。

普通列表

一个扁平的 { filename, x, y, w, h } 数组,没有任何嵌套。 没有哪个引擎能直接读它;它存在的理由是——它是唯一一种你不用查文档就能打开看懂的格式, 而这在你自己写加载器的时候很重要。

两种 TexturePacker 排布都带裁边信息。打开裁边后, 每一帧的 sourceSizespriteSourceSize 会记下它原本被画在多大的画布上、可见像素落在其中的什么位置, 于是加载器能把精灵还原回原本的边界,什么都不会错位。 这正是裁边之所以安全的原因:图集变小了,游戏里精灵的位置还是画师画的那样。

这里不会为了排得更紧而旋转帧,所以 rotated 永远是 false——如果你在拿它和付费打包器的输出做对比,这一点值得知道。

常见问题

怎么把一张张图片合成精灵图?

一次把所有帧选进来或拖进来。它们会按文件名排序,铺成一个网格——单元格尺寸取自最大的那一帧——然后画进一张 PNG。调好列数,下载图集和配套的 frames.json 清单即可。

为什么帧的顺序乱了?

几乎总是因为文件名没有补零。纯按字母排序会把 frame_10 排在 frame_2 前面,动画就乱了。这个工具改用数字比较——不管有没有补零,frame_2 都排在 frame_10 前面——而且你可以直接拖动任意缩略图手动调整。

该用网格还是紧凑排布?

做动画用网格。引擎的动画播放器是按帧序号去图里索引的,那要求每个格子一样大,浪费掉的空间就是这件事的代价。做 UI 图集或者一堆杂七杂八的精灵用紧凑排布——那里没人按位置索引,坐标由 JSON 清单来记。

能导出 TexturePacker 的 JSON 图集吗?

能。清单可以导出成 TexturePacker JSON Array、JSON Hash,或者一份普通列表。Array 是默认值,Phaser 3 可以直接用 this.load.atlas(key, png, json) 加载;Hash 是 PixiJS 期待的排布。两者都会记录裁边信息,所以被裁到可见像素的帧会被加载器还原回原本的边界。这里不会旋转帧,所以 rotated 永远是 false。

帧之间的间距是干什么用的?

防止相邻的帧渗到彼此身上。当 GPU 在非整数位置采样贴图时——任何被缩放、旋转,或者摄像机停在半个像素上的情况——它可能采到隔壁帧的一个像素,在精灵边缘画出一条杂线。留一两个像素的间距就够挡住了。

图集尺寸一定要是 2 的幂吗?

现在很少需要了。桌面和现代移动 GPU 都能正常处理任意贴图尺寸。它仍然在这些场合有意义:较老的移动平台、某些压缩贴图格式,以及你需要 mipmap 的时候。这个选项会把图集在两个方向都补齐到下一个 2 的幂,且不挪动任何一帧。

「底部居中」对齐是做什么的?

它把每一帧放在格子的底部而不是正中。对于各帧高度不一的走路循环,这能让角色的脚始终落在同一条线上,播放时精灵就不会上下抖。特效和投射物用居中对齐更合适。

我的图片会被上传到服务器吗?

不会。每一帧都是在你的浏览器里用 Canvas API 解码和绘制的。什么都不会离开你的电脑,页面加载完之后断网也照样能用。

相关教程

相关工具