Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Introduction to 3D Game Programming with DirectX.9.0 - F. D. Luna

.pdf
Скачиваний:
239
Добавлен:
24.05.2014
Размер:
6.94 Mб
Скачать

214 Chapter 13

 

 

Y

 

Figure 13.2: A heightmap as a grayscale

 

L

13.1.1 Creating a Heightmap

 

 

F

Heightmaps can be generated either procedurally or in an image editor

 

M

 

such as Adobe Photoshop. Using an image editor is probably the easiest

A

 

way to go, and it allows you to create the terrain interactively and visu-

ally as you want it. In addition, you can take advantage of your image

E

 

editor features, such as filters, to create interesting heightmaps. Figure

T

 

 

13.3 shows a pyramid-type heightmap created in Adobe Photoshop using the editing tools. Note that we specify a grayscale map when creating the image.

Figure 13.3: A grayscale image created in Adobe Photoshop

Once you have finished drawing your heightmap, you need to save it as an 8-bit RAW file. RAW files simply contain the bytes of the image one after another. This makes it very easy to read the image into our applications. Your software may ask you to save the RAW file with a header. Specify no header.

Team-Fly®

Basic Terrain Rendering 215

Note: You do not have to use the RAW format to store your height information; you can use any format that meets your needs. The RAW format is just one example of a format that we can use. We decided to use the RAW format because many popular image editors can export to that format and it is very easy to read the data in a RAW file into the application. The samples in this chapter use 8-bit RAW files.

13.1.2 Loading a RAW File

Since a RAW file is nothing more than a contiguous block of bytes, we can easily read in the block with this next method. Note that the variable _heightmap is a member of the Terrain class and defined as:

std::vector<int> _heightmap;

bool Terrain::readRawFile(std::string fileName)

{

// A height for each vertex std::vector<BYTE> in( _numVertices );

std::ifstream inFile(fileName.c_str(), std::ios_base::binary);

if( inFile == 0 ) return false;

inFile.read(

(char*)&in[0], // buffer

in.size());// number of bytes to read into buffer

inFile.close();

// copy BYTE vector to int vector _heightmap.resize( _numVertices ); for(int i = 0; i < in.size(); i++)

_heightmap[i] = in[i];

return true;

}

Observe that we copy the vector of bytes to a vector of integers; we do this so that we can scale the height values outside the [0, 255] interval.

The only restriction of this method is that the RAW file being read in must have at least as many bytes as there are vertices in the terrain. Therefore, if you are reading in a 256x256 RAW file, you must construct the terrain with, at most, 256x256 vertices.

13.1.3 Accessing and Modifying the Heightmap

The Terrain class provides the following two methods to access and modify an entry in the heightmap:

int Terrain::getHeightmapEntry(int row, int col)

{

return _heightmap[row * _numVertsPerRow + col];

P a r t I I I

216 Chapter 13

}

void Terrain::setHeightmapEntry(int row, int col, int value)

{

_heightmap[row * _numVertsPerRow + col] = value;

}

These methods allow us to refer to an entry by row and column and hide the way we must index a linear array when using it to describe a matrix.

13.2 Generating the Terrain Geometry

Figure 13.4: Properties of the triangle grid labeled. The dots along the grid lines are vertices.

Figure 13.4 shows some properties of a terrain, vocabulary, and special points that we refer to. We define the size of our terrain by specifying the number of vertices per row, the number of vertices per column, and the cell spacing. We pass these values into the constructor of the Terrain class. In addition, we also pass the device associated with the terrain, a string identifying the filename that the heightmap data is contained in, and a height scale value that is used to scale the heightmap elements.

class Terrain

