Creating a WinRT component using C++/CX Part 2: Adding a custom Direct2D effect to DeForm

The previous article introduced the DeForm Library: A WinRT component that used Direct2D to apply filters on a picture:

https://blogs.msdn.com/b/eternalcoding/archive/2012/08/13/creating-a-winrt-component-using-c-cx-deform-a-direct2d-effect-toolkit.aspx

The complete component can be found there:

https://deform.codeplex.com/

This article will show you how to use the Direct2D effect pipeline to create a custom Direct2D effect. This effect will try to apply some kind of Polaroïd effect by applying many filters in a row:

  • Black&White
  • Sepia tone
  • Saturation
  • Brightness

To do so, you have to create a COM component for Direct2D (handling reference counting and interfaces querying):

class PolaroidEffect: public IUnknown
{ public: IFACEMETHODIMP_(ULONG) AddRef(); IFACEMETHODIMP_(ULONG) Release(); IFACEMETHODIMP QueryInterface(_In_ REFIID riid, _Outptr_ void** ppOutput); private: LONG m_refCount; };

This class implements the IUnknown interface (which is the minimum you can do to be a COM component).

The associated code is obvious:

IFACEMETHODIMP_(ULONG) PolaroidEffect::AddRef() 
{ 
    m_refCount++; 
    return m_refCount; 
} 

IFACEMETHODIMP_(ULONG) PolaroidEffect::Release() 
{ 
    m_refCount--; 

    if (m_refCount == 0) 
    { 
        delete this; 
        return 0; 
    } 
    else 
    { 
        return m_refCount; 
    } 
} 

IFACEMETHODIMP PolaroidEffect::QueryInterface(_In_ REFIID riid, _Outptr_ void** ppOutput) 
{ 
    *ppOutput = nullptr; 
    HRESULT hr = S_OK; 

    if (riid == __uuidof(IUnknown)) 
    { 
        *ppOutput = reinterpret_cast<IUnknown*>(this); 
    }     
    else 
    { 
        hr = E_NOINTERFACE; 
    } 

    if (*ppOutput != nullptr) 
    { 
        AddRef(); 
    } 

    return hr; 
}

Then you have to implement the ID2D1EffectImpl interface which is the root interface of every Direct2D effects:

class PolaroidEffect: public ID2D1EffectImpl
{
public:
    IFACEMETHODIMP Initialize(
        _In_ ID2D1EffectContext* pContextInternal,
        _In_ ID2D1TransformGraph* pTransformGraph
        );

    IFACEMETHODIMP PrepareForRender(D2D1_CHANGE_TYPE changeType);
    IFACEMETHODIMP SetGraph(_In_ ID2D1TransformGraph* pGraph);

    static HRESULT Register(_In_ ID2D1Factory1* pFactory);
    static HRESULT __stdcall CreateEffect(_Outptr_ IUnknown** ppEffectImpl);

    IFACEMETHODIMP_(ULONG) AddRef();
    IFACEMETHODIMP_(ULONG) Release();
    IFACEMETHODIMP QueryInterface(_In_ REFIID riid, _Outptr_ void** ppOutput);

    // Properties
    float GetForce() const;
    HRESULT SetForce(float force);

private:
    PolaroidEffect();

    LONG m_refCount; 
    float m_force;

    ComPtr<ID2D1Effect> m_pColorMatrixBWEffect;
    ComPtr<ID2D1Effect> m_pColorMatrixSepiaEffect;
    ComPtr<ID2D1Effect> m_pBrightnessEffect;
    ComPtr<ID2D1Effect> m_pHueEffect;
    ComPtr<ID2D1Effect> m_pSaturationEffect;
    ComPtr<ID2D1TransformNode> m_pSaturationTransform;
    ComPtr<ID2D1TransformNode> m_pHueTransform;
    ComPtr<ID2D1TransformNode> m_pBrightnessTransform;
    ComPtr<ID2D1TransformNode> m_pColorMatrixBWTransform;
    ComPtr<ID2D1TransformNode> m_pColorMatrixSepiaTransform;
};

The PolaroidEffect by itself has a Force property you can get/set with GetForce()/SetForce() methods.

The ID2D1EffectImpl interface adds the following methods:

  • Initialize: This method is used to create the internal Direct2D objects
  • PrepareForRender: This method is called just before rendering if the effect has been previously initialized but not yet drawn or a property has changed or something in the context has changed (for instance the DPI, etc.)
  • SetGraph: This method is intended for composite effects which have variable number of inputs. We will get back to it in a future article

In our case, PrepareForRender and SetGraph are really simple:

