www.xbdev.net
xbdev - software development
Tuesday April 1, 2025
Home | Contact | Support | DirectX.. Its doing all the hard work for us... | DirectX.. Its doing all the hard work for us...
     
 

DirectX..

Its doing all the hard work for us...

 

3D Water Drip!

 

(Tutorial Under Construction)

 

One of my favourite special effects you can generate with DirectX 3D is 3D water.....again there are loads of ways of doing it...some ways look better than others, and other ways are more optimised than others - but hopefully this tutorial will show you a simple but brilliant way of doing a nice 3D water scene.  We'll start from the very beginning, as it helps explain why I'm doing certain things and then when the code is massive...you'll understand it in greater detail.

 

 

So lets start simple shall we...dont' want to get all messy and stuff straight away...so we'll define our class which will be our 3D water class...

 

class CWaterPlane

{

protected:

      IDirect3DDevice8* m_pD3DDevice;

      int               m_iGridDensity;

public:

      bool Create(IDirect3DDevice8** pDevice, int iGridDensity)

      {

            m_pD3DDevice = *pDevice;

            m_iGridDensity = iGridDensity;

 

            return true;

      }

 

      void Release()

      {

      }

 

      void Render()

      {

      }

};

 

Now I've put the basic functions and variables in...ones that I know you'll need.  As when you create your 3D Water Plane, you'll have to pass in its griddensity, which will be sort of like the quality of your 3D effect, and the pointer to you DirectX screen device.  And if you have a Create(..) function, I always like a Release(..) function as well, but feel free to use your own names if you want.

 

Then, we'll want to render our 3D water, so we have to add in a Render(..) function, which we'll call each time we render our scene().

 

This is it...it doesn't look so complicated...well it really isn't when you break it down and build it up yourself.  Remember no matter how complex the code gets, its your code!  You can break it..hehe...and dont' be scared to change a few things...the worse that could happen is your pc crashes....then you just reboot and carry on :)

 

But we compile that code above and call it from our DirectX Render code and what do we get?  Well nothing....as its just the starting point...but hold on...its about to grow!

 

One giant leap for coding...is adding in the vertex/index plane which will represent our water suface.  It will consist of an array of vertices, and an array of index values into it.  All this magic will be done in the CreateVertexGrid(..) function which we'll add in now.

 

 

ScreenShot of the code from below that has been compiled, and shows the 3D Surface rotating... again this will be our 3D Water Surface, which we'll create some little splashes in later on.  But so you can make out what it is that where doing I've rendered it as a wire frame.

 

Now that you have this array of vertice's, its just a matter of moving them around to create our water effect - so be sure you know how the points are created...try moving some of them around....

 

 

Man has our little CWaterPlane glass grown up quickly.... I've now added in a nice flat plane that we can play with in a bit to create that water effect.  Its only a single colour at the moment....later on we'll add a texture to it...and normals etc.... but those parts are easy.  You first need to understand what is happening :)

 

DownloadSourceCode
 

class CWaterPlane

{

protected:

      struct VERTEX_XYZ_DIFFUSE

      {

      D3DXVECTOR3 position; // The position

      D3DCOLOR    color;    // The color

      };

 

      // Our Flexible Vertex Format Desription.

      DWORD D3DFVF_XYZ_DIFFUSE; // = D3DFVF_XYZ|D3DFVF_DIFFUSE;

 

protected:

      IDirect3DDevice8* m_pD3DDevice;   // DirectX Screen Object

      int               m_iGridDensity; // Water Surface Parts

 

      // Water Surface Buffers

      IDirect3DVertexBuffer8* m_pVertexBuffer;

      IDirect3DIndexBuffer8*  m_pIndexBuffer;

 

public:

      // Called once when we create our water plane.

      bool Create(IDirect3DDevice8** pDevice, int iGridDensity)

