• Bugs
  • GIF export of "pixel art" animations not pixel perfect

Related Discussions
...

Hi!

First of all, Spine is great. This is not really a bug report, but more like a feature request, but this seemed like the most appropriate forum.

I'm trying to use Spine for something "pixel-arty". Still just playing around, but I noticed that the export to animated GIF is not that great for pixel art.

Here is the animation exported directly as an animated GIF:

Here is the animation exported as a PNG sequence and then turned into an animation by gifsicle:

In particular, you can see the colour of this character's face is "flickering" between yellow and green in Spine's GIF. If you open the two gifs with aseprite, you can see that each frame has different palettes (and that each frame uses between 8 and 38 colours!), whereas the GIF from gifsicle has the same 4-colour palette for each frame. Furthermore, gifsicle's output file size is just 2,624 bytes, whereas Spine's output file is 18,735!

Now this is not such a big problem, as obviously I can just export as PNG and then convert it to GIF myself. I assume the problem is that Spine is first generating a "video" which is then converted to GIF.

In conclusion, it would be nice if there was a "pixel art" checkbox in the GIF export screen that would not try to compress the animation as a video before turning it into a GIF but rely on the exact colours/palette used in the parts making up the animation.

Thanks,

Vegard

13일 후

Sorry for the delay. This is something we've wanted to fix for a while, maybe now is the time. Can you provide a Spine project and images for that walking guy? We can add it to our tests to ensure a new GIF solution works well. contact@esotericsoftware.com

7달 후

Thanks, and sorry for the delay too! I've just sent an email with my project files.

Better late than never I suppose! Please try our new export options in Spine 3.7 beta. We've completely redone all the exporting, it is much more powerful, especially for GIF:

Image removed due to the lack of support for HTTPS. | Show Anyway

Here is your project exported with a white matte background at 50 fps:

Image removed due to the lack of support for HTTPS. | Show Anyway


If it is blurry, probably your browser is scaling the image (try saving it locally and opening it with something like Image Glass). There are many more settings you can play with!

Also, we have added a "pixel grid scaling" setting, so you see your pixel art in the Spine editor viewport just like it will be exported. When enabled, Spine will render your pixel art at a 1:1 ratio, then scale the resulting image up to the viewport size, retaining all the pixely goodness:

Image removed due to the lack of support for HTTPS. | Show Anyway


