Parallel Scan Conversion
Rasterizing triangles without loop conditions

There are two broad families of approaches to rasterizing triangles.

1 Motivation

To know what pixels a triangle covers, three of the coordinates of the vertices are important: xx, yy, and ww. Other coordinates have value (zz for the depth buffer, ss and tt 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 Ax+By+C=0Ax + By + C = 0, or for homogeneous coordinates Ax+By+Cw=0Ax + By + Cw = 0. Pints on difference sides of a 2D line have different signs in this equation, meaning Ax+By+Cw>0Ax + By + Cw > 0 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 (x,y,w)(x,y,w) is inside a triangle only if A0x+B0y+C0w>0A1x+B1y+C1w>0A2x+B2y+C2w>0\begin{split} A_0x + B_0 y + C_0 w &> 0\\ A_1x + B_1 y + C_1 w &> 0\\ A_2x + B_2 y + C_2 w &> 0\\ \end{split} Or, written as a matrix, [A0B0C0A1B1C1A2B2C2][xyw]>[000] \begin{bmatrix}A_0&B_0&C_0\\A_1&B_1&C_1\\A_2&B_2&C_2\end{bmatrix} \begin{bmatrix}x\\y\\w\end{bmatrix} > \begin{bmatrix}0\\0\\0\end{bmatrix} 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, A0x+B0y+C0w>0A_0x + B_0 y + C_0 w > 0. 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 A0x0+B0y0+C0w0=k>0A0x1+B0y1+C0w1=0A0x2+B0y2+C0w2=0\begin{split} A_0 x_0 + B_0 y_0 + C_0 w_0 &= k > 0\\ A_0 x_1 + B_0 y_1 + C_0 w_1 &= 0\\ A_0 x_2 + B_0 y_2 + C_0 w_2 &= 0\\ \end{split} This is three equations but has four unknowns because of that pesky k>0k > 0. We can pick any positive kk we want; 1 is a common choice, but not necessary.

Re-writing in matrix form gives us [A0B0C0][x0x1x2y0y1y2w0w1w2]=[k00] \begin{bmatrix}A_0&B_0&C_0\end{bmatrix} \begin{bmatrix}x_0&x_1&x_2\\y_0&y_1&y_2\\w_0&w_1&w_2\end{bmatrix} = \begin{bmatrix}k\\0\\0\end{bmatrix} Repeating with the other edges and vertices (and picking the same kk each time to make a future step simpler) gives us similar equations: [A1B1C1][x0x1x2y0y1y2w0w1w2]=[0k0] \begin{bmatrix}A_1&B_1&C_1\end{bmatrix} \begin{bmatrix}x_0&x_1&x_2\\y_0&y_1&y_2\\w_0&w_1&w_2\end{bmatrix} = \begin{bmatrix}0\\k\\0\end{bmatrix} and [A2B2C2][x0x1x2y0y1y2w0w1w2]=[00k] \begin{bmatrix}A_2&B_2&C_2\end{bmatrix} \begin{bmatrix}x_0&x_1&x_2\\y_0&y_1&y_2\\w_0&w_1&w_2\end{bmatrix} = \begin{bmatrix}0\\0\\k\end{bmatrix} Combining all of those together, we get [A0B0C0A1B1C1A2B2C2][x0x1x2y0y1y2w0w1w2]=[k000k000k](1) \tag{1} \begin{bmatrix}A_0&B_0&C_0\\A_1&B_1&C_1\\A_2&B_2&C_2\end{bmatrix} \begin{bmatrix}x_0&x_1&x_2\\y_0&y_1&y_2\\w_0&w_1&w_2\end{bmatrix} = \begin{bmatrix}k&0&0\\0&k&0\\0&0&k\end{bmatrix} If we picked k=1k=1, this would be the defining equation for a matrix inverse; that is [A0B0C0A1B1C1A2B2C2]=[x0x1x2y0y1y2w0w1w2]1if k=1 \begin{bmatrix}A_0&B_0&C_0\\A_1&B_1&C_1\\A_2&B_2&C_2\end{bmatrix} = \begin{bmatrix}x_0&x_1&x_2\\y_0&y_1&y_2\\w_0&w_1&w_2\end{bmatrix}^{-1} \qquad\text{if }k = 1 Other kk would just make a scalar multiple of this equation, so [A0B0C0A1B1C1A2B2C2]=k[x0x1x2y0y1y2w0w1w2]1 \begin{bmatrix}A_0&B_0&C_0\\A_1&B_1&C_1\\A_2&B_2&C_2\end{bmatrix} = k \begin{bmatrix}x_0&x_1&x_2\\y_0&y_1&y_2\\w_0&w_1&w_2\end{bmatrix}^{-1}

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: M1=[x0x1x2y0y1y2w0w1w2]1=1det(M)[y1w2y2w1x2w1x1w2x1y2x2y1y2w0y0w2x0w2x2w0x2y0x0y2y0w1y1w0x1w0x0w1x0y1x1y0] \mathbf{M}^{-1} = \begin{bmatrix}x_0&x_1&x_2\\y_0&y_1&y_2\\w_0&w_1&w_2\end{bmatrix}^{-1} = \frac{1}{\operatorname{det}(\mathbf{M})} \begin{bmatrix} y_1 w_2 - y_2 w_1 & x_2 w_1 - x_1 w_2 & x_1 y_2 - x_2 y_1 \\ y_2 w_0 - y_0 w_2 & x_0 w_2 - x_2 w_0 & x_2 y_0 - x_0 y_2 \\ y_0 w_1 - y_1 w_0 & x_1 w_0 - x_0 w_1 & x_0 y_1 - x_1 y_0 \end{bmatrix} Note that each row of M1M^{-1} is the cross product of two columns of MM.

That 1det(M)\frac{1}{\operatorname{det}(\mathbf{M})} term is a bit of an annoyance, but if we make k=det(M)k = \operatorname{det}(\mathbf{M}) it conveniently vanishes1 If the three vertices are colinear, then M\mathbf{M} will be singular and det(M)=0\operatorname{det}(\mathbf{M}) = 0. In this case no pixels should be shown and the triangle skipped. Computing det(M)\operatorname{det}(\mathbf{M}) can be done by taking the dot product of the first column of M\mathbf{M} and the first row of the adjugate matrix.. The matrix we’re left with, [y1w2y2w1x2w1x1w2x1y2x2y1y2w0y0w2x0w2x2w0x2y0x0y2y0w1y1w0x1w0x0w1x0y1x1y0] \begin{bmatrix} y_1 w_2 - y_2 w_1 & x_2 w_1 - x_1 w_2 & x_1 y_2 - x_2 y_1 \\ y_2 w_0 - y_0 w_2 & x_0 w_2 - x_2 w_0 & x_2 y_0 - x_0 y_2 \\ y_0 w_1 - y_1 w_0 & x_1 w_0 - x_0 w_1 & x_0 y_1 - x_1 y_0 \end{bmatrix} is called the adjugate matrix2 Or sometimes the transpose of the adjugate matrix; confusingly, whether the adjugate refers to this matrix or its transpose varies by between sources..

2 Finding pixels

The pixels inside a triangle can be found as follows:

Pixel coverage

  1. Find the adjugate matrix of the matrix A\mathbf{A} made from the xx, yy, and ww coordinates of the three vertices
  2. If the matrix is singular, there are no pixels in this triangle
  3. For every pixel (x,y)(x,y),
    1. Compute s=A[x,y,1]T\vec s = \mathbf{A} [x,y,1]^{T}
    2. If all coordinates of ss are positive, the pixel is inside the triangle

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 x/wx/w and y/wy/w 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 sis_i to mean the point is inside the triangle if

Why this tie-breaker?

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 AiA_i is positive, that means that increasing xx 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 AiA_i is zero, then the edge is perfectly horizontal, so we check BiB_i to see if the edge is on the top or bottom side of the triangle for a similar final tie breaking.

3 Finding other coordinates

Every point on a triangle is a weighted average of its vertices, meaning p=λ0v0+λ1v1+λ2v2\vec p = \lambda_0 \vec v_0 + \lambda_1 \vec v_1 + \lambda_2 \vec v_2 where the λi\lambda_i are called the barycentric coordinates of point p\vec p and have the properties of all being non-negative and summing to 1.

Because we used the same kk for all three plane equations in (1), the vector s\vec s found in the pixel coverage algorithm is a linear multiple of λ=(λ0,λ1,λ2)\vec \lambda = (\lambda_0, \lambda_1, \lambda_2). However, because we are using homogeneous coordinates, what multiple of λ\vec \lambda it is will vary from one pixel to another. Fortunately, we knew that lambda\vec lambda has to sum to 1, meaning find lambda=s/s\vec lambda = \vec s / \sum \vec s.

For the most coordinates, the λ\lambda-based average of the vertex coordinates gives exactly the perspective-correct interpolation of that coordinate that we want. However, for zz it does not.

Projection matrices modify the xx, yy, zz, and ww coordinates to provide a frustum of (xw,yw,zw)\left({x \over w}, {y \over w}, {z \over w}\right). The use of homogeneous equations for the line equations has handled the xwx \over w and ywy \over w parts for us, but we still need zw{z \over w} to complete the frustum. Fortunately, we can get both zz and ww using λ\lambda, so we can also get zwz \over w with an additional division.

What if I don’t divide zz by ww?

If you forget to divide zz by ww, at first things will look correct. After all, zz doesn’t impact where things appear on the screen and generally doesn’t impact color either (unless you add something like fog).

Not dividing zz by ww means that all shapes will be curved in 3D space: scaled to a frustum in xx and yy but not in zz, 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, z/wz / w 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.

4 Clipping

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 0z10 \le z \le 1, 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.