      {

            m_pD3DDevice = *pDevice;

            m_iGridDensity = iGridDensity;

 

            // SetUp Our Grid Vertices which will represent our 3D water surface.

            D3DFVF_XYZ_DIFFUSE = D3DFVF_XYZ|D3DFVF_DIFFUSE;

            // Create our Water Surface.

            CreateVertexGrid(&m_pVertexBuffer, &m_pIndexBuffer,

                         1.0f,        // The size of our water plane (width=height=fTotalSize)

                                     0xffff0000, // Simple Red Colour

                                     m_iGridDensity, m_iGridDensity);

            return true;

      }//End of Create Method

 

      void Release()

      {

            m_pVertexBuffer->Release();

            m_pIndexBuffer->Release();

      }//End of Release Method

 

      // We call render to render our shape to the screen.

      void Render()

      {

            int iNumVerts = m_iGridDensity*m_iGridDensity;

 

            m_pD3DDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);

            m_pD3DDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE);

 

            m_pD3DDevice->SetStreamSource( 0, m_pVertexBuffer, sizeof(VERTEX_XYZ_DIFFUSE) );

            m_pD3DDevice->SetVertexShader(D3DFVF_XYZ_DIFFUSE);

 

            m_pD3DDevice->SetIndices( m_pIndexBuffer, 0L );

 

            // Well at this stage is almost impossible to see what you have, if you

            // render the triangles as solid, all you see is a flat square surface...

            // so I added a little option here, so you can switch on wire mesh view

            // which displays our triangles as lines...

            // Draw as a wire mesh or a solid fill

            if( false ) // Wire Mesh

            {

                  m_pD3DDevice->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0,

                                                                        iNumVerts,  // Number of points

                                                                        0, // Starting Triangle

                                                                        iNumVerts*2); // Number of Triangles

            }

            else       // Sold Fill

            {

                  m_pD3DDevice->DrawIndexedPrimitive( D3DPT_LINELIST, 0,

                                                                        iNumVerts,  // Number of points

                                                                        0, // Starting Triangle

                                                                        iNumVerts*3); // Number of Triangles

            }

 

 

      }//End of Render Method

 

     

      // This function creates our surface that will represent our water surface, its

      // made up of a grid of vertices and index pointers into that grid.

      bool CreateVertexGrid(IDirect3DVertexBuffer8** pVB, IDirect3DIndexBuffer8 **pIB,

                         float fTotalSize, DWORD dwColor, int iNumVerticesX, int iNumVerticesY)

      {

 

            //[1] - CREATE VERTEX BUFFER

            // create and fill vertex buffer

            m_pD3DDevice->CreateVertexBuffer(iNumVerticesX*iNumVerticesY*sizeof(VERTEX_XYZ_DIFFUSE),

                                                            0, D3DFVF_XYZ_DIFFUSE, D3DPOOL_MANAGED, pVB);

 

            VERTEX_XYZ_DIFFUSE *pVertices;

            float fSizeDiv2 = fTotalSize/2;

 

            (*pVB)->Lock( 0, iNumVerticesX*iNumVerticesY*sizeof(VERTEX_XYZ_DIFFUSE), (BYTE**)&pVertices, 0 );

 

            for (int x=0; x < iNumVerticesX; x++) {

                  for (int y=0; y < iNumVerticesY; y++) {

                        pVertices[(y*iNumVerticesX)+x].position = D3DXVECTOR3(

                        (iNumVerticesX > 1) ? (((float)x/(float)(iNumVerticesX-1))*fTotalSize)-fSizeDiv2 : 0,

                        0.0f,

                        (iNumVerticesY > 1) ? (((float)(iNumVerticesY-1-y)/(float)(iNumVerticesY-1))*fTotalSize)-fSizeDiv2 : 0);

 

                        pVertices[(y*iNumVerticesX)+x].color    = dwColor;

                  }

            }

            (*pVB)->Unlock();

 

            //[2] - CREATE INDEX BUFFER INTO OUR CREATED VERTEX BUFFER ABOVE

            // create index buffer

            m_pD3DDevice->CreateIndexBuffer(

                        iNumVerticesX*iNumVerticesY*2*3*2, // *2 (tris) *3 (indicies per tri) * 2 (bytes per index)

                        D3DUSAGE_WRITEONLY, D3DFMT_INDEX16,D3DPOOL_MANAGED, pIB);

 

            // lock and fill index buffer

            WORD *pIndices;

            (*pIB)->Lock(0, iNumVerticesX*iNumVerticesY*2*3*2, (unsigned char **)&pIndices, 0);

             

            WORD *pIndex = pIndices;

            for (int x=0; x < iNumVerticesX-1; x++) {

                  for (int y=0; y < iNumVerticesY-1; y++) {

                        // first triangle

                        *(pIndex++) = ((y)*iNumVerticesX)+x;

                        *(pIndex++) = ((y)*iNumVerticesX)+x+1;

                        *(pIndex++) = ((y+1)*iNumVerticesX)+x+1;

 

                        // second triangle

                        *(pIndex++) = ((y)*iNumVerticesX)+x;

                        *(pIndex++) = ((y+1)*iNumVerticesX)+x+1;

                        *(pIndex++) = ((y+1)*iNumVerticesX)+x;

                  }

            }

 

            (*pIB)->Unlock();

           

            return true;

      }//End of CreateVertexGrid Method

     

};

 

 

 

 

 

