Rotation with three shears

There's a graphics trick that used to be widely known that has now probably almost vanished from the graphics consciousness - you can do rotations by applying three shears in a row. This should surprise you! It surprised me. And it re-surprises me every time I remember it. ---------------------------------------- Shears are very simple graphics operations to do - you just render the sprite, but shifting the lines or columns a bit each time. But rotations are gnarly things that involve lots of maths and interpolation and so on. So how could you possibly construct one from the other? The derivation is relatively simple, but I won't do it here because there's a perfectly good explanation over here: https://www.ocf.berkeley.edu/~fricke/projects/israel/paeth/rotation_by_shearing.html [https://www.ocf.berkeley.edu/~fricke/projects/israel/paeth/rotation_by_shearing.html] But the TL;DR is you do three shears: 1. shear in X by -tan(angle/2) 2. shear in Y by sin(angle) 3. shear in X by -tan(angle/2) (that's not a typo - the third shear is exactly the same as the first!) This works for all angles -90 degrees to +90 degrees, which is fantastic! Beyond that you just need to apply an X and Y flip first (i.e. a rotation by 180), which can usually be done as part of the first shear. Here's a GIF of the R-Type fighter being smoothly rotated. Each image is a sheared version of the one to its left, with the final rotated image on the right. Sorry I didn't get the looping perfect. The three successive shears to make up a full rotation [https://staging.cohostcdn.org/attachment/4bac39e8-5454-425f-ab1a-e8b02dc6093e/Rotation%20with%20shears%201.gif] But why go to all this trouble though? Why not get your GPU to do it? Well, what if you don't have one? Back in the days of 16-bit and 32-bit machines, we didn't have fancy GPUs that could do arbitrary rotations. But we did have "blitters" that could copy rectangles of pixels from one place to another. And if you were clever you could persuade them to do a shear at the same time, because it's just offsetting the rows or columns as you go. So using this trick you could get arbitrary rotations done. Now, you are doing three of them for a single rotated sprite, so it's not exactly free, but the fact that you could do them at all was pretty magical. Doing rotations this way also has some very interesting characteristics that doing them with a more general GPU operation does not: 1. There is no "maths" needed on the pixel data. We're just copying bits - there's no interpretation of what the bits actually mean. This means you can do this with any pixel format - it can be bitplaned 4-bit-per-pixel, 8-bit palettised, 565 format, or true-colour - the algorithm doesn't know or care. 2. Every pixel in the source image is there exactly once in the final rotated image. Shears are exact - they copy each pixel exactly once. So therefore the rotated version has every pixel in the source exactly once. There's no duplicates, and none are removed. 3. Therefore the area of the final image is perfectly identical. It has to be - same number of pixels! 4. Therefore there are no problems with "aliasing" from over-sampling or under-sampling data. This is a problem with most image manipulation - if you have a single very bright pixel, as you manipulate the image, sometimes you miss it entirely, sometimes you duplicate it multiple times, and if this happens differently each frame, you get annoying flickers. Doesn't happen here - each source pixel is always present exactly once. (of course you get spatial aliasing because of "the jaggies") 5. It's perfectly reversible. I'm actually not sure how helpful this is in practice, but it's a cool fact! I find it very odd watching the final result, especially as it rotates veeeeery slowly. As it rotates, every pixel is always present - they don't appear and vanish, they just migrate. I find it very difficult to wrap my brain around how they move around the screen in circles, without causing any gaps, and without overwiritng each other. Mesmerising. Anyway, this knowledge is probably of limited use these days - I just thought it was neat, and as I happened to be working on a project that has a blitter but no GPU, I remembered this and decided to implement it - that's where the GIF above comes from. Marge says "I just think they're neat" [https://staging.cohostcdn.org/attachment/774c907e-f9ec-498d-81b0-118a522a2b9a/I%20just%20think%20theyre%20neat.png]

Tom Forsyth on cohost
@TomF the math kinda makes sense to me, since rotations and shears are the same spots in the 3x3 matrix, but the fact that it doesn't lose pixels is so coooooool.
@TomF Well there you go. That *is* neat!
@TomF This is a trick I have learned far too late!
@TomF “each source pixel is always present exactly once” Bwa! I’ve used this trick before, but that part never occurred to me… It simultaneously makes total sense because it’s just repeated shearing, and yet none at all because it seems like it should work like NN…
@slembcke I know right. It hurts my head too.
@TomF @slembcke It isn't _that_ hard to figure out once you know the idea. You can write the matrix of sines and cosines as a 2x2 matrix, and write the product of three separate shears (two in x, one in y) and then solve for the three unknowns (which actually boil down to two unknowns).
@brainwagon @slembcke It's very simple once you know it's possible, yes. That's not the clever bit.
@TomF well... it looks like the custom sprite fpga engine I'm designing for my fantasy console will have rotations too then 😍
@didier If you're designing hardware it is much much much simpler to just have X & Y fixed-point offsets that get merged together. The main reason older HW didn't have them is... er... because they didn't know what to build. Hindsight is a very powerful thing!
@TomF good point... I think designing is a very big word at this point but I'm putting together a fantasy console in an FPGA and I thought it would be fun to learn to design the GPU myself. So much to learn coming from the software side of things... 🤗
@didier Oh yeah it's a super fun journey - I recommend it to everybody. Hardware is so utterly different to software - it's amazing how much you have to reset your thinking. Enjoy!
@TomF this is amazing! it's fascinating to me you need exactly 3 - not 2, not 5, not 100 - no, 3.
@bazkie And that the 1st and 3rd are the same. Wuh? :-)
@TomF yes BUT that makes sense since the 2nd step 'rotates' it, so the 3rd step is not the same axis as the 1st. so that does make sense then :) (still pretty magical tho)
@TomF Dang, I am going to find uses for this. Because of its perfect 1:1 properties, it works on arrays of things that are not pixels and cannot be blended together. That could be significant.

