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.
  1. On the menu bar, choose Build > Build Solution. The build step compiles the code and also packages it as a Windows Store app.
  2. After you build the project, you must deploy it. On the menu bar, choose Build > Deploy Solution.
  3. 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.

XAML n DirectX QuickStart Image.png

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();
    }
}

LoadImage.png

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.

Last edited Dec 12, 2012 at 7:58 PM by blainew, version 10

Comments

No comments yet.