Introduction
In the summer of 2026, the maker community is buzzing with a new trend: vibe coding. Popularized by AI thought leaders, this approach involves using AI assistants (like GitHub Copilot or ChatGPT) to generate code in a creative, flow‑state manner — letting the AI handle boilerplate while you focus on the fun parts. Combine that with the explosion of affordable, powerful microcontrollers and small colour displays, and you get a perfect playground: building a tiny 3D renderer for a handheld device. No GPU, no OpenGL — just raw rasterization running on a chip that costs less than a coffee.
This article will walk you through the entire process: from choosing hardware to writing the core rendering pipeline, all while leveraging the vibe coding methodology to accelerate development. By the end, you’ll have a working 3D renderer that runs on a device you can hold in your palm — and you’ll understand the principles that power every modern graphics engine.
Why a Tiny 3D Renderer?
Handheld gaming consoles like the Game Boy or PlayStation Portable inspired a generation. Today, with microcontrollers like the ESP32‑S3 or RP2040 and 1.8‑inch IPS displays (240×320 pixels), you can recreate that magic from scratch. The challenge is that these devices lack hardware acceleration for 3D — every transformation, clipping, and pixel draw must be done in software. That’s where the real learning happens.
A software renderer teaches you:
- Linear algebra in practice (vectors, matrices, transforms)
- The graphics pipeline (model → view → projection → rasterization)
- Performance optimization on constrained hardware
And with vibe coding, you can prototype iterations in minutes rather than hours.
Hardware You’ll Need
| Component | Recommended Model | Purpose |
|---|---|---|
| Microcontroller | ESP32‑S3 (e.g., LilyGO T‑Display) | 240 MHz dual‑core, 16 MB flash, 8 MB PSRAM |
| Display | ST7789 1.9" IPS (170×320) or ILI9341 2.8" (240×320) | SPI interface, 16‑bit colour |
| Input | 2 buttons + joystick (or built‑in on some boards) | Camera control |
| Power | 16340 Li‑Po battery | Portability |
The ESP32‑S3 is ideal because of its higher clock speed and extra PSRAM for framebuffers and depth buffer. We’ll use the popular TFT_eSPI library for display communication, along with a custom software renderer written in C++.
Setting Up Your Vibe Coding Environment
Vibe coding relies on an AI assistant that understands your codebase. For embedded C++, the best setup in 2026 is:
- VS Code with Continue or GitHub Copilot Chat
- A local LLM (e.g., Llama 3.1 70B) for privacy, or a cloud model like Claude 3.5 Sonnet
- The Arduino IDE or PlatformIO for building and flashing
Start with a simple sketch that initialises the display and draws a 2D pixel. Then, ask your AI to “write a function that draws a filled triangle using Bresenham’s line algorithm and scanline filling.” It will generate the code, and you can verify its correctness on the device. This rapid feedback loop is the essence of vibe coding.
The Math Behind the Magic
Coordinate Systems and Transformations
A 3D renderer transforms points from object space → world space → view space → clip space → screen space.
We use 4×4 homogeneous transformation matrices:
- Model matrix: rotates, scales, translates the object.
- View matrix: places the camera in the world.
- Projection matrix: applies perspective (or orthographic) mapping.
For a simple perspective projection, the matrix looks like:
proj = {
{ f/aspect, 0, 0, 0 },
{ 0, f, 0, 0 },
{ 0, 0, (far+near)/(near-far), 2*far*near/(near-far) },
{ 0, 0, -1, 0 }
};
Where f = 1/tan(fov/2). This is standard – you can find the formula in any graphics textbook (e.g., Real‑Time Rendering, 4th ed.).
Clipping and Viewport Transform
After projection, vertices are in clip space (x,y,z,w). We clip triangles that cross the near plane or frustum boundaries. Then we divide by w to get NDC ([-1,1]), and map to screen coordinates:
screen_x = (ndc_x + 1) * 0.5 * screen_width;
screen_y = (1 - ndc_y) * 0.5 * screen_height; // flip y
Step‑by‑Step: Building the Renderer
We’ll implement a pipeline for triangle mesh rendering. Code snippets here are meant to be adapted – use your AI assistant to expand them.
1. Vector and Matrix Library
Write a minimal vec3, vec4, mat4 with multiplication, cross product, dot product. Example:
struct vec3 { float x, y, z; };
vec3 cross(vec3 a, vec3 b) {
return { a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x };
}
2. Triangle Rasterization
For each triangle, we need to:
- Transform its 3 vertices through the complete pipeline.
- Clip against the near plane (w < near).
- Project to 2D.
- Draw using a scanline fill.
A robust scanline algorithm loops over each scanline between minY and maxY, finds left and right edges, and fills pixels. Use fixed‑point arithmetic for speed – floats are slow on ESP32.
3. Depth Buffer
Allocate a uint16_t array of width*height and initialize to 0xFFFF. When rasterizing a pixel, compute its interpolated 1/z value (world‑space depth) and compare. Only write if the new depth is smaller. This prevents distant triangles from overwriting closer ones.
4. Shading and Colour
For a flat‑shaded renderer, compute the face normal in world space. The light direction is a fixed vector (e.g., normalize({0,0,1})). The diffuse intensity = max(0, dot(face_normal, light_dir)). Multiply by a base colour (e.g., 200,100,50). Output a 16‑bit RGB565 pixel to the TFT.
Code Example: Putting It Together
Below is a stripped‑down loop for one frame:
void drawFrame() {
clearDepthBuffer(0xFFFF);
tft.startWrite(); // SPI transaction
for each triangle in mesh {
// 1. Transform vertices
vec4 v0 = model * triangle[0];
vec4 v1 = model * triangle[1];
vec4 v2 = model * triangle[2];
// 2. View transform
v0 = view * v0; v1 = view * v1; v2 = view * v2;
// 3. Project
v0 = proj * v0; v1 = proj * v1; v2 = proj * v2;
// 4. Clip if all w < 0
if (v0.w <= 0 && v1.w <= 0 && v2.w <= 0) continue;
// 5. Perspective divide & screen mapping
ScreenPoint p0 = toScreen(v0);
ScreenPoint p1 = toScreen(v1);
ScreenPoint p2 = toScreen(v2);
// 6. Compute face normal & light
vec3 normal = computeNormal(p0, p1, p2); // in world space
float intensity = max(0.0, dot(normal, lightDir));
uint16_t color = rgb565(intensity*255, intensity*200, intensity*100);
// 7. Rasterize triangle
fillTriangle(p0, p1, p2, color, depthBuffer);
}
tft.endWrite();
}
Performance Optimisation Tips
- Use fixed‑point math: Replace
floatwithint32_tand shift operations. For example, store 1/z asuint16_tin 4.12 fixed point. - Parallelise with dual cores: Offload rasterization to core 0 while core 1 handles input and game logic. Use FreeRTOS tasks.
- Double‑buffer: Use two framebuffers: one being drawn, one being sent to display via DMA.
TFT_eSPIsupports this. - Cull back‑facing triangles: If the projected area is negative (or normal points away from camera), skip.
- Pre‑transform static geometry: If the mesh does not deform, bake the world‑space coordinates.
With these optimizations, a ESP32‑S3 at 240 MHz can render ~100 textured triangles per second at 320×240 – enough for a simple 3D game.
Real‑World Examples and Community Projects
- Pico‑3D: A MIT‑licensed software renderer for the Raspberry Pi Pico, demonstrating cube and teapot rendering. It achieves 30 FPS on a 1.14” display.
- TinyGL on ESP32: An open‑source subset of OpenGL ported by hobbyists. It supports immediate mode and basic textures.
- Voxel engine for handhelds: Many creators are building Minecraft‑style worlds on microcontrollers using ray‑marching or voxel traversal.
For those looking to integrate AI‑assisted development into their workflow, ASI Biont offers resources to help you get started — see ASIBiont.com/courses.
Conclusion
Building a tiny 3D renderer is more than a nostalgic project — it’s a deep dive into computer graphics fundamentals. With the vibe coding approach, you can iterate quickly, experiment with algorithms, and produce impressive results on minimal hardware. The skills you gain—matrix math, rasterization, memory management—translate directly to game engines, simulations, and even machine learning.
So grab an ESP32, a tiny display, and your favourite AI assistant. Start with a spinning cube, then add textures, reflections, and physics. The world of tiny 3D is waiting.
Further reading:
- Real‑Time Rendering, Fourth Edition by Akenine‑Möller et al. (CRC Press, 2018)
- TinyGL reference: https://bellard.org/TinyGL/
- Espressif ESP32‑S3 Technical Reference Manual.
Comments