Take Care with your Blend Function

“Everyone” knows that the usual “over” composition operation is as such:

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

However, this can in some cases lead to artifacting:

Correct Incorrect
Properly blended black text on orange background Improperly blended black text with white halo on orange background

Observe the halo effect in the “incorrect” image. I observed this effect when using WebGL on a web page, so in this case, the white color of the halo was the white background color of the web page peeking through. But why was the background peeking through?

Suppose we are on the border of the text, and the text’s alpha is 0.25. The background orange color is fully opaque, alpha 1. In this case, we’d compute the output pixel’s opacity as: 0.25 * 0.25 + 1 * (1 – 0.25) = 0.8125. This is not right! Observing carefully αo from the over operator definition as shown on Wikipedia:

αo = αa + αb(1−αa)

When computing the alpha component, the source factor should be 1, not the source alpha! (Otherwise, we end up squaring the first term.) For the color term, we do, however, want to multiply by source alpha (at least assuming we’re not using premultiplied alpha). Consequently, we really don’t want to use glBlendFunc, as it only lets us specify one function for both color and alpha. Instead, glBlendFuncSeparate is needed, as we can specify a different function for color and alpha:

glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

This yields the proper behavior, at least when the destination alpha is 1.

Remaining problems

The result is still not quite right when the destination alpha is not 1, however. Suppose we’re compositing (1, 0, 0, 0.5) onto (0, 1, 0, 0.5). According to the over operator definition from Wikipedia above, the color components are computed as:

C_o = \frac{C_a \alpha_a + C_b \alpha_b (1-\alpha_a)}{\alpha_o}

And so, αo = 0.5 + 0.5 * (1 – 0.5) = 0.75; and the proper red output = (1 * 0.5 + 0 * 0.5 * (1 – 0.5)) / 0.75 = 2/3. What OpenGL is computing is missing the “divide by αo” step – the color component will be wrong.

This turns out to be us fighting the platform. This is hinted elsewhere – the WebGL spec specifies that unless explicitly disabled during context creation, the back buffer is assumed to be in premultiplied alpha format. And, surprise surprise, if all colors are represented with premultiplied alpha, a single blend function is in fact sufficient:

glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

Who’d’a thunk it? The hidden subtext is that the platform is built for premultiplied alpha. Understanding that and working with it rather than fighting it leads to better results.

Covering your eyes and ignoring the world

The problem can also be masked if the back buffer is opaque. This is the case in many native environments, but in the Web environment, the back buffer does have an alpha channel by default. It can be turned off by specifying { alpha: false } to the second parameter of HTMLCanvasElement.getContext. This shields you from all the consequences of your actions, at least in this domain.

Tags: , ,

Leave a Reply