Depth sorting alpha blended objects

Depth sorting alpha blended objects

Rate This
  • Comments 17

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

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:

Untitled

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.

 

Painter’s Algorithm

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?

Untitled

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?

Untitled

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.

 

Backface Culling

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:

  1. Set DepthBufferEnable and DepthBufferWriteEnable to true
  2. Draw all opaque geometry
  3. Leave DepthBufferEnable set to true, but change DepthBufferWriteEnable to false
  4. 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
  • Really great article.  Thank you!

  • Have to agree with Tryz.

    Informative, clear and useful! Thanks Shawn. :¬)

  • Thanks for treating this topic, I can never find any clear information on it.  Now my intuition has been confirmed - there's no "perfect" method for doing arbitrary translucency blending.

  • Thank you very much. Youd did fix my bug!

  • It was very helpful in understanding the concept of depth buffer in XNA.

  • I'm quite late to the party, but I have a question about the sorting itself (for the Painter’s Algorithm). Do we use List.Sort(IComparer<T>) for that? Is it the best function to apply this principle?

  • DragonSix: you could use any of the many .NET sort functions, or implement your own sort. It really depends how you store these objects. If they are in a list, List.Sort would be a good way to sort them. If they are in an array, use Array.Sort. If they come from a LINQ query, use the LINQ sorting feature, etc.

  • I made .FBX model in Autodesk Maya. This model contain of bones which are drawn in wrong order. Can I sort bones in Model.Bones collection? Or I could organize every bone to separate object?

  • This assumes that you can actually control the order in which things are drawn. Yes, mesh1 and mesh2 as completely separate objects can be drawn in whatever order you want. But Mesh1 with multiple parts can't be. If you have a mesh with alpha blended textures (say DDS or PNG) with a gradient alpha channels (something between 0 and 1) that need to render on top of the other mesh parts, then you're hosed. The removal of Renderstates has completely borked the ability to do that.

  • David,

    Of course, if you want to sort geometry you need to split into pieces as fine grained as whatever level you want to sort it at. If you plan to sort at the individual triangle level (which will be crazy expensive, but is sometimes necessary for rendering complex translucent) then you need to be drawing individual triangles rather than meshes which each contain large numbers of triangles.

    This has nothing to do with any particular graphics API or XNA version - it's just how computer graphics rendering works.

    XNA 4.0 didn't remove renderstates, it just changed the API to use state objects.  It did remove one specific renderstate, which is the alphatest feature that used to be supported in D3D versions prior to 10. But that functionality is not gone, just implemented in a different place. If you want to entirely kill pixels based on their alpha value, use the clip() intrinsic in your pixel shader, which does the exact same thing as the old DX9 alphatest renderstate.

    Note that alpha test only works for binary on/off alpha. If you have fractional alpha values or gradients, you need full blending, which means you need to worry about sorting. This is true regardless of what D3D, XNA, or OpenGL version you happen to be using to implement this rendering.

  • If the alphatest renderstate has been moved I'd love to see a link that provides a working example of how to replace it. "Use the AlphaTestEffect" doesn't count as it only has one light, no normal map, and no specular map. There's the Stock Effects' sample, but that is so convoluted it's impossible to understand what's going on. It's also still on XNA3.1, which means it won't compile (Microsoft has already flagged a bug for that, but that doesn't help much). To add to the confusion, there are no comments in the shader code to explain anything. How does clip() know to kill a pixel but still leave the one that was behind it (because they'd be on the same mesh part)? I really can't see how clip() provides a replacement.

    That said, if adding clip() to a shader is the ONLY way to achieve what was previously achievable, then there needs to be a simple yet detailed XNA alpha test specific example. I'll be happy to provide the mesh for this becuase the ones used in the current examples don't prove extreme cases.  

    Better yet, the frame work itself needs a replacement for the Alphatest renderstate function. Telling people to go add it to their shader is an impractical solution as I'd have to believe that the majority of people are using the stock effects (even as limiting as those are... why don't they have normal and specular map support?) so that means they don't have access to changing the shader anyway.

    Thank you

    David

  • > I really can't see how clip() provides a replacement.

    The clip() intrinsic conditionally kills the current pixel, which prevents it from being rendered. This is exactly the same as killing a pixel through old style fixed function alpha testing, except more generic. The results are 100% identical.

    > Telling people to go add it to their shader is an impractical solution as I'd have to believe that the majority of people are using the stock effects

    Modern graphics programming is all about shaders, regardless of what API you choose to program with. The stock effects are intended as a simple starting point for beginners, and as a solution for more limited platforms like Windows Phone which do not support fully programmable shaders. I absolutely would expect anyone writing high end, visually detailed 3D games to learn and write their own shaders - that's really the only way to access the full power of a modern graphics API.

  • hi shawn

    My question is:

    Why is the graphics cards constructed in a way, that a programmer dont need to worry about alphablending at all. every pixel with an alpha-value <1 is a transparent one. So the graphics card can now decide according to the z-value of the pixel, how to blend this pixel with the pixel already in the backbuffer.

    So as a programmer u dont have to bother about sorting your objects or even switching on/ogg the depth buffer.

  • > Why is the graphics cards constructed in a way, that a programmer dont need to worry about alphablending at all

    How would that work?

    GPU hardware isn't magic. Regardless of whether you implement something in hardware or software, you still need an algorithm to implement it, and no algorithm can do the impossible.

    It would certainly be possible to create a hardware implementation of any of the techniques described in this article (and various hardware designs have done exactly that over the years) but the same strengths, weaknesses, and tradeoffs apply regardless of whether you implement them in hardware or software.

    People have generally found that zbuffer is a good algorithm to implement in hardware, but geometry sorting is better done in software, so that's where most modern designs have landed.

  • I've applied some of your solution to my Silverlight 3D project... works perfectly... 1M kudos ^_^y.

Page 1 of 2 (17 items) 12
Leave a Comment
  • Please add 1 and 8 and type the answer here:
  • Post