Remember when you first figured out Santa Claus wasn't real? The growing doubt, tempered by the fact that all your friends believed in him, and surely they can't ALL be wrong, then the gradual realization that everybody was in fact wrong...
Well, I've got another one for you: the way most people do alpha blending is bogus!
At a fundamental level, alpha has no special meaning at all. Graphics cards manipulate numeric values, but what they do with these values is entirely determined by what shader code you write and which render states you set. The hardware works with Vector4 data types. Because these often represent colors, it is natural to use the first three components for red, green, and blue. That leaves a spare component, called alpha, which can be used for absolutely anything we like.
Some people use alpha to store shininess, or ambient occlusion, or a collision material ID. In the MotoGP particle system I used it to pass a per-particle random number into my vertex shader physics. But most often, alpha is used to represent transparency. It is important to understand that this is just a convention, and there are several different ways it can be done.
Conventional Alpha Blending
The majority of programmers, programs, programming APIs, file formats, etc, define transparency as:
- RGB specifies the color of the object
- Alpha specifies how solid it is
blend(source, dest) = (source.rgb * source.a) + (dest.rgb * (1 - source.a))
RenderState.SourceBlend = Blend.SourceAlpha; RenderState.DestinationBlend = Blend.InverseSourceAlpha;
In this world, RGB and alpha are independent. You can change one without affecting the other. Even when an object is fully transparent it still has the same RGB as if it was opaque. Thus 100% transparency can be represented by many different color values.
There isn't really a direct physical analogy for this. I guess it's similar to how a magic cloak of invisibility works in a fantasy universe, where I can be wearing the same red sweater as I am right now, and it remains red even though it currently happens to be invisible.
Premultiplied Alpha Blending
Here's a different way to think about transparency:
- RGB specifies how much color the object contributes to the scene
- Alpha specifies how much it obscures whatever is behind it
blend(source, dest) = source.rgb + (dest.rgb * (1 - source.a))
RenderState.SourceBlend = Blend.One; RenderState.DestinationBlend = Blend.InverseSourceAlpha;
In this world, RGB and alpha are linked. To make an object transparent you must reduce both its RGB (to contribute less color) and also its alpha (to obscure less of whatever is behind it). Fully transparent objects no longer have any RGB color, so there is only one value that represents 100% transparency (RGB and alpha all zero).
This is more like how light behaves in the real world. What is the RGB of my car windscreen? None: it is transparent, so has no color. How about my sunglasses? These have a fractional alpha value (letting some light some through, while blocking some) and also contribute some RGB for that nice rose-tinted glow.
To use premultiplied alpha, in addition to setting the appropriate renderstates, you must also convert your source graphics into premultiplied format. Drawing a non premultiplied color with premultiplied blending will not give sensible results!
To convert a non premultiplied color into premultiplied format:
color.rgb *= color.a
(hence why this is called "premultiplied" format)
Look at the blend equations for conventional vs. premultiplied alpha. If you substitute this color format conversion into the premultiplied blend function, you get the conventional blend function, so either way produces the same end result. The difference is that premultiplied alpha applies the (source.rgb * source.a) computation as a preprocess rather than inside the blending hardware.
I will write more about how to convert graphics into premultiplied format in a later post.
Why premultiplied r0x0rz
Premultiplied alpha is better than conventional blending for several reasons:
- It works properly when filtering alpha cutouts (see below)
- It works properly when doing image composition (stay tuned for my next post)
- It is a superset of both conventional and additive blending. If you set alpha to zero while RGB is non zero, you get an additive blend. This can be handy for particle systems that want to smoothly transition from additive glowing sparks to dark pieces of soot as the particles age.
- It plays nice with DXT compression, which only supports transparent pixels with an RGB of zero.
Premultiplied alpha cutouts
Remember how filtering can produce ugly fringes around the edges of alpha cutouts? Not a problem when using premultiplied alpha! Revisiting the example from my previous post:
tree = (0, 255, 0, 255)
border = (0, 0, 0, 0)
background = (0, 0, 255)
filtered = (tree + border) / 2 = (0, 128, 0, 128)
With conventional alpha blending, the result is darker than we wanted:
result = lerp(background, filtered.rgb, filtered.a) = (0, 64, 128)
But premultiplied blending produces the right answer:
result = filtered.rgb + (background * filtered.a) = (0, 128, 128)
This works because texture filtering and premultiplied blending are both linear transforms, and are therefore associative:
blend(filter(a, b), c) == filter(blend(a, c), blend(b, c))