Skip to main content

Chapter 11 Z Buffering

A hidden surface is any surface that is represented within our polygon matrices but would not be visible in the generated image. There are genrally two reasons a surface would be hidden; either it is facing away from the viewer, or it is being obstructed by another surface. Backfacce culling takes care of the first case, and Z-Buffering is how we will handle the second. Importantly, we could not work on this kind of hidden surface removal until after working on scanline conversion. Backface culling happens per polygon, eitehr the entire surface is visible or not based on its orientation. We cannot be certain that when one object is in front of another, that an entire polygon is covered, so in order to accurately deal with this, we need to be working at the pixel level. Now that we are scanline converting our images, we have a fraemwork in palce to visit all the pixels that should be drawn.

Section 11.1 The Z-Buffer

A Z-Buffer is a 2D array of floating point z values that maps directly to the 2D array of colors/pixels that contains our image. For example, the Z-buffer value at \((4, 10)\) is the z coordinate for the pixel drawn at point \((4, 10)\text{.}\) Since z values represent the depth of a pixel, we can use the Z-Buffer to keep track of how forward or backward any plotted pixel is.
Currently when we plot a pixel we provide the \((x, y)\) coordinates and color. Now we will provide \((x, y, z)\) and a color. \((x, y)\) will determine the position of the color in the image and the \(z\) value in the Z-Buffer. Before we deicdee to plot a given pixel in the image, we will check the \(z\) value against the value currently in the Z-Buffer.
If the new \(z\) is larger than the value in the Z-Buffer, that means that whatever pixel is currently plotted in the image comes from a shape that is behind the new pixel (remember, +z is closer to the viewer). When this happens we will update the image and Z-Buffer at the specified \((x, y)\) position with the new color and \(z\) value. Otherwise, we will skip plotting the pixel in the image and Z-Buffer, as there must already be an object at that position that is more forward.
In order for this the Z-Buffer to work, we need a good value to initialize Z-Buffer entries to. What we want is a value that represents something so far behind that any object we add would be on top. This should be the smallest (most negative) value possible. In the math world, a resonable suggestion would be to use -∞. Turns out in the Computer Programming world, that is also the answer! There is in fact a standard way to represent both ∞ and -∞ with floating point values 1 . Often dividing a negative floating point value by 0 will do this (i.e. -1.0/0.0), but different programming languages often have constants defined for this value as well.

Section 11.2 Calculating \(z\) Values

In addition to maintaining a Z-Buffer, we need to now calculate z values whenever we draw lines. This includes our draw_line, scanline_convert, and draw_scanline (if you made a separate function to just draw scanlines) functions.

Subsection 11.2.1 scanline_convert

Here, \(z\) values can be calculated the same way we find the \(x\) values. We already have \(z\) coordinates stored in out polygon matrices, we just need to calcualte the \(z\) values as we move across each scanline. It is important to remember that unlike \(y\text{,}\) we cannot be certain that the \(z\) value is the same along the entire scanline.
\(z_0\) is on the line \(\overline{BT}\text{,}\) so it starts at \(z_b\) and goes to \(z_t\text{.}\) \(z_1\) is on the line \(\overline{BM}\) starting at \(z_b\) going to \(z_m\) and then it moves onto the line \(\overline{MT}\text{,}\) going from \(z_m\) to \(z_t\text{.}\)
You will need a \(\Delta z_0\) and \(\Delta z_1\text{.}\) These are calculated the same was as their \(x\) equivalents:
\begin{equation*} \Delta z = \dfrac{\text {total change in z}}{\text {number of scanlines}} \end{equation*}

Subsection 11.2.2 draw_line and draw_scanline

We actually have to go back to our very first graphics algorithm and modify it to take \(z\) values into account. On the plus side, we need \(z\) values to be floating point based, so we will not have to try to map them as best we can to integers. This makes the calculation more straightforward. You can think of generating \(z\) values in a similar way to what weed need to do in scanline_convert.
If we are drawing a line from \((x_0, y_0, z_0)\) to \((x_1, y_1, z_1)\text{,}\) that means as we draw the line, \(z\) must go from \(z_0\) to \(z_1\text{.}\) Since we draw our lines 1 pixel at a time, we need to figure out how much \(z\) changes with each pixel. Like above:
\begin{equation*} \Delta z = \dfrac{\text {total change in z}}{\text {number of pixels plotted}} \end{equation*}
Note that the denominator is the # of pixels not the length of the line. If the line is in octants I or VIII, the total # of pixels is based on the \(x\) values. If the line is in octants II or VII, the total # of pixels is based on the \(y\) values. Think about the value that controls the loop that generates the line. Which coordinate is guaranteed to go up by 1 each time?