Example: The Ripple Transform

This article discusses the Ripple transform as an example of how to write a three-dimensional (3-D) transform. The Microsoft® Visual Studio® project and source code can be found in the C:\Samples\Multimedia\Dtrans\C++\Ripple folder of the Microsoft DirectX® Media Software Development Kit (SDK).

Before reading this article, you should run the DXETool application and view the Ripple transform effect. It is located in the C:\DXMedia\bin directory of the SDK. Select the "Ripple Sample" transform and load an image from the File menu. If you right-click and hold the mouse button on the image and move the mouse, you can change the position of the camera on the z-axis, which changes the size of the object on the screen. You can change the orientation of the 3-D object by clicking on the object while moving the mouse. You can play the transform by selecting the right double-arrow button next to the Progress slider. This effect is best viewed with the surface turned slightly sideways, so that the displacements of the 3-D surfaces are easier to see.

As the animation proceeds over time, you should see a flat surface that quickly begins to distort and produce waves. The wave amplitude is a maximum when the Progress property is at 25 percent, after which the waves begin to gradually stop. This way of dividing the animation over Progress produces the effect of a stone being dropped in a still pond, producing large waves at the start and slowly dissipating over time.

This article examines the Ripple transform in the following sections.

Making Waves

When you produce a disturbance in a pond, you can see that displacements of the surface are different at different locations on the surface, and they change over time to produce a smooth, rolling effect. In addition, the amplitude of the waves gets smaller the farther away the wave is from the initial splash.

Waves on surfaces are represented in code as a series of z-axis displacements of a two-dimensional (2-D) plane formed by the x-axis and y-axis. The Ripple example code uses the following algorithm to calculate wave amplitudes.

z(r, Progress) = A(Progress)*[1 -  r/Max]^2 *
Sin[ 2*pi(r/Wavelength + (1 - Progress)) ]

For the Ripple transform, the previous algorithm controls the z-position of the vertices of a 3-D mesh. The amplitude is a value that depends on the Progress property. Progress starts and ends at zero, and achieves its maximum value at Progress = 0.25. The r variable is the distance of a given vertex from the location of the splash. The Wavelength property determines the size of the wave across the surface, and the Max property determines how many waves will extend across the surface.

The displacements previously calculated are applied to a grid of mesh vertices that forms the rectangle of the input image. You'll see how these vertices are created in the OnSetup method and how they are displaced in the OnExecute method.

Custom Properties

The Ripple transform custom properties affect the appearance of the waves on the surface. These can be set through the put_PropertyName method, where PropertyName can be any of the listed custom properties for the transform. Similarly, get_PropertyName enables you to retrieve current settings.

The following are the Ripple transform custom properties.

In addition, the SetQuality method accesses the m_fQuality data member. This data member is inherited as a protected member of the CDXBaseNTo1 class and defaults to the value of 0.5 when your transform is created.

For more information about these custom properties, see the Transform Reference.

Code Overview

The code that does the work for the Ripple transform is located in the CRipple.cpp and CRipple.h files. Open the Ripple.dsp file and take a look at CRipple.cpp.

The constructor for the CRipple object is located at the beginning of the file. It handles initialization of all the custom properties with default values. These values can be changed by the user with the put_Property and get_Property methods for each property. You can override the DetermineBnds method from CDXBaseNTo1, so that transform users can determine the volume that the output mesh will occupy.

Throughout the code you will see repeated calls to the CDXBaseNTo1::InputSurface and CDXBaseNTo1::OutputMeshBuilder helper functions. These are helper functions inherited from CDXBaseNTo1 that enable you to retrieve pointers to the transform inputs and outputs.

The bulk of the calculation and vertex manipulation is done with the CRipple::OnSetup and CRipple::OnExecute methods and their helper functions. They are overrides of the CDXBaseNTo1 base class methods used by all transforms.

The OnSetup Method

The OnSetup method is called by the IDXTransform interface when a user calls its Setup method on your transform.

The following code first calls the InitializeBitmap internal helper function, which converts the input DXSurface into a D3DRMTexture3 object.

HRESULT CRipple::InitializeBitmap(void)
{
    m_cpInputTexture = NULL;

    _ASSERT(InputSurface(0));
    return m_cpSurfFact->CreateD3DRMTexture(InputSurface(0), 
        m_cpDirectDraw,m_cpDirect3DRM, 
        IID_IDirect3DRMTexture3,(void**)&m_cpInputTexture);
}

