Frontend
Alpha Blending and WebGL
This article introduces alpha blending and some tips relating to the alpha channel in WebGL development.
Originally published by David Guan(opens in a new tab or window) at product.canva.com(opens in a new tab or window) on December 04, 2017
This article introduces alpha blending and some tips relating to the alpha channel in WebGL development. At Canva we use WebGL to provide image filters(opens in a new tab or window), similar to Instagram filters. While trying to fix a bug in Canva's image filters, I discovered that it was actually due to a WebGL alpha blending bug in Chrome, and decided the topic was worth writing an article about. I'll discuss the bug in depth later on, but first a short primer.
How CSS opacity works
Everything you see in the browser comes from pixels represented by an RGB value (or HSV, HSB, etc.)
For the two squares below, if I uncomment the
transform: translateX(-200px);
a new "merged" square will be
generated. What will the colour of that square roughly be as an RGB
value?
(Why would we even care what the value is? Well if we need transforming
raw RGB data between client and server, paint the rendering result from
<Canvas />
to <img />
or process images, then the value matters.)
The answer is RGB(230, 25, 48), although it is possible that the value could differ depending on your environment.
To figure out what happened, let's start from the first square:
This square initially has colour RGB(255, 26, 26), which comes from blending the page's background RGB(255, 255, 255) with the square's colour. Let's call the first square SRC and page background DST. The result value for the R channel is generated by
SRC.R * SRC.Opacity + DST.R * (1 — SRC.Opacity)
,
and likewise for the B and G channels. When Opacity is 1, the result purely comes from SRC, and vice versa. So we arrive at the result of RGB(255, 26, 26) by calculating:
R: 255 * 0.9 + 255 * 0.1
G: 0 * 0.9 + 255 * 0.1
B: 0 * 0.9 + 255 * 0.1
Back to the final result of RGB(230, 25, 48) — we arrive there by blending SRC, RGB(0, 0, 255) opacity 0.1, with DST, RGB(255, 26, 26) like this:
(0 * 0.1 + 255 * 0.9, 25 * 0.1 + 26 * 0.1, 255 * 0.1 + 26 * 0.9)
In conclusion: as mentioned in the W3C CSS specification(opens in a new tab or window), opacity indicates how to blend the target with existing content, and browsers help us deal with alpha blending automatically.
Opacity can be thought of as a postprocessing operation. Conceptually, after the element (including its descendants) is rendered into an RGBA offscreen image, the opacity setting specifies how to blend the offscreen rendering into the current composite rendering.
Alpha Blending in WebGL
Firstly, why use WebGL? As I've mentioned, at Canva(opens in a new tab or window) we use it to provide image filters, and it's also famous for powering 3D content on the web. Apart from these two use cases, this article(opens in a new tab or window) has more information about everything it can do for you.
Now, let's take a look at two cases when alpha needs to be taken into consideration with WebGL:
- How WebGL content blends with other existing DOM elements on a web page
- How elements inside WebGL content blend with each other (two shapes again, for example, but drawn with WebGL).
The Codepen below draws a triangle with colour RGB(0, 0, 255), opacity 0.3:
As you can see, the triangle painted by WebGL is blended with the webpage's background. Its colour is RGB(179, 0, 255). Based on what we learned previously, the 179 comes from 255* 0.7, but why isn't the 255 value closer to 255 * 0.3?
The 255 comes from WebGL's default behavior — premultiply alpha. Instead of SRC.B * SRC.A + DST.B(1 — SRC.A), browsers will treat the "* SRC.A" part as already done by the WebGL rendering pipeline, so we have 255 + 0 * 0.7.
One way to solve this issue is changing the WebGL configuration:
canvasDOM.getContext("webgl", {premultipliedAlpha: false,// Other configurations});
Now it works as expected.
Unfortunately we can't solve the problem this way, because alpha blending is not correctly handled in Chrome when premultipliedAlpha is disabled. You can find more details here in this Stack Overflow question(opens in a new tab or window) I asked and this Chrome bug ticket(opens in a new tab or window). Instead, we can manually multiply alpha or use WebGL's blendFunc(opens in a new tab or window) (more on that later).
Let's remove the premultipliedAlpha: false
and add
gl_FragColor.rgb *= gl_FragColor.a;
at the end of fragment shader. The
result is same as the previous image and now it also works in Chrome.
Now, let's think about rendering two shapes, one above another (as we did previously). The codepen below draws two half transparent triangles at the same position with colours RGB(255, 0, 0) and RGB(0, 0, 255):
I've made the page background RGB(0, 0, 0) so we can focus on the content generated by WebGL. It turns out the value of R is 0.
The reason behind this result is that WebGL does not provide alpha blending by default. We can enable the normal alpha blending by inserting the code below before rendering:
gl.enable(gl.BLEND);gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
One bonus we get by making this change is that we can remove the
gl_FragColor.rgb *= gl_FragColor.a;
in the fragment shader since its
job is done by gl.SRC_ALPHA
.
If you don't want to change the shader, change gl.SRC_ALPHA
to
gl.ONE
. You can read more about blendFunc
and other ways to blend
content here(opens in a new tab or window).
That's all, thanks for your time. Please stay tuned for more articles about what we've learned from building Canva!