These Doodle Insights are brought to you by my super generous patrons on Patreon!
This is the second part about reading and writing to the sprite-sheet in Pico-8. If you didn’t already, it is recommended to read the Doodle Insights #13, which is the first part, before reading this one.
Last week we saw the different methods to write to the sprite-sheet in Pico-8. We saw that it was easy and fast. And easily writing to the sprite-sheet allows us to do one particular type of optimization: pre-rendering! Today we’ll be seeing one classic use of pre-rendering and then some more unorthodox ones.
Starting off directly with a doodle!
In this doodle, the flower buds are being rotated at runtime. The algorithm doing the rotation is fairly straight-forward: we define a square in which the rotated sprite should fit and for each pixel of that square, we find the corresponding pixel on the original sprite. This method works great but it uses a lot of trigonometry and you probably know how much the CPU hates trigonometry. That is why there’s always only 6 flowers in this doodle, one more and we would be dropping frames.
Here’s the rotation code, in case you wanted to see it.
--dx and dy are where you want your rotated sprite --sx and sy are where your original sprite is --a is the rotation angle for x=0,7 do for y=0,7 do local aa=atan2(x-3.5,y-3.5)+a local l=sqrt(sqr(x-3.5)+sqr(y-3.5)) local ox=3.5+l*cos(aa) local oy=3.5+l*sin(aa) ox,oy=round(ox),round(oy) local c if ox<0 or ox>=8 or oy<0 or oy>=8 then c=0 else c=sget(sx+ox,sy) end pset(dx+x,dy+y,c) end end
Luckily, there’s a very simple way to optimize this!
Since we’re working with low-res sprites, the difference between two rotations of slightly different angles is only of a few pixels if even existent. Truth is that with 8×8 sprites, you only need 16 different rotated frames to have a rather smooth full rotation animation. Good thing we have a sprite-sheet where we can render our 16 frames to be drawn with ‘spr()’ later on!
This is exactly what’s happening in the ‘_init()’ of this doodle! Except we’re not rotating just one sprite but eight! These eight sprites are the 3D rotation, drawn manually beforehand. They are disposed in a column at the very right of the sprite-sheet. For each sprite, the 15 rotated frames are drawn on the line of the original, so that all the frames are set in a 16×8 grid.
Each pill object has two angle values, one for the original manual rotation and the second for the one generated by our algorithm. Both angles change constantly in the ‘_update()’. When drawing one of these pills, we check the first angle and figure out what line of our sprite grid that corresponds to, then we check the second angle and figure out what column that corresponds to. Our sprite index is ‘s=lin*16+col’.
In this particular doodle, each pill is even drawn two supplementary times with palette swaps and because we’re using ‘spr()’ it’s no problem at all for the CPU. The bubbles take more CPU than the pills.
This one is very much the same, with a manual 3D coin rotation being rotated in 2D by code in the ‘_init()’. But in this doodle I needed more space in the sprite-sheet for regular sprites and also the tiny procedural dithering frames on the right side of the machine. So instead of pre-rendering a full rotation we’re just doing half of that, generating 8 sprites for each frame of the manual rotation instead of 16. When we would have used the second half of the rotation, we just use the first half again except we reverse the direction of the animation with a little bit of maths.
Simple shapes like these coins can often be optimized further with this kind of thing, but also with the flip_x and flip_y arguments of the ‘spr(s,x,y,w,h,flip_x,flip_y)’ function, which will flip the sprite horizontally and vertically.
Rotated sprites are really useful and make for really cool effects! Because the sprite drawing functions ‘spr()’ and ‘sspr()’ are really light on the CPU, you can easily have a ton of rotating sprites on your screen. Add palette swaps for graphic diversity and you could make some really interesting scenes!
But sprite rotation is far from being the only use for pre-rendering. Anything that’s slow to render and small enough to fit inside the sprite-sheet can be considered a good candidate for pre-rendering.
This is a fun one!
Rendering just one crystal frame can be done in the _draw() at 30fps no problem. But I wanted lots of crystals.
The rendering of a crystal frame is mostly maths and magic numbers used in a linear generation. Also we’re only rendering the top half, as the bottom half is just the top half again but flipped vertically. One top half is 32×32 pixels and that means we can have 16 of them in the sprite-sheet, which is just enough to have a smooth animation at that sprite resolution, given we only have to do 1/6 of a rotation for our rotation loop!
So we render our 16 top half for 16 steps of a full 1/6 rotation, on the screen because we’re using the ‘line()’ function a lot. We do a ‘memcpy’ to the sprite-sheet, like we saw in the Doodle Insights #13. But then we only have halves of crystals in the sprite-sheet and we have to use the ‘spr()’ function to draw a full one, except that we will also want to resize that crystal to get that depth effect and we need to use ‘sspr()’ for that. And we don’t have any spare space on the sprite-sheet. Or do we? Well technically we don’t, but there is space in the rest of the Pico-8 ram memory so we’ll use that!
At the end of the ‘_init()’, the crystal halves are copied to 0x2000 (memory for the map and sound effects I think) instead of 0x0000 (the sprite-sheet). At the start of the ‘_draw()’, we copy our crystal halves from 0x2000 to the sprite-sheet. Then we draw 4 full crystals of different rotations on the screen with ‘spr()’. We ‘memcpy()’ the screen to the sprite-sheet. Finally we use ‘sspr()’ to draw lots of crystals, using ‘pal()’ for color diversity.
So basically, we pre-rendered crystal halves which we then used to pre-render full crystals which we then used to draw all the crystals. Saving our crystal halves to a unused space in the Pico-8 ram and ‘memcpy()’ being as fast as it is make this work.
I really like the ‘memcpy()’ function.
Back to something more simple!
No pre-rendered animations here, just one big pre-rendered 128×128 lighted orb! That orb is being resized with ‘sspr()’ like the crystals previously, except that the original is as big as it can be and that lets us resize it to any size we want from 4 to 128 pixel wide, with fairly smooth results! It’s also much faster than I used to think before making this doodle and can draw tons of tiny planets/asteroids!
This one actually uses the exact same pre-rendered 128×128 lighted orb. I don’t think I can explain the whole reasoning behind this trick but here it is put simply. each horizontal slice of the vase corresponds to a slice of the orb. All we have to do is find which slice that is and then draw it resized to the width of the vase on that line. To find what slice of the orb we want, we check the upper and lower slices of the vase to find a tangent to the current slice. Using its normal vector, we can find that tangent again on the orb and the slice that goes with it. It’s ok if you didn’t understand this paragraph.
This doodle shows that pre-rendering can also be used in more imaginative ways, while remaining very fast. You have to be brave with the maths though.
Pre-rendering is super useful. In fact it’s also used in other engines than Pico-8 and particularly in 3D games, to render things like big forests for example. And Pico-8’s sprite-sheet reading and writing lets us do it very effeciently! It can be used to rotate sprites, generate 3D animations, generate sprites to be resized, and can even be used more creatively for weird vase rendering and hopefully other stuff! Just draw the things to the sprite-sheet and then use ‘spr()’ or ‘sspr()’ to draw them as many times as you want on the screen!
That’s it for today’s Doodle Insights! I hope you enjoyed it! Next week we’ll be looking at masking, layering and other stuff we haven’t seen yet for the last part about sprite-sheet reading and writing in Pico-8!
The Doodle Insights series is possible thanks to the support of my awesome patrons on Patreon, whose names are written below!
Joseph White, Adam M. Smith, Ryan Malm, Matthew, Giles Graham, Luke Davies, Jake Meanwell, Tim and Alexandra, Sasha Bilton, berkfrei, Nick Hughes, Christopher Mayfield, Jearl, Dave Hoffman, Thomas Wright, Morgan Jensen, Zach Bracken, Cole Smith, Paul Nicholas, Marty Kovach, Anne Le Clech, Flo Devaux, babyjeans, Emerson Smith, Cathal O’Keeffe, Dan Sanderson, Andrew Reist, vaporstack, Dzozef, Jared Butowsky, Tony Sarkees and Justin TerAvest!
If you like this series, please consider supporting it as well, that would help me a lot!
Thank you for reading and enjoy the pre-rendering!