Now to bring movement into our little class, we'll need to add in the Update(..) function, which will be called before we render it, and will make any necessary changes to our 3D water surface.

 

 

 

 

Still Screen Capture

 

 

 

 

 

DownloadSourceCode
 

class CWaterPlane

{

protected:

      struct VERTEX_XYZ_DIFFUSE

      {

            D3DXVECTOR3 position; // The position

            D3DCOLOR    color;    // The color

      };

 

      // Our Flexible Vertex Format Desription.

      DWORD D3DFVF_XYZ_DIFFUSE; // = D3DFVF_XYZ|D3DFVF_DIFFUSE;

 

protected:

      IDirect3DDevice8* m_pD3DDevice;   // DirectX Screen Object

      int               m_iGridDensity; // Water Surface Parts

 

      // Water Surface Buffers

      IDirect3DVertexBuffer8* m_pVertexBuffer;

      IDirect3DIndexBuffer8*  m_pIndexBuffer;

 

      // Surface hights

      float *m_pActiveHeightArray;

      float *m_pScratchHeightArray;

 

      // Ripples and splashes must die away, so we set a damping value.

      float m_fDampValue;

 

      // Need a value

      float m_fRefractionIndex;

 

 

public:

      // Called once when we create our water plane.

      bool Create(IDirect3DDevice8** pDevice, int iGridDensity)

      {

            //[1]

            m_pD3DDevice = *pDevice;

            m_iGridDensity = iGridDensity;

 

            // SetUp Our Grid Vertices which will represent our 3D water surface.

            D3DFVF_XYZ_DIFFUSE = D3DFVF_XYZ|D3DFVF_DIFFUSE;

 

            //[2]

            // Create our Water Surface.

            CreateVertexGrid(&m_pVertexBuffer, &m_pIndexBuffer,

                         1.0f,        // The size of our water plane (width=height=fTotalSize)

                                     0xffff0000, // Simple Red Colour

                                     m_iGridDensity, m_iGridDensity);

 

            //[3]

            // Now we need to create a 2D array that will hold our 3D surface

            // heights. - initialize height arrays

            int iNumVerts = iGridDensity*iGridDensity;

            float* pfHeightArray1 = new float[iNumVerts];

            float* pfHeightArray2 = new float[iNumVerts];

 

            memset(pfHeightArray1, 0, sizeof(float)*iNumVerts);

            memset(pfHeightArray2, 0, sizeof(float)*iNumVerts);

 

            m_pActiveHeightArray  = pfHeightArray1;

            m_pScratchHeightArray = pfHeightArray2;

 

            // Set a damping factor.

            m_fDampValue = 1.1f;

 

            m_fRefractionIndex = 1.30f;

 

            return true;

      }//End of Create(..) Method

 

      void Release()

