There are two broad families of approaches to rasterizing triangles.
Rasterize the lines along the triangle’s edges, then the horizontal scanlines connecting the edges.
These approaches include the DDA and Bresenham algorithms. They minimize work per pixel at the cost of more up-front work per triangle, making them preferably for large triangles. They have loops with dynamically-computed iteration counts and accumulators, making them difficult to parallelize in hardware.
We have a separate text page about this approach.
Create inequalities that are positive only inside the triangle, and evalaute them at every (plausible) pixel.
These approaches minimize up-front work per triangle at the cost of most work per pixel, making them preferable for small triangles. They perform independent work at each pixel, making them easy to parallelize in hardware.
These approaches do not have generally accepted names, but include Fuchs et al 1985, Pineda 1988, and Olano and Greer 1997, among others. This page primarily focuses on the approach discussed by Olano and Greer, which I’ve also seen replicated without citation in several other places.
To know what pixels a triangle covers, three of the coordinates of the vertices are important: , , and . Other coordinates have value ( for the depth buffer, and for textures, etc) but we’ll come back to them later on.
Each edge of a triangle is a straight line on the 2D screen. In 2D, a straight line has the equation , or for homogeneous coordinates . Pints on difference sides of a 2D line have different signs in this equation, meaning
tells us all the points on one side. The interior of a triangle is on the inside
side of all three of the triangle’s edges. We can set up the line equations to have either side be positive; let’s arbitrarily pick the inside as positive and the outside as negative. Thus, a point is inside a triangle only if
Or, written as a matrix, If we could find the coefficients of these three line equations then telling if a pixel was inside of outside of a triangle would be as simple as a 3×3 matrix-vector multiply and checking three sign bits.
Consider one of the line equations, . Let’s call this the line that does not pass through vertex 0, meaning it does pass through vertices 1 and 2. Vertex 0 is on the inside side of this edge, so the equation should be positive for it. That means This is three equations but has four unknowns because of that pesky . We can pick any positive we want; 1 is a common choice, but not necessary.
Re-writing in matrix form gives us Repeating with the other edges and vertices (and picking the same each time to make a future step simpler) gives us similar equations: and Combining all of those together, we get If we picked , this would be the defining equation for a matrix inverse; that is Other would just make a scalar multiple of this equation, so
Matrix inverse is a tricky topic in general because of the possibility of singular matrices and the efficiency of complicated algorithms at scale, but for a 3×3 matrix there’s a simple equation for it: Note that each row of is the cross product of two columns of .
That term is a bit of an annoyance, but if we make it conveniently vanishes1 If the three vertices are colinear, then will be singular and
. In this case no pixels should be shown and the triangle skipped. Computing can be done by taking the dot product of the first column of and the first row of the adjugate matrix.. The matrix we’re left with,
is called the adjugate matrix
2 Or sometimes the transpose of the adjugate matrix; confusingly, whether the adjugate refers to this matrix or its transpose varies by between sources..
The pixels inside a triangle can be found as follows:
Pixel coverage
See notes for more on every pixel
and positive
.
We still need to find the other coordinates of the points this discovers are inside the triangle, but there are two other issues (highlighted above) to resolve as well.
Checking every pixel coordinate is very inefficient: most triangles cover only a very small percentage of the screen. Taking a bounding box of the triangle (by finding the minimum and maximum of and for the three vertices) can save significant time, but still means that long thin diagonal slivers might have a very large bounding box but cover very few triangles. Recursive approaches that rasterize at a low resolution to find squares of pixels to rasterize at full resolution can make large diagonal slivers faster, at the expense of extra steps for every triangle.
Checking for coordinates that are positive checks finds pixels inside a triangle, but what about pixels exactly on a pixel edge where the coordinate will be 0? The common use-case of triangles is subdividing a curves surface into many adjoining triangles: if we leave out on-edge pixels then there will be some missing pixels between these triangles while if we include them then there will be some pixels draw twice, which will be visible if the triangles are translucent. We need a tie-breaker that will ensure each such pixel is drawn exactly once. A common tie-breaker is to consider a coodinate to mean the point is inside the triangle if
Consider an pixel on an edge between two triangles. We want one triangle to draw that pixel and the other not to.
Because the edge divides the two triangles, what is on the positive side for one will be on the negative side for the other.
If is positive, that means that increasing makes the equation more positive meaning this edge is on the left side of the triangle, and hence must be on the right side of its adjoining triangle.
If is zero, then the edge is perfectly horizontal, so we check to see if the edge is on the top or bottom side of the triangle for a similar final tie breaking.
Every point on a triangle is a weighted average of its vertices, meaning where the are called the barycentric coordinates of point and have the properties of all being non-negative and summing to 1.
Because we used the same for all three plane equations in (1), the vector found in the pixel coverage algorithm is a linear multiple of . However, because we are using homogeneous coordinates, what multiple of it is will vary from one pixel to another. Fortunately, we knew that has to sum to 1, meaning find .
For the most coordinates, the -based average of the vertex coordinates gives exactly the perspective-correct interpolation of that coordinate that we want. However, for it does not.
Projection matrices modify the , , , and coordinates to provide a frustum of . The use of homogeneous equations for the line equations has handled the and parts for us, but we still need to complete the frustum. Fortunately, we can get both and using , so we can also get with an additional division.
If you forget to divide by , at first things will look correct. After all, doesn’t impact where things appear on the screen and generally doesn’t impact color either (unless you add something like fog).
Not dividing by means that all shapes will be curved in 3D space: scaled to a frustum in and but not in , making them into hyperbolic shapes. That will make the near and far clipping planes cut them on a curve, not on a straight line, and depending on the input can cause the depth buffer to put the wrong shape’s pixels in front.
Additionally, is important to create a visually-uniform depth buffer when the depth buffer is stored in a fixed-point way, as it is on graphics hardware. A floating-point depth buffer wont have that property, but it also uses a great deal more memory than a fixed-point one.
The top, bottom, left, and right clipping planes are handled implicitly in this approach by only checking the plane equations for pixels that are on screen.
The near and far clipping planes are handled per pixel by ensuring that , rejecting any pixel for which that is not true.
Other clipping planes can be handled on a per-pixel level similarly, checking the coordinates of the pixel against the equation of the plane.