Example: The Wipe Transform

This topic walks you through the Wipe transform sample code. This transform uses custom helper functions and elements common to all DLL files that use Microsoft DirectX Transform routines.

This topic assumes that you are familiar with Component Object Model (COM) and that you have run WipeDlg.exe so that you are familiar with the appearance of the transform.

The Example: The WipeDlg Application article walks you through WipeDlg.exe, which uses a transform to gradually change one image into another. For WipeDlg.exe, WipeDlg.cpp takes care of loading and displaying the images, and Microsoft DirectX Transform takes care of the detailed combination of the images. See the Visual Filters and Transitions Reference for information about other transforms included with Windows Internet Explorer that you can add to your applications.

The Microsoft DirectX Transform architecture enables you to develop your own ideas for transitions and effects and implement them in code in two ways. You can load the images as part of an application into DXSurface objects and manipulate them sample-by-sample to form a new image by using the IDXARGBReadPtr and IDXARGBReadWritePtr interfaces. Or you can place the algorithm that produces the effect into a separate transform that applications can invoke by using COM methods. The latter way is preferred because it makes the transform separate from the application and enables it to be easily reused and distributed.

This article contains the following sections.

  • Identifying the Regions
  • Building the Gradient
  • Combining the Images
  • Summary

Identifying the Regions

The Wipe transform divides the viewing area of the source surfaces into three areas.

  1. A rectangular area that is from only Image A.
  2. A rectangular area that is from only Image B.
  3. A rectangular gradient area that is a mixture of Image A and Image B.

The previous illustration shows a typical Wipe transform. Because the default transform sweeps from left to right, the boundary between the Image A area and the gradient is called the leading edge. Similarly, the boundary between Image B and the gradient is called the trailing edge. The positions of these two boundaries relative to the image frame determine how the samples in a given row are combined to form a row in the output image. For samples from the left edge of the image to the trailing edge, there is no mixing; all samples are from Image B. From the leading edge to the right edge of the image, all the samples are from Image A. In the region from the trailing edge to the leading edge, samples from the two images are mixed to produce a smooth transition between the two.

The following code example shows two variables that determine these positions, which are set by the calling application. The IDXEffect::Progress property is a number from 0.0 to 1.0, indicating how much of the transition has completed. The m_GradPercentSize variable specifies what percentage of the total image width to use as the gradient region. This percentage can be greater than 100 percent, making the gradient width larger than the entire image width. This also means that the leading and trailing edges are not necessarily inside the image boundaries.

_ComputeStartPoints is a helper function in DxtWipe.cpp that calculates the location of the leading and trailing edges, which are used to locate the AStart and GradStart positions. In addition, it calculates AWid and BWid, which are used later to retrieve the correct number of samples from each image.

void CDXTWipe::_ComputeStartPoints(const CDXDBnds & bnds,
    ULONG & AWid, ULONG & AStart, ULONG & BWid, ULONG & BUnpackWid,
    ULONG & GradStart, ULONG & GradWid, ULONG & GradWgtStart)
{
    AWid = 0, AStart = 0, BWid = 0, BUnpackWid = 0;
    GradStart = 0, GradWid = 0, GradWgtStart = 0;
    ULONG BndsWid = bnds.Width();
    ULONG ulRange     = m_InputSize.cx + m_GradientSize;
    long LeadEdgePos  = (long)(ulRange * GetEffectProgress());
    long TrailEdgePos = LeadEdgePos - m_GradientSize;

    if( LeadEdgePos >= bnds.Left() )
    {
        if( LeadEdgePos <= bnds.Right() )
        {
            if( TrailEdgePos < bnds.Left() )
            {
                // Leading in between, trailing out.
                GradWid =
                m_GradientSize - ( bnds.Left() - TrailEdgePos );
                GradWgtStart = bnds.Left() - TrailEdgePos;
            }
            else
            {
                // Leading and trailing in between.
                GradWid = m_GradientSize;
                BWid = TrailEdgePos - bnds.Left();
            }
            GradStart = m_GradientSize - GradWid;
            AWid = BndsWid - GradWid - BWid;
        }
        else
        {
            if( TrailEdgePos < bnds.Left() )
            {
                // Leading is past right, trailing is before left.
                GradWid      = BndsWid;
                GradWgtStart = bnds.Left() - TrailEdgePos;
            }
            else if( TrailEdgePos < bnds.Right() )
            {
                // Leading is past right, trailing is in between.
                GradWid = bnds.Right() - TrailEdgePos;
                BWid    = BndsWid - GradWid;
            }
            else
            {
                // All B.
                BWid = BndsWid;
            }
        }
        GradStart  = BWid;
        BUnpackWid = BWid + GradWid;
        AStart     = GradStart + GradWid;
    }
    else
    {
        // All A.
        AWid = BndsWid;
    }
}

