Teensy 3D Tests

A project log for Arduino Minecraft

Making a Minecraft clone that runs on the Teensy 4.1 in the Arduino environment.

dylan-brophyDylan Brophy 05/19/2024 at 17:540 Comments
Framebuffer produced by a test on the Teensy 4.1. Split image for raw hex data vs rendering.


I found out about the Teensy 4.0 shortly after it came out, and it quickly became my favorite MCU due to the wide range of capabilities, incredible horsepower, and low price.  I think that MCU marked a huge change for the Arduino platform, because it was such a leap forward in processing power - it opens up some interesting avenues in the Arduino environment that just weren't open before.  Since then I've hacked together a few interesting devices with the Teensy 4.1, but I haven't done much on these devices.  They haven't really done anything cool.  Part of the issue, is that I didn't build any great devices to run anything on.  Everything sorta had problems or was hacked together.  So, in the latest iteration of the #Arduino Desktop project, I designed a new platform that I can actually build some cool stuff on.  We also have a new #uPD7220 Retro Graphics Card (and VGA hack) which I designed, which goes with the new Teensy system.  With these in mind, I can contemplate a lot more interesting software to load to the Teensy: Namely, a 3D game.

Is the Teensy fast enough?

This is the first big question.  With speeds approaching old phone processors, which ran Minecraft Pocket Edition, it feels like there's a chance; but I imagine those phones had hardware to accelerate graphics.  We don't have that.  So, I've translated some of my Java LWJGL code into something arduino compatible, and ran some speed tests:
Note: 720Mhz overclock for these tests
As you can see, we can draw a quad in less than 0.25ms.  This includes a basic fragment shader, which handles basic transparency and blending, as well as an 8-bit depth buffer.  These numbers should be realistic enough, as almost every part of the render pipeline is accounted for - the only important omitted part is sending pixel data to the GPU.  That is a big concern though, as it could eat up a lot of CPU time that could be used for rendering.  Another thing to note, is that each quad drawn in the quad test is 30x30 pixels, which is really quite large.

Considering the above numbers, we can draw about 65 30x30 pixel quads every frame, if we want to hit 60hz.  That's not much, but it's certainly something to work with.  Each block is drawn with 3 quads, but at least one quad is almost always very small, so we can call it 2 quads - this gives us about 32 blocks we can render each frame at 60hz.  I'm sure there's some optimizations or hacks I can do to improve this - it's a work in progress.  Nonetheless, this is certainly enough to run a (small) 3D game, no?

Other math: I want a display with about 192000 pixels.  If about 2/3 of the screen is to be rendered on average, then this gives us 128k pixels to render every frame.  Each pixel is rendered as one or more fragments, so we have a minimum of 128k fragments to render.  Each fragment takes about 0.3us to render when including quad processing, so the minimum render time should be approximately 38.4ms, or 26 FPS.  That's actually not that bad, all things considered; I expected it to be much lower.  I mean, I wouldn't notice that FPS; I used to play Minecraft at 14 FPS when I was a kid :)

Display Limitations, Color, and Loading Minecraft's old terrain.png File

To make things extra fun, I'm going to run this with a uPD7220 GDC to handle the video output.  Due to limitations of graphics memory size and bandwidth, we only get 16 colors.  We can't use 24 bit color anyway, as the Teensy 4.1 would run out of memory.  Also, for textures and the framebuffer, the Teensy 4.1 has to track transparency along with the color.  Thus:

I'm using the odd 8-bit format above because it's simpler to implement, and because having more colors is of no advantage since I cannot display more than 4 bits of color anyway.

Another issue is loading the Minecraft terrain.png.  Typically we load BMP files to get images into the Arduino environment, but these are uncompressed and don't have an alpha channel.  PNG seems complex to load, and I didn't want to deal with it.  For these reasons I created a simple image encoding and compression scheme which is easy for an Arduino to load, and I'll attach a Python script that converts the terrain.png to the .nif format for you.  I don't think it's legal for me to distribute the terrain.png or a derivative like the terrain.nif image data, but it is easy to find online for you to convert.  Or you can load whatever 256x256 image suits you.  Anyway, the code loads the terrain.nif into a 256x256 byte image buffer in the 8-bit format described above.  Some of the textures look good, some don't, but almost all are recognizable in the 16-color format:terrain16.png
terrain.png, in 16 colors
I think that, ideally, a new terrain.png file would be made specifically to work with 4-bit color.

Without a Display

I don't actually *have* any of the Teensy hardware I want to run this on yet, so I'm actually running all the tests only through the Arduino serial monitor:Since each hex character is 4 bits, it is perfect for outputting our 4-bit color data.  The above image is the top-left corner of the first image in this log - but represented in hex.  To see the output from the render code, I have to copy and paste the serial output into a Python script which displays the image using matplotlib.  This will work until I get my graphics hardware setup with drivers written; I only need to see one frame at a time for now.  I can measure the FPS without seeing the output too.

Well, I hope you found this at least interesting, and stay turned for further developments.  I'll be working on getting an actual render pipeline working, with a camera and scene.  At some point we'll get very basic worldgen too.