This post is about order independent transparency, what it is and why it matters. Wikipedia has a stub on order independent transparency, which briefly explains what it is. Is order independent transparency necessary? Yes, sometimes, but it depends on the function used to simulate transparency effects. The most often used function is probably the one that is commonly known as alpha blending, where each pixel has an associated alpha value, which is used as its opacity. The problem is, order matters.
Order Dependent Transparency
In traditional alpha blending, the equation implemented by blending hardware looks like this:
Here, is the source RGB vector (the one output by your shader) and is the destination RGB vector (the content of the framebuffer). is the source alpha value. would be the destination alpha value if it appeared in the equation. This can be produced in OpenGL like this:
glBlendEquation(GL_FUNC_ADD); glBendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
Note that the call to
glBlendEquation isn’t strictly necessary as the default blend equation is addition. This particular configuration is so commonly used that the OpenGL reference page for
glBlendFunc explicitly calls it out:
Transparency is best implemented using blend function (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) with primitives sorted from farthest to nearest.
Now, this is where our order independent conundrum comes in. Notice the words, “sorted from farthest to nearest”. You only get the results you are expecting if you sort your geometry from back to front. The order in which fragments are blended together affects the outcome. We can rewrite the above equation like this:
As you can see, the term accumulated into the framebuffer is formed from the difference between the new source color and its existing content and this is computed using subtraction. Subtraction is not commutative; the order in which the values are subtracted from each other matters. There are many approaches to ensure correct ordering and they range from simply sorting complete objects and rendering them back to front, to complex methods involving fragment lists constructed using atomics and resolved using compute. They all come with trade-offs and which you might use depends on the architecture of your rendering engine.
Order Independent Blending
Now, what if we could instead use a blending function that was commutative? The commutative operations include simple multiplication and addition. When the blending function consists only of multiplications or only of additions, then it doesn’t matter what order the operations are performed in, and it doesn’t matter what order you render your transparent geometry in.
Let’s tackle multiplication first. We want an equation that looks like this:
We can set this up with a call to glBlendFunc like this:
GL_ZERO there. If you think about what this does, you’ll realize that it throws away the source part of the equation and rather uses the source color as the destination factor, resulting in the framebuffer content simply being multiplied by the value produced by your shader. Now, think about what translucency (the effect we’re trying to simulate here) really means. Translucency is effectively attenuation.
If you hold a piece of colored glass up in front of a light, the glass doesn’t add anything to the scene. Rather, it absorbs a fraction of the light passing through it. Thinking purely in primaries, this means that it multiplies each channel of the light the light by a factor that’s dependent on the material’s color. Holding a piece of green glass in front of a white light will make the light look green, but holding that light in front of a red light will make the glass appear black. This configuration of blending does exactly this.
We can use this to render translucent objects into a scene. First, render all of the opaque geometry with the depth test turned on. Next, leave the depth test turned on but disable depth writes. This allows OpenGL to discard any fragments that’ll be occluded by the opaque geometry you just rendered. Enable blending, use the multiplicative blending equation above, and then render all the translucent objects. No alpha is used here and the translucent geometry can be rendered in any order. The result is quite convincing and is shown below.
Notice how we’ve cleared the window to white before rendering. Because the blended geometry attenuates whatever’s behind it, clearing to black would produce a black output. In a more complex application, the background would be the scene surrounding the object.
The second simple commutative function is basic addition. Really, what we want to do is:
We can configure OpenGL’s blending operations as follows:
This will simply add your shader’s output to whatever’s already in the framebuffer. Again, order doesn’t matter. This type of blending is often used for particle effects, where each particle might be a spark or other, small lighted point. It can also be used to simulate flames. In these phenomena, light from behind the particle essentially passes through it untouched, and the particle simply adds its own energy to the scene. An example of this blending configuration applied to the same dragon model is shown below.
Again, because addition is commutative, it doesn’t matter what order fragments are rendered into the framebuffer. This means that you don’t need to sort objects, worry about concave models or take on any of that complexity.
When Order Matters
So when does order matter? It seems that for a large class of effects, simple additive or multiplicative blending might suffice. However, most materials are not simple transmitters of light. Considering our example of colored glass, it’s likely that a piece of glass would not only transmit light that enters it, but that it would also reflect some of the light that hits it. Other effects often simulated using particles, such as smoke or clouds also have inscattering, where the path that light takes through the particles isn’t necessarily straight.
In these cases, order matters. To get ordering right, it’s common to simply sort translucent objects in order of depth and render them furthest to nearest. This works well if all the objects are convex and there is no possibility for self-intersection. However, if there is some possibility of self intersection or overlap – where a single object may end up hitting the same pixel twice – we’ll need something more advanced.
The A-buffer is a well known technique for implementing order-independent-transparency. In this method, a per-pixel linked list is maintained by using an atomic counter, a buffer into which to store fragment data and an image which stores the starting position in the list for each pixel in the scene. The buffer serves as a linked list of fragments and the image as the head pointer into the list. We start by rendering our opaque objects as normal, which also serves to initialize our depth buffer. Then, we render our transparent and translucent objects, again with depth test on but depth writes off, but rather than outputting to a framebuffer, we output to our fragment buffer. At each fragment, we increment the atomic counter, write fragment data into the fragment buffer, and then update the head image. In a second pass (either with a full-frame quad or with a compute shader), we walk the list, sort the fragments into order and then blend them in the shader rather than using OpenGL’s built-in blending functions. The result of rendering with A-buffer OIT is shown below.
Some criticism has been leveled at this method as having unbounded complexity. In practice, though, most scenes won’t have significant overdraw at every pixel and so allocating enough buffer space for two or three fragments per pixel should be sufficient.
What’s the Deal With Order Independent Transparency?
So what’s the big deal with order independent transparency? Is it necessary? Well, speaking pragmatically, many effects can be implemented using commutative blending – additive or multiplicative. In many more cases, simply sorting objects by depth may be sufficient. For those cases where the application absolutely must use a non-commutative blending function and where sorting objects is either impossible or ineffective, we have ways to sort fragments using, for example, A-buffers. Ordering is not well defined in graphics or compute shaders today. Would blend shaders or some other form of programmable blending be handy? Sure, why not? However, there has been a lot of research into deferred shading, and most rendering engines support some form of resolve or post-processing pass. It’s not a stretch to incorporate late resolve of sorted fragments into this kind of architecture. This trend seems to be continuing. Will programmable blending arrive in a performant form before the traditional graphics pipeline goes away and we’re on our own? Let’s wait and see.