Blend Modes, Part I
A while back (now some time ago), I was inspired by Robby Ingebretsen’s post on blend modes that were made possible with pixel shader effects, a new feature of .NET 3.5 SP1. In his post, he suggested that someone take a weekend and create a library of these effects for the community to use. Well, a little late, but I have eventually gotten around to this and done so … because I wanted to give a designer I work with some power tools that he is familiar with in applications like Photoshop.
In this trio (1, 2, 3) of blog posts, I will not be covering how to create a shader effect in WPF, Greg Schlecter has a great introductory series on that already. In particular, it is the multi-input shader effects that make blend modes possible, so be sure to read all the way through his series.
In order to write this library of blend mode effects, I first needed to find the math behind them. Then, I needed to convert this math into the proper HLSL. And finally, I needed some way to verify that the HLSL that I had written … was actually correct. I will cover these topics in the first two blog (first, second) posts.
So … let’s jump in.
I quickly started off with a simple Google search and it led me to Nathan Moinvaziri’s post which is all the blend mode math in C. So, I decided to buckle down and see if I could do Linear Burn … which is the subject of Robby’s post. Nathan had the math (for each channel) for Linear Burn as:
#define ChannelBlend_Subtract(B, L) ((uint8)((B + L < 255) ? 0 : (B + L – 255))) #define ChannelBlend_LinearBurn(B, L) (ChannelBlend_Subtract(B, L))
Just plugging in the macro definitions into the .fx file didn’t work and through some trial and error, I discovered that the correct HLSL was:
inputColor.r = inputColor.r + blendColor.r - 1; inputColor.g = inputColor.g + blendColor.g - 1; inputColor.b = inputColor.b + blendColor.b - 1;
This finally gave me the blending Robby had talked about in his post:
Following is the xaml for the above rectangle which is using the Linear Burn blend mode effect:
<Border Width="300" Height="100" Margin="0,10" Background="#FF6AB400" > <Border.Effect> <bme:LinearBurnEffect> <bme:LinearBurnEffect.Texture> <ImageBrush> <ImageBrush.ImageSource> <DrawingImage> <DrawingImage.Drawing> <GeometryDrawing> <GeometryDrawing.Geometry> <RectangleGeometry Rect="0,0,1,1"/> </GeometryDrawing.Geometry> <GeometryDrawing.Brush> <LinearGradientBrush StartPoint="0,0" EndPoint="0,1" > <GradientStop Color="#FFFFFFFF" Offset="0" /> <GradientStop Color="#FF808080" Offset="1" /> </LinearGradientBrush> </GeometryDrawing.Brush> </GeometryDrawing> </DrawingImage.Drawing> </DrawingImage> </ImageBrush.ImageSource> </ImageBrush> </bme:LinearBurnEffect.Texture> </bme:LinearBurnEffect> </Border.Effect> </Border>
As you can see above, it is rather easy to apply the blend mode effect. Each blend mode effect takes two shader effect inputs (where input is a shader effect sampler input).
The first input is the element it is set on which is in this case the Border element with the #FF6AB400 background.
The second input is the ImageBrush (it can also be a VisualBrush) that is set on the Texture property. Notice that the alpha channels for the GradientStop(s) are set 100% and not 50% like the Border with opacity overlay (see the attached code, .zip file below). That is, the two layers are being blended by the blend mode effect and not with opacity.
Another thing to mention is that the first input maps to A in the picture above and the second input maps to B in the picture above. R maps to the blend result which is what the user sees. In the next post, I will use this mapping (A + B = R) to illustrate what each blend mode effect does.
(To build the above sample code, you need .NET 3.5 SP1 and you need a version of the DirectX SDK installed on your machine. Besides all that, you must also install the Shader Effects BuildTask and Templates from the WPF Futures CodePlex site.)