After I wrote my trio of blog posts (1, 2, 3) about blend modes using pixel shader effects, I was toying around with it …
The Problem
… and noticed that it didn’t handle an opacity changes on the upper layer (B).
In fact, this was brought to my attention by a great blog post by Angie Bowen. In it she explains how the blend modes work and, she says:
Remember that to get better results you can also adjust the opacity of the upper layer.
Trying it out, revealed that some of the blend modes were okay, but most were not. Most of the blend modes would simply result in a black square if you pulled all the opacity out of the upper layer (B). This was obviously wrong, for if you pull all the opacity out of the upper layer (B), you should get the lower layer (A).
Argh!
The Solution
So, I dove back in on the blend mode math, trying to figure out what I needed to do to make these blend modes … opacity aware. That’s got a nice ring to it, doesn’t it!?
I started at the top of the list and got the NormalEffect working:
float4 main(float2 uv : TEXCOORD) : COLOR { float4 inputColor; inputColor = tex2D(input, uv); float4 blendColor; blendColor = tex2D(blend, uv); inputColor.rgb = (1 - blendColor.a) * inputColor.rgb + blendColor.rgb; return inputColor; }
Ok, the above math made sense. When the opacity of the upper layer (blendColor.a) was 1 (opaque), the result was just blendColor. Otherwise when the opacity of the upper layer was 0 (transparent), the result was inputColor.
So, I then started to tackle the darken blend modes (Darken, Multiply, …) and quickly ran into problems. It was at that point, that I ran into this post in the WPF Forum. My blend modes were rendering full black (not white), but it provided a crucial piece of knowledge.
Namely, WPF uses pre-multiplied alpha everywhere for performance reasons.
What does that mean? Well, it means that the RGB values for inputColor are already multiplied by the alpha value for inputColor and that the RGB values for blendColor are already multiplied by the alpha value for blendColor.
Ah! Do you see it? This explains why the blend modes were going to a black square when pulling the opacity out. Take the Multiply blend mode. In the above HLSL, it would be:
// R = Base * Blend resultColor.rgb = inputColor.rgb * blendColor.rgb
So, if the alphas were pre-multiplied in and you were pulling opacity out of the blend (upper) layer … then blendColor.rgb would go to zero … which would cause resultColor to go to zero … which would cause the gradient square to go to black!
Thinking about this … brought about the general solution for making these blend modes opacity aware. I needed to simply:
- Un-pre-multiply the blend layer alpha value out.
- Apply the blend mode math.
- Then re-multiply the blend layer alpha value in again.
- Here is the HLSL for the opacity aware Multiply blend mode:
float4 main(float2 uv : TEXCOORD) : COLOR { float4 inputColor; inputColor = tex2D(input, uv); float4 blendColor; blendColor = tex2D(blend, uv); float4 resultColor; resultColor.a = inputColor.a; // un-premultiply the blendColor alpha out from blendColor blendColor.rgb = clamp(blendColor.rgb / blendColor.a, 0, 1); // apply the blend mode math // R = Base * Blend resultColor.rgb = inputColor.rgb * blendColor.rgb; // re-multiply the blendColor alpha in to blendColor // weight inputColor according to blendColor.a resultColor.rgb = (1 - blendColor.a) * inputColor.rgb + resultColor.rgb * blendColor.a; return resultColor; }
A few comments about the above code. Notice that I am clamp(ing) when I un-premultiply (i.e. divide) the alpha out. This assures that the RGB values will be between 0 and 1 (where 0 is black and 1 is white … HLSL operates in ScRGB color space). This is necessary since dividing by values close to 0 (blendColor.a) can yield large numbers or even positive infinity … which throws off the math.
Secondly, when I re-multiply the blend layer alpha value back in … I need to also properly weight the inputColor … just like I did in the NormalEffect above.
Finally, notice that I really don’t have worry about the opacity on the lower layer (A). I just pass its value off to the result by setting resultColor.a equal to inputColor.a.
Applying this general formula worked in all cases!
The Gradient Contour Test Harness
In order to verify that I was doing math correctly, and to see the effect of pulling the opacity out of the blend modes … I have built a new gradient test harness. I have called it the gradient contour test harness since it not only shows the A + B = R gradient squares but it also shows the R gradient square with contours … just like Paul Dunn’s post does when you mouse over the R squares.
It is extremely interesting (to me at least) watching the contours as you pull out the opacities.
For example, Here are three gradient contour squares for the Pin Light blend mode at opacity values of 1.0, 0.5, and 0.0:
I have also included a button labeled ‘Swap’ which swaps the A and B layers … since not all blend mode effects are commutative.
The gradient contours were made possible via Dwayne Need’s library. Check out the code (the class Grayscale4Bitmap) and see this post for more info.
The Image Test Harness
I’ve also put opacity sliders in the image test harnesses. Let’s take a look at the Pin Light effect at opacity values of 1.0, 0.5, and 0.3.
Images |
Opacity |
![]() |
1.0 |
![]() |
0.50 |
![]() |
0.3 |
As you can see … pulling out the opacity … lessens the effect that the upper layer/texture has on the lower layer … and proves the truth of what Angie Bowen was saying earlier about using the opacity of the upper layer to achieve better results.
The Binaries and the Source Code (aka The Goods)
So, as I’m fond of saying … without further adieu … here is source code for the Blend Mode library and here are the library binaries … now opacity aware!
I also have updated the Silverlight test harness (as always you will need the Silverlight 3.0 runtime).
p.s.
The gradient contour test harness is WPF only … you won’t find that on the Silverlight side. Maybe someday I’ll get my Silverlight test harnesses up to parity with what’s in the WPF test harnesses … but I can’t see when. Any one want to do it for me? Bueller? Bueller?