Generating image rotations (with outline or dithering optimisation)

TLDR;
cleanRotation is a set of functions to generate and export image table containing all rotations for an image. It creates rotations with cleaner edges than the SDK rotation but there is also an option to have cleaner dithering instead.

CleanRotation v1.zip (174.1 KB)

@samdze also made a command line to generate this type of spritesheet
https://devforum.play.date/t/spriterot-handy-command-line-tool-to-generate-rotations-spritesheets/


Long version

Clean Outlines

If you tried to rotate images using the SDK function, you might have noticed that often the result get a bit fuzzy or even missing pixels.

ship2-zoom-fuzzy

After posting about cleanEdge, I decided to look a bit into this topic and the technic, often called RotSprite, is actually not that complicated so I made my own rotation function.

ship2-zoom-clean

The idea is simply to upscale the image and afterward to rotate and downscale it. That's it.
cleanEdge is using a custom upscale algorithm but I used the much simpler scale2X which work quite well and is often enough for the type of resolution used on playdate.

Overall it improves a lot the outline of an image, removing the fuzziness we can see with the normal image rotation.

But one aspect it does not help is the dithering. If your image have a lots of dithering you will still have a lot of flickering and moiré pattern as you rotate your picture.

Clean Dithering

While I was finishing up with generating clean rotation, I saw this cohost post about an old trick to rotate an image using only shearing functions.

Rotation with shears 1

The results do not create cleaner image but the technic has one interesting property, since you just shift pixels with shearing (if you don't simply use an image transform) you just move pixels around. The result will have the same number of white and black pixels basically which would be great news to keep the dithering as close as possible to the original image.

And it works even better than I was expecting:
test2 rotation by shearing

The dithering does not have the same pattern but the pattern give the same shade level and there is no flickering as the image is rotated. The drawback is that the outline can get even fuzzier than the SDK rotation.

Generating Image Table

Since these rotations are more expensive to use than the image rotation in the SDK, you cannot use them on the playdate itself (until someone rewrite everything in C). The way to use it in a game is to save them in an image table and at runtime use the image corresponding to the angle you want.

I've added the functions to generate the image table and also to export them as an image.

if playdate.isSimulator then
	image = playdate.graphics.image.new( "my_image"  )
	rotation_table = cleanRotation.generate_imagetable( image, 90, "dithering" )
	cleanRotation.export_imagetable( rotation_table, "my_image_table" )
else
	rotation_table = playdate.graphics.imagetable.new( "my_image_table" )
end

You can choose to optimise the rotation for outline or dithering, it's depends mainly on the type of images you use.
By default the number of angle generated is 90, but that also depend of your image and how it is used in your game. If you have a big image, you might need a greater number of frame to look fluid.

There is also a handy function to get the correct image for an angle

	angle = playdate.getCrankPosition()
	cleanRotation.get_image_from_angle( rotation_table, angle ):drawCentered( 200, 120 )

If you want to rotate stuff, that might be handy!

11 Likes

Nice work Nic!

The results are great.

And the extra work you've put in to encourage best practices is to be applauded. :clap:

Oh dang this is pretty cool. Ill have to try this out sometime.

1 Like

This is very interesting.

I just happened to be listening to the hyper meteor episode of the podcast on my way home today where they were telling of having to precompute some 3000 or so images like this.

It would be so much cleaner to supply one image and do this all at the C-layer on the game's launch. The PD can crunch a good amount of data if a ~5s or so start up time is acceptable.

I'll take a look at this lua varient at least...

Yeah the way Hyper Meteor does it is really good and offer the best result. They make the assets as svg and use a scripting written for Processing to render the vector shapes at every angle they need. It really give the best image for each angle.

When I needed to decide to write it in Lua and C the choice was pretty easy. It would be handy to have a faster generation so that it would run on device but ultimately loading an image table file is simply the fastest solution and a good practice. A Lua library would also be more useful to more people.

But that would still be interesting to have a C version just as an option.

2 Likes

First - I wanted to say this is awesome.

Some notes as I've come into this in Aug 2024 a noob:

I am guessing that the behavior of datastore.writeImage has shifted since this was created.

From the docs:

When running in the Simulator, all Playdate file operations will happen in the SDK’s Disk/Data/(bundleID) folder. bundleID is as specified in your project’s metadata file.

This breaks the virtuous loop in cleanRotation in that image tables won't be saved in the source/ folder and be usable on device.

For example on mac, the simulator will save the generated pdi/gif images to:

~/Developer/PlaydateSDK/Disk/Data/{game}/images

So, this works great in the simulator, but in order to have these images to be bundled into the app & usable on the device, these need to be copied back into your source/images/ dir.

This is workable for now but tripped me up for a bit, so I thought I'd leave a note.

I might switch over to spriterot but this post was super helpful in explaining the approach.