Home > Blend Modes, Pixel Shader Effects, Silverlight, WPF > Blend Modes: Now Opacity Aware

Blend Modes: Now Opacity Aware

November 6th, 2009

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:

  1. Un-pre-multiply the blend layer alpha value out.
  2. Apply the blend mode math.
  3. 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:

Gradient Squares

Gradient Contour Squares

Opacity

PinLight1.00nc PinLight1.00

1.0

PinLight0.50nc PinLight0.50

0.5

PinLight0.00nc PinLight0.00

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
PinLightImage1.0 1.0
PinLightImage0.75 0.50
image 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?

Share/Save

Blend Modes, Pixel Shader Effects, Silverlight, WPF

  1. Daniel
    December 16th, 2013 at 03:31 | #1

    I have a question about your code above. When you get the color with tex2D(), and you have a input with an effect, do you get the original color from the input or does the color you get for the input already have the effect applied (i.e. after the first rendering)?

    Greetings,
    Daniel

  2. cplotts
    December 16th, 2013 at 09:41 | #2

    @Daniel,

    To answer your question directly, it is definitely the former … it is the original color from the input (i.e. tex2d(input, uv)).

    In general these blend mode affects all follow the general pattern of A + B = R. A is the image you blending into (i.e. the bottom one), B is the image you are blending in (i.e. the top one), and R is the result of the blend (what you return from each pixel shader HLSL).

    Given that, tex2d(input, uv) is the orignal color of the A input (i.e. the bottom one) … and tex2d(blend, uv) is the original color of the B input (i.e. the top one).

    Hope that helps explain it a bit better.

  3. Daniel
    December 16th, 2013 at 10:27 | #3

    Okay, I hope I understand it in the right way. Because my general pattern is:
    A + B = R
    A = R
    R + B = R

  4. cplotts
    December 16th, 2013 at 11:04 | #4

    @Daniel this might be at the heart of what you are doing wrong.

    If you are setting A = R via a VisualBrush, it would seem to me that you are setting up a loop in the pixel shader … where the result would keep on getting fed into input of the shader … which each successive pass of the shader.

  5. Daniel
    December 17th, 2013 at 02:14 | #5

    The problem is that is that the visual brush contains ever the rectancle because the rectancle is a part of control which is viewing through the visual brush. My idea was that i can check in the effect via if statement if the visualbrush pixel the same pixel like the rectangle (because after the first effect rendering is it true) then the effect is not using but the statement works false. i have no idea why the statement works uncorrectly.

  1. December 1st, 2009 at 22:08 | #1