Alpha Sparkles

 

Philip Taylor
Microsoft Corporation

September 18, 2000

Download Msdnsparkles.exe.

Welcome to Driving DirectX. This month, I continue the exploration of alpha blending by developing a Direct3D screensaver that uses alpha.

Sparkles screen saver screen shot

Figure 1. MSDNSparkles screen shot

Figure 1 shows a screen shot of the sample; to truly appreciate the images, you must watch the screensaver morph over time.

Based on the Direct3D "ScreenSaver" sample, the MSDNSparkles screensaver marries a particle system with some random magic; when this is combined with alpha blending, mesmerizing images result. This is an example of additive blending using ONE:ONE operators, which results in some really interesting glowing effects. **

In this column, I will briefly cover the Direct3D screensaver sample framework, then I'll dive into the details of how MSDNSparkles does its magic. I won't cover the details of writing a screensaver for Win32, but you can search on MSDN for articles that discuss this topic.

First Word

The Direct3D screensaver framework consists of two parts: the skeleton and the app-provided functions that are overridden by each screensaver. Figure 2 shows the screensaver sample project view from the SDK. Screensaver.cpp provides a skeleton that enables D3DFrame screensavers, such as d3dapp.cpp, which, in turn, provides the skeleton for D3DFrame applications. Simply provide the overridden functions and you have a screensaver. Remember that to test screensavers, you need to launch them from Visual C++ with a command line parameter of –s.

Figure 2. Direct3D screensaver sample project view

The skeleton uses functions similar to those D3DFrame applications use to perform initialization, shutdown, and rendering. The prototypes for these functions—Initialize3DEnvironment, Cleanup3DEnvironment, and Render3DEnvironment—are shown below.

//--------------------------------------------------------------------------
// Local function-prototypes
//--------------------------------------------------------------------------
HRESULT Initialize3DEnvironment( HWND );
HRESULT Render3DEnvironment( HWND );
VOID    Cleanup3DEnvironment( HWND );

These three functions handle all the work of setting up Direct3D, shutting down Direct3D, and rendering the scene. All the screensaver has to do is to provide implementations for the functions that can be overridden. These functions are similar to the D3Dframe counterparts we have come to know and love; see the function prototypes below.

//--------------------------------------------------------------------------
// External function-prototypes
//--------------------------------------------------------------------------
HRESULT App_ConfirmDevice( DDCAPS*, D3DDEVICEDESC* );
HRESULT App_OneTimeSceneInit();
VOID    App_DeleteDeviceObjects( HWND, LPDIRECT3DDEVICE7 );
HRESULT App_InitDeviceObjects( HWND, LPDIRECT3DDEVICE7 );
HRESULT App_FrameMove( LPDIRECT3DDEVICE7, FLOAT );
HRESULT App_Render( LPDIRECT3DDEVICE7 );
HRESULT App_RestoreSurfaces();
HRESULT App_FinalCleanup();

I use screensaver.cpp from the sample with minimal changes. I did find that the call to read the configuration data from the registry needed to swap parameters from:

      if( ERROR_SUCCESS == RegOpenKeyEx( HKEY_CURRENT_USER, strRegPath,
                                           KEY_READ, NULL, &hKey ) )

to:

      if( ERROR_SUCCESS == RegOpenKeyEx( HKEY_CURRENT_USER, strRegPath,
                                           NULL, KEY_READ, &hKey ) )

The sample also fails to deal with potential movement of the rendering window, so I added the following block to the Render3Denvironment() function right after the call to TestCooperativeLevel().

   //make sure we work in scr control panel monitor thingy when it moves
   RECT rTmp;
   GetWindowRect( hWnd, &rTmp );
      g_pFramework->Move(rTmp.left, rTmp.top );

The Direct3D screensaver framework does provide the beginnings of a ConfigureDialog function and a method for storing configuration data, but I haven't extended it for MSDNSparkles, as it isn't a key detail.

