Speed up 3d in TIC-80, or reimplementing decades-old tricks from scratch

My game is made in TIC-80 fantasy console, and uses 3d "wireframe" graphics to draw everything. It looks fine and all, but TIC-80's Lua interpreter can't do much math and line drawing per second, especially in Web build. So I took several steps to optimize rendering, which I wanna demonstrate below. These are old tricks, but it's still kinda fun to watch how they work. Hope it'll be fun to follow my steps.

To demonstrate, I've added two debug prints to the game: line count and FPS count.

So, at the beginning, it looked like this:

init_3.gif

Initial state: max = 470 lines per frame

Here it shows ~60 FPS, but things were much worse in Web builds. Moreover, rails are disappearing!

Each rail is just a very long 3d line, and I can't render a line if one point is behind a camera. So they disappear as soon as camera is over. At first, I decided to split each long 3d line into several short ones.

Complex rails: max = 700 lines per frame

Here things started to get slow. Web version would be dead at this point. Also, lines still were disappearing right in front of a camera. Not cool.

So I wondered. If part of the line is behind a camera. Can I just draw another part, which is in front?. Well duh! Assume we have two points, p1 and p2, with coordinates (x1,y1,z1) and (x2,y2,z2), and p1 is behind a camera (so z1 < 0). Then we can calculate how much of a line we should draw, which is z2 / abs(z1-z2). Muliply line length by that, calculate new position of p1, and boom!

Back to ~470 LPF

Next up: drawing distance. That was easy: object too far? Don't draw it! I selected drawing distance = 100. Everything with avg(z1,z2) > 100 is ignored.

Drawing distance: 400 LPF

It still maxes out at around 400 lines, but on average is well lower than before.

Next up: Viewport! So, from side to side, the frame in a game is around 100 degrees. What if angle between given point and a camera is larger than that? Well, it won't be visible then! So why render it? If angles from camera to both points of the line were larger than 120 degrees, I didn't draw it. Calculating angle difference was a bit hard, even StackOverflow got me useful answer from the third time, but at the end everything worked fine.

Angle distance: max = 200 LPF

Huh, we just halved our lines with this one! But can we do better? Yes we can! Models simplification! When object is at a distance, we can draw it with less lines! For example, each rail is actually not one, but two lines: top and bottom ones. We can skip one of these if rail is far enough. To do this, I mark lines which can be hidden with a flag, and then check how far they are. This allow me to save couple more math operations and line drawings:

draw_angle.gif

Model simplification: max = 150 lines

Bonus: change line color depending on it's distance from the camera!

Yeah, we can paint lines more pale to show that they are at a distance, it's pretty easy but looks cool.

So, that was fun to code, and fun to learn new stuff. Hope you enjoyed this article, and now you better understand what is going on under the hood.

You can check out my game and see everything for yourself here

PS: It still works kinda slow on Firefox. Should I implement fixed-point maths and pre-calculated sin/cos tables next time?