IFACEMETHODIMP PolaroidEffect::PrepareForRender(D2D1_CHANGE_TYPE changeType)
{
    return S_OK;
}

IFACEMETHODIMP PolaroidEffect::SetGraph(_In_ ID2D1TransformGraph* pGraph) 
{ 
    return E_NOTIMPL; 
} 

All the job will be done Inside the Initialize method:

IFACEMETHODIMP PolaroidEffect::Initialize(
    _In_ ID2D1EffectContext* pEffectContext, 
    _In_ ID2D1TransformGraph* pTransformGraph
    )
{   
    // Effects

    // Create the b&w effect.
    HRESULT hr = pEffectContext->CreateEffect(CLSID_D2D1ColorMatrix, &m_pColorMatrixBWEffect);

    if (SUCCEEDED(hr)) 
    {
        D2D1_MATRIX_5X4_F matrix = D2D1::Matrix5x4F(
            0.2125f, 0.2125f, 0.2125f, 0.0f,
            0.7154f, 0.7154f, 0.7154f, 0.0f,
            0.0721f, 0.0721f, 0.0721f, 0.0f,
            0.0f, 0.0f, 0.0f, 1.0f,
            0.0f, 0.0f, 0.0f, 0.0f);
        m_pColorMatrixBWEffect->SetValue(D2D1_COLORMATRIX_PROP_COLOR_MATRIX, matrix);
    }

    // Create the sepia
    hr = pEffectContext->CreateEffect(CLSID_D2D1ColorMatrix, &m_pColorMatrixSepiaEffect);

    if (SUCCEEDED(hr)) 
    {
        D2D1_MATRIX_5X4_F matrix = D2D1::Matrix5x4F(
            0.90f, 0.0f, 0.0f, 0.0f,
            0.0f, 0.70f, 0.0f, 0.0f,
            0.0f, 0.0f, 0.30f, 0.0f,
            0.0f, 0.0f, 0.0f, 1.0f,
            0.0f, 0.0f, 0.0f, 0.0f);
        m_pColorMatrixSepiaEffect->SetValue(D2D1_COLORMATRIX_PROP_COLOR_MATRIX, matrix);
    }

    // Create the saturation effect.
    if (SUCCEEDED(hr)) 
    {
        hr = pEffectContext->CreateEffect(CLSID_D2D1Saturation, &m_pSaturationEffect);
    }

    if (SUCCEEDED(hr)) 
    {
        hr = m_pSaturationEffect->SetValue(D2D1_SATURATION_PROP_SATURATION, m_force);
    }

    // Create the brightness effect.
    if (SUCCEEDED(hr)) 
    {
        hr = pEffectContext->CreateEffect(CLSID_D2D1Brightness, &m_pBrightnessEffect);
    }

    if (SUCCEEDED(hr)) 
    {
        hr = m_pBrightnessEffect->SetValue(D2D1_BRIGHTNESS_PROP_WHITE_POINT, D2D1::Vector2F(1.0f, 1.0f));
    }

    if (SUCCEEDED(hr)) 
    {
        hr = m_pBrightnessEffect->SetValue(D2D1_BRIGHTNESS_PROP_BLACK_POINT, D2D1::Vector2F(0.0f, 0.15f));
    }

    // Transforms

    // Create the saturation transform from the saturation effect.
    if (SUCCEEDED(hr))
    {
        hr = pEffectContext->CreateTransformNodeFromEffect(m_pSaturationEffect.Get(), 
&m_pSaturationTransform); }
// Create the brightness transform from the brightness effect. if (SUCCEEDED(hr)) { hr = pEffectContext->CreateTransformNodeFromEffect(m_pBrightnessEffect.Get(),
&m_pBrightnessTransform); }
// Create the sepia transform from the sepia effect. if (SUCCEEDED(hr)) { hr = pEffectContext->CreateTransformNodeFromEffect(m_pColorMatrixBWEffect.Get(),
&m_pColorMatrixBWTransform); }
// Create the sepia transform from the sepia effect. if (SUCCEEDED(hr)) { hr = pEffectContext->CreateTransformNodeFromEffect(m_pColorMatrixSepiaEffect.Get(),
&m_pColorMatrixSepiaTransform); }
// Register transforms with the effect graph. if (SUCCEEDED(hr)) { hr = pTransformGraph->AddNode(m_pSaturationTransform.Get()); } if (SUCCEEDED(hr)) { hr = pTransformGraph->AddNode(m_pBrightnessTransform.Get()); } if (SUCCEEDED(hr)) { hr = pTransformGraph->AddNode(m_pColorMatrixBWTransform.Get()); } if (SUCCEEDED(hr)) { hr = pTransformGraph->AddNode(m_pColorMatrixSepiaTransform.Get()); } // Connect the custom effect’s input to the shadow transform’s input. if (SUCCEEDED(hr)) { hr = pTransformGraph->ConnectToEffectInput( 0, // Input index of the effect. m_pColorMatrixBWTransform.Get(), // The receiving transform. 0 // Input index of the receiving transform. ); } // Connect nodes if (SUCCEEDED(hr)) { hr = pTransformGraph->ConnectNode( m_pColorMatrixBWTransform.Get(), // ‘From’ node. m_pColorMatrixSepiaTransform.Get(), // ‘To’ node. 0 // Input index of the ‘to’ node. ); } if (SUCCEEDED(hr)) { hr = pTransformGraph->ConnectNode( m_pColorMatrixSepiaTransform.Get(), // ‘From’ node. m_pBrightnessTransform.Get(), // ‘To’ node. 0 // Input index of the ‘to’ node. ); } if (SUCCEEDED(hr)) { hr = pTransformGraph->ConnectNode( m_pBrightnessTransform.Get(), // ‘From’ node. m_pSaturationTransform.Get(), // ‘To’ node. 0 // Input index of the ‘to’ node. ); } // Connect the transform’s output to the custom effect’s output. if (SUCCEEDED(hr)) { hr = pTransformGraph->SetOutputNode( m_pSaturationTransform.Get() ); } return hr; }