One other "feature" of the sample screensaver is its use of WM_TIMER to control rendering. On Windows 2000-based operating systems, this results in a reasonable frame rate, but Windows9x-based operating systems don't have a WM_TIMER granularity that allows a good frame rate. With that in mind, I rewrote the screensaver to use a thread to control rendering. I don't cover that code, because it's a straightforward thread implementation. The ambitious programmer could consider adding multi-monitor capability, power management, and passwords, because the Direct3D screensaver framework doesn't provide this functionality.

High-Level View

With those details out of the way, let's get started. Figure 3 shows the contents of the Visual Studio Workspace window for the MSDNSparkles sample. The project consists of the screensaver.cpp file, which is derived from the Direct3D sample as discussed above, and sparkles.cpp.

Figure 3. MSDNSparkles project view

Sparkles.cpp contains two sets of functions. The first set is the overridable functions, which I will examine later. The second set contains the sparkles functions. MSDNSparkles defines a mini-API of its own for dealing with the sparkles functions, which are RandomTexture, RandomSparkle, InitSparkles, UpdateSparkle, and DrawSparkle.

// sparkle functions 
Int RandomTexture(int overflow);
Sparkle RandomSparkle(void);
Void InitSparkles(void);
Void UpdateSparkles(void);
BOOL DrawSparkles(LPDIRECT3DDEVICE7 lpDev, D3DVECTOR from, D3DVECTOR at);

All of these functions, except RandomTexture, operate on the Sparkle structure. The Sparkle structure contains information used for both the appearance and behavior of each particle as part of the particle system.

//sparkle struct
typedef struct t_sparkle {
   int         texture;
   int         age, cur_age;  // start at age and tick down to 0
   float       scale, delta_scale;
   D3DVECTOR   position;
   D3DVECTOR   velocity;
   D3DVECTOR   color;
} Sparkle;

Element position and velocity, along with scale and delta_scale, are used by the particle system for generating particle locations. Element texture and color define particle appearance as part of the appearance-randomizing algorithm.

Appearance randomizing controls transitions by using an aging system, where element age and cur_age provide a countdown system. The transitions happen in three ways:

  • A texture from a list is randomly selected when the texture age expires.
  • color_mode is used to generate a base color.
  • color_modifier_mode is used to modify the base color in some interesting ways.

These random textures and colors then completely define the particle appearance. It's pretty amazing what some clever random math can generate.

RandomTexture helps with selecting a texture from the texture list.

int RandomTexture(int texture, int overflow)
{
      int retVal;

      if (texture == NumTextures) 
      {
            retVal = overflow;// init to random from the n case, overflow
      } 
      else 
      {
            retVal = texture; // init to current in the 0..n-1 case
      }
      return retVal;
}

RandomSparkle starts by generating some color delta values. It then goes about populating a Sparkles structure that represents a particle with age, scale, and position values. The texture for the sparkle is selected by invoking RandomTexture. Then color_mode and color_modifier_mode are used to generate a color for the particle.

Color_mode determines which of two methods, random mode or rgb bounce mode, are used to generate the base color. Random mode just randomly picks a color. RGB bounce mode does some clever color delta calculations to migrate smoothly through a color range. Color_modifier_mode controls modification of the base color. Four variants can be selected: full saturation, jewel tones, pastels, or brighter. Each of these modes performs a slightly different calculation to modify the base color.

