其实无论平原,山地,丘陵,沙漠,沙滩都可以用地图网格来表示,巨大的地形网格(网格有顶点向量信息,材质光照,纹理信息)和场景物体,摄像机位置等信息来表达就可以了(2D中需要用45度菱形地图实现3D场景效果,3D中不需要的)。
实现网格信息的步骤:
1.地图高度:从raw灰色图中读取高度图,直接从二进制图片中读取,BYTE转换为int存储因为会放大高度。
2.顶点缓存:在地图网格中构建顶点信息,从传入的像素宽度和格子行列数量和格子大小,还有纹理大小来计算网格的顶点向量和uv信息。
3.索引缓存:在地图网格中构建索引信息,从左到右从上到下的遍历小方格的形式记录三角形的索引,通过假设未知量可以很容易得方格中的三角形顶点和索引的关系。
4.预计算地图中的光照,光照方程来自于L是指光源向量和H是三角形法向量之间夹角来简单决定明暗因子,夹角越小明暗因子越大和光照的镜面反射或者漫反射模型类似,但比他们简单没有计算材质反射值和光泽,观察者方向等信息。 这样SetUp预先计算光照颜色,比实时计算光照性能大幅下降,如果需要切换昼夜可以定时触发计算也比实时计算好。
5.创建一个空纹理,填入材质信息和将纹理通过D3DXFilterTexture得到多级渐进纹理mipMap 或者直接从文件里面D3DXCreateTextureFromFile加载多个纹理,用于地图中指定的网格,当绘制不同地图时候需要切换纹理。或者用纹理随机技术生成随机大小和数量植被地表的地图。
6. 传入当前的摄像机像素位置,挪动坐标算出当前所处的格子位置,由格子位置得到左上角的摄像机高度 。再由向量相加的三角法则,对y方向的向量进行加法得到摄像机当前真实高度(当前所处的格子三角形左上还是右下需要判断,还需要对当前摄像机位置偏移左上角位置或者右下角位置,进行插值计算得到当前高度信息,插值是线性插值a0 + (b-a)t计算即可)。
7.丰富地表表现实现方法:多层纹理的融合的混合地表实现,根据alpha值来计算纹理值,可以得到草地和沙地,岩石和雪贴图根据随机的alpha随机融合的得到满意的地表效果。如果要美术绘制复杂地表,也是可以做到的,但是要改变就比较麻烦。
#ifndef __terrainH__ #define __terrainH__ #include "d3dUtility.h" #include <string> #include <vector> class Terrain { public: Terrain( IDirect3DDevice9* device, std::string heightmapFileName, int numVertsPerRow, int numVertsPerCol, int cellSpacing, // space between cells float heightScale); ~Terrain(); int getHeightmapEntry(int row, int col); void setHeightmapEntry(int row, int col, int value); // 传入当前的摄像机像素位置,挪动坐标算出当前所处的格子位置,由格子位置得到左上角的摄像机高度 // 再由向量相加的三角法则,对y方向的向量进行加法得到摄像机当前真实高度(当前所处的格子三角形左上还是右下需要判断, // 还需要对当前摄像机位置偏移左上角位置或者右下角位置,进行插值计算得到当前高度信息,插值是线性插值a0 + (b-a)t计算即可)。 float getHeight(float x, float z); bool loadTexture(std::string fileName); // 创建一个空纹理,填入材质信息和将纹理通过D3DXFilterTexture得到多级渐进纹理mipMap // 或者直接从文件里面D3DXCreateTextureFromFile加载多个纹理,用于地图中指定的网格,当绘制不同地图时候需要切换纹理。 // 或者用纹理随机技术生成随机大小和数量植被地表的地图。 bool genTexture(D3DXVECTOR3* directionToLight); bool draw(D3DXMATRIX* world, bool drawTris); private: IDirect3DDevice9* _device; // 地图包含了一张纹理图片,自己创建的 IDirect3DTexture9* _tex; // 顶点缓存 IDirect3DVertexBuffer9* _vb; // 索引缓存 IDirect3DIndexBuffer9* _ib; // 每行顶点数 int _numVertsPerRow; // 每列顶点数 int _numVertsPerCol; // 每个小方格的像素数 int _cellSpacing; // 每行的小方格数是每行顶点数减去1 int _numCellsPerRow; // 每列的小方格数是每列顶点数减去1 int _numCellsPerCol; // x方向像素宽度 int _width; // z方向像素深度 int _depth; // 地图的顶点个数 int _numVertices; // 地图的三角形个数 int _numTriangles; // 实际虚拟空间中对高度图的缩放系数,一般比高度图1大 float _heightScale; // 存放对高度图缩放后的地图高度的数据,对应每个顶点 std::vector<int> _heightmap; // helper methods // 1.从raw文件中读取高度图,直接从二进制图片中读取,BYTE转换为int存储因为会放大高度 bool readRawFile(std::string fileName); // 2.在地图网格中构建顶点信息,从传入的像素宽度和格子数量,还有纹理大小计算顶点向量和uv信息 bool computeVertices(); // 3.在地图网格中构建索引信息,从左到右从上到下的遍历小方格的形式记录三角形的索引, // 通过假设未知量可以很容易得方格中的三角形顶点和索引的关系 bool computeIndices(); // 4.预计算地图中的光照,光照方程来自于L是指光源向量和H是三角形法向量之间夹角来简单决定明暗因子,夹角越小明暗因子越大 // 和光照的镜面反射或者漫反射模型类似,但比他们简单没有计算材质反射值和光泽,观察者方向等信息 // 这样SetUp预先计算光照颜色,比实时计算光照性能大幅下降,如果需要切换昼夜可以定时触发计算也比实时计算好。 bool lightTerrain(D3DXVECTOR3* directionToLight); // 传入单元格,和到光源的向量,得到明暗因子 float computeShade(int cellRow, int cellCol, D3DXVECTOR3* directionToLight); 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; }; }; #endif
#include "terrain.h" #include <fstream> #include <cmath> const DWORD Terrain::TerrainVertex::FVF = D3DFVF_XYZ | D3DFVF_TEX1; Terrain::Terrain(IDirect3DDevice9* device, std::string heightmapFileName, int numVertsPerRow, int numVertsPerCol, int cellSpacing, float heightScale) { _device = device; _numVertsPerRow = numVertsPerRow; _numVertsPerCol = numVertsPerCol; _cellSpacing = cellSpacing; _numCellsPerRow = _numVertsPerRow - 1; _numCellsPerCol = _numVertsPerCol - 1; _width = _numCellsPerRow * _cellSpacing; _depth = _numCellsPerCol * _cellSpacing; _numVertices = _numVertsPerRow * _numVertsPerCol; _numTriangles = _numCellsPerRow * _numCellsPerCol * 2; _heightScale = heightScale; // load heightmap if( !readRawFile(heightmapFileName) ) { ::MessageBox(0, "readRawFile - FAILED", 0, 0); ::PostQuitMessage(0); } // scale heights // 将高度信息,直接乘以缩放比例得到高度图信息 for(int i = 0; i < _heightmap.size(); i++) _heightmap[i] *= heightScale; // compute the vertices if( !computeVertices() ) { ::MessageBox(0, "computeVertices - FAILED", 0, 0); ::PostQuitMessage(0); } // compute the indices if( !computeIndices() ) { ::MessageBox(0, "computeIndices - FAILED", 0, 0); ::PostQuitMessage(0); } } Terrain::~Terrain() { d3d::Release<IDirect3DVertexBuffer9*>(_vb); d3d::Release<IDirect3DIndexBuffer9*>(_ib); d3d::Release<IDirect3DTexture9*>(_tex); } int Terrain::getHeightmapEntry(int row, int col) { return _heightmap[row * _numVertsPerRow + col]; } void Terrain::setHeightmapEntry(int row, int col, int value) { _heightmap[row * _numVertsPerRow + col] = value; } bool Terrain::computeVertices() { HRESULT hr = 0; // 地图网格中,直接创建顶点缓存,顶点缓存的个数行顶点数乘以列顶点数 hr = _device->CreateVertexBuffer( _numVertices * sizeof(TerrainVertex), D3DUSAGE_WRITEONLY, TerrainVertex::FVF, D3DPOOL_DEFAULT, &_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. // 纹理坐标的u方向的增长,确保了j等于_numCellsPerRow时候为1 float uCoordIncrementSize = 1.0f / (float)_numCellsPerRow; // 纹理坐标的v方向的增长,确保了i等于_numCellsPerCol时候为1 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; } bool Terrain::computeIndices() { HRESULT hr = 0; hr = _device->CreateIndexBuffer( _numTriangles * 3 * sizeof(WORD), // 3 indices per triangle D3DUSAGE_WRITEONLY, D3DFMT_INDEX16, D3DPOOL_DEFAULT, &_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 int baseIndex = 0; // loop through and compute the triangles of each quad // 从左到右,从上到下,得到每个三角形的索引 // 可以通过很简单的观察得到i * _numVertsPerRow + j,i * _numVertsPerRow + j + 1,(i+1) * _numVertsPerRow + j // 的规律 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; } bool Terrain::loadTexture(std::string fileName) { HRESULT hr = 0; hr = D3DXCreateTextureFromFile( _device, fileName.c_str(), &_tex); if(FAILED(hr)) return false; return true; } 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, 0, // create a complete mipmap chain 0, // usage D3DFMT_X8R8G8B8,// 32 bit XRGB format D3DPOOL_MANAGED, &_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*/); 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; 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. // lockedRect.Pitch是上一行中结束位置的字节数,lockedRect.Pitch / 4就是DWORD个数 imageData[i * lockedRect.Pitch / 4 + j] = (D3DCOLOR)c; } } _tex->UnlockRect(0); // 这里的光照和纹理颜色,是将纹理颜色和光照的明暗因子进行相乘得到最后颜色的结果,并非融合的方式 if(!lightTerrain(directionToLight)) { ::MessageBox(0, "lightTerrain() - FAILED", 0, 0); return false; } // 计算出其它多级纹理层中的纹理元 // 例如_tex是源纹理是256x256的,那么D3DXFilterTexture可以生成其它大小的纹理,例如128x128的,64x64的 // 这样自定义填充的纹理,就类似于D3DXCreateTextureFromFile一样拥有mipMap Texture多级纹理的纹理。 hr = D3DXFilterTexture( _tex, 0, // default palette 0, // use top level as source level D3DX_DEFAULT); // default filter if(FAILED(hr)) { ::MessageBox(0, "D3DXFilterTexture() - FAILED", 0, 0); return false; } return true; } bool Terrain::lightTerrain(D3DXVECTOR3* directionToLight) { HRESULT hr = 0; 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 level in mipmap chain &lockedRect,// pointer to receive locked data 0, // lock entire texture image 0); // no lock flags specified DWORD* imageData = (DWORD*)lockedRect.pBits; for(int i = 0; i < textureDesc.Height; i++) { for(int j = 0; j < textureDesc.Width; j++) { // index into texture, note we use the pitch and divide by // four since the pitch is given in bytes and there are // 4 bytes per DWORD. // lockedRect.Pitch是上一行中结束位置的字节数,lockedRect.Pitch / 4就是DWORD个数 int index = i * lockedRect.Pitch / 4 + j; // get current color of quad D3DXCOLOR c( imageData[index] ); // shade current quad // 颜色乘以明暗因子,得到光照计算后的颜色 c *= computeShade(i, j, directionToLight);; // save shaded color imageData[index] = (D3DCOLOR)c; } } _tex->UnlockRect(0); return true; } float Terrain::computeShade(int cellRow, int cellCol, D3DXVECTOR3* directionToLight) { // get heights of three vertices on the quad float heightA = getHeightmapEntry(cellRow, cellCol); float heightB = getHeightmapEntry(cellRow, cellCol+1); float heightC = getHeightmapEntry(cellRow+1, cellCol); // build two vectors on the quad // 计算uv,y方向上用向量三角规则来进行计算 D3DXVECTOR3 u(_cellSpacing, heightB - heightA, 0.0f); D3DXVECTOR3 v(0.0f, heightC - heightA, -_cellSpacing); // find the normal by taking the cross product of two // vectors on the quad. // 得到平面法向量 D3DXVECTOR3 n; D3DXVec3Cross(&n, &u, &v); D3DXVec3Normalize(&n, &n); // L.N得到明暗因子 float cosine = D3DXVec3Dot(&n, directionToLight); if(cosine < 0.0f) cosine = 0.0f; return cosine; } bool Terrain::readRawFile(std::string fileName) { // Restriction: RAW file dimensions must be >= to the // dimensions of the terrain. That is a 128x128 RAW file // can only be used with a terrain constructed with at most // 128x128 vertices. // 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 // 这个拷贝也是因为BYTE类型转换为int类型需要引入的 _heightmap.resize( _numVertices ); for(int i = 0; i < in.size(); i++) _heightmap[i] = in[i]; return true; } float Terrain::getHeight(float x, float z) { float fWidthLimit = _width * 0.5f; if( x < -fWidthLimit || x > fWidthLimit ) { return 0.0f; } float fDepthLimit = _depth * 0.5f; if( z < -fDepthLimit || z > fDepthLimit ) { return 0.0f; } // Translate on xz-plane by the transformation that takes // the terrain START point to the origin. // x像素宽度从[-w/2,w/2]到[0,w] x = ((float)_width / 2.0f) + x; // z像素深度从[d/2,-d/2]到[0,d] z = ((float)_depth / 2.0f) - z; // Scale down by the transformation that makes the // cellspacing equal to one. This is given by // 1 / cellspacing since; cellspacing * 1 / cellspacing = 1. // x,z从像素深度,除以一个小方格的宽度,得到当前所属于的方格数 x /= (float)_cellSpacing; z /= (float)_cellSpacing; // From now on, we will interpret our positive z-axis as // going in the 'down' direction, rather than the 'up' direction. // This allows to extract the row and column simply by 'flooring' // x and z: // 得到所属的方格数,去小于等于的整数,也就是左上角的方格数 float col = ::floorf(x); float row = ::floorf(z); // get the heights of the quad we're in: // // A B // *---* // | / | // *---* // C D float A = getHeightmapEntry(row, col); float B = getHeightmapEntry(row, col+1); float C = getHeightmapEntry(row+1, col); float D = getHeightmapEntry(row+1, col+1); // // Find the triangle we are in: // // Translate by the transformation that takes the upper-left // corner of the cell we are in to the origin. Recall that our // cellspacing was nomalized to 1. Thus we have a unit square // at the origin of our +x -> 'right' and +z -> 'down' system. // 得到x,z相对于左上角方格数的偏差,也就是当前高度需要的dx,dz浮点值 float dx = x - col; float dz = z - row; // Note the below compuations of u and v are unneccessary, we really // only need the height, but we compute the entire vector to emphasis // the books discussion. // 选择三角形,在x + y = 1直线的左上部分,还是右下部分 // dz < 1.0f - dx是左上的三角形 float height = 0.0f; if(dz < 1.0f - dx) // upper triangle ABC { float uy = B - A; // A->B float vy = C - A; // A->C // Linearly interpolate on each vector. The height is the vertex // height the vectors u and v originate from {A}, plus the heights // found by interpolating on each vector u and v. // a0 + (uy-a0)dx线性插值,这里a0是0,也就是得到uy*dx的值 // 由三角形法则,因为uy,vy可正可负,且dx,dz是两点间的[0,1]之间的偏差值 // 三角形法则中因为不需要x,z方向的,只需要y方向的值,所以拿高度进行A + dxuy + dzvy就会得到当前的高度值 height = A + d3d::Lerp(0.0f, uy, dx) + d3d::Lerp(0.0f, vy, dz); } else // lower triangle DCB { // A B // *---* // | / | // *---* // C D float uy = C - D; // D->C float vy = B - D; // D->B // Linearly interpolate on each vector. The height is the vertex // height the vectors u and v originate from {D}, plus the heights // found by interpolating on each vector u and v. // 因为dx是[0,dx,1]之间的一个值,现在从上限处开始算所以是1-dx;1-dz也是指后面部分的值 // uy,vy来决定正负,1-dx, 1-dy只是个相对量。 height = D + d3d::Lerp(0.0f, uy, 1.0f - dx) + d3d::Lerp(0.0f, vy, 1.0f - dz); } // 得到射线机的高度,就可以在上面行走了 return height; } bool Terrain::draw(D3DXMATRIX* world, bool drawTris) { HRESULT hr = 0; if( _device ) { _device->SetTransform(D3DTS_WORLD, world); _device->SetStreamSource(0, _vb, 0, sizeof(TerrainVertex)); _device->SetFVF(TerrainVertex::FVF); _device->SetIndices(_ib); // 设置创建好颜色且填充好光照明暗的多级纹理 _device->SetTexture(0, _tex); // turn off lighting since we're lighting it ourselves _device->SetRenderState(D3DRS_LIGHTING, false); // 绘制时候直接绘制顶点缓存和索引缓存即可 hr =_device->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 0, _numVertices, 0, _numTriangles); _device->SetRenderState(D3DRS_LIGHTING, true); // 绘制三角形网格 if( drawTris ) { // 填充模式 默认是D3DFILL_SOLID,The default value is D3DFILL_SOLID _device->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME); hr =_device->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 0, _numVertices, 0, _numTriangles); _device->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID); } if(FAILED(hr)) return false; } return true; }
bool Setup() { // // Create the terrain. // D3DXVECTOR3 lightDirection(0.0f, 1.0f, 0.0f); TheTerrain = new Terrain(Device, "coastMountain64.raw", 64, 64, 10, 0.5f); TheTerrain->genTexture(&lightDirection); //TheTerrain->loadTexture("grass.bmp"); // // Create the font. // FPS = new FPSCounter(Device); // // Set texture filters. // Device->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR); Device->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR); Device->SetSamplerState(0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR); // // Set projection matrix. // D3DXMATRIX proj; D3DXMatrixPerspectiveFovLH( &proj, D3DX_PI * 0.25f, // 45 - degree (float)Width / (float)Height, 1.0f, 1000.0f); Device->SetTransform(D3DTS_PROJECTION, &proj); return true; } void Cleanup() { d3d::Delete<Terrain*>(TheTerrain); d3d::Delete<FPSCounter*>(FPS); } bool Display(float timeDelta) { // // Update the scene: // if( Device ) { // walk是沿_look轴的移动,地面摄像机不能绕_look旋转 if( ::GetAsyncKeyState(VK_UP) & 0x8000f ) TheCamera.walk(100.0f * timeDelta); if( ::GetAsyncKeyState(VK_DOWN) & 0x8000f ) TheCamera.walk(-100.0f * timeDelta); // yaw是绕_up轴偏航,地面摄像机是绕世界y轴旋转;地面摄像机不能沿_up上下移动 if( ::GetAsyncKeyState(VK_LEFT) & 0x8000f ) TheCamera.yaw(-1.0f * timeDelta); if( ::GetAsyncKeyState(VK_RIGHT) & 0x8000f ) TheCamera.yaw(1.0f * timeDelta); // MN是沿_right左右摆动 if( ::GetAsyncKeyState('N') & 0x8000f ) TheCamera.strafe(-100.0f * timeDelta); if( ::GetAsyncKeyState('M') & 0x8000f ) TheCamera.strafe(100.0f * timeDelta); // pith是绕_right轴的仰俯 if( ::GetAsyncKeyState('W') & 0x8000f ) TheCamera.pitch(1.0f * timeDelta); if( ::GetAsyncKeyState('S') & 0x8000f ) TheCamera.pitch(-1.0f * timeDelta); D3DXVECTOR3 pos; TheCamera.getPosition(&pos); float height = TheTerrain->getHeight( pos.x, pos.z ); if( !d3d::gFltEquip( height, 0.0f) ) { pos.y = height + 5.0f; // add height because we're standing up } TheCamera.setPosition(&pos); D3DXMATRIX V; TheCamera.getViewMatrix(&V); Device->SetTransform(D3DTS_VIEW, &V); // // Draw the scene: // Device->Clear(0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0xff000000, 1.0f, 0); Device->BeginScene(); D3DXMATRIX I; D3DXMatrixIdentity(&I); // 绘制地形,直接使用设置好的纹理和预计算的光照明暗纹理颜色,根据顶点索引绘制顶点缓存即可 if( TheTerrain ) TheTerrain->draw(&I, true/*false*/); // 绘制FPS文字,因为整个顶点缓存比较影响性能 if( FPS ) FPS->render(0xffffffff, timeDelta); Device->EndScene(); Device->Present(0, 0, 0, 0); } return true; }