@TomF This is really cool!

I wanted to see it in action so implemented it in shadertoy:
https://www.shadertoy.com/view/dlfXRM

I'm manipulating texture read coords rather than blitting shears but it's basically the same, just backwards.

Anyway, I love that all the pixels are retained! It's amazing that the eyes are never duplicated or lost, and it's fun watching the cheek tetronimoes wiggle about.

This would be super useful for rotating a lowres minimap.

@Farbs Starting with some straight lines is really interesting because they don't turn into diagonals, they turn into shark-teeth. And of course they have to, because the length is preserved but also the number of pixels is also preserved, and the only way you can do that for a diagonal is to bunch it up like that. Very cool - thanks for doing that!

@TomF Even less widely known: it generalizes to higher dimensions and to any volume preserving linear transformation A, not just rotations. So you could use this to linearly transform voxel data or go even higher dimensional. To do that you decompose A (permuting/flipping axes as required) into 3 unitriangular matrices and those are each just a bunch of axis aligned shears (exactly one shear each in the 2d case).

This is the algorithm I came up with a while ago: https://gist.github.com/jix/3df036fba9d1f1a4ae78a40bf6c67aac

unitri.py

GitHub Gist: instantly share code, notes, and snippets.

Gist
@TomF And @rygorous also has a reference for this kind of decomposition (https://core.ac.uk/download/pdf/81971189.pdf https://mastodon.gamedev.place/@rygorous/109740001796717376). I haven't checked if it describes the same idea as the algorithm I ended up using, when I needed this I didn't came across that paper, but it wasn't too hard to come up with something from scratch starting from the LDU decomposition + using the extra matrix to make the diagonal terms all 1. Quite happy to know a proper reference now, though :)
@TomF Three shears for rotation! Hip hip hurrah!
@TomF George Wolberg's "Digital Image Warping". A couple of years of being super useful to obsolete overnight.

@TomF I think the part about aliasing is not true, or at least not how people use the term - no account for temporal aliasing.

The resampling is like translating with a nearest-neighbor filter, which definitely exhibits very strong aliasing (different temporal movement visible than the one in the source data).

@BartWronski I tried to be pretty clear about what sort of aliasing I meant?

@TomF it's somewhat clarified (my initial reaction was "no!", but after re-reading it, I understood what you mean), but for example: "you get annoying flickers" this implies that continuous resampling suffers from temporal flicker/aliasing, and the nearest-neighbor not; while the opposite is true?

Anyway, I don't want to be nitpicky about it, the observation of a bijective mapping and no resampling is really cool.

@TomF
new guy: Hey what's the deal with the renderer, they's no rotations, only three shears?

someone else: He doesn't know how to use the three shears!

(the whole studio erupts in laughter)

@aeva @TomF *nonchalantly walks past, discreetly dropping this off* https://core.ac.uk/download/pdf/81971189.pdf keep on keeping on, kid
@aeva @TomF I love this one because you might write this off as a curiosity that happens to work in 2D and 3D, but nope, it's always 3 shears, works in any dimension on almost all unit (i.e. area/volume/hypervolume-preserving) matrices, which is wild to me considering what a large class of transforms that is
@aeva @TomF not your usual math paper, this one goes into the image rotation application among other things! Also features this gem [sorry no alt text, but this is a scanned doc and I don't have OCR SW]
@rygorous @aeva Wait it's always 3 *however high the dimension*? That is indeed wild.
@TomF The game graphics people might have forgotten, but this is still bread-and-butter for CPU-based image processing libraries!
@TomF this is a cool reminder, and also a good push that I should really just dumpster those Graphics Gems books I've got on my shelf... Sigh.
@TomF I'm fairly old so I knew about this/used this in the late 1980s (but haven't had the opportunity to do so since). I believe this was first published in 1986 by Alan Paeth, in "A Fast Algorithm for General Raster Rotation", which I found more useful than Fant's "A non-aliasing, real-time spatial transform technique." which can handle scales and subpixel filtering, but finds more work to get right.
@TomF What sorcery is this?!