The code creates every filter, gets the transform interface of each filter and connects the transforms like this:

  • Input to black and white transform
  • Black and white to sepia
  • Sepia to brightness
  • Brightness to saturation
  • Saturation to output

Simple, isn’t it?

Then you have to create a method to register your effect with the factory:

DEFINE_GUID(CLSID_Polaroid, 0x59820389, 0xbbd5, 0x40e8, 0x9a, 0xf2, 0x30, 0x9b, 0xb2, 0x94, 0xb3, 0x95);

HRESULT PolaroidEffect::Register(_In_ ID2D1Factory1* pFactory)
{
    PCWSTR propertyXml =
        XML(
        <?xml version='1.0'?>
        <Effect>
        <!-- System Properties -->
        <Property name='DisplayName' type='string' value='Polaroïd Effect'/>
        <Property name='Author' type='string' value='David Catuhe'/>
        <Property name='Category' type='string' value='Bitmap Effect'/>
        <Property name='Description' type='string' value='Apply a Polaroïd like effect.'/>
        <Inputs>
        <Input name='Source'/>
        </Inputs>
        <!-- Effect-specific Properties -->
        <Property name='Force' type='float' value='0'>
        <Property name='DisplayName' type='string' value='Force value'/>
        <Property name='Default' type='float' value='1.0'/>
        </Property>
        </Effect>
        );

    D2D1_PROPERTY_BINDING bindings[] =
    {
        D2D1_VALUE_TYPE_BINDING(L"Force",  &SetForce,  &GetForce),
    };

    // Register the effect using the data defined above.
    return pFactory->RegisterEffectFromString(
        CLSID_Polaroid,
        propertyXml,
        bindings,
        ARRAYSIZE(bindings),
        CreateEffect
        );
}

HRESULT __stdcall PolaroidEffect::CreateEffect(_Outptr_ IUnknown** ppEffectImpl)
{
    *ppEffectImpl = static_cast<ID2D1EffectImpl*>(new PolaroidEffect());

    if (*ppEffectImpl == nullptr)
    {
        return E_OUTOFMEMORY;
    }

    return S_OK;
}

The goal of the Register method is to create a XML description string. This XML describes the effect and each parameter handled by the effect.

In our case, we can expose the Force parameter through the GetForce()/SetForce() methods:

float PolaroidEffect::GetForce() const
{
    return m_force;
}

HRESULT PolaroidEffect::SetForce(float force)
{
    m_force = force;
    return m_pSaturationEffect->SetValue(D2D1_SATURATION_PROP_SATURATION, m_force);
}

Please note the use of the CLSID_Polaroid in order to uniquely identify the effect when you call CreateEffect:

// Create the effect
ComPtr<ID2D1Effect> d2dEffect;
Tools::Check(
    context->CreateEffect(CLSID_Polaroid, &d2dEffect)
    );

So finally to allow DeForm to instantiate this new effect, you just have to call the following code:

// Register the custom Polaroïd effect.
Tools::Check(
    PolaroidEffect::Register(m_d2dFactory.Get())
    );

The next article will be about creating custom effects using HLSL shader code!