The transform inherits a pointer to an IDXSurfaceFactory interface from the CDXBaseNTo1 base class. The IDXSurfaceFactory::CreateD3DRMTexture method performs the conversion and stores the pointer as a data member for later use.

Next, a quick calculation determines the number of vertices and polygons in the mesh, based on the MinSteps, MaxSteps, and m_fQuality. This produces a square mesh of vertices, with two triangular polygons per square.

m_Steps = m_MinSteps + (long)((m_MaxSteps - m_MinSteps) *
  m_fQuality);
m_cVertices = (m_Steps + 1) * (m_Steps + 1);
    
// Number of polygons
int cPolys = m_Steps * m_Steps * 2;

The remaining code is responsible for preparing the rectangular mesh that will be reshaped into the series of waves. Many of the methods of the IDirect3DRMMeshBuilder3 interface are required to do this. The Reset internal helper function creates the vertex and calculates the distance of each vertex from the origin, as shown in the following code.

HRESULT CRipple::Reset()
{
    // Allocate the vertices lookup table.
    delete[] m_prgvert;
    m_prgvert = new D3DRMVERTEX[m_cVertices];
    if (NULL == m_prgvert)
    {
        return E_OUTOFMEMORY;
    }

    delete[] m_pflDistanceTable;
    m_pflDistanceTable = new float[m_cVertices];
    if (!m_pflDistanceTable) {
        return E_OUTOFMEMORY;
    }
    HRESULT hr;

    CDXDBnds bndsBounds;

    if(FAILED(hr = bndsBounds.SetToSurfaceBounds(
      InputSurface(0)))) return hr;

    if(bndsBounds.Width() == 0 || bndsBounds.Height() == 0)
    {
        m_flHeight = TARGET_HEIGHT;
        m_flWidth = TARGET_WIDTH;
    }
    else
    {
        double dHeightToWidthRatio = sqrt(
          (double)bndsBounds.Height()/
          (double)bndsBounds.Width());

        m_flHeight =
          (float)(TARGET_HEIGHT * dHeightToWidthRatio);
        m_flWidth =
          (float)(TARGET_WIDTH / dHeightToWidthRatio);
    }

    // Generate the vertices.
    D3DRMVERTEX* pvert = m_prgvert;
    float *pDist = m_pflDistanceTable;
    for (int y = 0; y <= m_Steps; y++)
    {
        for (int x = 0; x <= m_Steps; x++)
        {
            pvert->position.x =
              (-m_flWidth  / 2.0f) + ((m_flWidth  * (float)x) /
              (float)m_Steps);
            pvert-<position.y =
              (-m_flHeight / 2.0f) + ((m_flHeight * (float)y) /
              (float)m_Steps);
            float flXDelta = (pvert->position.x - m_XOrigin);
            float flYDelta = (pvert->position.y - m_YOrigin);
            *pDist = (float)sqrt
              ((flXDelta * flXDelta) + (flYDelta * flYDelta));
            pvert->position.z = 0.0f;
            pvert->normal.x   = 0.0f;
            pvert->normal.x   = 0.0f;
            pvert->normal.z   = 1.0f;
            pvert->tu    = (float)x / (float)m_Steps;
            pvert->tv    = 1.0f - ((float)y / (float)m_Steps);
            pvert->color = D3DRGB(255, 0, 0);
            pvert++;
            pDist++;
        }
    }
    return S_OK;   
}

After allocating the vertex and distance arrays and setting the volume boundaries, the routine enters a double loop over all the vertices, setting the x-position and y-position for each. The pvert variable is a D3DRMVERTEX structure that holds the vertex position, texture coordinates, normal vectors, and color. These are calculated relative to the global constants gc_flWidth and gc_flHeight, which define the 3-D volume used for the mesh. The distance of each vertex from the m_XOrigin and m_YOrigin are also calculated. These two data members are custom properties that are accessed with the corresponding put_Property and get_Property methods.

When the Reset helper function returns, the created vertices and normals are added to the mesh.

    // Just need one normal. Initially, everything is facing
    // the same direction.
    // Direct3D Retained Mode will compute its own face normals
    // as needed later on.
    OutputMeshBuilder()->AddNormal(0.0f,0.0f,1.0f);

    // Add all the vertices and normals. We assume the indices
    // of the vertices increase monotonically from zero.
    for (long i = 0; i < m_cVertices; i++)
    {
        int iVertex =
          OutputMeshBuilder()->AddVertex(
          m_prgvert[i].position.x,
          m_prgvert[i].position.y,
          m_prgvert[i].position.z);
          
    _ASSERT(iVertex == i);
        hr = OutputMeshBuilder()->SetTextureCoordinates(
          iVertex, m_prgvert[i].tu, m_prgvert[i].tv);
          
        if (FAILED(hr))
        {
            return hr;
        }
        int iNormal =
          OutputMeshBuilder()->AddNormal(
          m_prgvert[i].normal.x,
          m_prgvert[i].normal.y,
          m_prgvert[i].normal.z);
          
        _ASSERT(iNormal == i);
    }
    .
    .
    .

