go back...

Writing a Software Renderer in C

A series of articles about the development of an old-school 16-bit software renderer

Wireframe cube

Part 1. Starting with the cube


I started developing my software renderer by drawing a simple cube to test my 3D transformation algorithm.

Wireframe Pyramid Head

Part 2. Loading .OBJ models


The next step was to write a .obj file loader.



Solid Pyramid Head

Part 3. Filling triangles with a solid color


From that point on, I began to write the actual rasterization code, rendering solid triangles using a 16-bit color palette for a low-fi aesthetic.

Textured Pyramid Head

Part 4. Adding textures


Now we're talkin'!!

Just don't look too closely at the lack of a z-buffer...

Textured Pyramid Head

Part 5. Depth


Everything started to look right after I added the depth test.
Bonus: Photorealistic in the reverse perspective

Wobbly textures

Part 6. Wobbly textures fix


That funny PS1-style effect unintentionally caused by the incorrect UV calculations.

Looks aesthetic, but let's get rid of it with the simple fix.

Pixelated textures fix

Part 7. Pixelated textures fix


Linear texture interpolation proved to be the key to achieving more detail at an affordable cost.

Part . Animations


It took me a significant amount of time to comprehend the mechanisms behind animations, but it's moving now!

Part . Fog and depth-based lighting


Modelling sad modernist blocks. They give me a vibe and they're easy to work with because they're basically just a blocky shapes made of concrete. I also have some scale now to test the fog effect.

Part . Normals and dynamic lighting


Per-vertex light via face normals

Part . Cubemapping and skybox


In the first iteration, my skybox was just a plain image, which worked fine for a static camera but broke the illusion when the camera started to move.

The next attempt was to create a scrolling texture. It looks better in motion now, but only when the camera rotates left and right rather than looking straight up or down.

The ultimate solution for this problem was cubemaps. It took me a few days to implement this algorithm in my software renderer, but I'm happy with the results.






































Basic cube


I started by drawing a simple cube to test my 3D transformation algorithm.

    
      void Renderer_DrawPoints(Mesh *mesh)
      {
          // Render mesh points
          for (int i = 0; i < mesh->points_count; ++i) {

              double x = mesh->points[i].x;
              double y = mesh->points[i].y;
              double z = mesh->points[i].z;

              // Model-space rotation
              {
                  double rx = x * mesh_rot_cos - z * mesh_rot_sin;
                  double rz = x * mesh_rot_sin + z * mesh_rot_cos;
                  x = rx;
                  z = rz;
              }

              // Model-space transform
              x += pos_x;
              y += pos_y;
              z += pos_z;

              // Camera-space transform
              x += camera.x;
              y += camera.y;
              z += camera.z;

              // Camera-space rotation
              {
                  double rx = x * camera_rot_cos - z * camera_rot_sin;
                  double rz = x * camera_rot_sin + z * camera_rot_cos;
                  x = rx;
                  z = rz;
              }

              // Culling
              if (z <= 0) {
                  continue;
              }

              // Perspective divide
              x /= z;
              y /= z;

              // Screen-space transform (remap from [-0.5, 0.5] to [0, screen_dimension]
              x = (0.5 + x) * screen_dimension + screen_dimension_offset_x;
              y = (0.5 + y) * screen_dimension + screen_dimension_offset_y;

              // Draw points
              DrawPoint(screen_buffer, screen_width, screen_height, x, y, 0xFFFF);
          }
      }
    
  

.OBJ models


Next, I successfully parsed an .obj file, initially loading and rendering a cloud of points.

In the following step, I loaded edges and connected the points with lines.


A solid triangles


From that point on, I began to write the actual rasterization code, rendering solid triangles using a 16-bit color palette for a low-fi aesthetic.


Textures


Now we're talkin'...


Depth


Everything started to look right after I added the depth test.

Bonus: Cirno fumo in the reverse perspective


Texture wobbling


That funny PS1-style effect unintentionally caused by the incorrect UV calculations.

Looks aesthetic, but let's get rid of it with the simple fix.


Texture interpolation


Proved to be the key to achieving more detail at an affordable cost.


Animations


It took me a significant amount of time to comprehend the mechanisms behind animations, but it's moving now!


Fog and depth-based lighting


Modelling sad modernist blocks. They give me a vibe and they're easy to work with because they're basically just a blocky shapes made of concrete. I also have some scale now to test the fog effect.


Normals and dynamic lighting


Per-vertex light via face normals.


Cubemapping and skybox


In the first iteration, my skybox was just a plain image, which worked fine for a static camera but broke the illusion when the camera started to move.

The next attempt was to create a scrolling texture. It looks better in motion now, but only when the camera rotates left and right rather than looking straight up or down.

The ultimate solution for this problem was cubemaps. It took me a few days to implement this algorithm in my software renderer, but I'm happy with the results.


Point lights and movement system


It's starting to look more like a game now, and I was pleasantly surprised by how well the cheap vertex lighting turned out.