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:

clip_image001

clip_image002

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