Tutorial 5. Terrain - Light & Vertex Normal Vector
In this tutorial I will add light and calculate Vertex Average Normal for each terrain vertex in order to achieve a smoothly shaded terrain surface. See the screenshot below.


Figure 1. Terrain with calculated average vertex normals.
 
1. Normals & Average Normals & shading
We can calculate normals per surface (used for flat shading) and normals per each vertex (used for Gouraud shading default in DirectX) as indicated below:




Figure 2. Normal calculation per surface & per vertex.

For our terrain demo we will concentrate only on calculating normals per vertex as this method is going to give us a much more realistic shading effect. See below how objects differ in appearence when the two different shading effects are applied to the same sphere.




Figure 3. Two different shading effects applied to the same sphere. Big difference?


2. Add normals to Direct3D

In order to add add normals to Direct3D we have to add normals to our vertex structure and also define in FVF (Flexible Vertex Buffer). We also have to define a pointer to Direct3D vector pointing at the vertex normals storage
1. 

	// Denfines my own custom D3D vertex format used by the vertex buffer
	struct TERRAINVERTEX
	{
		float posX, posY, posZ;         // vertex position
		float normX, normY, normZ;      // vertex normal <-- new
	};



2.

	// Untransformed vertex for unlit, untextured, Gouraud-shaded.
	#define D3DFVF_TERRAINVERTEX (D3DFVF_XYZ|D3DFVF_NORMAL)


3.

	D3DXVECTOR3 *m_pvTerrainNormals;	// Pointer to terrain normal vector storage (private member in Terrain class)

Once the correct vertex structure and normals vector storage are defined then we can start calculating terrain per vertex normals (step 4). However before we start our normal calculation (step 4) we have to make sure that we performed the following 3 steps:

/**************************************************************************************************************************
 * First method executed. Loads geometry data of models that are part of the scene
 **************************************************************************************************************************/
HRESULT CTerrain::OneTimeSceneInit()
{
    	// 1. Load Heightmap Data
	GenerateTerrainHeightmap( "terrain.bmp", 0, 0, 255 );

    	// 2. Set pointers to 0
	m_pTerrVertexBuffer = 0;	//set pointer to null
	m_pTerrIndexBuffer  = 0;	//set pointer to null
	m_pvTerrainNormals  = 0;	//set pointer to null

    	// 3. Dynamically allocate Vector Normals storage
	m_pvTerrainNormals = new D3DXVECTOR3[ m_iNumOfVerticesX * m_iNumOfVerticesZ ]; 

    	// 4. FINALLY : Calculate normals for each vertex in the whole terrain mesh
	CalculateTerrainNormals();

	return S_OK;
}

 
3. Retrieve vertex values (xyz)


The below function retrieves five nearest vertices on grid from the terrain vertex storage and passes them to CalculateNormalFromFourTriangles() function which will calculate the average normal (Gouraud shading). Once average vertex normal is calculated it is stored in m_pvTerrainNormals array which is then passed to Direct3D.


/**************************************************************************************************************************
 * Function retrieves five nearest vertices on grid from the terrain vertex storage and passes them to 
 * CalculateNormalFromFourTriangles() function which will calculate the average normal (Gouraud shading). 
 * Once normal calculated they are stored in a normal array.  
 **************************************************************************************************************************/
void CTerrain::CalculateTerrainNormals()
{
	D3DXVECTOR3 VertexA, VertexB, VertexC, VertexD, VertexE;
	D3DXVECTOR3 NormalVector;
	int iDataOffset;

	for( int z = 0; z < m_iNumOfVerticesZ; z++ )
	{
    	for( int x = 0; x < m_iNumOfVerticesX; x++ )
		{
			Get3DVertexFromTerrainAtPoint( &VertexA, x,   z+1 );
			Get3DVertexFromTerrainAtPoint( &VertexB, x-1, z   );    
			Get3DVertexFromTerrainAtPoint( &VertexC, x,   z   );
			Get3DVertexFromTerrainAtPoint( &VertexD, x+1, z   );
			Get3DVertexFromTerrainAtPoint( &VertexE, x,   z-1 );    

			CalculateNormalFromFourTriangles(&VertexA, &VertexB,&VertexC, &VertexD, &VertexE, &NormalVector);
			iDataOffset= x + ( z * m_iNumOfVerticesX );
			m_pvTerrainNormals[iDataOffset] = NormalVector;	
		}
	}
}


/**************************************************************************************************************************
 * Function retrieves a 3D vertex (xyz) at a specified point (x,z) from the terrain vertex buffer storage
 **************************************************************************************************************************/
void CTerrain::Get3DVertexFromTerrainAtPoint( D3DXVECTOR3 *pVertex, int x, int z )
{
	// make sure X & Z isn't negative value. if so set it to 0 (might get strage lighting at the terrain edges but 
	// I don't care as the viewer will not see the terrain edge anyway
    if( x < 0 ) x = 0;
    if( z < 0 ) z = 0;

	pVertex->x = (float)x; // Note not wrapped vertex ids
	pVertex->y = (float)m_pucHeightmapBmpData[ x + (z * m_iNumOfVerticesZ) ];  //get height
	pVertex->z = (float)z; // Note not wrapped vertex ids	
}