{

public:

Terrain(

IDirect3DDevice9* device, std::string heightmapFileName,

int numVertsPerRow, int numVertsPerCol,

int cellSpacing, // space between cells float heightScale); // value to scale heights by

... methods snipped

Basic Terrain Rendering 217

private:

...device/vertex buffer etc snipped

int _numVertsPerRow; int _numVertsPerCol; int _cellSpacing;

int _numCellsPerRow;

int _numCellsPerCol; int _width;

int _depth;

int _numVertices; int _numTriangles;

float _heightScale;

};

See the source code in the companion files for the complete class definition of Terrain; it is too big to include here.

From the values passed into the constructor, we can compute these other variables of the terrain as:

_numCellsPerRow

= _numVertsPerRow - 1;

_numCellsPerCol

= _numVertsPerCol - 1;

_width

= _numCellsPerRow * _cellSpacing;

_depth

= _numCellsPerCol * _cellSpacing;

_numVertices

= _numVertsPerRow * _numVertsPerCol;

_numTriangles

= _numCellsPerRow * _numCellsPerCol * 2;

Also, the vertex structure of our terrain is defined as:

struct TerrainVertex

{

TerrainVertex(){}

TerrainVertex(float x, float y, float z, float u, float v)

{

_x = x; _y = y; _z = z; _u = u; _v = v;

}

float _x, _y, _z; float _u, _v;

static const DWORD FVF;

};

const DWORD Terrain::TerrainVertex::FVF = D3DFVF_XYZ | D3DFVF_TEX1;

Note that TerrainVertex is a nested class inside the Terrain class. This was done because TerrainVertex is not needed outside the

Terrain class.

P a r t I I I

13.2.1 Computing the Vertices

Refer to Figure 13.4 during this discussion. To compute the vertices of our triangle grid, we are simply going to begin generating vertices at start and then go row by row generating vertices until we reach end, leaving a gap defined by the cell spacing between the vertices. This will

218 Chapter 13

give us our x- and z-coordinate, but what about the y-coordinate? The y-coordinate is easily obtained by finding the corresponding entry in the loaded heightmap data structure.

Note: This implementation uses one large vertex buffer to hold all of the vertices for the entire terrain. This can be problematic due to hardware limitations. For example, there is a maximum primitive count limit and maximum vertex index limit that is set for the 3D device. Check the MaxPrimitiveCount and MaxVertexIndex members of the

D3DCAPS9 structure to see what your particular device’s limits are. Section 13.7 discusses a solution to the problems of using one vertex buffer.

To compute the texture coordinates, consider Figure 13.5, which gives us a simple scenario allowing us to see that the (u, v) texture coordinate that corresponds to the terrain vertex at (i, j) is given by:

Figure 13.5: The correspondence between the terrain vertices and the texture vertices

u j uCoordIncrementSize v i vCoordIncrementSize

And where:

1

uCoordIncrementSize

numCellCols

1

vCoordIncrementSize

numCellRows

Finally, the code to generate the vertices:

bool Terrain::computeVertices()

{

HRESULT hr = 0;

hr = _device->CreateVertexBuffer( _numVertices * sizeof(TerrainVertex), D3DUSAGE_WRITEONLY,

Basic Terrain Rendering 219

TerrainVertex::FVF, D3DPOOL_MANAGED, &_vb,

0);

if(FAILED(hr)) return false;

//coordinates to start generating vertices at int startX = -_width / 2;

int startZ = _depth / 2;

//coordinates to end generating vertices at int endX = _width / 2;

int endZ = -_depth / 2;

//compute the increment size of the texture coordinates

//from one vertex to the next.

float uCoordIncrementSize = 1.0f / (float)_numCellsPerRow; float vCoordIncrementSize = 1.0f / (float)_numCellsPerCol;

TerrainVertex* v = 0; _vb->Lock(0, 0, (void**)&v, 0);

int i = 0;

for(int z = startZ; z >= endZ; z -= _cellSpacing)

{

int j = 0;

for(int x = startX; x <= endX; x += _cellSpacing)

{

//compute the correct index into the vertex buffer

//and heightmap based on where we are in the nested

//loop.

int index = i * _numVertsPerRow + j;

v[index] = TerrainVertex( (float)x, (float)_heightmap[index], (float)z,

(float)j * uCoordIncrementSize, (float)i * vCoordIncrementSize);

j++; // next column

}

i++; // next row

}

_vb->Unlock();

return true;

}

P a r t I I I

220Chapter 13

13.2.2Computing the Indices—Defining the Triangles

To compute the indices of the triangle grid, we simply iterate through each quad, starting in the upper left and ending in the lower right of Figure 13.4, and compute the two triangles that make up that quad.

Figure 13.6: A quad’s vertices

The trick is to come up with the general formulas to compute the two triangles of the ijth quad. Using Figure 13.6 to develop our general formulas, we find that for quad (i, j):

ABC i numVertsPe rRow j

i numVertsPe rRow j 1

i 1 numVertsPe rRow j#

CBD i 1 numVertsPe rRow j

i numVertsPe rRow j 1

i 1 numVertsPe rRow j 1#

The code to generate the indices:

bool Terrain::computeIndices()

{

HRESULT hr = 0;

hr = _device->CreateIndexBuffer(

_numTriangles * 3 * sizeof(WORD), // 3 indices per triangle D3DUSAGE_WRITEONLY,

D3DFMT_INDEX16,

D3DPOOL_MANAGED, &_ib,

0);

if(FAILED(hr)) return false;

WORD* indices = 0;

_ib->Lock(0, 0, (void**)&indices, 0);

//index to start of a group of 6 indices that describe the

//two triangles that make up a quad

Basic Terrain Rendering 221

int baseIndex = 0;

// loop through and compute the triangles of each quad for(int i = 0; i < _numCellsPerCol; i++)

{

for(int j = 0; j < _numCellsPerRow; j++)

{

indices[baseIndex]

=

i

* _numVertsPerRow + j;

indices[baseIndex + 1] =

i

* _numVertsPerRow +

 

 

 

j + 1;

indices[baseIndex + 2] =

(i+1)

* _numVertsPerRow + j;

indices[baseIndex + 3] =

(i+1)

* _numVertsPerRow + j;

indices[baseIndex + 4] =

i

* _numVertsPerRow +

 

 

 

j + 1;

indices[baseIndex + 5] =

(i+1)

* _numVertsPerRow +

 

 

 

j + 1;

// next quad baseIndex += 6;

}

}

_ib->Unlock();

return true;

}

13.3 Texturing

The Terrain class provides two ways to texture the terrain. The obvious way is to simply load a previously made texture file and use that. The following method implemented by the Terrain class loads a texture from the file into the _tex data member, which is a pointer to an

IDirect3DTexture9 interface. Internally, the Terrain::draw method sets _tex before rendering the terrain.

bool loadTexture(std::string fileName);

At this point in the book its implementation should be straightforward to you. It is:

bool Terrain::loadTexture(std::string fileName)

{

HRESULT hr = 0;

hr = D3DXCreateTextureFromFile( _device,

fileName.c_str(), &_tex);

if(FAILED(hr)) return false;

P a r t I I I

222 Chapter 13

return true;

}

13.3.1 A Procedural Approach

An alternative way to texture the terrain is to compute the texture procedurally; that is, we create an “empty” texture and compute the color of each texel in code based on some defined parameter(s). In our example, the parameter will be the height of the terrain.

We generate the texture procedurally in the Terrain::genTexture method. It first creates an empty texture using the D3DXCreateTexture method. Then we lock the top level (remember a texture has mipmaps and can have multiple levels). From there we iterate through each texel and color it. We color the texel based on the approximate height of the quad to which it corresponds. The idea is to have lower altitudes of the terrain colored a sandy beach color, medium altitudes colored as grassy hills, and the high altitudes colored as snowy mountains. We define the approximate height of the quad as the height of the upper-left vertex of the quad.

Once we have a color for each texel, we want to darken or brighten each texel based on the angle at which sunlight (modeled by a directional light) strikes the cell to which the texel corresponds. This is done in the Terrain::lightTerrain method, whose implementation is covered in the next section.

The Terrain::genTexture method concludes by computing the texels of the lower mipmap levels. This is done using the D3DXFilterTexture function. The code to generate the texture:

bool Terrain::genTexture(D3DXVECTOR3* directionToLight)

{

//Method fills the top surface of a texture procedurally. Then

//lights the top surface. Finally, it fills the other mipmap

//surfaces based on the top surface data using

//D3DXFilterTexture.

HRESULT hr = 0;

// texel for each quad cell

int texWidth = _numCellsPerRow; int texHeight = _numCellsPerCol;

// create an empty texture

 

hr = D3DXCreateTexture(

 

_device,

 

texWidth, texHeight,

// dimensions

0,

// create a complete mipmap chain

0,

// usage - none

D3DFMT_X8R8G8B8,

// 32-bit XRGB format

D3DPOOL_MANAGED,

// memory pool

Basic Terrain Rendering 223

&_tex);

if(FAILED(hr)) return false;

D3DSURFACE_DESC textureDesc; _tex->GetLevelDesc(0 /*level*/, &textureDesc);

//make sure we got the requested format because our code

//that fills the texture is hard coded to a 32-bit pixel depth. if( textureDesc.Format != D3DFMT_X8R8G8B8 )

return false;

D3DLOCKED_RECT lockedRect;

_tex->LockRect(0/*lock top surface*/, &lockedRect, 0 /* lock entire tex*/, 0/*flags*/);

// fill the texture

DWORD* imageData = (DWORD*)lockedRect.pBits; for(int i = 0; i < texHeight; i++)

{

for(int j = 0; j < texWidth; j++)

{

D3DXCOLOR c;

// get height of upper-left vertex of quad.

float height = (float)getHeightmapEntry(i, j)/_heightScale;

//set the color of the texel based on the height

//of the quad it corresponds to.

if( (height) < 42.5f )

 

c = d3d::BEACH_SAND;

else if( (height) < 85.0f )

c = d3d::LIGHT_YELLOW_GREEN;

else if( (height) < 127.5f )

c = d3d::PUREGREEN;

else if( (height) < 170.0f

)

c = d3d::DARK_YELLOW_GREEN;

else if( (height) < 212.5f

)

c = d3d::DARKBROWN;

else

 

c = d3d::WHITE;

//fill locked data, note we divide the pitch by four

//because the pitch is given in bytes and there are

//4 bytes per DWORD.

imageData[i * lockedRect.Pitch / 4 + j] = (D3DCOLOR)c;

}

}

_tex->UnlockRect(0);

//light the terrain if(!lightTerrain(directionToLight))

{

::MessageBox(0, "lightTerrain() - FAILED", 0, 0); return false;

}

//fill mipmaps

hr = D3DXFilterTexture(

_tex,// texture to fill mipmap levels 0, // default palette

P a r t I I I