Rob Jenkins (robj++at++barney.reading.sgi.com)
Wed, 17 Apr 1996 09:00:45 +0100
Cheers
Rob
PS I'm sure several people will have implemented this in code - If you don't
get any examples let me know and I'll try to track one down.
-- ________________________________________________________________ Rob Jenkins, Software Support Group, Silicon Graphics UK Ltd. Forum 1, Station Road, Theale, Reading, UK, RG7 4SB. tel 01734 257736, fax 01734 257553, email robj++at++reading.sgi.com,
ABCs of the Z-Buffer
IRIS graphics systems utilize a hardware z-buffer to render images with hidden surfaces eliminated. The z-buffer hardware is configurable, meaning not only can its use be optimized for hidden surface elimination, it can be used for additional operations as well. This article describes the z-buffer, suggests how it can be used for simple hidden surface elimination, and provides solutions for some of the more complex operations, including applying decals, highlighting surface tessellations and generating line drawings with hidden lines removed. Many of the techniques described here are applicable to all IRIS graphics systems. The hardware-specific approaches, all of which have been noted as such, work with the GT/GTX architecture.
The z-buffer is a set of 24-bit numbers, each of which relates to a pixel on the screen. Use of the z-buffer is enabled with the zbuffer command:
zbuffer(TRUE);
Points, lines, polygons, and characters are reduced during rendering to pixels, each with its own color and x and y coordinates. Also, when the z- buffer is enabled, a z coordinate is computed for each pixel. This coordinate effectively specifies the distance from pixel to eye.
Before a pixel's color is written to the framebuffer, its z value is compared with the value already stored in the z-buffer. If the incoming pixel is nearer (that is, if it has a lower z value), its color is written into the framebuffer and its z is written into the z-buffer. If it is farther (that is, if it has a greater z value), the incoming color and z are discarded. Thus, at any point in the drawing, the values in the z-buffer represent the distance to the item which currently is closest to the eye. By first clearing the z-buffer to the maximum z value and then rendering all primitives using the zbuffer algorithm, an image including only the nearest surfaces to the eye is produced.
Mapping Transformed Z to Screen Z ---------------------------------
Mathematically, z coordinates are treated just like x and y coordinates. After transformation, clipping, and perspective division, they occupy the range -1.0 through 1.0. Just as you specify a viewport transformation to map x and y from this range to a window's boundaries, so must you specify a mapping of z coordinates. Because the 24-bit z-buffer on the GT and GTX graphics systems is unsigned, it supports the range OxOOOOOO through Oxffffff. Only values in the range OxOOOOOO through Ox7fffff are correctly iterated during rendering, however, so only 23 of the 24 z-buffer bits typically are used. (A use for the 24th bit will be demonstrated later in this article.)
By default, GT and GTX graphics systems map those pixels nearest the eye (i.e. the ones at the near clip plane) to OxOOOOOO, while the pixels farthest from the eye (i.e., those at the far clip plane) are mapped to Ox7fffff. This can be changed with the lsetdepth command:
lsetdepth(OxOOOOOO,Ox3fffff);
Called like this. the mapping will be reduced to use only half of the hardware range.
Recall also that each z-buffer location must be initialized to farthest prior to rendering. Application performance is increased if the z-buffer and the framebuffer are initialized at the same time. To accomplish this, replace the code sequence:
cpack(Oxffffff); clear(); zclear(); /*clears z-buffer to Oxffffff*/
with:
czclear(Oxffffff,Oxffffff);
On GT and GTX hardware, czclear operates on the framebuffer and the z-buffer simultaneously only if the color argument (first) and the depth argument (second) are the same (see the czclear man page for details). This example has a white background and the z-buffer has been cleared to its maximum value. The fact that this value is greater than the greatest rendered value (Ox7fffff) has no effect on the rendering operation.
It is also possible to achieve simultaneous clears with a black background, like so:
czclear(OxOOOOOO,OxOOOOOO);
For this to provide correct results, however, both the z mapping and the z comparison must be reversed:
lsetdepth(Ox7fffff,0xOOOOOO); zfunction(ZF_GEQUAL);
This mapping gives nearer pixels greater z-buffer values than farther pixels, with pixels at the near plane mapped to Ox7fffff, and those at the far plane mapped to OxOOOOOO (the value the z-buffer was cleared to). In order to correctly select the nearest pixel, the comparison function is simply reversed to select the pixel with the greater z value. (The default zfunction is ZF_LEQUAL.)
It was previously asserted that a z-buffer value, in effect, specifies the distance from pixel to eye. While this is true, the relationship between distance and z is linear only in an orthographic projection. In the case of a true perspective projection, such as:
perspective(fovy,aspect,near,far);
the relationship is non-linear, sometimes very much so.
The degree of non-linearity is controlled by the ratio of far to near in the perspective call. The greater the ratio, the greater the non-linearity. Figure 1 shows the equations relating screen z to eye z as a function of the ratio far/near in the perspective call.
------------------------------------------------------------------------------
When the projection matrix is defined by
perspective (fovy, aspect, near, far);
and the z viewport transformation is defined by
lsetdepth (near ,far ); vp vp
then z and z are related by the following equations: eye screen
-- -- -- -- | | |far - near | far + near |far + near 2 far near | | vp vp| vp vp z = |---------- + -----------------| |--------------| + -------------- screen |far - near z (far - near)| | 2 | 2 | eye | | | -- -- -- --
far near(far - near ) vp vp ------------------------ far - near z = ------------------------------------------------------------------ eye (far + near) (far - near ) far + near vp vp vp vp z - ----------------------------- - -------------- screen 2 (far - near) 2
------------------------------------------------------------------------------ Figure 1. The equations relating screen z to eye z.
In practice, non-linearity increases z-buffer precision in a small range adjacent to the near clipping plane and reduces precision throughout the rest of the viewing volume. While some precision increase near the viewer can be desirable, the effect of substantial non-linearity is to defeat z-buffer operation throughout much of the viewing volume. Experience shows that ratios greater than 1000 have this undesired result.
The ratio of far to near is most easily controlled by simply moving the near clipping plane away from the eye position. Note that changing near in the perspective call has no effect on the projection of x and y and so has little effect on the resulting image. Instead, its effect is limited to the position of the near clipping plane and the projection of z values. Because of this, you should always move the near plane as far from the eye as possible.
Z-Buffered Decals -----------------
Because of the limitations of z-buffer precision and because the precision that exists is lost due to non-linear mapping, the IRIS z-buffer has difficulty in computing the correct intersection of nearly coplanar objects. As a result, objects intended to be coplanar, such as a stripe on a runway, or a marking on an airplane wing, are rendered very poorly when nothing more than the standard z-buffer algorithm is used (e.g. a runway shows through a stripe on it or a wing shows through a marking on it). This problem is referred to as the decal problem.
GL commands writemask and zwritemask support a simple solution to the decal problem. In effect, they allow the use of a painter's algorithm (the last object written overwrites) among coplanar polygons, and yet they use the z- buffer algorithm to integrate coplanar polygons into the scene.
The following pseudo-code sequence implements the algorithm (the GL commands designate real code). The GrayMaterial object is the surface that the RedMaterial object is drawn onto:
zwritemask(OxOOOOOO); lmbind(MATERIAL,GrayMaterial); draw the underlying object lmbind(MATERIAL,RedMaterial); draw the overwriting object zwritemask(Oxffffff); wmpack(OxOOOOOOOO); /*RGB version of writemask*/ draw the underlying object again wmpack(Oxffffffff);
Note that material does not matter during the second drawing of the GrayMaterial object because framebuffer colors are not modified. The writemask is returned to Oxffffffff when done to ensure that subsequent z-buffer operation will be normal.
Outlined Polygons and Hidden Lines ----------------------------------
Now consider what's involved in outlining a polygon. The problem seems similar to rendering decals, except in this instance an outline segment shared between two (typically non-coplanar) polygons must be taken into account. The decal algorithm fails when a single decal is shared among multiple, non-coplanar polygons.
The decal problem was solved by working around the inaccuracies of z projection and iteration. Assume now that the z-buffer works perfectly, meaning that coplanar objects match values pixel for pixel during rendering. In this case, the painting effect could be had simply by selecting the appropriate zfunction, thus avoiding the need for writemask tricks. For example, selecting the zfunction ZF_LEQUAL (pass if less than or equal) causes the second coplanar polygon to completely overwrite the first, because the z- values of the coplanar polygons are equal at each pixel where they coincide and equality passes the z test. Likewise, if zfunction ZF_LESS is chosen, the second coplanar polygon will be completely ignored, because the equal z values will never pass the test.
This demonstrates that it is possible to accurately composite coplanar objects if their pixels match z for z. All that remains is to postulate an outline method that ensures matched z values. The method exists, and is known as hollow polygons.
(Definition: A hollow polygon fills a subset of the pixels that would have been filled had the polygon been drawn normally. This subset is comprised of the pixels adjacent to the polygon perimeter. All filled pixels have color and values identical to their counterpart pixels in the filled polygon.)
When a filled polygon is composited with its hollow counterpart, zfunction can be used to accurately control the results because the z values of shared pixels are identical. We can, for example, draw a gray solid object with each polygon outlined in red using the following code sequence:
zbuffer(TRUE); zfunction(ZF_LEQUAL); /*this is the default*/ lmbind(MATERIAL,GrayMaterial); draw all polygons -- filled lmbind(MATERIAL,RedMaterial);
draw all polygons -- hollow
Changing the zfunction to ZF_LESS will arrive at same result by first drawing all the polygons hollow, and then drawing them filled:
zbuffer(TRUE); zfunction(ZF_LESS); lmbind(MATERIAL,RedMaterial); draw all polygons -- hollow lmbind(MATERIAL,GrayMaterial);
draw all polygons -- filled
In both cases, all hidden surfaces are correctly removed and all outlines are drawn without pixel errors.
A hollow polygon primitive is available in Silicon Graphics' new PowerVision. However, until you have PowerVision, you'll have to generate the hollow polygons yourself. This is best done using the 24th bit of the z-buffer as a stencil bit to control whether a pixel is writable or not. By doing this independently of the contents of the 23 depth bits, it's possible to stencil and z-buffer simultaneously. (WARNING: This algorithm operates correctly only on GT and GTX graphics systems.)
The algorithm for hollow polygon generation is substantially more complex than any of the algorithms previously discussed. It will be described here one step at a time, first explaining what will happen, and then providing a pseudo code implementation. First, the z-buffer must be set up with a reverse mapping:
zbuffer(TRUE); lsetdepth(Ox7fffff,0xOOOOOO); zfunction(ZF_GEQUAL); czclear(OxOOOOOOOO,OxOOOOOO);
Then, to draw a single hollow polygon, first disable (for drawing purposes) all of the pixels in the polygon. Do this by setting the 24th bit of each pixel's z value, effectively making all values nearer than Ox7fffff, the mapping of the near plane:
zbuffer(FALSE); backbuffer(FALSE); zdraw(TRUE); wmpack(Ox800000); cpack(Ox800000);
fill the polygon
Now enable only those pixels on the perimeter of the polygon. Do this by drawing a width-2 line around the perimeter, clearing the 24th z-buffer bit at each pixel drawn:
cpack(OxOOOOOO);
outline the polygon with a double-width line
At this point, pixels on the perimeter of the polygon will have their original z values. Those in the interior will have values nearer than any generated during rendering and are therefore effectively masked. Now, we simply fill the polygon using normal z-buffer operation, which in fact fills only perimeter pixels -- creating a hollow polygon:
zbuffer(TRUE); backbuffer(TRUE); zdraw(FALSE); wmpack(Oxffffffff); cpack(color);
fill the polygon -- changing only perimeter pixel values
The 24th bit of all z-buffer pixels that still can be set now must be cleared to ensure that future hollow polygons are not corrupted:
zbuffer(FALSE); backbuffer(FALSE); zdraw(TRUE); wmpack(Ox800000); cpack(OxOOOOOO);
fill the polygon
Finally, all drawing states should be returned to reasonable values:
zbuffer(TRUE); backbuffer(TRUE); zdraw(FALSE); wmpack(Oxffffffff);
When hollow polygons are drawn back-to-back, many of the mode changes in the first and last steps can be skipped, resulting in improved performance.
You may draw any solid as a line drawing with hidden lines removed by simply filling the hollow polygons with background color rather than with surface color.
Conclusion ----------
The GT/GTX z-buffer is a powerful tool for use with applications -- beyond simple hidden-surface removal. In addition to the examples provided, the z- buffer has been used in programs to perform constructive solid geometry, shadow casting, and more.
This article was reprinted with permission from Issue Number 11 of the IRIS Universe, a quarterly magazine of visual processing published by Silicon Graphics. To subscribe to the magazine, call (415) 962-3320. ABCs of the Z-Buffer
IRIS graphics systems utilize a hardware z-buffer to render images with hidden surfaces eliminated. The z-buffer hardware is configurable, meaning not only can its use be optimized for hidden surface elimination, it can be used for additional operations as well. This article describes the z-buffer, suggests how it can be used for simple hidden surface elimination, and provides solutions for some of the more complex operations, including applying decals, highlighting surface tessellations and generating line drawings with hidden lines removed. Many of the techniques described here are applicable to all IRIS graphics systems. The hardware-specific approaches, all of which have been noted as such, work with the GT/GTX architecture.
The z-buffer is a set of 24-bit numbers, each of which relates to a pixel on the screen. Use of the z-buffer is enabled with the zbuffer command:
zbuffer(TRUE);
Points, lines, polygons, and characters are reduced during rendering to pixels, each with its own color and x and y coordinates. Also, when the z- buffer is enabled, a z coordinate is computed for each pixel. This coordinate effectively specifies the distance from pixel to eye.
Before a pixel's color is written to the framebuffer, its z value is compared with the value already stored in the z-buffer. If the incoming pixel is nearer (that is, if it has a lower z value), its color is written into the framebuffer and its z is written into the z-buffer. If it is farther (that is, if it has a greater z value), the incoming color and z are discarded. Thus, at any point in the drawing, the values in the z-buffer represent the distance to the item which currently is closest to the eye. By first clearing the z-buffer to the maximum z value and then rendering all primitives using the zbuffer algorithm, an image including only the nearest surfaces to the eye is produced.
Mapping Transformed Z to Screen Z ---------------------------------
Mathematically, z coordinates are treated just like x and y coordinates. After transformation, clipping, and perspective division, they occupy the range -1.0 through 1.0. Just as you specify a viewport transformation to map x and y from this range to a window's boundaries, so must you specify a mapping of z coordinates. Because the 24-bit z-buffer on the GT and GTX graphics systems is unsigned, it supports the range OxOOOOOO through Oxffffff. Only values in the range OxOOOOOO through Ox7fffff are correctly iterated during rendering, however, so only 23 of the 24 z-buffer bits typically are used. (A use for the 24th bit will be demonstrated later in this article.)
By default, GT and GTX graphics systems map those pixels nearest the eye (i.e. the ones at the near clip plane) to OxOOOOOO, while the pixels farthest from the eye (i.e., those at the far clip plane) are mapped to Ox7fffff. This can be changed with the lsetdepth command:
lsetdepth(OxOOOOOO,Ox3fffff);
Called like this. the mapping will be reduced to use only half of the hardware range.
Recall also that each z-buffer location must be initialized to farthest prior to rendering. Application performance is increased if the z-buffer and the framebuffer are initialized at the same time. To accomplish this, replace the code sequence:
cpack(Oxffffff); clear(); zclear(); /*clears z-buffer to Oxffffff*/
with:
czclear(Oxffffff,Oxffffff);
On GT and GTX hardware, czclear operates on the framebuffer and the z-buffer simultaneously only if the color argument (first) and the depth argument (second) are the same (see the czclear man page for details). This example has a white background and the z-buffer has been cleared to its maximum value. The fact that this value is greater than the greatest rendered value (Ox7fffff) has no effect on the rendering operation.
It is also possible to achieve simultaneous clears with a black background, like so:
czclear(OxOOOOOO,OxOOOOOO);
For this to provide correct results, however, both the z mapping and the z comparison must be reversed:
lsetdepth(Ox7fffff,0xOOOOOO); zfunction(ZF_GEQUAL);
This mapping gives nearer pixels greater z-buffer values than farther pixels, with pixels at the near plane mapped to Ox7fffff, and those at the far plane mapped to OxOOOOOO (the value the z-buffer was cleared to). In order to correctly select the nearest pixel, the comparison function is simply reversed to select the pixel with the greater z value. (The default zfunction is ZF_LEQUAL.)
It was previously asserted that a z-buffer value, in effect, specifies the distance from pixel to eye. While this is true, the relationship between distance and z is linear only in an orthographic projection. In the case of a true perspective projection, such as:
perspective(fovy,aspect,near,far);
the relationship is non-linear, sometimes very much so.
The degree of non-linearity is controlled by the ratio of far to near in the perspective call. The greater the ratio, the greater the non-linearity. Figure 1 shows the equations relating screen z to eye z as a function of the ratio far/near in the perspective call.
------------------------------------------------------------------------------
When the projection matrix is defined by
perspective (fovy, aspect, near, far);
and the z viewport transformation is defined by
lsetdepth (near ,far ); vp vp
then z and z are related by the following equations: eye screen
-- -- -- -- | | |far - near | far + near |far + near 2 far near | | vp vp| vp vp z = |---------- + -----------------| |--------------| + -------------- screen |far - near z (far - near)| | 2 | 2 | eye | | | -- -- -- --
far near(far - near ) vp vp ------------------------ far - near z = ------------------------------------------------------------------ eye (far + near) (far - near ) far + near vp vp vp vp z - ----------------------------- - -------------- screen 2 (far - near) 2
------------------------------------------------------------------------------ Figure 1. The equations relating screen z to eye z.
In practice, non-linearity increases z-buffer precision in a small range adjacent to the near clipping plane and reduces precision throughout the rest of the viewing volume. While some precision increase near the viewer can be desirable, the effect of substantial non-linearity is to defeat z-buffer operation throughout much of the viewing volume. Experience shows that ratios greater than 1000 have this undesired result.
The ratio of far to near is most easily controlled by simply moving the near clipping plane away from the eye position. Note that changing near in the perspective call has no effect on the projection of x and y and so has little effect on the resulting image. Instead, its effect is limited to the position of the near clipping plane and the projection of z values. Because of this, you should always move the near plane as far from the eye as possible.
Z-Buffered Decals -----------------
Because of the limitations of z-buffer precision and because the precision that exists is lost due to non-linear mapping, the IRIS z-buffer has difficulty in computing the correct intersection of nearly coplanar objects. As a result, objects intended to be coplanar, such as a stripe on a runway, or a marking on an airplane wing, are rendered very poorly when nothing more than the standard z-buffer algorithm is used (e.g. a runway shows through a stripe on it or a wing shows through a marking on it). This problem is referred to as the decal problem.
GL commands writemask and zwritemask support a simple solution to the decal problem. In effect, they allow the use of a painter's algorithm (the last object written overwrites) among coplanar polygons, and yet they use the z- buffer algorithm to integrate coplanar polygons into the scene.
The following pseudo-code sequence implements the algorithm (the GL commands designate real code). The GrayMaterial object is the surface that the RedMaterial object is drawn onto:
zwritemask(OxOOOOOO); lmbind(MATERIAL,GrayMaterial); draw the underlying object lmbind(MATERIAL,RedMaterial); draw the overwriting object zwritemask(Oxffffff); wmpack(OxOOOOOOOO); /*RGB version of writemask*/ draw the underlying object again wmpack(Oxffffffff);
Note that material does not matter during the second drawing of the GrayMaterial object because framebuffer colors are not modified. The writemask is returned to Oxffffffff when done to ensure that subsequent z-buffer operation will be normal.
Outlined Polygons and Hidden Lines ----------------------------------
Now consider what's involved in outlining a polygon. The problem seems similar to rendering decals, except in this instance an outline segment shared between two (typically non-coplanar) polygons must be taken into account. The decal algorithm fails when a single decal is shared among multiple, non-coplanar polygons.
The decal problem was solved by working around the inaccuracies of z projection and iteration. Assume now that the z-buffer works perfectly, meaning that coplanar objects match values pixel for pixel during rendering. In this case, the painting effect could be had simply by selecting the appropriate zfunction, thus avoiding the need for writemask tricks. For example, selecting the zfunction ZF_LEQUAL (pass if less than or equal) causes the second coplanar polygon to completely overwrite the first, because the z- values of the coplanar polygons are equal at each pixel where they coincide and equality passes the z test. Likewise, if zfunction ZF_LESS is chosen, the second coplanar polygon will be completely ignored, because the equal z values will never pass the test.
This demonstrates that it is possible to accurately composite coplanar objects if their pixels match z for z. All that remains is to postulate an outline method that ensures matched z values. The method exists, and is known as hollow polygons.
(Definition: A hollow polygon fills a subset of the pixels that would have been filled had the polygon been drawn normally. This subset is comprised of the pixels adjacent to the polygon perimeter. All filled pixels have color and values identical to their counterpart pixels in the filled polygon.)
When a filled polygon is composited with its hollow counterpart, zfunction can be used to accurately control the results because the z values of shared pixels are identical. We can, for example, draw a gray solid object with each polygon outlined in red using the following code sequence:
zbuffer(TRUE); zfunction(ZF_LEQUAL); /*this is the default*/ lmbind(MATERIAL,GrayMaterial); draw all polygons -- filled lmbind(MATERIAL,RedMaterial);
draw all polygons -- hollow
Changing the zfunction to ZF_LESS will arrive at same result by first drawing all the polygons hollow, and then drawing them filled:
zbuffer(TRUE); zfunction(ZF_LESS); lmbind(MATERIAL,RedMaterial); draw all polygons -- hollow lmbind(MATERIAL,GrayMaterial);
draw all polygons -- filled
In both cases, all hidden surfaces are correctly removed and all outlines are drawn without pixel errors.
A hollow polygon primitive is available in Silicon Graphics' new PowerVision. However, until you have PowerVision, you'll have to generate the hollow polygons yourself. This is best done using the 24th bit of the z-buffer as a stencil bit to control whether a pixel is writable or not. By doing this independently of the contents of the 23 depth bits, it's possible to stencil and z-buffer simultaneously. (WARNING: This algorithm operates correctly only on GT and GTX graphics systems.)
The algorithm for hollow polygon generation is substantially more complex than any of the algorithms previously discussed. It will be described here one step at a time, first explaining what will happen, and then providing a pseudo code implementation. First, the z-buffer must be set up with a reverse mapping:
zbuffer(TRUE); lsetdepth(Ox7fffff,0xOOOOOO); zfunction(ZF_GEQUAL); czclear(OxOOOOOOOO,OxOOOOOO);
Then, to draw a single hollow polygon, first disable (for drawing purposes) all of the pixels in the polygon. Do this by setting the 24th bit of each pixel's z value, effectively making all values nearer than Ox7fffff, the mapping of the near plane:
zbuffer(FALSE); backbuffer(FALSE); zdraw(TRUE); wmpack(Ox800000); cpack(Ox800000);
fill the polygon
Now enable only those pixels on the perimeter of the polygon. Do this by drawing a width-2 line around the perimeter, clearing the 24th z-buffer bit at each pixel drawn:
cpack(OxOOOOOO);
outline the polygon with a double-width line
At this point, pixels on the perimeter of the polygon will have their original z values. Those in the interior will have values nearer than any generated during rendering and are therefore effectively masked. Now, we simply fill the polygon using normal z-buffer operation, which in fact fills only perimeter pixels -- creating a hollow polygon:
zbuffer(TRUE); backbuffer(TRUE); zdraw(FALSE); wmpack(Oxffffffff); cpack(color);
fill the polygon -- changing only perimeter pixel values
The 24th bit of all z-buffer pixels that still can be set now must be cleared to ensure that future hollow polygons are not corrupted:
zbuffer(FALSE); backbuffer(FALSE); zdraw(TRUE); wmpack(Ox800000); cpack(OxOOOOOO);
fill the polygon
Finally, all drawing states should be returned to reasonable values:
zbuffer(TRUE); backbuffer(TRUE); zdraw(FALSE); wmpack(Oxffffffff);
When hollow polygons are drawn back-to-back, many of the mode changes in the first and last steps can be skipped, resulting in improved performance.
You may draw any solid as a line drawing with hidden lines removed by simply filling the hollow polygons with background color rather than with surface color.
Conclusion ----------
The GT/GTX z-buffer is a powerful tool for use with applications -- beyond simple hidden-surface removal. In addition to the examples provided, the z- buffer has been used in programs to perform constructive solid geometry, shadow casting, and more.
This article was reprinted with permission from Issue Number 11 of the IRIS Universe, a quarterly magazine of visual processing published by Silicon Graphics. To subscribe to the magazine, call (415) 962-3320.
This archive was generated by hypermail 2.0b2 on Mon Aug 10 1998 - 17:52:43 PDT