Sparkle RandomSparkle(void)
{
      Sparkle     ret;
      static float      red = 1.0f, grn = 1.0f, blu = 1.0f;
      static float      d_red = -(min_color_delta + rnd()*max_color_delta);
      static float      d_grn = -(min_color_delta + rnd()*max_color_delta);
      static float      d_blu = -(min_color_delta + rnd()*max_color_delta);

      ret.age           = min_age + (int)(rnd() * (max_age-min_age));
      ret.cur_age       = ret.age;
      ret.scale         = start_scale;
      ret.delta_scale   = min_delta + rnd() * (max_delta - min_delta);
      ret.position      = D3DVECTOR(world_size * (rnd()-rnd()), 
                                    world_size * (rnd()-rnd()), 
                                    world_size * (rnd()-rnd()));
      ret.velocity      = D3DVECTOR(0.0f);
      ret.texture       = RandomTexture(rand() % (NumTextures-1));

      switch (color_mode) {
            case 0 : //random
                  ret.color = D3DVECTOR(rnd(), rnd(), rnd());
                  break;
            case 1 : //rgb bounce
                  red += d_red;
                  if (red > 1.0f) {
                        red = 1.0f;
                        d_red = -(min_color_delta + rnd()*max_color_delta);
                  } else if (red < 0.0f) {
                        red = 0.0f;
                        d_red = min_color_delta + rnd()*max_color_delta;
                  }
                  grn += d_grn;
                  if (grn > 1.0f) {
                        grn = 1.0f;
                        d_grn = -(min_color_delta + rnd()*max_color_delta);
                  } else if (grn < 0.0f) {
                        grn = 0.0f;
                        d_grn = min_color_delta + rnd()*max_color_delta;
                  }
                  blu += d_blu;
                  if (blu > 1.0f) {
                        blu = 1.0f;
                        d_blu = -(min_color_delta + rnd()*max_color_delta);
                  } else if (blu < 0.0f) {
                        blu = 0.0f;
                        d_blu = min_color_delta + rnd()*max_color_delta;
                  }

                  ret.color = D3DVECTOR(red, grn, blu);
                  break;
            default :
                  ret.color = D3DVECTOR(0.0f, 0.5f, 1.0f);
                  break;
      }

      switch (color_modifier_mode) {
            case 0 :    // no change
                  break;
            case 1 :    // full saturation
                  ret.color /= Max(ret.color);
                  break;
            case 2 :    // jewel tones
                  ret.color -= Min(ret.color);
                  ret.color /= Max(ret.color);
                  break;
            case 3 :    // pastels
                  ret.color -= Min(ret.color);
                  ret.color /= Max(ret.color);
                  ret.color = D3DVECTOR(0.6f) + 0.4f * ret.color;
                  break;
            case 4 :    // brighter, cool and could be for the masses
                  ret.color *= 1.2f;
                  break;
            default :
                  break;
      }

      return ret;
}

InitSparkles picks a starting texture, then allocates memory for the particle list. A particle is essentially a two-triangle quad. See Figure 4 for an illustration of a particle.

Figure 4. A particle

Next, each sparkle is randomly initialized using RandomSparkle. Finally, the indices for indexed drawing are generated for each particle.

void InitSparkles(void)
{ 
   texture = 1;// start with dx7 bitmap
   sparkle = (Sparkle *)malloc(nMaxNumSparkles * sizeof(Sparkle));
   for (UINT i=0; i<nCurNumSparkles; i++) {
      sparkle[i] = RandomSparkle();
   }

   // set up indices
   for (i=0; i<nMaxNumSparkles; i++) {
      s_indices[i*6+0] = 4*i + 0;
      s_indices[i*6+1] = 4*i + 1;
      s_indices[i*6+2] = 4*i + 2;
      s_indices[i*6+3] = 4*i + 0;
      s_indices[i*6+4] = 4*i + 2;
      s_indices[i*6+5] = 4*i + 3;
   }
}  // end of InitSparkles()

UpdateSparkles decrements the current values for texture_age, color_age, and color_modifier_age. Then, if the age value has counted down to zero, new random values for texture (using RandomTexture), color_mode, and color_modifier_mode are generated. Once this is done, RandomSparkle is used again to randomly generate positions, textures, and colors for the next frame for each particle. Finally, the scale is adjusted.

