把一段动画GIF丢进游戏引擎,看起来是最省事的做法。但GIF大概是分享动画最方便、上线最糟糕的格式之一:它没有真正可用的alpha通道,每一帧都要从增量编码的流里重新渲染,引擎也没法按帧号去索引它。
所以拿到一个想放进游戏里的GIF,第一件事是把它转成精灵图加一份图集文件。下面是这条流水线的完整走法、踩过的坑,以及Phaser和Godot两边的代码。
![]()
GIF到底给了你什么
"把帧抽出来不就行了"这句话藏了三个问题。
第一是帧处置方式。每一帧都是相对前一帧的增量,还带一个处置方法,说明前一帧的像素该怎么处理——保留、恢复成背景、还是恢复成前一帧。如果你天真地按字节偏移去读帧,得到的是拖影和只更新了一半的图像。你需要一个真正的解码器,把每一帧合成到一张持续累积的画布上。浏览器里这件事是免费的:用ImageDecoder,或者把img指向那个GIF再逐帧从canvas上读。
第二是1位透明。GIF的像素要么完全不透明,要么完全透明,没有半透明。原本在纯色背景上看着很顺滑的抗锯齿边缘,一旦放到别的背景上就变成一圈硬边。如果源文件是游戏动画,先确认这个GIF是不是从PNG序列导出的——原始PNG永远比GIF更适合当输入。
第三是调色板量化。每帧256色,通常还带抖动。角色身上平滑的渐变变成了噪点。转成精灵图后体积可能是原来的两倍,但这笔交易划算。
结论很直接:GIF适合当帧顺序和时长的来源,不适合当颜色的来源。手上有原始帧,就用原始帧。
第一步,把帧抽出来
已经在流水线里的话,命令行就够了:
ffmpeg -i run.gif -vsync 0 -f image2 frame_%04d.png
-vsync 0这个参数很关键。不加它,ffmpeg会为了凑一个恒定帧率而复制或丢弃帧,你的走路循环就会多一帧或者少一帧。
在浏览器里、不上传文件,同样的活几行canvas代码就能干:解码GIF,逐帧画出来,toBlob()。素材不属于我、不能往服务器上发的时候我就走这条路,这也是我维护的那个GIF转精灵图工具完全跑在客户端的原因——逐帧createImageBitmap、打包、canvas.toBlob、在内存里打成ZIP。
不管走哪条路,帧的命名要按动作来,不要按你恰好抽出来的序号来:idle0001.png、idle0002.png、idle0003.png、idle0004.png。四位数字、补零、动作名在前。这不是为了好看——FNF/Flixel那一系是从名字前缀推导动画的,而且当walk和walk_back同时存在时,你自己的导入器也会感谢你。
第二步,打包
两种布局,选错了要么浪费字节,要么直接出错。
- 网格布局:每个格子一样大,第N帧在(col, row)。简单,元数据极小。适合所有帧画布尺寸一致的动画,也就是大多数角色动画。帧尺寸不一时会浪费空间。
- 自动打包:把不同尺寸的矩形紧排成行。省纹理空间,但帧位置变得不规则,图集元数据从此不再是可选项。如果你在野外看到一张旁边没有JSON的精灵图,那它一定是网格布局。
有两个设置在实践中不是可选项:padding,整张图外围的空像素;spacing,帧与帧之间的空像素。两者都是为了挡住纹理渗色——当GPU用线性过滤或mipmap在非整数位置采样精灵时,会读到相邻纹素的一小部分。在排得很紧的图上,这一小部分就是下一帧的胳膊肘。留2像素的沟,瑕疵就消失了。如果加了spacing之后精灵边缘还是有一像素的亮边,那多半是预乘alpha的舍入问题,不是渗色——导出成PNG-32,并且在整数坐标上绘制。
比较工具时还有一件事值得知道:旋转帧。有些打包器会把帧旋转90度来省空间。对通用引擎来说没问题,但会破坏那些假定帧未旋转的帧名/图集约定。如果这条流水线要喂给一个有命名约定的引擎,把旋转关掉。
第三步,接进引擎
Phaser 3这边,图集JSON是TexturePacker形状的,内置加载器直接能吃:
this.load.atlas("hero", "assets/hero.png", "assets/hero.json");
然后在create里,从纹理里取出帧名,按前缀过滤出idle那一组,显式排序,再创建动画,frameRate设12,repeat设-1,最后把精灵加上去播放。
frames数组必须显式排序。加载器返回的是图集顺序,而对自动打包的图来说,那就是打包器摆放它们的顺序,不是动画顺序。这是我踩过最多的一个bug:动画在播,姿势也对,但顺序是乱的。
Godot 4要的是一个SpriteFrames资源。要么用编辑器插件导入图集,要么直接构建这个资源——一个.tres,每个动作一条动画。新建SpriteFrames,删掉默认动画,加上idle,速度设12.0,循环设true,加载纹理,然后按网格的列数循环,每列建一个AtlasTexture,atlas指向整张图,region设成精确的像素矩形,再add_frame进去。
AtlasTexture.region是精确像素,不裁剪、不旋转。这就是为什么对手写导入代码来说,网格布局更友好。
Unity那边,用Sprite Atlas配多个sprite,或者把纹理的Sprite Mode设成Multiple,再把图集JSON作为sprite元数据导入。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.