As comments in the above code suggest we have to retrieve the vertex values (xyz) from the surrounding vertices on the below grid in the order as indicated in blue colour.
Red vertex is the one for which we must calculate the average vertex normal.

Example. Find the middle vertex. See X & Z example values below The Original (XZ) Values are:

if: 
X = 1
Z = 2 

then 
Vertex A 
X = 1 
Z = Z + 1 = 3 

Vertex B 
X = X - 1 = 0 
Z = 2 

Vertex C 
X = 1 
Z = 2 

Vertex D 
X = X + 1 = 2 
Z = 2 

Vertex E 
X = 1 
Z = Z - 1 = 1 

 

 

 

4. Calculate the average vertex normal for vertex 3

Subtract vectors (vertices)

CA = A - C
(0, -2, 1) = (1,4,3) - (1,6,2)

CB = B - C
(-1,-2,0) = (0,4,2) - (1,6,2)

CD = D - C
(1,-2,0) = (2,4,2) - (1,6,2)

CE = E - C
(0,-2,-1) = (1,4,1) - (1,6,2)


Then calculate Vertex Normals - Vector Cross Product calculation




Example: (using our CA, CB, CD, CE vectors apply the above equation)

ACB = CA x CB = (0,-2,1) x (-1,-2,0)
ACBx = (-2 * 0) - (1 * -2) = 0 - (-2) = 2
ACBy = (1 * -1) - (0 * 0) = (-1) - 0 = -1
ACBz = (0 * -2) - (-2 * -1) = 0 - 2 = -2
ACB = (2, -1, -2)


That's it! One normal calculated. Do same calculation (vector cross product) for the rest of vectors ie.

BCE = CB x CE = (-1,-2,0) x (0,-2,-1)
ECD = CE x CD = (0,-2,-1) x (1,-2,0)
DCA = CD x CA = (1,-2,0) x (0,-2,1)

using the same equation (see figure 5).

/**************************************************************************************************************************
 * Function calculates normals and average normal. Adds 4 neighbouring vertex normals then divides them by 4 to get 
 * the average normal for the vertex in the middle   
 **************************************************************************************************************************/
 void CTerrain::CalculateNormalFromFourTriangles(D3DXVECTOR3* pVectorA, D3DXVECTOR3* pVectorB, D3DXVECTOR3* pVectorC, 
						 D3DXVECTOR3* pVectorD, D3DXVECTOR3* pVectorE, D3DXVECTOR3* pNormalVector)
{
		D3DXVECTOR3 VectorCA,VectorCB,VectorCD,VectorCE;
		D3DXVECTOR3 NormalACB,NormalBCE,NormalECD,NormalDCA;
		D3DXVECTOR3 NormalizedACB,NormalizedBCE,NormalizedECD,NormalizedDCA;
		D3DXVECTOR3 AverageNormal;
	
		// Subtract two 3-D vectors ie. v3 = v1 - v2.
		D3DXVec3Subtract(&VectorCA,pVectorA,pVectorC);
		D3DXVec3Subtract(&VectorCB,pVectorB,pVectorC);
		D3DXVec3Subtract(&VectorCD,pVectorD,pVectorC);
		D3DXVec3Subtract(&VectorCE,pVectorE,pVectorC);
	
		D3DXVec3Cross(&NormalACB,&VectorCA,&VectorCB);
		D3DXVec3Cross(&NormalBCE,&VectorCB,&VectorCE);
		D3DXVec3Cross(&NormalECD,&VectorCE,&VectorCD);
		D3DXVec3Cross(&NormalDCA,&VectorCD,&VectorCA);
	
		D3DXVec3Normalize(&NormalizedACB, &NormalACB);
		D3DXVec3Normalize(&NormalizedBCE, &NormalBCE);
		D3DXVec3Normalize(&NormalizedECD, &NormalECD);
		D3DXVec3Normalize(&NormalizedACB, &NormalACB);
	
		AverageNormal.x=(NormalizedACB.x + NormalizedBCE.x + NormalizedECD.x + NormalizedACB.x)/(float)4.0;
		AverageNormal.y=(NormalizedACB.y + NormalizedBCE.y + NormalizedECD.y + NormalizedACB.y)/(float)4.0;
		AverageNormal.z=(NormalizedACB.z + NormalizedBCE.z + NormalizedECD.z + NormalizedACB.z)/(float)4.0;
	
		D3DXVec3Normalize(pNormalVector, &AverageNormal);
}

See Figure 4. again if you are not clear. See also the below diagram showing Vertex Normal Vectors (green) and Average Normal Vectors (in dark red)

 



Then load all your normals into vertex structure (see below)

/**************************************************************************************************************************
 * Function fills the vertex buffer. To do this, we need to Lock() the Vertex Buffer to
 * gain access to the vertices. This mechanism is required becuase vertex buffers may be in device memory.
 * We're storing vertex position and normal unit vector.
 **************************************************************************************************************************/