void UpdateSparkles(void)
{
   UINT  i;
//0..n 0..n-1==current, n==random
   texture_age--;
   if (texture_age == 0) {
      texture_age = min_texture_age + (unsigned int)(rnd() * 
(max_texture_age - min_texture_age));
      texture = rand() % (NumTextures);
      texture = RandomTexture(rand() % (NumTextures-1));
   }
//0..1 0==random, 1==rgb bounce
   color_age--;
   if (color_age == 0) {
      color_age = min_color_age + (int)(rnd()*(max_color_age –
 min_color_age));
      color_mode = rand() % NumColorModes;
   }
//0..5 0==no change, 1==full saturation, 2==jewel tones, 3==pastels,
    //     4==brighter, 5==darker, cool but not for the masses, 
   color_modifier_age--;
   if (color_modifier_age == 0) {
      color_modifier_age = min_color_modifier_age + (int)(rnd()* 
(max_color_modifier_age - min_color_modifier_age));
      color_modifier_mode = rand() % NumColorModifierModes;
   }
   //update sparkles
   for (i=0; i<nCurNumSparkles; i++) {
      sparkle[i].cur_age--;
      if (sparkle[i].cur_age == 0) {
         sparkle[i] = RandomSparkle();
      }
      sparkle[i].scale *= sparkle[i].delta_scale;
   }

}  // end of UpdateSparkles()

DrawSparkles calculates a front-facing direction used in the particle system positioning code. This value is used to generate the offsets for the vertices for each particle. Figure 5 illustrates this.

Figure 5. Particle positioning

For efficiency, sparkles are sorted by texture for drawing. Indexed lists and DrawIndexedPrimitive are used for drawing the particle system quads.

BOOL DrawSparkles(LPDIRECT3DDEVICE7 lpDev, D3DVECTOR from, D3DVECTOR at)
{
      D3DVECTOR   view_dir, position, dx, dy;
      UINT        i;

      view_dir = Normalize(at - from);
      dx = CrossProduct(view_dir, D3DVECTOR(0.0f, 1.0f, 0.0f));
      dy = CrossProduct(view_dir, dx);
      dx = CrossProduct(view_dir, dy);

      // draw the sparkles
      // in order to be more efficient we want to batch up all the sparkles
      // that use the same texture and just do a single DrawPrim call
      int   flags[NumTextures];
      for (int tex=0; tex<NumTextures; tex++) {
            flags[tex] = 0;
      }
      // figure out which textures are being used
      for (i=0; i<nCurNumSparkles; i++) {
            flags[sparkle[i].texture]++;
      }
      // for each texture that is used, batch up the sparkles and draw them
      for (tex=0; tex<NumTextures; tex++) {
            if (flags[tex] == 0)
                  continue;

            // set the right material/texture combo 
            lpDev->SetTexture(0,g_ptexSparkleTextures[tex]);
            
            //cons up the quads for the batch
            int   num = 0;
            for (i=0; i<nCurNumSparkles; i++) {
                  if (sparkle[i].texture != tex)
                        continue;

                  D3DVECTOR   sx  = dx * sparkle[i].scale;
                  D3DVECTOR   sy  = dy * sparkle[i].scale;
                  float       color_scale = (float)sparkle[i].cur_age /
                                     sparkle[i].age;
                  D3DVECTOR   cur_color = sparkle[i].color * color_scale;
                  D3DCOLOR    color = D3DRGB(cur_color[0], cur_color[1], 
                                     cur_color[2]);
                  position = sparkle[i].position;
                  s_mesh[num*4+0] = D3DLVERTEX(position+sx+sy, color, 0, 
                                                 1.0f, 1.0f);
                  s_mesh[num*4+1] = D3DLVERTEX(position-sx+sy, color, 0,
                                                 0.0f, 1.0f);
                  s_mesh[num*4+2] = D3DLVERTEX(position-sx-sy, color, 0,
                                                 0.0f, 0.0f);
                  s_mesh[num*4+3] = D3DLVERTEX(position+sx-sy, color, 0,
                                                 1.0f, 0.0f);
                  num++;
            }
            // done creating the batch, now render it
            if (lpDev->DrawIndexedPrimitive(D3DPT_TRIANGLELIST,
                                           D3DFVF_LVERTEX, (LPVOID)s_mesh,
                                           4*flags[tex], s_indices,
                                           6*flags[tex], 0) != D3D_OK)
                  return FALSE;
      }
      return TRUE;
}     // end of DrawSparkles()