      {

            m_pVertexBuffer->Release();

            m_pIndexBuffer->Release();

 

            delete[] m_pActiveHeightArray;

            delete[] m_pScratchHeightArray;

      }//End of Release() Method

 

      // We call render to render our shape to the screen.

      void Render()

      {

            int iNumVerts = m_iGridDensity*m_iGridDensity;

 

            m_pD3DDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);

            m_pD3DDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE);

 

            m_pD3DDevice->SetStreamSource( 0, m_pVertexBuffer, sizeof(VERTEX_XYZ_DIFFUSE) );

            m_pD3DDevice->SetVertexShader(D3DFVF_XYZ_DIFFUSE);

 

            m_pD3DDevice->SetIndices( m_pIndexBuffer, 0L );

 

            // Well at this stage is almost impossible to see what you have, if you

            // render the triangles as solid, all you see is a flat square surface...

            // so I added a little option here, so you can switch on wire mesh view

            // which displays our triangles as lines...

            // Draw as a wire mesh or a solid fill

            if( false ) // Wire Mesh

            {

                  m_pD3DDevice->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0,

                                                                        iNumVerts,  // Number of points

                                                                        0, // Starting Triangle

                                                                        iNumVerts*2); // Number of Triangles

            }

            else       // Sold Fill

            {

                  m_pD3DDevice->DrawIndexedPrimitive( D3DPT_LINELIST, 0,

                                                                        iNumVerts,  // Number of points

                                                                        0, // Starting Triangle

                                                                        iNumVerts*3); // Number of Triangles

            }

 

 

      }//End of Render() Method

 

     

      // This function creates our surface that will represent our water surface, its

      // made up of a grid of vertices and index pointers into that grid.

      bool CreateVertexGrid(IDirect3DVertexBuffer8** pVB, IDirect3DIndexBuffer8 **pIB,

                         float fTotalSize, DWORD dwColor, int iNumVerticesX, int iNumVerticesY)

      {

 

            //[1] - CREATE VERTEX BUFFER

            // create and fill vertex buffer

            m_pD3DDevice->CreateVertexBuffer(iNumVerticesX*iNumVerticesY*sizeof(VERTEX_XYZ_DIFFUSE),

                                                            0, D3DFVF_XYZ_DIFFUSE, D3DPOOL_MANAGED, pVB);

 

            VERTEX_XYZ_DIFFUSE *pVertices;

            float fSizeDiv2 = fTotalSize/2;

 

            (*pVB)->Lock( 0, iNumVerticesX*iNumVerticesY*sizeof(VERTEX_XYZ_DIFFUSE), (BYTE**)&pVertices, 0 );

 

            for (int x=0; x < iNumVerticesX; x++) {

                  for (int y=0; y < iNumVerticesY; y++) {

                        pVertices[(y*iNumVerticesX)+x].position = D3DXVECTOR3(

                        (iNumVerticesX > 1) ? (((float)x/(float)(iNumVerticesX-1))*fTotalSize)-fSizeDiv2 : 0,

                        0.0f,

                        (iNumVerticesY > 1) ? (((float)(iNumVerticesY-1-y)/(float)(iNumVerticesY-1))*fTotalSize)-fSizeDiv2 : 0);

 

                        pVertices[(y*iNumVerticesX)+x].color    = dwColor;

                  }

            }

            (*pVB)->Unlock();

 

            //[2] - CREATE INDEX BUFFER INTO OUR CREATED VERTEX BUFFER ABOVE

            // create index buffer

            m_pD3DDevice->CreateIndexBuffer(

                        iNumVerticesX*iNumVerticesY*2*3*2, // *2 (tris) *3 (indicies per tri) * 2 (bytes per index)

                        D3DUSAGE_WRITEONLY, D3DFMT_INDEX16,D3DPOOL_MANAGED, pIB);

 

            // lock and fill index buffer

            WORD *pIndices;

            (*pIB)->Lock(0, iNumVerticesX*iNumVerticesY*2*3*2, (unsigned char **)&pIndices, 0);

             

            WORD *pIndex = pIndices;

            for (int x=0; x < iNumVerticesX-1; x++) {

                  for (int y=0; y < iNumVerticesY-1; y++) {

                        // first triangle

                        *(pIndex++) = ((y)*iNumVerticesX)+x;

                        *(pIndex++) = ((y)*iNumVerticesX)+x+1;

                        *(pIndex++) = ((y+1)*iNumVerticesX)+x+1;

 

                        // second triangle

                        *(pIndex++) = ((y)*iNumVerticesX)+x;

                        *(pIndex++) = ((y+1)*iNumVerticesX)+x+1;

                        *(pIndex++) = ((y+1)*iNumVerticesX)+x;

                  }

            }

 

            (*pIB)->Unlock();

           

            return true;

      }//End of CreateVertexGrid(..) Method

 

      //=============================Dyamic Part of our code=======================

 

      void Update(float fElapsedTime)

      {

            // For this tutorial I just added a few lines of code.. not the way it should be

            // done but introduces a small delay.  So the effect is better.

            static float delay=0.0f;

            delay+= fElapsedTime;

 

            if( delay < 1.0f)

                  return;

 

            delay = 0.0f;

     

            ProcessWater();

 

            ApplyHeightArrayToVB();

 

            // Really simple way to switch buffers, when I first did this, I thought

            // of doing a memcpy...but why do all that work when we can just switch

            // pointers!...da-da!

            // flip-flop the water buffers.

            float *temp = m_pActiveHeightArray;

            m_pActiveHeightArray = m_pScratchHeightArray;

            m_pScratchHeightArray = temp;

 

      };//End of Update(..) Method

 

 

      /****************************************************************************

      ProcessWater: this function processes our water.  It takes two input buffers,

      the water dimensions, and the cooling amount.  It calculates the new water

      values from waterfield1 and puts them into waterfield2.

      ****************************************************************************/

      void ProcessWater()

      {

            int iNumVerts = m_iGridDensity;

            // loop through all the water values...

            for (int y=0; y < iNumVerts; y++) {

                  for (int x=0; x < iNumVerts; x++) {

 

                  // add up the values of all the neighboring water values...

                  float value;

                  int xminus1 = x-1; if (xminus1 < 0) xminus1 = 0;

                  int xminus2 = x-2; if (xminus2 < 0) xminus2 = 0;

                  int yminus1 = y-1; if (yminus1 < 0) yminus1 = 0;

                  int yminus2 = y-2; if (yminus2 < 0) yminus2 = 0;

 

                  int xplus1 = x+1; if (xplus1 >= iNumVerts) xplus1 = (iNumVerts)-1;

                  int xplus2 = x+2; if (xplus2 >= iNumVerts) xplus2 = (iNumVerts)-1;

                  int yplus1 = y+1; if (yplus1 >= iNumVerts) yplus1 = (iNumVerts)-1;

                  int yplus2 = y+2; if (yplus2 >= iNumVerts) yplus2 = (iNumVerts)-1;

 

                  // Method 1: Slower but yields slightly better looking water

                  {

                   

                        value  = m_pActiveHeightArray[((y)      *iNumVerts)+xminus1];

                        value += m_pActiveHeightArray[((y)      *iNumVerts)+xminus2];

                        value += m_pActiveHeightArray[((y)      *iNumVerts)+xplus1];

                        value += m_pActiveHeightArray[((y)      *iNumVerts)+xplus2];

                        value += m_pActiveHeightArray[((yminus1)*iNumVerts)+x];

                        value += m_pActiveHeightArray[((yminus2)*iNumVerts)+x];

                        value += m_pActiveHeightArray[((yplus1) *iNumVerts)+x];

                        value += m_pActiveHeightArray[((yplus2) *iNumVerts)+x];

                        value += m_pActiveHeightArray[((yminus1)*iNumVerts)+xminus1];

                        value += m_pActiveHeightArray[((yminus1)*iNumVerts)+xplus1];

                        value += m_pActiveHeightArray[((yplus1) *iNumVerts)+xminus1];

                        value += m_pActiveHeightArray[((yplus1) *iNumVerts)+xplus1];

                 

                        // average them

                        value /= 6.0f;

                  }

 

                  // subtract the previous water value

                  value -= m_pScratchHeightArray[(y*iNumVerts)+x];

 

                  // dampen it!

                  value /= m_fDampValue;

 

                  if (value > 10.0f) value = 10.0f;

                  if (value < -10.0f) value = -10.0f;

 

                  // store it in array

                  m_pScratchHeightArray[(y*iNumVerts)+x] = value;

                  }

            }

      }//End of ProcessWater() Method

     

      // Helper Function, used by ApplyHeightArrayToVB()

      float CalcDisplacement(float fHeightdiff)

      {

            float fRefractionIndex = 1.0f;

            float fDepth = 1.0f;

 

            // the angle is the arctan of the height difference

            float angle = (float)atan(fHeightdiff);

 

            // now, calculate the angle of the refracted beam.

            float beamangle = (float)asin(sin(angle) / fRefractionIndex);

 

            // finally, calculate the displacement, based on the refracted beam

            // and the height difference.

            return(tan(beamangle) * (fHeightdiff+ fDepth));

      }//End of CalcDisplacement(..) Method

 

      void ApplyHeightArrayToVB()

      {

            // SET CONSTANT

            float fEnvBlendFactor = 1.0f;

 

            int iNumVerts = m_iGridDensity;

 

            VERTEX_XYZ_DIFFUSE *pVertices;

            float fSizeDiv2 = 0.5f;

 

            m_pVertexBuffer->Lock( 0, iNumVerts*iNumVerts*sizeof(VERTEX_XYZ_DIFFUSE),

                              (BYTE**)&pVertices, 0 );

 

            for (int x=0; x < iNumVerts; x++) {

                  for (int y=0; y < iNumVerts; y++) {

                        float fValue = m_pActiveHeightArray[(y*iNumVerts)+x];

                        if (fValue > 2.0f) fValue = 2.0f;

                       

                       

                        pVertices[(y*iNumVerts)+x].position = D3DXVECTOR3(

                              (iNumVerts > 1) ? (((float)x/(float)(iNumVerts-1)))-fSizeDiv2 : 0,

                              fValue,

                              (iNumVerts > 1) ? (((float)(iNumVerts-1-y)/(float)(iNumVerts-1)))-fSizeDiv2 : 0);

                       

 

                              pVertices[(y*iNumVerts)+x].color =

                              D3DXCOLOR(1.0f, 1.0f, 1.0f, fEnvBlendFactor);

                  }

 

            }

             

            m_pVertexBuffer->Unlock();

      }//End of ApplyHeightArrayToVB() Method

 

      void CreateWaterDroplet(int iX, int iY, int iSize,

                                     float iSplashStrength)

      {

            int iNumVerts = m_iGridDensity;

           

            for (int x=iX-iSize; x <= iX+iSize; x++) {

                  for (int y=iY-iSize; y <= iY+iSize; y++) {

                        // make sure we're in bounds

                        if (x < 0 || x >= iNumVerts || y < 0 || y >= iNumVerts) continue;

                       

                        // see if the point at (x,y) is within the circle of radius size

                        int square_x    = (x-iX)*(x-iX);

                        int square_y    = (y-iY)*(y-iY);

                        int square_size = iSize*iSize;

 

                        if (square_x+square_y <= square_size) {

                              // it's within the size circle!  apply it to the water buffer.

                              m_pActiveHeightArray[(y*iNumVerts)+(x)] += (float)iSplashStrength*sqrt((float)square_x+square_y);

                        }

                  }

            }

      }//End of CreateWaterDroplet(..) Method

};

 

 

 

Well for the final trick...we need to add texturing and normals to our 3D surface...this will make it look completely amazing.

 

 

 

 

 

 

 
Advert (Support Website)

 
 Visitor:
Copyright (c) 2002-2025 xbdev.net - All rights reserved.
Designated articles, tutorials and software are the property of their respective owners.