Alpha Blending, Part 2
Rough Sorting
Last time I explained how “normal” alpha blending works, and why the objects should be drawn back to front. Sorting the objects can be a hassle, though. If it suits your game, you could just try controlling the order your draw calls happen. If you’re using DrawableGameComponent, this is an ideal use of the DrawOrder property.
For example, let’s say I’m writing a 2.5D shooter game. My plane is flying over a desolate gray industrial wasteland, with giant smokestacks belching out flames (Blade Runner, anyone?). Every now and then, the plane will be partially obscured as it flies under some thick pollution clouds. If I don’t do any sorting, it’s possible that the first thing to be drawn will be the clouds. If that happens, they’ll write their depth value to the depth buffer, causing everything else to fail the depth test. The clouds would obscure the whole scene, even though they’re transparent. So I know I need to sort my objects somehow.
Since this is a 2.5D game, and the player has no control over the camera, I know that objects in the foreground will always be in the foreground, and the background objects will always be the background. So, I can just hardcode my objects’ draw order. Assuming all of the things I need to draw are DrawableGameComponents, I can just set the DrawOrder property in the object’s constructor. First, we’ll draw our opaque objects. Next, the transparent background objects should draw: flames from the smokestacks. Then the transparent playfield objects, like explosions from blowing up enemies. Finally the transparent foreground objects: that nasty pollution cloud. I’d recommend defining an enum somewhere and then using that enum, rather than just sprinkling numbers everywhere.
public enum ShooterDrawOrder : int
{
ForegroundOpaque = 100,
PlayfieldOpaque = 200,
BackgroundOpaque = 300,
BackgroundTransparent = 400,
PlayfieldTransparent = 500,
ForegroundTransparent = 600
};
public class SmokeStackFlames : DrawableGameComponent
{
public SmokeStackFlames(Game game)
: base(game)
{
this.DrawOrder = (int)ShooterDrawOrder.BackgroundTransparent;
}
}
This is a good first try, but it’s not perfect. It’ll make foreground and background transparent objects play nicely together, which in some cases might be good enough. But think about what happens once we start putting a whole bunch of transparent objects together in the same place. What if each smokestack emits 15 transparent flames? Those are all in the background, and none are sorted, so we’re right back where we started.
Disabling Depth Write:
If you remember, the whole reason we have to worry about this is because transparent things will write to the depth buffer and obscure things behind them. Another possible hack to avoid this problem is to disable depth writing. Bear in mind though, this technique won’t give correct results: just less objectionable ones.
Remember how the depth buffer works? When a pixel is drawn, first a check is made in the depth buffer to see if there is already something that is closer than this new pixel. This is the depth test. If there’s nothing closer, the pixel is drawn, and the pixel’s depth value is written to the depth buffer. This is the depth write. Think about what happens, then, if we disable depth writing when transparent objects are drawn. If they can’t write to the depth buffer, it’ll be impossible for them to obscure anything. However, the depth test is still enabled, so the transparent objects won’t draw over any opaque objects that are in front of them.
At first glance, this looks perfect: we’ll draw our opaque objects, then turn off the depth write and draw transparent objects. The transparent objects can’t obscure one another, and it looks like our problem is solved. Unfortunately, standard alpha blending is order dependent: if we have two transparent polygons, the final blended color depends on the order in which the objects are drawn. To see why, consider this example:
We’ve cleared the back buffer to white, and now we’re drawing our transparent objects with depth buffering disabled. The gray circle is behind the blue circle. Both circles are transparent. Using the standard blending function from the previous article:
FinalColor = (SourceColor * SourceAlpha) + (DestinationColor * InverseSourceAlpha)
Let’s see what happens to the blue channel when these circles are drawn.
FinalColorB = (SourceColorB * SourceAlpha) + (DestinationColorB * InverseSourceAlpha)
Here’s what happens when we draw the gray circle first. The gray circle is (.5, .5, .5, .5), and the back buffer is white (1, 1, 1).
FinalColorB = (SourceColorB * SourceAlpha) + (DestinationColorB * InverseSourceAlpha)
FinalColorB = (.5 * .5) + (1 * (1 - .5))
FinalColorB = (.5 * .5) + (1 * .5)
FinalColorB = (.25) + (.5)
FinalColorB = .75
Now let’s blend in the blue circle, which is colored (0, 0, 1, .5). DestinationColorB will the output of that last blend, .75.
FinalColorB = (SourceColorB * SourceAlpha) + (DestinationColorB * InverseSourceAlpha)
FinalColorB = (1 * .5) + (.75 * (1 - .5))
FinalColorB = (1 * .5) + (.75 * .5)
FinalColorB = .5 + .375
FinalColorB = .875
So, when we draw the gray circle first, then the blue one, the resulting color is .875. What if we do it the other way around? Remember, the depth test is disabled, and we’re not doing any sorting, so it’s pretty much arbitrary which will get drawn first. So, starting from the white back buffer and blending in the blue circle…
FinalColorB = (SourceColorB * SourceAlpha) + (DestinationColorB * InverseSourceAlpha)
FinalColorB = (1 * .5) + (1 * (1 - .5))
FinalColorB = 1
Then the gray circle…
FinalColorB = (SourceColorB * SourceAlpha) + (DestinationColorB * InverseSourceAlpha)
FinalColorB = (.5 * .5) + (1 * (1 - .5))
FinalColorB = .25 + .5
FinalColorB = .75
The result is different; clearly, for standard alpha blending, order matters even if depth write is turned off.
Gray then blue (correct):
Blue then gray (wrong):
If the results are still wrong, why am I even wasting time talking about this? To a certain extent, it’s because your game might be able to get away with it: the user might not notice. The results certainly look much less objectionable than the same scene with depth write turned on. If depth writing is left on when drawing transparent objects, it’s possible for transparent objects to obscure things they are in front of. This is really noticeable; most users will catch it. If depth writing is turned off, like I just described above, the colors will still be wrong, but if you’re lucky, you might get close.
The real reason I’m mentioning depth write disabled, though, is as an introduction to additive alpha blending. Depth write disabled and additive blending are like peanut butter and jelly, especially for explosions :) But I’ll talk more about that in my next post. (I know I promised to write about it in this one, but this post is already stretching it.)
Comments
Anonymous
December 12, 2006
I like Lisp . I've never had the chance to write anything significant using it, but it always struckAnonymous
May 30, 2007
So today while I was riding the MAX to and from work I was reading some great articles from the CornflowerAnonymous
February 20, 2008
转自:http://blogs.msdn.com/etayrien/archive/2006/12/07/alpha-blending-part-3.aspx Ok,inthispostI'...