Since Voxatron’s scripting update is still not out, let’s just bring Voxatron to Pico-8!
If you were already following me in January 2017, you probably heard of Tiny-TV Jam! Tiny-TV Jam was a gamejam where you had to make a game that could be played at a resolution of 10×11 on the screen of a tiny voxel TV, in Pico-8. The jam was hosted on the Lexaloffle by yours truly and you can (and should) check out all the entries there! (there’s 2 pages) It turned out that people had a lot of fun modeling their TVs and some of the TVs were actually super cool!! (and the games were super interesting as well!!!)
Today I want to share with you how I did the voxel rendering in the first place!
And everything began with PROCJAM 2016! I wanted to make procgen voxel islands in Pico-8 and I needed voxel rendering!
The main reason I wanted to do voxel in Pico-8 was that I had seen a gif of voxel in Pico-8 on Twitter and I thought it was really cool. In fact, the gif was a wip of Rez‘s game ZEPTON which you should totally check out!
What I found out while looking at the gif is that they weren’t rendered as cubes but only as squares. And you probably guessed it, squares are WAY cheaper to draw than cubes!
So I set off to do my own voxel renderer and here’s what I had three days later:
Let’s start by the simplest part, the voxel data was drawn directly in the sprite-sheet, as layers of 16×16 cells and it is being read at runtime using ‘sget()’.
Surprisingly enough, this didn’t require too much maths. Of course there was a bunch of trigonometry to calculate the position of each square but it was nothing against a pen, some paper and some patience. But doing all the trigonometry for every cell was very CPU-heavy and it turned out that it could be optimized very easily!
So here’s what I did!
local ocx=cos(cama) local osx=-sin(cama) local ocy=cos(cama+0.25) local osy=-sin(cama+0.25)
‘cama’ is the angle of the ‘camera’ (which is completely an abstractized here) around the center of the sculpture. Those variables are the only trigonometry we need to draw our whole voxel model!
With these values, we can effectively translate any coordinates in our voxel model to coordinates in our rendered voxel. Here are the maths:
--xx and yy are our coordinates on a 16x16 voxel layer local cx,cy=xx-7.5,yy-7.5 local x=3.99*(cx*ocx+cy*ocy) local y=1.2*(cx*osx+cy*osy) x+=64 y+=ly --ly is the height of the layer currently being drawn --finding our cell's color in the sprite sheet local c=sget(xx+(l%8)*16,yy+flr(l/8)*16) --drawing the cell (if it's not blank) if c>0 then rectfill(x-2,y-2,x+1,y+1,c) end
Do this for every cell in every layer and you have all your cells drawn!
Except for one small detail. Because you’ll always be drawing your cells in the same order, this works well only at a certain angle. If you take the opposite angle, you’ll have cells from the back being drawn after (=above) cells from the front, and that will look terribly wrong. Just look.
So we need to draw them in an order that makes sense with the camera angle! No problem, all we need is a way to change the order of our cell drawing loops and then figure out what maths and conditions to set in order to get the right order every time.
Here’s something you may not know: in Lua too, for loops can go both ways. Most of the time, when you use a for loop it goes ‘for i=start,end do’ and all’s clean and good. But you can also do ‘for i=start,end,pace do’ and ‘pace’ here will be added to ‘i’ at the end of every iteration. You can set ‘pace’ to a negative value, which will reverse the sense of the for loop and you’ll need to have a ‘start’ higher than the ‘end’. (I hope that makes sense but if it doesn’t, feel free to read that paragraph again)
We can put this to good use here by setting ‘start’, ‘end’ and ‘pace’ as variables, with one set of variables for the ‘x’ loop and another one for the ‘y’ loop. To get one of the loops in the regular order, you’ll have ‘start=0 end=15 pace=1’ and for the reversed order you’ll have ‘start=15 end=0 pace=-1’.
That’s our way to change the drawing order, now we need to find the right drawing order. You could probably solve that with paper, pen and patience but personally I got this one with trial-and-error and I prefer to spare you the pain, so here is the code you want!
local xstart,xend,xpace if cama%1>0.5 then xstart,xend,xpace=15,0,-1 else xstart,xend,xpace=0,15,1 end local ystart,yend,ypace if cama%1>0.25 and cama%1<0.75 then ystart,yend,ypace=15,0,-1 else ystart,yend,ypace=0,15,1 end for l=0,15 do local ly=96-l*4 for xx=xstart,xend,xpace do for yy=ystart,yend,ypace do --do the cell drawing here! end end end
If you put that together with the previous bit of code for drawing the cells individually, you can now render voxels! Just draw random stuff in the first tab, run the code and your random stuff is now in 3D and it even runs correctly at 30fps!! Here’s some cake for getting there!
But there’s more! For my Island generator, I needed to modify the voxel data at runtime. Since our voxel data is stored in the spritesheet, I can just go and ‘sset()’ the cells I want!
The algorithm I used to generate the island itself is some custom randomized 2D cellular automata with each layer being the next step from the previous one!
To make it really interesting visually, I made it so that the island would still get rendered while being generated, so you can actually watch the generation happen! (people love to watch procedural generation happen) Here’s the final result!
You can go and generate some islands over there!
Months later, while preparing Tiny-TV Jam, I found that the voxel rendering could be optimized by storing the voxel data in a 3D table grid! ‘sget()’ is pretty slow because of all the checks it does on whether or not the coordinates you give are valid and then actually reading the pixel. Accessing tables is much faster!
All you have to do is create your table structure and then fill it with the voxel data from the spritesheet! Your structure should be a table of layers which are tables of lines which are tables of cells, in such a way that you can access your cells with ‘data_grid[l][x][y]’.(that’s what you’ll use instead of ‘sget()’)
And for our final trick, the tiny-TVs have dynamic screens!
This is also quite simple! In the voxel data, the screen is of a certain color. When drawing the voxels, we’ll check whether the cell being drawn is of that color. If it is, we look up the corresponding pixel on our tiny screen, which we drew on the sprite-sheet beforehand. And that’s it! (there’s actually a bunch more checks to make sure the cell is in the screen but let’s not care about that)
Voxels can be drawn as squares and no one will notice at first glance! You only need a little maths to find the projected coordinates of all the voxels on your screen! The drawing order of the voxels is super important but can be managed quite easily! Store your voxel data in the spritesheet and you can modify it at runtime like it was nothing! The whole thing can be optimized by putting the voxel data in a table rather than in the spritesheet directly! Have fun with the code and you could end up with a tiny-TV on which you could play awesome tiny games!
Before I go, let’s quickly bring up the fact that the ‘camera’ only rotates on one plane. In theory, it could be made to rotate on a second plane with some more maths but it might well be more complicated than you think and I was lazy so I didn’t even really try. :X (hit me up if you manage to do it, that would look super cool!)
These Doodle Insights are here thanks to the awesome people supporting it on Patreon! Here are their names!
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, Anne Le Clech, Flo Devaux, babyjeans, Emerson Smith, Cathal O’Keeffe, Dan Sanderson, Andrew Reist, vaporstack, Dzozef, Cole Smith, Jared Butowsky, Tony Sarkees and Justin TerAvest!
Thank you for reading and enjoy the Pico-8 voxels!