Figures 6 and 7 show additional screen shots of the visuals that result from the alpha-blended particle system.

screen shot of screensaver

Figure 6. MSDNSparkles screen shot number 2

screen shot of screensaver

Figure 7. MSDNSparkles screen shot number 3

MSDNSparkles Internals

Let's move on to the implementation of the overloaded application functions. I'm repeating the function prototypes below to save you from scrolling up to the beginning of the article. I'll examine each of the overloaded functions that the Direct3D screensaver framework allows to be overridden.

//--------------------------------------------------------------------------
// External function-prototypes
//--------------------------------------------------------------------------
HRESULT App_ConfirmDevice( DDCAPS*, D3DDEVICEDESC* );
HRESULT App_OneTimeSceneInit();
VOID    App_DeleteDeviceObjects( HWND, LPDIRECT3DDEVICE7 );
HRESULT App_InitDeviceObjects( HWND, LPDIRECT3DDEVICE7 );
HRESULT App_FrameMove( LPDIRECT3DDEVICE7, FLOAT );
HRESULT App_Render( LPDIRECT3DDEVICE7 );
HRESULT App_RestoreSurfaces();
HRESULT App_FinalCleanup();

ConfirmDevice

The MSDNSparkles App_ConfirmDevice function verifies that the device can do ONE:ONE alpha blending. This is the simplest form of alpha—and, in my experience, all cards can perform this alpha blending. Below is the MSDNSparkles implementation of App_ConfirmDevice.

HRESULT App_ConfirmDevice( DDCAPS* pddDriverCaps,
                           D3DDEVICEDESC7* pd3dDeviceDesc )
{
   // get triangle caps (Hardware or software) and check for alpha blending
   LPD3DPRIMCAPS pdpc = &pd3dDeviceDesc->dpcTriCaps;

   if( 0 == ( pdpc->dwSrcBlendCaps & pdpc->dwDestBlendCaps & D3DBLEND_ONE ) )
      return E_FAIL;

    return S_OK;
} 

OneTimeSceneInit

The App_OneTimeSceneInit function first seeds the random number generator, sets the background color, and initializes the texture list. Then, using the texture list, it creates the textures that MSDNSparkles uses. Finally, InitSparkles is called to initialize the particle system.

HRESULT App_OneTimeSceneInit()
{
   // seed the random number generator
   srand(time(0));

   //init the background color
   bckColor = D3DRGB(0,0,0);

   //load texture data
   memcpy(g_szSparkleTextures[0],"dx5.bmp",sizeof("dx5.bmp"));
   memcpy(g_szSparkleTextures[1],"dx7.bmp",sizeof("dx7.bmp"));
   memcpy(g_szSparkleTextures[2],"flare1.bmp",sizeof("flare1.bmp"));
   memcpy(g_szSparkleTextures[3],"flare2.bmp",sizeof("flare2.bmp"));
   memcpy(g_szSparkleTextures[4],"flare3.bmp",sizeof("flare3.bmp"));
   memcpy(g_szSparkleTextures[5],"flare4.bmp",sizeof("flare5.bmp"));
   memcpy(g_szSparkleTextures[6],"flare5.bmp",sizeof("flare5.bmp"));
   memcpy(g_szSparkleTextures[7],"flare6.bmp",sizeof("flare6.bmp"));
   memcpy(g_szSparkleTextures[8],"flare7.bmp",sizeof("flare7.bmp"));
   memcpy(g_szSparkleTextures[9],"flare8.bmp",sizeof("flare8.bmp"));
   memcpy(g_szSparkleTextures[10],"shine1.bmp",sizeof("flare1.bmp"));
   memcpy(g_szSparkleTextures[11],"shine2.bmp",sizeof("flare2.bmp"));
   memcpy(g_szSparkleTextures[12],"shine3.bmp",sizeof("flare3.bmp"));
   memcpy(g_szSparkleTextures[13],"shine4.bmp",sizeof("flare5.bmp"));
   memcpy(g_szSparkleTextures[14],"shine5.bmp",sizeof("flare5.bmp"));
   memcpy(g_szSparkleTextures[15],"shine6.bmp",sizeof("flare6.bmp"));

   for ( int i = 0; i < NumTextures; i++)
      D3DTextr_CreateTextureFromFile( (char *)g_szSparkleTextures[i] );

   InitSparkles();

   return S_OK;
}

