Microcontrollers do not have GPUs. So when a small board draws a spinning 3D model on a panel, every triangle in that model was set up, clipped, binned and filled in software, by the same core running your application code — which is a fundamentally different engineering problem from anything on a desktop, and a more interesting one.
a3d, by Eric (@0015), is a header-only C++17 renderer that does exactly this on an ESP32 and pushes the result to a panel.
That Project — Eric’s own channel — demonstrates animated low-poly models running on hardware.
The interface design is the story
Two decisions define the library, and both are about not owning things it doesn’t need to own.
A panel is five function pointers. Not a driver, not a class to inherit, not a supported-displays list. Five functions. Which means a3d works with whatever display you already have working — SPI TFT, parallel RGB, a LED matrix, an e-paper panel if you are feeling perverse — without the library needing to know anything about it. This is the difference between a library you can use and a library you fight.
Threading is one interface you implement with FreeRTOS, std::thread, or nothing at all. Single-core, dual-core, or run it synchronously in your main loop — a3d does not impose a concurrency model. On embedded, where the application usually already owns the scheduler and the timing, that restraint is worth more than a feature.
Header-only removes the build-system negotiation entirely. It works as an ESP-IDF component and as an Arduino library — two ecosystems that normally require separate packaging.
The vertex ceiling is 65,535 per mesh, which is a 16-bit index and exactly the right limit: far beyond what an ESP32 can push in real time, so it will never be what constrains you.
What it deliberately does not do
No PBR. No shadow mapping. No alpha blending. No post-processing.
That list is the most valuable part of the README, because it is a specification rather than an apology. Every item on it is expensive in a specific way:
- PBR wants per-pixel material maths and an environment lookup. On a software rasterizer with no floating-point throughput to spare, it is not close.
- Shadow mapping needs a second render pass into a depth buffer you then sample. On a board where the framebuffer itself is a meaningful fraction of RAM, a shadow map is a non-starter.
- Alpha blending means read-modify-write per pixel and, worse, sorting geometry back-to-front per frame.
- Post-processing is a full-screen pass over a buffer you can barely afford to hold once.
What is left — flat or Gouraud-shaded triangles, a depth test, a transform pipeline — is the subset that actually fits, and it is the subset that 1995 shipped games with.
The benchmarks are measured
From the release notes: the performance tables were generated from device logs on real boards, not estimated.
That is a small sentence and it should be the default. Embedded graphics claims are usually quoted from a best case at a convenient resolution with a convenient model, and “estimated” is doing heavy lifting. An animated fox at 240×… (the figure varies by board and panel) measured from logs is a number you can plan around.
What you would build with it
The honest answer is interfaces, not games. An ESP32 rendering low-poly 3D in software is not going to run a game loop with input and audio and networking alongside. What it is very good for:
- Instrument and device UIs that want a rotating object — a 3D model of the thing being measured, an orientation indicator driven by an IMU, a product visualisation on a shelf display
- Kinetic sculpture and installation panels where a small board drives a screen and nothing else
- Retro-rendering pieces where the aesthetic is the constraint — flat-shaded low-poly is a style with a large and enthusiastic audience, and here it is not pastiche
- Teaching — a software rasterizer you can read in a few header files, running on a board a student can hold, is a better introduction to how the graphics pipeline works than any desktop API
The broader point: the ESP32 line has been quietly crossing thresholds for years — enough RAM for a framebuffer, enough clock for a transform pipeline, parallel display interfaces fast enough to push the result. a3d is what it looks like when someone takes those thresholds seriously and writes the library that fits inside them rather than the one that wishes it were somewhere else.