Software Rasterizer: Coloring

Filling a triangle with different colors

In the previous post, We could fill a triangle with a single color that's not enough If you ask me we may need to draw a quad with different colors for. That's where we introduce linear gradients. Before explaining let's create a new header called Color.h. That will be our representation for a color and our way to convert it to  uint32 to be used inside the pixel directly:

#pragma once
#include <cstdint>

struct Color {
    uint8_t r, g, b, a;

    Color(uint8_t r_, uint8_t g_, uint8_t b_, uint8_t a_ = 255) : r(r_), g(g_), b(b_), a(a_) {
    }

    uint32_t to_uint32() const {
        return (static_cast<uint32_t>(a) << 24) |
               (static_cast<uint32_t>(b) << 16) |
               (static_cast<uint32_t>(g) << 8)  |
               static_cast<uint32_t>(r);
    }
    [[nodiscard]] static Color from_uint32(uint32_t v) {
        uint8_t b = static_cast<uint8_t>(v & 0xFF);
        uint8_t g = static_cast<uint8_t>((v >> 8) & 0xFF);
        uint8_t r = static_cast<uint8_t>((v >> 16) & 0xFF);
        uint8_t a = static_cast<uint8_t>((v >> 24) & 0xFF);
        return {r, g, b, a};
    }
};

Let's take this even further. Let's make another struct called Vertex which will hold our vertex position and color. Let's create a new header for that called Vertex.h

#pragma once
#include "Color.h"
#include "Point.h"

struct Vertex {
    Point position;
    Color color;
};

Now each vertex in our triangles list will have it's own color. Now how will we fill our colorful triangles? 

From the lost post we explained Barycentric Coordinate System and how we it to calculate three weights which help use decide whether our point inside or outside the triangle. Surprisely we will use these weights $w_0, w_1, w_3$ to decide the color of our pixel inside this colorful triangle.

$$
\text{final\_color} = v_0.\text{color} \cdot w_0 + v_1.\text{color} \cdot w_1 + v_2.\text{color} \cdot w_2
$$

where $v_0, v_1, v_2$ are our triangles vertices in counter-clockwise  winding order. the final color will be our pixel color. I have updated some things in my draw_triangle function. So, I will post everything with some comments explaining what's going on 💀

That's how my draw.cpp looks like

#include "draw.h"
#include <cstdint>
#include <algorithm>
#include <cmath>
#include <vector>


void draw_triangle(uint32_t *pixels, int width, int height,
                   const Vertex &v0, const Vertex &v1, const Vertex &v2) {
    // Positions
    Point p0 = v0.position;
    Point p1 = v1.position;
    Point p2 = v2.position;

    // Signed area (positive if CCW, negative if CW)
    float area = det2D(p1 - p0, p2 - p0);
    if (area == 0.0f) return; // Degenerate triangle

    //Calculating the invert of an area one and multiply by it later as multiplying usually faster than dividing
    float inv_area = 1.0f / area;

    // Bounding box
    int minX = std::floor(std::min({p0.x, p1.x, p2.x}));
    int maxX = std::ceil(std::max({p0.x, p1.x, p2.x}));
    int minY = std::floor(std::min({p0.y, p1.y, p2.y}));
    int maxY = std::ceil(std::max({p0.y, p1.y, p2.y}));

    // Clamp to framebuffer
    minX = std::max(minX, 0);
    maxX = std::min(maxX, width - 1);
    minY = std::max(minY, 0);
    maxY = std::min(maxY, height - 1);

    // Pre computing edges outside the loop as their values  won't change for a triangle
    Point p21 = p2 - p1;
    Point p02 = p0 - p2;
    for (int y = minY; y <= maxY; y++) {
        for (int x = minX; x <= maxX; x++) {
            Point p = {
                static_cast<float>(x) + 0.5f,
                static_cast<float>(y) + 0.5f
            }; // pixel center

            // Compute barycentric weights
            float w0 = det2D(p21, p - p1) * inv_area; // weight for v0
            float w1 = det2D(p02, p - p2) * inv_area; // weight for v1
            float w2 = 1.0f - w0 - w1; // weight for v2

            // Inside test
            if (w0 >= 0 && w1 >= 0 && w2 >= 0) {
                // Interpolated color rgba
                float srcR = v0.color.r * w0 + v1.color.r * w1 + v2.color.r * w2;
                float srcG = v0.color.g * w0 + v1.color.g * w1 + v2.color.g * w2;
                float srcB = v0.color.b * w0 + v1.color.b * w1 + v2.color.b * w2;
                float srcA = v0.color.a * w0 + v1.color.a * w1 + v2.color.a * w2;

                // Converting this color to uint32 to be inserted inside the buffer
                pixels[y * width + x] = Color(srcR, srcG, srcB, srcA).to_uint32();
            }
        }
    }
}

void draw_indexed(uint32_t *pixels, int width, int height, const std::vector<Vertex> &vertices,
                  const std::vector<int> &indices) {
    // We iterate through the indices, 3 at a time, to form triangles.
    for (size_t i = 0; i < indices.size(); i += 3) {
        // Get the vertices for the current triangle from the vertex buffer
        const Vertex &v0 = vertices[indices[i]];
        const Vertex &v1 = vertices[indices[i + 1]];
        const Vertex &v2 = vertices[indices[i + 2]];

        // Call our triangle drawing function.
        // The arguments a, c, b correspond to the vertices of a CCW triangle.
        draw_triangle(pixels, width, height, v0, v1, v2);
    }
}

 

Let's update what are we drawing inside our event loop inside main.cpp

while(running){
  // event and texture stuff  
 std::vector<Vertex> vertices = {
            { Point(100, 300), {255,   0,   0, 255} }, // Red
            { Point(200, 300), {  255, 0,   0, 255} }, // Green
            { Point(200, 400), {  0,   0, 255, 255} }, // Blue
            { Point(100, 400), {0,0,   255, 255} }  // Yellow
        };
        std::vector<int> indices = {
            0, 1, 2,  // First triangle
            0, 2, 3   // Second triangle
        };
draw_indexed(pixels, width, height, vertices, indices);
}

If everything went smooth, We should be able to see this:

 

This post was a bit short because the next one will be a big loner than the rest because we will talk about textures and how it's done in a CPU rasterizer.

This article was updated on September 11, 2025