Vertex Colouring

Each vertex has a colour associated with it. The aim is to rasterise the triangle and use the vertex colours blended across the whole triangle.

Doing this requires an Edge Function

It is used when drawing the triangle by 1. Placing a bounding box around the triangle's coordinates, clamped to the screen 2. Walking through every pixel in the bounding box 3. Using the edge function to work out if the point is within all three vertices to generate barycentric weights 4. Normalising the three weights 5. Using the three weights to blend the vertex colours 6. Plot the pixels

static void draw_triangle(Color *pixels,
                           Vec2 p0, Vec2 p1, Vec2 p2,
                           Color c0, Color c1, Color c2)
{
    /* Step 1: bounding box, clamped to the screen so we never write outside
     * the pixel buffer. */
    int min_x = (int)floorf(fminf(p0.x, fminf(p1.x, p2.x)));
    int max_x = (int)ceilf (fmaxf(p0.x, fmaxf(p1.x, p2.x)));
    int min_y = (int)floorf(fminf(p0.y, fminf(p1.y, p2.y)));
    int max_y = (int)ceilf (fmaxf(p0.y, fmaxf(p1.y, p2.y)));
    if (min_x < 0) min_x = 0;
    if (min_y < 0) min_y = 0;
    if (max_x > WINDOW_WIDTH)  max_x = WINDOW_WIDTH;
    if (max_y > WINDOW_HEIGHT) max_y = WINDOW_HEIGHT;
    /* The full triangle's "signed area" (twice the actual area, but the
     * factor of 2 cancels out once we normalise below, so we don't bother
     * dividing it out). We use this to normalise the edge function results
     * into barycentric weights that sum to 1. */
    float area = edge_function(p0, p1, p2);
    /* A degenerate triangle (zero area - e.g. all points on one line) can't
     * be filled, so bail out to avoid dividing by zero. */
    if (fabsf(area) < 1e-6f) {
        return;
    }
    /* Step 2 + 3: walk every pixel in the bounding box. */
    for (int y = min_y; y < max_y; y++) {
        for (int x = min_x; x < max_x; x++) {
            /* Sample at the centre of the pixel, not its corner, for more
             * accurate/symmetric coverage. */
            Vec2 sample = { (float)x + 0.5f, (float)y + 0.5f };
            /* Signed area of the sub-triangle formed by each edge and our
             * sample point. These are the (unnormalised) barycentric weights
             * for vertices p2, p0 and p1 respectively. */
            float w0 = edge_function(p1, p2, sample); /* weight for p0 */
            float w1 = edge_function(p2, p0, sample); /* weight for p1 */
            float w2 = edge_function(p0, p1, sample); /* weight for p2 */
            /* The point is inside the triangle if all three weights have the
             * same sign as the overall triangle area (i.e. the point is on
             * the same side of every edge as the triangle's interior). */
            bool inside = (w0 >= 0 && w1 >= 0 && w2 >= 0) ||
                          (w0 <= 0 && w1 <= 0 && w2 <= 0);
            if (!inside) {
                continue;
            }
            /* Normalise so w0 + w1 + w2 == 1. These are now true barycentric
             * coordinates: how much of the pixel's colour should come from
             * each vertex. */
            w0 /= area;
            w1 /= area;
            w2 /= area;
            /* Blend the three vertex colours using the barycentric weights -
             * this produces a smooth gradient across the triangle's surface. */
            Color final_color;
            final_color.r = (Uint8)(w0 * c0.r + w1 * c1.r + w2 * c2.r);
            final_color.g = (Uint8)(w0 * c0.g + w1 * c1.g + w2 * c2.g);
            final_color.b = (Uint8)(w0 * c0.b + w1 * c1.b + w2 * c2.b);
            final_color.a = 255;
            pixels[y * WINDOW_WIDTH + x] = final_color;
        }
    }
}