Taking a brief digression from our regularly scheduled reminiscences, I’m going to tackle a question that comes up regularly on the Creators Club forums, but which (to my surprise) I have been unable to find a comprehensive answer for on the web.
This is a simple question with a complex answer:
“How come my transparent objects are drawn in the wrong order, or parts of them are missing?”
When drawing a 3D scene, it is important to depth sort the graphics, so things that are close to the camera will be drawn over the top of things from further away. We do not want those distant mountains to be drawn over the top of the building that is right in front of us!
There are three depth sorting techniques in widespread use today:
Unfortunately, all have limitations. To get good results, most games rely on a combination of all three.
Depth buffering is simple, efficient, and it gives perfect results, as long as you only draw opaque objects. But it doesn’t work at all for alpha blended objects!
This is because the depth buffer only keeps track of the closest pixel that has been drawn so far. For opaque objects, that is all you need. Consider this example of drawing two triangles, A and B:
If we draw B first, then A, the depth buffer will see that the new pixels from A are closer than the ones previously drawn by B, so it will draw them over the top. If we draw in the opposite order (A followed by B) the depth buffer will see that the pixels coming in from B are further away than the ones already drawn by A, so it will discard them. In either case we get the correct result: A is on top, with B hidden behind it.
But what if this geometry is alpha blended, so B is partially visible through the translucent A triangle? This still works if we draw B first, then A over the top, but not if we draw A followed by B. In that case, the depth buffer will get a pixel from B, and notice that it has already drawn a closer pixel from A, but it has no way to deal with this situation. It’s only choices are to draw the B pixel (which will give the wrong result, because it would be blending the more distant B over the top of the closer A, and alpha blending is not commutative) or it could discard B entirely. Not good!
Summary: depth buffering is perfect for opaque objects, but useless for alpha blended ones.
If the depth buffer cannot deal with drawing alpha blended objects in the wrong order, there is an easy fix, right? Just make sure we always draw them in the right order! If we sort all the objects in our scene, we can draw the more distant ones first, then the closer ones over the top, which makes sure the above example will always draw B before A.
Unfortunately, this is easier said than done. There are many situations where sorting objects is not sufficient. For instance, what if objects A and B intersect each other?
This could happen if A was a wineglass and B was a glass marble placed inside it. Now there is no correct way to sort these objects, because part of A is closer than B, but another part of it is further away.
We don’t even need two separate objects to run into this problem. What about the individual triangles that make up our wineglass? For this to appear correctly, we need to draw the back side of the glass before the front side. So it is not enough just to sort by object: we really need to sort each individual triangle.
Trouble is, sorting individual triangles is very expensive! And even if we could afford that, it would still not be enough to get correct results in all situations. What if two alpha blended triangles intersect each other?
There is no possible way to sort these triangles, because we need to draw the top part of B over A, but the bottom part of A over B. The only solution is to detect when this happens and split the triangles where they intersect, but that would be prohibitively expensive.
Summary: painter’s algorithm requires you to make a tradeoff when deciding what granularity to sort at. If you sort just a few large objects it will be fast but not very accurate. If you sort many smaller objects (up to the extreme case of sorting individual triangles) it will be slower but more accurate.
People tend not to think of backface culling as a sorting technique, but it is actually an important one. The limitation is that it only works for convex objects.
Consider a simple convex shape such as a sphere or cube. No matter what angle you look at it from, each screen pixel will be covered exactly twice: once by the front side of the object, then again by the back. If you use backface culling to reject triangles from the back side of the object, you are left with only the front. Tada! If each screen pixel is covered only once, you automatically have perfect alpha blending with no need to sort anything.
But of course, most games want to draw something more interesting than just a single sphere or cube :-) So backface culling alone is not an adequate solution.
Summary: backface culling is perfect for convex objects, but useless for anything else.
How Do I Make My Game Look Good?
The most common approach:
- Set DepthBufferEnable and DepthBufferWriteEnable to true
- Draw all opaque geometry
- Leave DepthBufferEnable set to true, but change DepthBufferWriteEnable to false
- Sort alpha blended objects by distance from the camera, then draw them in order from back to front
This relies on a combination of all three sorting techniques:
- Opaque objects are sorted by the depth buffer
- Alpha versus opaque objects are also handled by the depth buffer (so you will never see an alpha blended object through a closer opaque one)
- Painter’s algorithm sorts alpha blended objects relative to each other (which causes sorting errors if two alpha blended objects intersect)
- Relies on backface culling to sort the individual triangles within a single alpha blended object (which causes sorting errors if alpha blended objects are not convex)
The results are not perfect, but this is efficient, reasonably easy to implement, and good enough for most games.
There are various things you can do to improve the sorting accuracy:
Avoid alpha blending! The more things you can make opaque, the easier and more accurate your sorting will be. Do you really need alpha blending everywhere you are using it? If your level design calls for layer upon layer of glass windows, consider changing the design make it easier to implement. If you are using alpha blending for cut-out shapes such as trees, consider using alpha test instead, which is a binary accept/reject decision where the accepted pixels remain opaque and can be sorted by the depth buffer.
Relax, don’t worry. Maybe the sorting errors aren’t actually so bad? Perhaps you can tweak your graphics (making the alpha channel softer and more translucent) to make the mistakes less obvious. This is the approach used by our Particle 3D sample, which makes no attempt to sort individual particles within each cloud of smoke, but chose a particle texture that makes this look ok. If you change the smoke texture to something more solid, the sorting errors will be noticeable.
If you have alpha blended models that are not convex, maybe you could change them to make them more convex? Even if they cannot be perfectly convex, the closer they become, the fewer sorting errors will result. Consider splitting complex models into multiple pieces that can be sorted independently. A human body is nowhere near convex, but if you separate the torso, head, arms, etc, each individual piece is approximately convex.
If you have texture masks that are basically on/off cut-outs, but which include a few alpha blended pixels for antialiasing around their edges, you can use a two pass rendering technique:
- Pass 1: draw the solid part: alpha blending disabled, alpha test set to only accept the 100% opaque areas, and depth buffer enabled
- Pass 2: draw the fringes: alpha blending enabled, alpha test set to only accept pixels with alpha < 1, depth buffer enabled, depth writes disabled
At the cost of rendering everything twice, this provides 100% correct depth buffer sorting for the solid interior of each texture, plus less accurate sorting for the alpha blended fringes. It can be a good way to get some antialiasing around the edges of texture cut-outs, while still taking advantage of the depth buffer to avoid having to manually sort individual trees or blades of grass. We used this technique in our Billboard sample: see the comment and effect passes in Billboard.fx.
Use a z prepass. This is a good technique if you want to fade out an object that would normally be opaque, without seeing through the near side of the object to other parts of itself. Consider a human body, viewed from the right. If this was made of glass you would expect to see through the right arm to the torso and left arm. But if it is a solid person in the process of fading out (maybe they are a ghost, or being teleported, or respawning after being killed) you would expect to see only the translucent right arm, plus the background scenery behind it, without the torso or left arm being visible at all. To achieve this:
- Set ColorWriteChannels=None, and enable the depth buffer
- Draw the object into the depth buffer (which will not affect the color buffer)
- Set ColorWriteChannels=All, DepthBufferFunction=Equal, and enable alpha blending
- Draw the object again, which will blend only its closest side into the color buffer