InitDeviceObjects

The App_InitDeviceObjects function uses helper functions SetTextureState, SetRenderState, and SetViewState. These set up the texture list, the render states that the app uses, and the viewing system, respectively. Note that alpha blending is enabled, and the blend states are set to ONE:ONE for additive blending.

void SetTextureState(LPDIRECT3DDEVICE7 pd3dDevice )
{
   // set up texture states   
    D3DTextr_RestoreAllTextures( pd3dDevice );
   // load texture surfaces
   for( int i=0; i<NumTextures; i++ )
      g_ptexSparkleTextures[i] = D3DTextr_GetSurface( (char *)
                                     g_szSparkleTextures[i]);
}

void SetRenderState(LPDIRECT3DDEVICE7 pd3dDevice )
{
   // alphablend states
   pd3dDevice->SetRenderState(D3DRENDERSTATE_ALPHABLENDENABLE, TRUE);   
   pd3dDevice->SetRenderState(D3DRENDERSTATE_SRCBLEND,  srcBlend);
   pd3dDevice->SetRenderState(D3DRENDERSTATE_DESTBLEND, dstBlend);

   // filter states
   pd3dDevice->SetTextureStageState(0,D3DTSS_MINFILTER, D3DFILTER_LINEAR);
   pd3dDevice->SetTextureStageState(0,D3DTSS_MAGFILTER, D3DFILTER_LINEAR);
   pd3dDevice->SetTextureStageState(0,D3DTSS_MIPFILTER, D3DFILTER_LINEAR);

   // set up non-texture render states
   pd3dDevice->SetRenderState(D3DRENDERSTATE_DITHERENABLE, FALSE);
   pd3dDevice->SetRenderState(D3DRENDERSTATE_SPECULARENABLE, FALSE);
   pd3dDevice->SetRenderState(D3DRENDERSTATE_ZWRITEENABLE, FALSE);
   pd3dDevice->SetRenderState(D3DRENDERSTATE_ZENABLE, FALSE);
   pd3dDevice->SetRenderState(D3DRENDERSTATE_CULLMODE, D3DCULL_NONE);

   // note: in DX7, setting D3DRENDERSTATE_LIGHTING to FALSE is needed to 
   // turn off vertex lighting (and use the color in the D3DLVERTEX instead)
   pd3dDevice->SetRenderState( D3DRENDERSTATE_LIGHTING, FALSE );

}

void SetViewState(LPDIRECT3DDEVICE7 pd3dDevice )
{
   // get the aspect ratio
   pd3dDevice->GetViewport(&vp);
   FLOAT fAspect = ((FLOAT)vp.dwHeight) / vp.dwWidth;

   // set up transform matrices
   D3DUtil_SetProjectionMatrix( proj, 
                         g_PI/4,//1.0f;
                         fAspect, 
                         1.0f, MAX_DIST );
   pd3dDevice->SetTransform( D3DTRANSFORMSTATE_PROJECTION, &proj);
   D3DUtil_SetViewMatrix( view, from, at, up );
   pd3dDevice->SetTransform( D3DTRANSFORMSTATE_VIEW, &view);
   D3DUtil_SetIdentityMatrix( world );
   pd3dDevice->SetTransform( D3DTRANSFORMSTATE_WORLD, &world);
}

HRESULT App_InitDeviceObjects( HWND hWnd, LPDIRECT3DDEVICE7 pd3dDevice )
{
    // check parameters
    if( NULL==pd3dDevice )
        return E_INVALIDARG;

   // set up texture states   
    SetTextureState( pd3dDevice );

   // set up renderstates
    SetRenderState( pd3dDevice );
   
   // set up view system
    SetViewState( pd3dDevice );

    return S_OK;
}

