Fork me on GitHub

Welcome

This is yet another learning project of mine, again in Rust. This time, I’m building a 3D software rasterizer in Rust.

Our current progress

Current render output

Motivations

There are two things I want to learn with this project, running side by side.

The first is 3D rasterization itself. I started programming in high school, and almost immediately became fascinated by the demoscene: tiny programs that made very limited PCs draw things that felt nearly impossible at the time. Rotating 3D shapes, lights, shadows, strange graphical effects packed into 64K or even 4K binaries — I had no idea how any of it worked, but I badly wanted to understand it.

Back then, that curiosity had nowhere easy to go. Learning resources were scarce, and I was missing too many basics: mathematics, computer science, and, maybe most importantly, the skill of learning itself. Now, after more than twenty years as a software developer and with the whole internet available as a reference shelf, I can return to that old fascination properly.

In that sense, this project is an homage to my adolescent self: curious, ignorant, and frustrated, but still pulled toward the same questions. So, we’ll do some 3D graphics.

The plan is to build the math and rasterization path on the CPU first, then add optional GPU rendering with wgpu (Metal on Mac) as a stretch goal. Scenes stay small and procedural — working up shapes like a cube, a sphere, and eventually a torus — with lossless WebP stills and animations as the main artifacts.

Working with AI agents

The second motivation, equally important in the modern day and age, is to deliberately practice AI-assisted (“agentic”) coding. Tools like Cursor and similar agents have changed the day-to-day of writing software, and I want to develop a real, hands-on intuition for working with them — not as a passive autocomplete user, but as someone delegating meaningful work and reviewing the result.

A few things I want to figure out along the way:

  • Use AI as much as possible, but consciously. Where does it shine? Where does it struggle? Where am I tempted to lean on it instead of actually understanding something?
  • AI as a learning partner, not just a code generator. Asking it to explain trade-offs, derive the math, propose alternatives, and critique my reasoning — and then verifying what it tells me against a primary source.
  • Keeping the code clean in spite of AI’s tendency to produce slop: redundant abstractions, over-engineered helpers, dead code, premature generality, and tests that look plausible but assert nothing useful. I want to find prompting patterns, review habits, and project conventions that push back on this.

We’ll be co-authoring the project diary together: me (Sergey) as the project owner and reviewer, Cursor as the coding partner who helps capture what happened, explain the trade-offs, and keep the notes connected to the code. Over time, I want to trust Cursor more and more with keeping this diary up to date, while still reading critically and steering the voice.

Project scope

I’ll consider the project meaningfully accomplished when I can:

  • Render a small set of procedural 3D shapes — at minimum a cube, a sphere, and a torus — with a working camera and basic lighting
  • Produce smooth animations of those scenes, captured as lossless WebP
  • Run the whole pipeline on the CPU, with the optional wgpu-based GPU path as a stretch goal
  • Look at the codebase six months from now and still be happy to work in it

We are going to be working in intentionally small steps, so that there’s enough time to learn, collaborate, and improve.

Project diary

  • One White Pixel

    We shipped the first milestone of the rasterizer: version 0.0.1. It is intentionally tiny. The program writes an 800x600 lossless WebP image with a single white pixel at the center. That is not much of a renderer yet, but it is enough to prove the first important thing: we can own a framebuffer in Rust, put pixels into it, encode it, write it to disk, and verify that the resulting file is a real image.

  • Lines Without Guesswork

    We shipped the second milestone of the rasterizer. This one feels like a real step forward: instead of writing one known pixel, we now draw full line segments across the framebuffer and export the result as a valid lossless WebP.

  • A Cube Takes Shape

    We shipped the third milestone of the rasterizer. Version 0.0.1 proved the export path; Version 0.0.2 gave us dependable line segments. Version 0.0.3 is what we have been building toward since then: a wireframe cube in orthographic projection, exported as the same 800×600 lossless WebP still image.

  • The Cube Starts Spinning

    Version 0.0.3 froze the orthographic wireframe cube in one pose. Version 0.0.4 adds animation: model orientation changes every frame, exported as a lossless animated WebP on the same 800×600 raster path. That goal drove a small refactor — geometry and pose in one place so we can tilt, spin, or scale the cube each frame without entangling the line rasterizer in mesh details.

  • The Cube Sheds Its Hidden Edges

    Version 0.0.4 gave us a wireframe cube that tumbles smoothly — and that was real progress. A wireframe is an honest way to learn: twelve edges, a line rasterizer, no fills yet. But every frame still drew all twelve hull segments, including the ones that belong to faces on the far side of the box. The cube looked like a cage — you could see edges that a solid object would hide.

  • The Cube Paints Its Six Faces

    Version 0.0.5 left the cube as a clean wireframe: back faces culled, cornflower-blue strokes, no see-through cage. The natural next step was always solid facets — six flat colors, one per side, still without lighting or a depth buffer. Version 0.0.6 ships that look. The exporters now fill visible quads instead of drawing twelve edges.

  • The Bug: The Near Face Was Classified as Back

    Version 0.0.6 shipped the filled, six-color cube — a real milestone. Back-face culling arrived one release earlier, in Version 0.0.5, with CubeFace::is_back driving Cube::visible_edges. Little did we know the sign in that helper was backwards relative to Camera::direction — we had shipped the wrong facing test without noticing. The mistake stayed hidden until we started the lighting milestone on top of the filled cube.

subscribe via RSS