If you are developing a Windows Store app using C++ and XAML and you want to use Microsoft DirectX to improve performance or to reuse the assets in your app more effectively, you can use this article to get started.
Download
You will learn
- How to perform interop between XAML and DirectX in a Windows Store app using the SurfaceImageSource class.
- How to use DirectX to create and apply an affect to an image.
How to control a Direct2D image effect in a XAML-based Windows Store app.
Applies to
- Windows Runtime for Windows 8
- Visual C++ component extensions (C++/CX)
- XAML
- DirectX
Building and running the quickstart
Build the DirectXRipple project as you would build a standard project.
- On the menu bar, choose Build > Build Solution. The build step compiles the code and also packages it as a Windows Store app.
- After you build the project, you must deploy it. On the menu bar, choose Build > Deploy Solution.
- After you deploy the project, choose the DirectXRipple tile to run the app. Alternatively, in Microsoft Visual Studio, on the menu bar, choose
Debug > Start Debugging. If you run the app in the debugger, Visual Studio deploys the project.
On the initial screen, choose the
Load Image button. Use the file picker to select a JPG or PNG file, and then choose the
Open button. The image is loaded and displayed on the screen. Choose any point on the image to observe the ripple effect. If you choose another point while the ripple is animating, the center of the ripple moves to that location. You can also drag the
ripple while it's animating.
Solution structure
The DirectXRipple Visual Studio solution contains one project, which includes these key classes:
- App class. Specifies the entry point for the app and handles activation and suspension events.
- BasicReaderWriter class. Reads the data from the compiled ripple effect pixel shader.
- BasicTimer class. A timer class that's used to drive the render loop of the app.
- D2DImageSource class. The interop class between DirectX and XAML.
- MainPage class. Defines a XAML-based page class that contains a simple user interface.
- RippleEffect class. Implements the ripple effect as a wrapper around a pixel shader.
- RippleImageSource class. Inherits from the D2DImageSource class and applies the moving ripple effect to the image.
- WICImage class. Uses the Windows Imaging Component (WIC) to load and display an image.
With little or no modification, you can reuse many of the classes in DirectXRipple in another app. You can also adapt the organization and ideas that DirectXRipple provides.
Choosing an image
The
MainPage class defines the app's UI. It connects the XAML Image control to the
WICImage and
RippleImageSource classes, and ensures that events on the page are handled. In the
OnNavigatedTo method for the page, instances of the
WICImage and
RippleImageSource classes are initialized. Here's the code:
C++
void MainPage::OnNavigatedTo(NavigationEventArgs^ e)
{
(void) e; // Unused parameter
auto width = static_cast<int>(XamlImage->ActualWidth);
auto height = static_cast<int>(XamlImage->ActualHeight);
m_image = ref new WICImage();
m_rippleImageSource = ref new RippleImageSource(width, height, true);
}
The last parameter in the call to the
RippleImageSource constructor indicates that the image is opaque.
The
MainPage class has a
Click event handler for the
Button and three event handlers—
OnPointerPressed,
OnPointerMoved, and
OnPointerReleased—for the Image control. The click event handler enables the selection of an image file from a
FileOpenPicker and the display of the selected image on the page.
MainPage.cpp
C++
void MainPage::OnLoadImageClicked(Object^ sender, RoutedEventArgs^ e)
{
auto openPicker = ref new FileOpenPicker();
openPicker->ViewMode = PickerViewMode::Thumbnail;
openPicker->SuggestedStartLocation = PickerLocationId::PicturesLibrary;
openPicker->FileTypeFilter->Append(".jpg");
openPicker->FileTypeFilter->Append(".png");
create_task(openPicker->PickSingleFileAsync()).then([](StorageFile^ file)
{
if (file != nullptr)
{
return file->OpenAsync(FileAccessMode::Read);
}
cancel_current_task();
}).then([this](IRandomAccessStream^ stream)
{
m_image->LoadImage(stream);
DrawImage();
if (!m_isImageLoaded)
{
CompositionTarget::Rendering += ref new EventHandler<Object^>(this, &MainPage::OnRenderDisplaySurface);
m_isImageLoaded = true;
}
});
}
The stream that's obtained when an image is opened is passed to the
LoadImage method of the
WICImage instance, and then the
DrawImage method is called to display the image on the XAML
Image control.
C++
void MainPage::DrawImage()
{
// Set as a source for the XAML Image control.
XamlImage->Source = m_rippleImageSource;
// Begin updating the SurfaceImageSource.
m_rippleImageSource->BeginDraw();
// Clear background.
m_rippleImageSource->Clear(Colors::Black);
// Draw background.
m_rippleImageSource->DrawImage(m_image);
// Stop updating the SurfaceImageSource and draw its contents.
m_rippleImageSource->EndDraw();
}
The event handlers for the pointer events on the
Image control are discussed in the Controlling the ripple effect section. The method that's called by the
RippleImageSource::DrawImage method is described in the Drawing the image section.
Initializing the drawing surface
The
RippleImageSource class inherits from the
D2DImageSource class and extends it with a ripple effect. The
D2DImageSource class initializes the drawing surface on the XAML page and derives from the
Windows::UI::Xaml::Media::Imaging::SurfaceImageSource class, which enables interop between DirectX and XAML but leaves XAML in charge of the render loop for updating the UI. You can use the
SurfaceImageSource class only after you query for the
ISurfaceImageSourceNative interface. The
D2DImageSource class provides methods such as
BeginDraw,
Clear,
FillSolidRect, and
EndDraw to any calling code. (This includes JavaScript and C# as well as C++.)
The constructor of the
RippleImageSource class initializes an instance of the
SurfaceImageSource class and registers the ripple effect with the Direct2D image effects system. Here's the constructor code that initializes an instance of the
SurfaceImageSource class:
RippleImageSource.cpp
C++
RippleImageSource::RippleImageSource(int pixelWidth, int pixelHeight, bool isOpaque) :
D2DImageSource(pixelWidth, pixelHeight, isOpaque)
D2DImageSource.cpp
C++
D2DImageSource::D2DImageSource(int pixelWidth, int pixelHeight, bool isOpaque) :
SurfaceImageSource(pixelWidth, pixelHeight, isOpaque)
The
RippleImageSource constructor also calls the
CreateDeviceIndependentResources and
CreateDeviceResources methods. The
CreateDeviceIndependentResources method queries for the
ISurfaceImageSourceNative interface. The
CreateDeviceResources method initializes hardware-dependent resources such as the display's DPI value.
As part of exposing the
ISurfaceImageSourceNative interface, the
D2DImageSource class has
BeginDraw and
EndDraw methods. The
BeginDraw method uses the interface that was provided by the
CreateDeviceIndependentResources method to establish the render target for the
m_d2dContext member variable, and then calls
BeginDraw on the render target, as shown here:
D2DImageSource.cpp
C++
void D2DImageSource::BeginDraw(Windows::Foundation::Rect updateRect)
{
POINT offset;
ComPtr<IDXGISurface> surface;
// Express target area as a native RECT type
m_updateRectNative.left = (LONG)updateRect.Left;
m_updateRectNative.top = (LONG)updateRect.Top;
m_updateRectNative.right = (LONG)updateRect.Right;
m_updateRectNative.bottom = (LONG)updateRect.Bottom;
// Begin drawing - returns a target surface and an offset to use as the top left origin when drawing
HRESULT beginDrawHR = m_sisNative->BeginDraw(m_updateRectNative, &surface, &offset);
if (beginDrawHR == DXGI_ERROR_DEVICE_REMOVED || beginDrawHR == DXGI_ERROR_DEVICE_RESET)
{
// If the device has been removed or reset, attempt to recreate it and continue drawing
CreateDeviceResources();
BeginDraw();
}
else
{
// Notify the caller by throwing an exception if any other error was encountered
ThrowIfFailed(beginDrawHR);
}
// Create render target
ComPtr<ID2D1Bitmap1> bitmap;
ThrowIfFailed(
m_d2dContext->CreateBitmapFromDxgiSurface(surface.Get(), nullptr, &bitmap)
);
// Set context's render target
m_d2dContext->SetTarget(bitmap.Get());
// Begin drawing using D2D context
m_d2dContext->BeginDraw();
// Apply a clip and transform to constrain updates to the target update area.
// This is required to ensure coordinates within the target surface remain
// consistent by taking into account the offset returned by BeginDraw, and
// can also improve performance by optimizing the area that is drawn by D2D.
// Apps should always account for the offset output parameter returned by
// BeginDraw, since it may not match the passed updateRect input parameter's location
m_d2dContext->PushAxisAlignedClip(
D2D1::RectF(
static_cast<float>(offset.x),
static_cast<float>(offset.y),
static_cast<float>(offset.x + updateRect.Width),
static_cast<float>(offset.y + updateRect.Height)
),
D2D1_ANTIALIAS_MODE_ALIASED
);
m_d2dContext->SetTransform(
D2D1::Matrix3x2F::Translation(
static_cast<float>(offset.x),
static_cast<float>(offset.y)
));
}
The
EndDraw method removes the transform and clipping that's applied in the
BeginDraw method, and ends the drawing updates that were started by the call to the
BeginDraw method.
D2DImageSource.cpp
C++
void D2DImageSource::EndDraw()
{
// Remove the transform and clip applied in BeginDraw since
// the target area can change on every update
m_d2dContext->SetTransform(D2D1::IdentityMatrix());
m_d2dContext->PopAxisAlignedClip();
// Clear render target and end drawing
ThrowIfFailed(
m_d2dContext->EndDraw()
);
m_d2dContext->SetTarget(nullptr);
ThrowIfFailed(
m_sisNative->EndDraw()
);
}
The
D2DImageSource class also exposes the
Clear,
FillSolidRect, and
DrawImage methods. The
Clear method sets the render target to a color that's specified by the method parameter.
D2DImageSource.cpp
C++
void D2DImageSource::Clear(Windows::UI::Color color)
{
m_d2dContext->Clear(ConvertToColorF(color));
}
In the following code, the
Clear method accepts a
Windows::UI::Color as a parameter and uses the
ConvertToColorF function in DirectXHelpers.h to convert it to a
ColorF for DirectX.
DirectXHelpers.h
C++
inline D2D1_COLOR_F ConvertToColorF(Color color)
{
return ColorF(color.R / 255.0f, color.G / 255.0f, color.B / 255.0f, color.A / 255.0f);
}
The
FillSolidRect method creates a Direct2D brush and uses it to fill a rectangle, and converts both color types and rectangle types.
D2DImageSource.cpp
C++
void D2DImageSource::FillSolidRect(Windows::UI::Color color, Windows::Foundation::Rect rect)
{
// Create a solid color D2D brush
ComPtr<ID2D1SolidColorBrush> brush;
m_d2dContext->CreateSolidColorBrush(ConvertToColorF(color), &brush);
// Draw a filled rectangle
m_d2dContext->FillRectangle(ConvertToRectF(rect), brush.Get());
}
The
DrawImage method uses the
WICImage class to draw the image.
D2DImageSource.cpp
C++
void D2DImageSource::DrawImage(WICImage^ image)
{
auto bounds = D2D1::RectF(0.f, 0.f, static_cast<float> (m_width), static_cast<float> (m_height));
image->DrawBitmap(m_d2dContext.Get(), bounds);
}
Drawing the image
The
WICImage constructor, which is called from the
OnNavigatedTo method of the
MainPage class, initializes the WIC factory object that's used to extract the image bitmap from a stream and create the WIC format converter.
The
LoadImage method is called from the Button Click handler in the
MainPage class. It creates a decoder for the image stream, decodes the image, extracts the width and height of the image to set the display bounds, and then creates a WIC format
converter to convert the pixel format to a format that's compatible with Direct2D.
The
DrawBitmap method is called from the
D2DImageSource::DrawImage and uses the WIC format converter to obtain the bitmap data, derives the scaling that's required to fit the bitmap into the bounds that are presented by the XAML Image control,
and uses the context that's passed to it as a parameter to draw the bitmap.
Controlling the ripple effect
Typically, a DirectX app has a loop that's used to render the results of an animation such as a ripple effect. However, in a XAML-based app, the DirectX loop is not directly accessible, and so the processing that would be handled by the loop is divided
into a render loop, as follows:
- The MainPage class handles the (PointerPressed, PointerMoved, and
PointerReleased* events. The handlers for these events set variables that control the ripple effect.
- The MainPage class controls the rendering of the ripple effect, which includes the calculation of its duration. This data is then passed to the Ripple method in the RippleImageSource instance.
- An instance of the BasicTimer class controls the ripple effect.
- The D2DImageSource class draws the ripple each time the image is rendered.
As shown in the next code, the
PointerPressed event handler in the
MainPage class sets the point in the image where the ripple begins.
MainPage.cpp
C++
void MainPage::OnPointerPressed(Object^ sender, PointerRoutedEventArgs^ e)
{
m_pointerPressed = true;
OnPointerMoved(sender, e);
}
This event handler sets a flag to indicate that the point in the image has been pressed and calls the
OnPointerMoved event handler.
OnPointerMoved is also called every time that the
PointerMoved event fires.
MainPage.cpp
C++
void MainPage::OnPointerMoved(Object^ sender, PointerRoutedEventArgs^ e)
{
if (m_pointerPressed)
{
if (!m_rippleEffectIsActive)
{
m_rippleEffectIsActive = true;
m_rippleTimer->Reset();
}
m_pointerPosition = e->GetCurrentPoint(XamlImage)->RawPosition;
}
}
If a ripple effect is not active, the event handler sets a flag to indicate that it is active, and resets the instance of the
BasicTimer class that's used to control the ripple effect. Then, the point where the ripple will begin is returned as a Point structure.
The event handler for
PointerReleased event just clears the flag that was set in the
OnPointerPressed method. Here's the code.
MainPage.cpp
C++
void MainPage::OnPointerReleased(Object^ sender, PointerRoutedEventArgs^ e)
{
m_pointerPressed = false;
}
The
OnRenderDisplaySurface event handler is called each time that a frame is to be rendered by the
CompositionTarget::Rendering event.
MainPage.cpp
C++
void MainPage::OnRenderDisplaySurface(Object^ sender, Object^ args)
{
m_frames++;
m_appTimer->Update();
if (m_appTimer->Total > 1.0f)
{
wstringstream ss;
ss << static_cast<float>(m_frames / m_appTimer->Total);
Fps->Text = "Frames per second: " + ref new String(ss.str().c_str());
m_frames = 0;
m_appTimer->Reset();
}
if (!m_rippleEffectIsActive)
{
return;
}
m_rippleTimer->Update();
auto delta = m_rippleTimer->Total;
if (delta < RIPPLETIME)
{
m_rippleImageSource->Ripple(m_image, m_pointerPosition, delta);
}
else
{
m_rippleEffectIsActive = false;
DrawImage();
}
}
In addition, the event handler either calls the Ripple method of the
RippleImageSource class to calculate the current ripple position, or turns off the ripple after the specified amount of time has elapsed.
Calculating the ripple effect
The constructor of the
RippleImageSource class initializes a Direct2D factory and calls the Register method of the
RippleEffect class to register the ripple effect with the Direct2D image effects system, as shown in this code:
RippleImageSource.cpp
C++
D2D1_FACTORY_OPTIONS options;
ZeroMemory(&options, sizeof(D2D1_FACTORY_OPTIONS));
#if defined(_DEBUG)
// If the project is in a debug build, enable Direct2D debugging via SDK Layers
options.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION;
#endif
ThrowIfFailed(
D2D1CreateFactory(
D2D1_FACTORY_TYPE_SINGLE_THREADED,
__uuidof(ID2D1Factory1),
&options,
&m_d2dFactory
)
);
ThrowIfFailed(
RippleEffect::Register(m_d2dFactory.Get())
);
CreateDeviceIndependentResources();
CreateDeviceResources();
The
CreateDeviceResources method calls the
CreateDeviceResources method in the
D2DImageSource class, and then creates the ripple effect and sets the bitmap source effect as the input to the ripple effect.
RippleImageSource.cpp
C++
D2DImageSource::CreateDeviceResources();
ThrowIfFailed(
m_d2dContext->CreateEffect(CLSID_CustomRippleEffect, &m_rippleEffect)
);
// Set the BitmapSource effect as the input to the custom Ripple effect.
m_rippleEffect->SetInputEffect(0, m_bitmapSourceEffect.Get());
The Ripple method, which is called by the
OnRenderDisplaySurface event handler in the
MainPage class, sets the bitmap scaling and the center property of the ripple effect before it calls the
UpdateRipple method.
RippleImageSource.cpp
C++
ThrowIfFailed(
m_bitmapSourceEffect->SetValue(D2D1_BITMAPSOURCE_PROP_WIC_BITMAP_SOURCE, image->GetWicFormatConverter())
);
D2D1_VECTOR_2F scale;
int displaywidth = static_cast<int>(image->GetDisplayBounds().right - image->GetDisplayBounds().left);
int displayheight = static_cast<int>(image->GetDisplayBounds().bottom - image->GetDisplayBounds().top);
scale.x = scale.y = min((float)m_width / displaywidth, (float)m_height / displayheight);
ThrowIfFailed(
m_bitmapSourceEffect->SetValue(D2D1_BITMAPSOURCE_PROP_SCALE, scale)
);
D2D1_POINT_2F position = Point2F(center.X * m_dpi / 96.0f, center.Y * m_dpi / 96.0f);
ThrowIfFailed(
m_rippleEffect->SetValue(RIPPLE_PROP_CENTER, position)
);
UpdateRipple(delta);
The properties of the ripple are calculated in the
UpdateRipple method.
RippleImageSource.cpp
C++
ThrowIfFailed(
m_rippleEffect->SetValue(RIPPLE_PROP_FREQUENCY, 140.0f - delta * 30.0f)
);
ThrowIfFailed(
m_rippleEffect->SetValue(RIPPLE_PROP_AMPLITUDE, 60.0f - delta * 15.0f)
);
ThrowIfFailed(
m_rippleEffect->SetValue(RIPPLE_PROP_PHASE, -delta * 20.0f)
);
ThrowIfFailed(
m_rippleEffect->SetValue(RIPPLE_PROP_SPREAD, 0.01f + delta / 10.0f)
);
RenderRipple();
After it calculates the properties of the ripple, the
UpdateRipple method calls the
RenderRipple method to render the ripple on the screen. The
RenderRipple method initializes the render target and renders the effect by calling methods in the
D2DImageSource class.
RippleImageSource.cpp
C++
POINT offset;
ComPtr<IDXGISurface> surface;
// Begin drawing - returns a target surface and an offset to use as the top left origin when drawing
HRESULT beginDrawHR = m_sisNative->BeginDraw(m_updateRectNative, &surface, &offset);
if (beginDrawHR == DXGI_ERROR_DEVICE_REMOVED || beginDrawHR == DXGI_ERROR_DEVICE_RESET)
{
// If the device has been removed or reset, attempt to recreate it and continue drawing
CreateDeviceResources();
BeginDraw();
}
else
{
// Notify the caller by throwing an exception if any other error was encountered
ThrowIfFailed(beginDrawHR);
}
// Now we set up the Direct2D render target bitmap linked to the swapchain.
// Whenever we render to this bitmap, it will be directly rendered to the
// swapchain associated with the window.
D2D1_BITMAP_PROPERTIES1 bitmapProperties =
BitmapProperties1(
D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW,
PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED),
m_dpi,
m_dpi
);
// Create render target
ComPtr<ID2D1Bitmap1> bitmap;
ThrowIfFailed(
m_d2dContext->CreateBitmapFromDxgiSurface(surface.Get(), &bitmapProperties, &bitmap)
);
// Set context's render target
m_d2dContext->SetTarget(bitmap.Get());
m_d2dContext->BeginDraw();
m_d2dContext->Clear(ColorF(ColorF::Black));
m_d2dContext->DrawImage(m_rippleEffect.Get());
HRESULT hr = m_d2dContext->EndDraw();
m_d2dContext->SetTarget(nullptr);
ThrowIfFailed(
m_sisNative->EndDraw()
);
The end result is a movable ripple effect that is rendered at a user specified location over the displayed image.