FrameMove

The App_FrameMove function uses UpdateSparkles to update the particle system simulation.

HRESULT App_FrameMove( LPDIRECT3DDEVICE7 pd3dDevice, FLOAT fTimeKey )
{
   // okay, now play with sparkles
   UpdateSparkles();
   
    return S_OK;
}

Render

The App_Render function is pretty simple. First, we use time and RandomViewpoint to modify the viewpoint over time. Then we let DrawSparkles draw the particle system. Too easy!

void RandomViewpoint(LPDIRECT3DDEVICE7 pd3dDevice,float tic)
{
   float fromX, fromY, fromZ;

   fromX = (float)sin(tic*0.59);
   fromZ = (float)cos(tic*0.59);
   if ( texture <= 9 )
      fromY = (float)sin(tic*0.72);
   else
      fromY = (float)cos(tic*0.72);
   from = D3DVECTOR(orbit_size*fromX, 
             orbit_size*fromY, 
              orbit_size*fromZ );
   D3DUtil_SetViewMatrix( view, from, at, up );
   pd3dDevice->SetTransform( D3DTRANSFORMSTATE_VIEW, &view);
}

HRESULT App_Render( LPDIRECT3DDEVICE7 pd3dDevice )
{
   static float      tic = -rnd() * 10000.0f;

   // clear the viewport
   pd3dDevice->Clear( 0UL, NULL, D3DCLEAR_TARGET, bckColor, 1.0f, 0L );

   // begin the scene
   if( FAILED( pd3dDevice->BeginScene() ) )
      return S_OK; // Don't return a "fatal" error
   
   // tic to move things around
   tic += 0.005f;

   // used in random positioning
   start_scale = 0.05f + (float)(sin(tic * 0.100) + 1.0f)*0.4f;
   world_size  = 0.10f + (float)(cos(tic * 0.072) + 1.0f)*10.0f;

   // modify viewpoint 
   RandomViewpoint(pd3dDevice,tic);

   // draw sparkles
   if (!DrawSparkles(pd3dDevice, from, at)) 
      return E_FAIL;
 
   // end the scene
   pd3dDevice->EndScene();

   return S_OK;
}

DeleteDeviceObjects

The ** App_DeleteDeviceObjects ** function invalidates the textures from the texture list.

VOID App_DeleteDeviceObjects( HWND hWnd, LPDIRECT3DDEVICE7 pd3dDevice)
{
    D3DTextr_InvalidateAllTextures();
}

RestoreSurfaces

For the MSDNSparkles sample, App_RestoreSurfaces is a no-op.

HRESULT App_RestoreSurfaces()
{  
    return S_OK;
}

FinalCleanup

The App_FinalCleanup function deletes the memory used by the sparkle list.

HRESULT App_FinalCleanup()
{
   free(sparkle);

    return S_OK;
}

That finishes off the implementation of MSDNSparkles. When you build the project, you can test, configure, and install the resulting screensaver by right-clicking the MSDNSparkles.scr file. Figure 6 illustrates this. The result is basic but somewhat pleasing.

Figure 8. Right-click to test, configure, and install screensavers

Last Word

Your feedback is welcome. Feel free to drop me a line, via the address below, with your comments, questions, topic ideas, or links to your own variations on topics the column covers. Please, though, don't expect an individual reply or send me support questions. Remember, Microsoft maintains an active mailing list, DirectXDev, as a forum for like-minded developers to share information. The Web interface is found at https://DISCUSS.MICROSOFT.COM/archives/DIRECTXDEV.html. Be sure to read Microsoft DirectX 7 Developer FAQ before asking questions.

After evangelizing DirectX for the past four years, Philip Taylor has moved on and is now the DirectX SDK PM. He has been working with DirectX since the first public beta of DirectX 1, and, once upon a time, actually shipped DirectX 2 games. In his spare time, he can be found lurking on many 3-D graphics programming mailing lists. You can reach him at msdn@microsoft.com.