HRESULT CTerrain::FeedVertexBuffer()
{	
	if( FAILED( m_pTerrVertexBuffer->Lock( 0, 0, (BYTE**)&m_pVertices, 0 ) ) ) return E_FAIL;

	//-----------------------------------------------------------------------------------------------------------
	// Generate 3D mesh.
	//-----------------------------------------------------------------------------------------------------------
	// PLEASE NOTE: Initially when the terrain is generated from a greyscale image the 2D terrain 
	// (regularly spaced) grid is represented on X & Z axes as indicated below.
	//
	//		+---+---+---+ ^	 			
	//		|   |   |   | |	   
	//		+---+---+---+ |			  
	//		|   |   |   | Z				vetices = n stripes + 1 			
	//		+---+---+---+ |		 e.g    4 vertices = 3 stripes + 1   
	//		|   |   |   | |
	//		+---+---+---+ v
	//		<---- X ---->
	//
	// Then when the terrain is converted from 2D (X,Z) into a 3D mesh (X,Y,Z) the axes set up will change. 
	// The axes are:
	//							X (pointing to the right) 
	//							Z (pointing out of the screen) 
	//							Y (pointig up) is the vertical axis to indicate the terrain heights as shown below.
	//
	//							+---+---+---+ ^				
	//							|   |   |   | |				
	//							+---+---+---+ |			  
	//							|   |   |   | Z			|\            /\			
	//							+---+---+---+ |			| \   /\    /   \/\
	//							|   |   |   | |		      Y |  \/   \  /       \
	//							+---+---+---+ v			|________\/_________\
	//							<---- X ---->
	//
	//-----------------------------------------------------------------------------------------------------------

	D3DXVECTOR3 NormalVector;
	TERRAINVERTEX* pVertex;
	int iVertexBuffOffset = 0;
    int terrScale = 2;

	// loop through all of the heightfield points, calculating the coordinates for each terrain vertex (X,Y,Z) and store 
	// in VB in a linear fashion. NOTE: the 2D regular grid (X,Y) is being transformed into 3D (X,Y,Z) terrain grid with heights.
	// NOTE: pVertex->posY = (float)m_pucHeightmapBmpData[ x + (y * m_iNumOfGridCellsY) * 3 ]; //0, 3. 9 (get red from index) (multiply * 3 when 24 bits bitmap)
	for (int z = 0; z < m_iNumOfVerticesZ; z++)	//vertical rows
	{
		for (int x = 0; x < m_iNumOfVerticesX; x++) //horizontal rows
		{
			iVertexBuffOffset = x +( z * m_iNumOfVerticesX);
			pVertex = &( m_pVertices[ iVertexBuffOffset ] );
            
                        // 1. Calculate each terrain vertex along X,Z and set Height on Y axis.
			pVertex->posX = (float)x * terrScale;
			pVertex->posY = m_pucHeightmapBmpData[ x + (z * m_iNumOfVerticesZ) ]; //0, 3. 9 (get colour as height) (8-bit bitmap)   
			pVertex->posZ = (float)z * terrScale;

                      // 2. Set normal vector for each vertex 
			NormalVector=m_pvTerrainNormals[iVertexBuffOffset];
			pVertex->normX=NormalVector.x;
			pVertex->normY=NormalVector.y;
			pVertex->normZ=NormalVector.z;
		}
	}

	m_pTerrVertexBuffer->Unlock();
	return S_OK;

    // NOTE: to set the Y height using 24-bit bitmap use the following code : 
    // pVertex->posY = (float)m_pucHeightmapBmpData[ x + (y * m_iNumOfGridCellsY) * 3 ];  //0, 3. 9 (get red from index) 
    // (multiply * 3 when 24 bits bitmap)
}

See Figure 4. again if you are not clear. See also the below diagram showing Vertex Normal Vectors (green) and Average Normal Vectors (in dark red)

 



Adding light to the scene is quite simple just add the following 5 lines of code to the RestoreDeviceObjects() function.


	// Set up lighting states
    D3DLIGHT8 light;
    D3DUtil_InitLight( light, D3DLIGHT_DIRECTIONAL, 2.0f, 6.0f, -1.0f );
    m_pd3dDevice->SetLight( 0, &light );
    m_pd3dDevice->LightEnable( 0, TRUE );
    m_pd3dDevice->SetRenderState( D3DRS_LIGHTING, TRUE );



The last step is just to render the terrain mesh as in previous tutorial 3. The result should be something similar as below:



Figure 7. Terrain with average nomal vector calculation & light.


Now if you want to draw (optional) a line simulating the normal verctor on your terrain then You have to add the cross product result (see figure 5.) vector ACB = (2, -1, -2) with the original joining A & B vector C.

i.e

ACB + C = (2,-1,-2) + (1,6,2) = (3,5,0)

will give a point laying on the normal direction vector (see example below):



Next: Tutorial 6 - Terrain - Spline Interpolation ( mesh Level Of detail)