The routine continues by using these vertex definitions to create two polygons for each square of the mesh, looping over all the squares. When that is done, the input surface texture is associated with the mesh by the IDirect3DRMMeshBuilder3::SetTexture method.

Finally, the CDXBaseNTo1::ClearDirty function sets a value that indicates that the transform is ready to produce output. It is an accessor function inherited from CDXBaseNTo1, which tracks whether any of the transform set-up variables have changed. Note that every put_Property call includes a call to SetDirty, so that the transform can be notified whether it needs to perform a set up before execution.

The OnExecute Method

This is the part of the transform code that uses the wave equation to calculate the z-axis displacements of the vertices. This method is called implicitly when a transform user calls the IDXTransform::Execute method.

The code starts by checking whether any of the inputs have changed since the last call to Execute. If a custom property has changed, the OnSetup method is called again. If only the input image has changed, a full set up is not required. However, the 3-D texture needs to change, which is done with another call to SetTexture.

The GetCurrentAmplitude internal helper function uses the Progress property to determine the maximum amplitude of the waves.

float CRipple::GetCurrentAmplitude(void)
{
    if (m_Progress == 0.0f || m_Progress == 1.0f) {
    return 0.0f;
    }
    if (m_Progress <= 0.25f) {
    return m_Amplitude * (1.0f - ((0.25f - m_Progress) * 4.0f));
    } 

    return m_Amplitude *
    (1.0f - ((m_Progress - 0.25f) / 3.0f * 4.0f));
}

This amplitude is designed to have a maximum at a Progress of 0.25.

Finally, the code calculates the new z-axis value and normal vector for each vertex based on the Progress value, its position, and its distance from the origin. This new vertex value is changed in the output mesh with the Direct3D Retained Mode IDirect3DRMMeshBuilder3::SetVertex and IDirect3DRMMeshBuilder3::SetNormal methods.

When the following loop is done, the modifications to the output mesh are complete, and the mesh is ready to be displayed by the calling routine.

// Apply the wave to the vertices.
float flMaxDistance = (float)m_NumWaves * m_Wavelength;
D3DRMVERTEX* pvert = m_prgvert;
float * pDistance = m_pflDistanceTable;
for (y = 0; y <= m_Steps; y++)
{
    for (x = 0; x <= m_Steps; x++)
    {
        float flDistance = *pDistance;
        if (flDistance < flMaxDistance && flCurAmplitude > 0.0f)
        {
            float flTmp = (1.0f - (flDistance / flMaxDistance));
            float flAmp = flCurAmplitude * flTmp * flTmp;
            float flAngle = (1.0f - m_Progress + (flDistance /
              m_Wavelength)) * M_2PI;
            pvert->position.z = flAmp * (float)sin(flAngle);

             if ((y == 0) || (x == 0))
            {
                pvert->normal.x = 0.0f;
                pvert->normal.y = 0.0f;
                pvert->normal.z = 1.0f;
            } else {
                // CW around current point.                                                                    
                pvert->normal = ComputeNormal( pvert->position,
                  (pvert - 1)->position,
                  (pvert - m_Steps)->position);
                }
       } else {
            pvert->position.z = 0.0;
            pvert->normal.x = 0.0f;
            pvert->normal.y = 0.0f;
            pvert->normal.z = 1.0f;
        }
        pvert++;
        pDistance++;
    }
}
pvert = m_prgvert;
for (int i = 0; i < m_cVertices; i++)
{
    hr = OutputMeshBuilder()->SetVertex(i,
      pvert->position.x,
      pvert->position.y,
      pvert->position.z);
    if (FAILED(hr))
    {
        ::DebugBreak();
        return hr;
    }
    hr = OutputMeshBuilder()->SetNormal(i,
      pvert->normal.x,
      pvert->normal.y,
      pvert->normal.z);

    if (FAILED(hr))
    {
        ::DebugBreak();
        return hr;
    }
    pvert++;
}

return S_OK;
}

Top of Page Top of Page
© 2000 Microsoft and/or its suppliers. All rights reserved. Terms of Use.