As shown in the previous code, after all the variables are initialized, the routine locates the leading and trailing edges. Notice that the leading edge position is the product of the image width and the IDXEffect::Progress. After initialization, each possible combination of edge locations is tested, with the width and positions of the gradient, the A region, and the B region set accordingly. In cases where the trailing edge is located to the left of the image boundary, the GradWgtStart variable records how much of the gradient region lies outside the image. Notice also that ulRange is equal to the sum of the full image width and the gradient regions. This means that the trailing edge of the gradient is at the right boundary of the image when IDXEffect::Progress is equal to one, and only Image B is visible.

Building the Gradient

When the IDXTransform::Setup method is called for this transform by the application, the transform interface calls the OnSetup function.

HRESULT CDXTWipe::OnSetup( DWORD dwFlags )
{
    // Compute the effect step resolution and weights.
    HRESULT hr;
    CDXDBnds InBounds(InputSurface(0), hr);
    if (SUCCEEDED(hr))
    {
        InBounds.GetXYSize(m_InputSize);
        _UpdateStepRes();
        hr = _UpdateGradWeights();
    }
    return hr;

The function in the previous code starts when a CDXBnds object is created and initialized with the bounds of one of the input surfaces. Inside the .dll code, the input and output surface pointers are obtained with the CDXBaseNTo1::InputSurface and CDXBaseNTo1::OutputSurface methods of the CDXBaseNTo1 class. Because the input index of zero corresponds to Image A, this stores the bounds of Image A in the InBounds object. The m_InputSize data member of the Wipe transform object is initialized with this width, and two other functions are called. _UpdateStepRes calculates m_GradientSize based on the custom property member m_GradPercentSize, as shown in the following code example.

m_GradientSize   = (long)(m_InputSize.cx * m_GradPercentSize);

The following code example shows _UpdateGradWeights, which creates an array of gradient weights that step smoothly from 255 to zero. The size of the array is equal to the width of the gradient region. Elements of this array are used later as an alpha channel to mix different amounts of Image A and Image B samples.

HRESULT CDXTWipe::_UpdateGradWeights( void )
{
    HRESULT hr = S_OK;

    delete m_pGradientWeights;
    m_pGradientWeights = new ULONG[m_GradientSize];

    if( !m_pGradientWeights )
    {
        hr = E_OUTOFMEMORY;
    }
    else
    {
        float fWeight = 1.0, fInc = 1.f / m_GradientSize;
        for(long i = 0; i < m_GradientSize; ++i, fWeight -= fInc )
        {
            m_pGradientWeights[i] = (ULONG)(fWeight * 255.);
        }
    }

    return hr;

Combining the Images

When the application calls the IDXTransform::Execute method, the WorkProc function of DxtWipe.cpp is executed, and the two images are combined row-by-row into a single output image. The WI variable is a pointer to a CDXTWorkInfoNTo1 object passed to the function that contains the bounds of output surface and an HRESULT that indicates to the calling routine whether the transform was successful. The pbContinue parameter is used for multithreading, causing the .dll to exit if its thread is stopped.

HRESULT CDXTWipe::WorkProc(
const CDXTWorkInfoNTo1& WI, BOOL* pbContinue )
{
    HRESULT hr = S_OK;

    // Get input sample access pointer for the requested region.
    // Note: Lock might fail due to a lost surface.
    CComPtr<IDXARGBReadPtr> pInA;
    hr = InputSurface( 0 )->LockSurface(
    &WI.DoBnds, m_ulLockTimeOut, DXLOCKF_READ,
    IID_IDXARGBReadPtr, (void**)&pInA, NULL );
    if( FAILED( hr ) ) return hr;

    CComPtr<IDXARGBReadPtr> pInB;
    hr = InputSurface( 1 )->LockSurface(
    &WI.DoBnds, m_ulLockTimeOut, DXLOCKF_READ,
    IID_IDXARGBReadPtr, (void**)&pInB, NULL );
    if( FAILED( hr ) ) return hr;

    // Put a write lock only on the region being updated so
    // multiple threads do not conflict.
    // Note: Lock may fail due to a lost surface.
    CComPtr<IDXARGBReadWritePtr> pOut;
    hr = OutputSurface()->LockSurface(
    &WI.OutputBnds, m_ulLockTimeOut, DXLOCKF_READWRITE,
    IID_IDXARGBReadWritePtr, (void**)&pOut, NULL );
    if( FAILED( hr ) ) return hr;

The preceding code begins by declaring COM pointer variables to Microsoft DirectX Transform read and read/write interfaces. The input and output surfaces for the transform are not referenced directly, but with the CDXBaseNTo1::InputSurface and CDXBaseNTo1::OutputSurface member functions of the CDXBaseNTo1 class. Multiple surface inputs are usually stored in arrays and indexed as previously shown. The two input surfaces are locked with the IDXSurface::LockSurface method. Because the lock is made with the DXLOCKF_READ and IID_IDXARGBReadPtr values, the method returns read pointers to the two input surfaces. Then the output surface is locked for writing by specifying the DXLOCKF_READWRITE and IID_IDXARGBReadWritePtr values.

The following code example continues the setup for the image combination.

    // Allocate a working buffer.
    ULONG DoBndsWid = WI.DoBnds.Width();
    
    DXPMSAMPLE* pRowBuff = DXPMSAMPLE_Alloca( DoBndsWid );

    if (pRowBuff == NULL)
    {
        return;
        // Handle sample allocation failure.
    }
    
    // Allocate an output buffer if needed.
    DXPMSAMPLE *pOutBuff = NULL;
    if( OutputSampleFormat() != DXPF_PMARGB32 )
    {
       // TODO: Add error handling code here.
        pOutBuff = DXPMSAMPLE_Alloca( DoBndsWid );
    }

    // Compute the width of each region.
    ULONG ulDoNumRows = WI.DoBnds.Height();

    ULONG AWid, AStart, BWid, BUnpackWid, GradStart, GradWid,
    GradWgtStart;
    _ComputeStartPoints(WI.DoBnds, AWid, AStart, BWid, BUnpackWid,
    GradStart, GradWid, GradWgtStart);

    // Allocate working buffers for the gradient area.
    DXPMSAMPLE *pGradBuff = DXPMSAMPLE_Alloca( GradWid );
    // TODO: Add error handling code here.

    //
    // Set up the dither structure.
    //
    DXDITHERDESC dxdd;
    if (DoDither())
    {
        dxdd.x = WI.OutputBnds.Left();
        dxdd.y = WI.OutputBnds.Top();
        dxdd.pSamples = pRowBuff;
        dxdd.cSamples = DoBndsWid;
        dxdd.DestSurfaceFmt = OutputSampleFormat();
    }

To combine the samples from the two input images, the code requires a working buffer that can hold an entire row of image data. The pRowBuff array serves this purpose and is allocated with the DXPMSAMPLE_Alloca helper function. In cases where the transform uses the IDXARGBReadWritePtr::OverArrayAndMove method to do a composite onto the destination surface, the code needs to allocate a scratch buffer for the operation. This is needed only if the output surface's pixel format is not PMARGB32, so that is checked before allocation.

Next, the leading and trailing edge calculations are performed by _ComputeStartPoints, using the bounds of the output surface. The function returns all the parameters that are needed to unpack the image data, including the gradient width. Combining the data in the gradient region also requires a working buffer, which is allocated after the function call. Finally, if dithered output is required, a structure is initialized to enable it to be produced.

The code then enters the main loop where it reads image data from each of the regions into arrays and combines samples in the gradient region, as shown in the following code example.

// === Main loop =================================================
for(ULONG OutY = 0; *pbContinue && (OutY < ulDoNumRows); ++OutY)
{
    // Get leading solid B samples.
    if( BUnpackWid )
    {
        pInB->MoveToRow( OutY );
        pInB->UnpackPremult( pRowBuff, BUnpackWid, FALSE );
    }

If there is an Image B region that shows in the output, the routine moves to the current row in pInB and reads the samples into the working buffer. This is done with the IDXARGBReadPtr::UnpackPremult method, which extracts a number of samples in the native pixel format and converts them to PMARGB32 format, if needed. This saves the step of multiplying each color component by alpha before combining the samples. Notice also that because BUnpackWid = BWid + GradWid, the routine is unpacking samples all the way to the leading edge.

The loop continues by calculating sample colors for the mixed area of the gradient region.

        // Compute gradient transition area.
        if( GradWid )
        {
            pInA->MoveToXY( BWid, OutY );
            pInA->UnpackPremult( pGradBuff, GradWid, FALSE );

            for( ULONG i = 0; i < GradWid; ++i )
            {
                ULONG Wgt = m_pGradientWeights[GradWgtStart+i];
                pRowBuff[BWid+i] =
                DXScaleSample( pGradBuff[i], Wgt ^ 0xFF ) + 
                DXScaleSample( pRowBuff[BWid+i], Wgt ); 
            }
        }

If there is a gradient region, MoveToXY jumps back to the trailing edge position on the same row as Image A and reads enough samples into the gradient buffer to fill the gradient region. In the loop over the gradient row that follows, the samples are combined based on the value of the gradient weight. Each sample is scaled by the helper function DXScaleSample, which takes a sample color and a weight from zero to 255 and returns the scaled color. Samples from Image B are scaled by Wgt, and samples from Image A are scaled by 1-Wgt, and the sum of the two represents the output.

       // Get trailing solid A samples.
        if( AWid )
        {
            pInA->MoveToXY( AStart, OutY );
            pInA->UnpackPremult( pRowBuff + AStart, AWid, FALSE );
        }

If there is any region of Image A showing, it is read into pRowBuff, completing this row of the output image. All that remains is to write the completed row to the output surface.

        // Get the output row.
        pOut->MoveToRow( OutY );
        if (DoDither())
        {
            DXDitherArray(&dxdd);
            dxdd.y++;
        }
        if (DoOver())
        {
            pOut->OverArrayAndMove(pOutBuff, pRowBuff, DoBndsWid);
        }
        else
        {
            pOut->PackPremultAndMove(pRowBuff, DoBndsWid);
        }
    } // End for
    return hr;
}

First, the write pointer is moved to the correct row of the output surface. If the transform has specified that the output should be dithered, that is done by the DXDitherArray helper function. The method used to write the output row on the destination surface depends on whether the output row should be blended onto the surface. If so, the IDXARGBReadWritePtr::OverArrayAndMove method is used. This is the method that requires a scratch buffer to hold the blended output of pRowBuff and the destination surface, which was allocated earlier as pOutBuff. If the pRowBuff should replace the corresponding row on the destination surface, the IDXARGBReadWritePtr::PackPremultAndMove method performs the correct operation.

The loop is executed for each row of the two images, until the output surface is filled with the combined image.

Summary

This particular image transform uses a number of custom helper functions to produce an image effect. There are a number of elements to this routine, however, that are common to all .dll files that use Microsoft DirectX Transform routines.