3D floor rendering in Pico-8

Background

Pico-8 rules. A fantasy console giving life to a wonderful alternate universe with an open console lying somewhere between 8 and 16 bits. As such, it’s great at rendering tons of sprites (2D images) quickly and throwing them around the screen. Even so, people have pushed it to do things it wasn’t intended to do, including 3D rendering. Dipping my toes in these waters has been the most fun hobby programming I have done in yeeeeeeaaaaaars.

Raycasting

My first encounter with raycasting as a serious programming proposition was when Matt Hughson posted this. Thanks to Wolfenstein 3D-like rendering and a link to a tutorial — suddenly 3D (2.5D, whatever) rendering from scratch was within reach.

So I followed the tutorial (from lodev.org) and it’s follow-up and the third follow-up. I implemented the raycasting renderer in Pico-8 and on Android (using AIDE on my commute). Everything was great whilst I implemented the first tutorial. The walls can be rendered using Pico-8’s sspr() function which can quickly render scaled vertical slices of the spritesheet — and you only need to do it 128 times per frame. When it came to the floor though (the algorithm is explained here), I ran into problems. The method in the tutorial calculated the floor texture texel colour for each pixel between the bottom pixel of the wall to the bottom of the screen. Using Pico-8’s pset(), this was veeeeeeery slow. Over the course of the next {insert-long-period-of-time-here} I improved the speed by around x2.

Code

You can see some code which implements most of what I talk about in this post on Github. There’s a few more optimisations but I’ll go over them next.

From slow to fast

Pico-8 runs at 30fps (60fps is possible, but is not supported on all versions), so that’s my goal. What follows are the steps I took to get to a stable frame-rate.

Version 1: Just fill every vertical line from a few pixels (10) beneath where the wall ends. (CPU at 1.582 — any more than 1.0 and the frame rate automatically drops to 15fps)

This version had really obvious artefacts as well from fixed-point number inaccuracy.

Version 2: This is when I started thinking about the problem. How could I make less calculations? Easiest way is to only render every other line using pset() — thus removing half the texel lookups. But now there are big gaps in the floor. (CPU at 1.004)

Around this time I start to play around with exactly which points of the floor I draw and come up with a new method, by skipping vertical lines and using line() to start to fill the space. It looks a bit weird though and lots of space appears unfilled. This would prove to be “on the right track” though.

Version 3: Now with the idea of using line() to cover more space, I wonder if using a checkerboard pattern would work (as in the pixels chosen to work out the floor for).

At this point, still rendering most of the floor pixels. So why not restrict that to give a depth-of-field/darkness effect?

Version 4: This just uses sin() to give a nice curve for the edge of the ‘detailed’ section of the floor. Also shaves a load off the CPU.

Still calculating the texel too many times. What we really want, is to halve that again.

Version 5: This is done by only drawing the floor for every other vertical column. A value to calculate offset to allow the checkerboard has to be incremented as the walls are rendered, but it’s a small price to pay for a huge drop in CPU. This offset value increases by one for each rendered column, and the value of ‘offset % 2' is used to start each column in the right place.

You still get a really nice impression of the floor textures too. But that can be improved, and now there’s headroom from the CPU to do fancier stuff.

Version 6: Using line()s of length 4, the gaps between the sparse pixels can be filled. It’s not even that expensive…

Version 7: …but can be improved on by swapping the line()s for rectfill()s. Whether this performance bump will remain in future versions of Pico-8 is to be seen, but for now it’s most welcome.

Version 8: As a finishing touch, the big block of navy blue in the middle of the screen can be filled using the sparse checkerboard pattern seen in Version 5.

This makes the further away areas of the floor look darker and less detailed, which helps the overall look, and reduces visual noise of the colour of pixels changing every frame as you move. The CPU cost has increased, but the finished product looks really nice.

Other Optimisations

I did a few other things to make floor rendering really speedy.

  • Split the near/far floor rendering into two loops so no more if-statements in a loop covering the whole floor
  • Dumped the 2D map of floor-texture indexes into Pico-8 user memory space to avoid costly table lookups for a large number of pixels
  • Pre-calculate as much as possible
  • Render the walls from the bottom of the screen to the limit of the wall. The tutorial I read has it going the other way, but this requires a calculation of the position of the wall. You can work out the floor texture relative to the player for less cost
  • Passed around all the data as function parameters. I started off with the 2D world array and player referenced (and everything, really) as global variables but this is slow in Lua — local variables are much faster!

Conclusion

The CPU values are relative as some of the optimisations above are in play. I hope it still gives you an idea of what is achievable by making some compromises, and avoiding doing as much maths as possible.

I went from this…

…to this…

…and got a >x2 speed improvement for a (reasonable) dip in visual fidelity. I hope this has been of some use to you, and even if you don’t fancy doing some raycasting I think these techniques can be used elsewhere too! :) One thing I think would be cool, would be to remove the walls, and just use the floor rendering to get a Mode-7 effect!

Fairly recent code available on Github here! Better code coming soon.

Other people doing similar kinds of engines

Lots of other people have been doing similar work and I definitely have been inspired by these and others!

If you’re interested in seeing what other people have done implementing this sort of Wolfenstein 3D engine in fantasy consoles, check these Twitter accounts out:

Also check out lodev.org for some great graphics tutorials that I found really useful