I noticed your head image is 9x9 and is placed at 0,35.5 (world position). The position is the center of the image, so when an image has odd dimensions, you may want to offset the position by 0.5 so the image pixels map 1:1 with the exported GIF pixels (assuming you haven't rotated or scaled or translated by a partial pixel). By moving the head to 0.5,35.5 the GIF then looks like this:

Image removed due to the lack of support for HTTPS. | Show Anyway


Notice the head no longer uses 2 pixels for the outline on the left edge. That came from Spine needing to map the image pixel to the GIF pixel, and if it falls between GIF pixels then Spine has to smudge the pixel (called "filtering"). This doesn't matter for things like the arms, which are in motion. Also for parts of the skeleton that you rotate or scale, those operations almost ensure filtering artifacts (unless you rotate 90, 180, or 270 degrees, or scale 2, 4, etc). You'd only care about this for something like the head, or where a logo stops at the end of an animation, etc. If you check Linear filtering then you'll get more averaging of the pixels, which is usually not nice for pixel art.

Turning on MSAA can smooth hard image edges (where you didn't leave transparent pixels around the edge):

Image removed due to the lack of support for HTTPS. | Show Anyway

MSAA can only help GIF when using a matte background though. Without a background, GIF only has on or off alpha. Sometimes that can help, as you get a hard edge rather than pixels that don't map 1:1 getting blurred, like the top of his head with MSAA. That happens because his body (and therefore his head) is keyed to move up/down fractions of a pixel, and is of course interpolated between keys. Here it is with a blank background:

Image removed due to the lack of support for HTTPS. | Show Anyway

Thank you for the response, this is all awesome! I still have to try it out for myself, but especially the pixel art rendering in the editor is a killer feature.

For the filtering artifacts for parts which are not rotated and not scaled but only translated, shouldn't the filtering snap all pixels from the same image the same way? In the fixed GIF you posted there is still one filtering artifact on one frame of the head (because of the bouncing up and down):

(For scaled/rotated parts I don't think there is a right answer but the runtime could do something like https://en.wikipedia.org/wiki/Pixel-art_scaling_algorithms#RotSprite)

Anyway, this is something that I can easily deal with in the runtime.

The head moves a fraction of a pixel down, so filtering kicked in: the bottom row of pixels for the head is gone, and the top row is doubled. The various settings handle it in different ways, eg with a background and MSAA, the head pixels flicker.

As you said, this could be dealt with at runtime by rounding the bone world positions. I wonder if Spine should have such a setting? Without it, interpolation will put bones (and therefore attachments) between pixels, leading to filtering artifacts. While it can be handled at runtime, that doesn't help image or video exports. I also wonder if it would be useful to see bones in the editor viewport restricted to integer world coordinates? Would the rounding setting be just for visualization in the editor? Or would it be a checkbox on the skeleton and the runtimes would do the rounding when enabled? Or would it be a checkbox per bone? Per slot? Is bone position interpolation without rounding OK on some bones but not others? Note you'll still need to be careful to position attachments so they are on integer (even dimensions) or integer + 0.5 (odd dimensions) coordinates.

Here is what it looks like with rounding, a background color, and MSAA 2x:

Image removed due to the lack of support for HTTPS. | Show Anyway


By rounding, I mean this was added to Bone:

worldX = Math.round(worldX);
worldY = Math.round(worldY);

After some testing, we found that rounding is only useful for slow translations (and when using pixel art). Faster translations, rotation, or scale usually hide any filtering artifacts, even without rounding. Since bones are usually what gets translated, I think it makes sense to have a checkbox for bones to turn on rounding that bone's world position. It won't be keyable for now, but could be in the future. Any thoughts?

In other news, we've improved the selection outline for pixel art. Here's the old:

Image removed due to the lack of support for HTTPS. | Show Anyway


And the new:

Image removed due to the lack of support for HTTPS. | Show Anyway

Nate wrote

Since bones are usually what gets translated, I think it makes sense to have a checkbox for bones to turn on rounding that bone's world position. It won't be keyable for now, but could be in the future. Any thoughts?

I'm not sure I'm really the best person to ask, but I can always give my thoughts anyway 8)

First of all, I personally don't see any reason to ever want it keyable.

Second, I am not sure if it even makes sense as a per-bone property. To me it would be enough to have it per Spine project, although I could see it being useful to set it per animation. Having it per-bone just seems like it might be easy to forget to set it somewhere and then just never notice (although the result would be technically incorrect).

Finally, when you say "rounding that bone's world position" that happens purely during rendering, right? Does the rounding get applied to child bones as well or just for the attachments of that bone? I think it's important to avoid "double rounding" where an unlucky combination of translations/rotations could cause attached images to drift too far from where they're supposed to be. Say you have two bones of length 5.5 and the first bone's position tweens from 0 to 1, then you probably wouldn't want any attachments on the second bone to move more than 1 pixel in the render. If you do the rounding before calculating the second bone's attachment's position then it will gain 1 pixel from the first bone (5.5 -> 6) and 1 pixel from the second (6 + 5.5 -> 12).

I'm wondering if it wouldn't just make more sense to round (all) the final image/attachment positions?

Anyway, I'm sure you know better. I don't really feel like a typical Spine user since I'm still just playing with it and testing things out!

PS: new outlines :yes: :love:

It turns out this filtering artifact only occurs at image borders. If you pack your images into an atlas with 1px or more padding, it won't be a problem at runtime. To solve the problem for those who are exporting images or video rather than using an atlas/runtime, just add 1px of blank space around your images.

A similar problem can occur at mesh edges. For the same reasons, it is suggested to leave 1px of blank space between the mesh edge and your pixels.

So, we don't need rounding! :party:

Ah, interesting. That works, I guess! I hope to give this all a try some time over the holidays :-) Thanks a lot and cheers :clap:


I had the opportunity to try 3.7.76-beta just now and here are my impressions:

1) Right away you notice the half-pixel issue you pointed out (you wrote: "you may want to offset the position by 0.5 so the image pixels map 1:1 with the exported GIF pixels") because the head is rendered with a doubled column of pixels on the left and a missing column on the right. I suppose this is the exact same issue we had with the head going up and down and I should probably also be able to fix it by the .5 offset OR adding a 1-pixel invisible border to the sprite (or both). Like you did, moving it by half a pixel solved the problem. It will probably take some time for me to also fix up all the images and redo my model/animations to actually see the difference in the editor so I haven't been able to try that yet.

2) Because of the filtering issue it feels a bit weird to move things around (they just don't look exactly how I'd expect). But it's really a good thing to be able to immediately see how the result is going to look, so maybe it just takes a bit of time to get used to.

3) I also find myself missing a snapping feature (not sure if snap to pixel or half-pixel would be better) because especially when posing the neutral pose I don't really ever want to place parts at other intervals from each other. I guess you could argue that other people might use higher-resolution images (that get scaled down) and so it might matter more that you are able to place images at sub-pixel locations (since I imagine movements smaller than .5 pixels could change the appearance of the sprite in that case). Maybe there's already a modifier key for snapping that I'm not aware of, though!

4) When playing back my walking animation there is an intermediate frame where a single pixel (from the foot) appears below the horizontal line (the "ground"), but I cannot see this artifact in any of the frames of the animation when I select frames by hand (or step through the animation frame by frame). I suppose it's because of an unlucky combination of the interpolation of rotation/position which does not appear in any selectable frame but may appear between the selectable frames (which are shown when you actually play back the animation). Maybe it should be possible to position the slider between integer frames so it becomes possible to figure out where exactly in the animation the problem is and correct it by adding more keyframes?

I think it might be useful for the future to have a "Spine pixel art" guide detailing best practices with respect to image sizes (even/odd), image borders, and positioning (and probably animation, but I didn't really get there yet :-)) because these things are not quite intuitive, at least not to me. That's not your fault, mind!

Again, very cool that you are adding more support for pixel art :clap:

Cool, thanks taking the time to give your feedback!

1 & 2) Try adding a 1px border to the images to get rid of the filtering problem. We briefly explored solving this in other ways, so the 1px border isn't needed, but it would take too much time and affect or schedule for the other things we have planned. We may revisit it in the future.

3) There is currently no snapping. Once you don't have filtering issues, it doesn't matter as much, though I still understand snapping would be useful.

4) If you hold shift while scrubbing the timeline, you can set the timeline position between integer frames and can more easily see what the interpolation is doing. Not to say that the 1px border is the solution to everything, but you might not see the artifact one you do that. 🙂