Pre-multiplied Alpha blending

游戏开发 专栏收录该内容
47 篇文章 0 订阅

Standard Alpha blending uses this formula:

result = foreground.alpha * foreground + (1 - foreground.alpha) * background

This is a linear interpolation between the foreground and background colours, using the foreground's alpha as the interpolation weight, effectively the same as:

result = lerp(background, foreground, foreground.alpha)

This means the colour of a texel is treated entirely independently from its transparency, which is intuitively simple, but has some perplexing implications: it's possible for an object to simultaneously be both "purple" and "invisible" (ie. rgba(0.5, 0, 1, 0)), as odd as that may sound.

Pre-multiplied Alpha blending uses a different formula:

result = foreground + (1 - foreground.alpha) * background

It's so-named because the multiplication of the first term by foreground.alpha has been baked-in ahead of time.

So you can see, anything we can do with conventional alpha blending, we can do with pre-multiplied alpha blending. You can take any texture set up for "standard" alpha blending, multiply the rgb channels by the alpha channel, and now you have an equivalent texture ready for pre-multiplied blending.

But pre-multiplied blending has some extra super-powers...

Super-Power #1: Pre-Multiplied Alpha can combine layer and additive blending

This is because the pre-multiplication means we're storing data in terms of:

  • how much colour to add to the background (rgb), and
  • how much to obscure the background (alpha).

By leaving the obscuring alpha factor at zero, any values in the RGB channels get applied additively on top of the background, without being dimmed themselves - a classic "ADD" blend mode. By setting alpha to something between zero and one, we can get blend modes that are intermediate between additive and "standard/layer" blending.

This can be used, for instance, to combine multiple different sprites into a single batched draw call, even when they want different blend modes that would ordinarily force us to separate the batches.

We can even make a single effect that combines both blending modes, like a fire particle system that transitions from bright additive flames to opaque obscuring smoke with a single material/texture sheet:


Rendering this same spritesheet with standard alpha blending makes the flames disappear entirely (since they have zero alpha in the texture sheet above). Changing the texture sheet to assign an alpha to the flames makes them look too solid under alpha blending, without the characteristic brightness of an additive effect:


Super-Power #2: Pre-Multiplied Alpha blending works cheaply in either direction

Let's say we wanted to pre-compose some layered sprites - like a character made of several body part and clothing layers - into a sprite sheet with a transparent background, so we can later just stamp that pre-composed character into our scene as a single unit.

Here we don't yet know the colour of the background we're drawing over, so applying the standard blending formula using the simple lerp style will actually give us the wrong result when we later layer it onto the background.

To get the same result with standard blending as if we'd layered the image from bottom up, we have to use a slightly more complicated version of the formula:

result.rgb = ( foreground.rgb * foreground.alpha
              + middle.rgb * (1 - foreground.alpha) * middle.alpha)
              / (foreground.alpha * middle.alpha * (1 - foreground.alpha));

result.alpha = foreground.alpha * middle.alpha * (1 - foreground.alpha);

Note the division there, making the blend substantially more expensive to calculate, not natively supported in the GPU's blend stage, forcing us to implement the composition in our shader. When we can assume the background is opaque, the denominator comes out to 1 and we can skip it, but we can't do that and maintain correctness when blending transparency with transparency.

With pre-multiplied alpha though, there's no division required, and we can do this pre-composition directly in the native blending pipeline:

result.rgb = foreground.rgb + middle.rgb * (1 - foreground.alpha);

result.alpha = foreground.alpha * middle.alpha * (1 - foreground.alpha);

Much simpler! And the same formula works whether we're blending onto an opaque background or a transparent one.

Super-Power #3: No discoloured fringes

With regular alpha blending can suffer from discoloured fringes at the edges between opaque and transparent regions - particularly if it's drawn with bilinear filtering, or mipmapped to lower resolutions.

This is caused by the lack of weighted blending that Roy T. explains in detail in the other answer.

Look closely at the edges of this sprite: you'll notice in all but the top-left version, the colour shifts just at the fall-off from opaque to transparent - getting a dark shadow or a light whispy halo.


(Gem sprite by Kenney)

The reason is that many authoring programs will output fully-transparent texels as back or white, or some other colour chosen by the compressor. When blending between two texels along this edge, the alpha results are correct, but the colour results are wrong: the adjacent texel we're blending with isn't a colour the artist painted there (they left it transparent/empty), but one the tool filled in.

This is where the "simultaneously invisible and purple" issue comes back to bite us. There are dragons lurking in those unseen regions!

We can often fix this by bleeding out the colour we want to average with into the transparent regions, which is what I did to remove the artifact in the top-left case.

Because this is a widespread problem, game engines can often fix this automatically - for example, in Unity checking the "Alpha is Transparency" box in a texture's import settings will usually eliminate fringing artifacts, doing this colour bleed or something like it under the hood. (I left the box unchecked for the example above, so Unity was forced to take the PNG's saved colour values literally)

But if you use pre-multiplied alpha, storing plain black in the transparent regions, this problem goes away. Now instead of meaning "an invisible black monster lurks here" a black transparent pixel means "absorb no light, add no light," exactly what we want from a transparent region. And it blends correctly with adjacent texels - a 50% blend of "absorb all light, add 100% green light" with transparent black yields "absorb 50% of light, add 50% green light," a correct reduction of opacity without distorting the colour.

This is of particular significance for texture formats like DXT1, which can only store 1 bit of alpha as transparent black. So you get perfectly correct blending if you interpret the texture as premultiplied alpha, but you have dark fringes with standard alpha blending that you can't fix with colour bleeding - since there's no way to store colour and transparency in the same texel in such a format.



  • 0
  • 0
  • 1
  • 一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
钱包余额 0