How to apply an “Opacity Mask” to an image by mixing XAML and Direct2D
If you are a XAML graphics dev you may be lamenting the fact that in Windows Store 8.x apps the “opacity mask” property is conspicuously missing from the XAML image based controls. This is an unfortunate omission because there really isn’t an easy way to facilitate the opacity mask functionality using XAML alone.
Let’s take a step back for a moment…
What is an opacity mask?
The Silverlight documentation defines it like this:
“Gets or sets the brush that is used to alter the opacity of select regions of this object.”
The WPF documentation defines it like this:
“Opacity masks enable you to make portions of an element or visual either transparent or partially transparent.”
To my reckoning a picture is worth a thousand definitions:
Beginning to see the possibilities?
How do I get this functionality in my Store app?
I was working with a developer recently that desperately needed this functionality to allow them to port their app from WPF to a Windows Store app. Ingeniously they came up with a workaround using a “WriteableBitmap” object. Unfortunately this solution requires iterating over every pixel in both the source and the mask and then transferring the alpha channel from the mask to the source image. While the code isn’t that complex the operations required can be very processor intensive and really can’t be used for anything close to real time (you could likely cache the output but caching large images can cause memory pressure problems).
Luckily the Direct2D technology has a rich set of tools that easily allow you to apply an opacity mask to an image. Direct2D leverages the GPU so there is very little overhead using this technique. If you are really careful you can even use this technique to apply the opacity mask in real time.
Now I know what you are thinking… “But I have a huge investment in XAML and I don’t want to rewrite everything in D2D, not to mention that I don’t have the time to learn C++.” Well you are in luck. Using the amazing graphics interop technologies that we shipped in Windows 8, extending a standard “Image” control to use D2D is as easy as setting the “Source” property. Well… It’s that easy if someone has already written the D2D part for you. Well once again you are in luck! I’ve done the necessary work in C++ and D2D for you. You can just “plug-and-play” and magically have a XAML control that can implement an opacity mask, at least that is my intention.
How does the code work?
The code is relatively simple and straightforward. Unfortunately with this technique it isn’t all that easy to “data bind” to an image and mask. Don’t let me stop you from trying though. You might be able to get something to work with dependency properties but that is way beyond the scope of this blog. Let’s just write some simple code and have things up and running quickly.
Here is the important XAML bit:
<Image x:Name="Image1" Height="300" Width="400" Margin="113,132,853,336" />
This is how we wire things up:
public MainPage()
{
this.InitializeComponent();
// Create a new D2DImageSource and set the initial size
_D2DImageSource = new D2DImageSource(
(int)Image1.Width,
(int)Image1.Height,
false);
// Use _D2DImageSource as a source for the Image control
Image1.Source = _D2DImageSource;
}
And here is the render code:
private void _btnRender_Click(object sender, RoutedEventArgs e)
{
// Load the source (background) bitmap
_D2DImageSource .SetSource("Assets/Fern.png");
// Load the opacity mask bitmap
_D2DImageSource .SetMask("Assets/BitmapMask.png");
// Begin updating the SurfaceImageSource
_D2DImageSource .BeginDraw();
// Clear background
_D2DImageSource .Clear(Colors.Transparent);
// Render the source and apply the mask
_D2DImageSource .RenderBitmap();
// Stop updating the SurfaceImageSource and draw its contents
_D2DImageSource .EndDraw();
}
One word of caution. If you use this code as is and fail to set the “Height” and “Width” of the “Image” control you are using to host the “D2DImageSouce” you will get an error when you create the control. So make sure to set the “Height” and “Width” of the “Image” control or modify the constructor of the “D2DImageSouce” to use different values.
How does the D2D stuff work?
The intent of this article really is to give you a plug-and-play solution for C# developers. In order to write D2D based solutions you need to know a lot about COM and C++ Cx. That said I do want to point out a few “gotchas” in the D2D C++ Cx code that tripped me up. In order to use the opacity mask in D2D you need to make sure that you set the ainti-alias mode by calling “SetAntialiasMode” with a value of “D2D1_ANTIALIAS_MODE_ALIASED”. If you don't you will get a very non descript error. You also need to make sure to set the “isOpaque” parameter in the "D2DImageSource" constructor to “false”. If you fail to set this you will get a black background instead of the nice blended XAML elements.
The complete sample code can be found here:
Don’t forget to follow the Windows Store Developer Solutions team on Twitter @wsdevsol. Comments are welcome, both below and on twitter.
Cheers!
-James
SurfaceImagerSourceOpacityMask.zip
Comments
- Anonymous
March 24, 2014
Wouldn't attaching to Application::Current->Suspending at the constructor of D2DImageSource cause a leak if we don't unsubscribe when we no longer need a D2DImageSource instance?