这节教程是关于加载高度图(HeightMap),程序的结构如下:
第一,高度图是什么?
这个得联系到上一节的教程
D3D11进阶教程二之TerrainGrid(地形网格),上一教程我们有一张图:
上一个教程我们把一个大的长方形分为多个小的三角形来绘制,并且所有的三角形都处于同一个XZ平面上,也就是所有小三角形的坐标的Y值都是相同的,因此得到了下面的渲染图:
此时试想我们把小三角形的某些顶点坐标的Y值改变,而某些保持不变,然后我们就得到下面的那样的情形:
这时候问题来了,怎么改变那些小三角形的顶点坐标的Y值?高度图(HeightMap)的作用自然就发挥出来了,高度图的每个像素存储着地形网格相应顶点的Y值,当然高度图很多时候是8bit的RAW文件,每个像素8位,保证了读取的值(整数)在0-255之间,我们本节教程用的不是raw,而是24位的bitmap(或者jpg,png等等),24位的bitmap每个像素由R,G,B三种色彩组成,也就是R,G,B每个分别是8位,而在24位的高度图里R,G,B的值是相等的,所以高度图表现为灰度图,上面只有白黑灰三种颜色。
看看我们的高度图,如下所示,白色部分像素值大,代码地形的突起部分; 而黑色部分像素值小,代表地形的平缓部分。
读取高度图代码:
struct HeightMap
{
float x, y, z;
};
std::vector<HeightMap*> mHeightMap;
//24位高度图的像素为24位,R=G=B都为8位
bool TerrainClass::LoadHeightMap(std::wstring HeightMapFileName)
{
//GDIplus库
Gdiplus::Bitmap* texture;
Gdiplus::GdiplusStartupInput gdiplusstartupinput;
Gdiplus::Color color;
ULONG_PTR gdiplustoken;
Gdiplus::GdiplusStartup(&gdiplustoken, &gdiplusstartupinput, NULL);
//读取纹理
texture = new Gdiplus::Bitmap(HeightMapFileName.c_str());
//获取纹理分辨率的宽度和高度
mTerrainHeight = texture->GetHeight();
mTerrainWidth = texture->GetWidth();
for (int i = 0; i < mTerrainHeight; ++i)
{
for (int j = 0; j <mTerrainWidth; ++j)
{
//获取相应数组[j][i]的像素,注意这里下标从0开始
texture->GetPixel(j, i, &color);
HeightMap* mHeightMapPixel = new HeightMap();
mHeightMapPixel->x = (float)j;
mHeightMapPixel->y = (float)color.GetR(); //0-255
mHeightMapPixel->z = (float)i;
mHeightMap.push_back(mHeightMapPixel);
}
}
delete texture;
Gdiplus::GdiplusShutdown(gdiplustoken);
return true;
}
加载顶点缓存和索引缓存:
bool TerrainClass::InitializeBuffer(ID3D11Device* d3dDevice)
{
Vertex* vertexs = NULL;
unsigned long *indices = NULL; //一个字为两个字节
int index = 0;
int mem;
int i = 0;
int j = 0;
float PosX, PosZ;
mVertexCount = (mTerrainHeight - 1)*(mTerrainWidth - 1) * 6;
mIndexCount =mVertexCount;
//创建顶点数组
vertexs = new Vertex[mVertexCount];
if (!vertexs)
return false;
//创建索引数组
indices = new unsigned long[mIndexCount];
if (!indices)
return false;
//赋予顶点数组数据和索引数组数据
//注意起始点是从左下角开始的
for (j = 0; j < (mTerrainHeight - 1); ++j)
{
for (i = 0; i < (mTerrainWidth - 1); ++i)
{
//右三角形
//左下点
PosX = (float)i;
PosZ = (float)j;
mem = PosZ*mTerrainWidth + PosX;
vertexs[index].pos = XMFLOAT3(PosX, mHeightMap[mem]->y, PosZ);
vertexs[index].normal = XMFLOAT3(mHeightMap[mem]->nx, mHeightMap[mem]->ny, mHeightMap[mem]->nz);
indices[index] = index;
++index;
//右上点
PosX = (float)(i + 1);
PosZ = (float)(j + 1);
mem = PosZ*mTerrainWidth + PosX;
vertexs[index].pos = XMFLOAT3(PosX, mHeightMap[mem]->y, PosZ);
vertexs[index].normal = XMFLOAT3(mHeightMap[mem]->nx, mHeightMap[mem]->ny, mHeightMap[mem]->nz);
indices[index] = index;
++index;
//右下点
PosX = (float)(i + 1);
PosZ = (float)j;
mem = PosZ*mTerrainWidth + PosX;
vertexs[index].pos = XMFLOAT3(PosX, mHeightMap[mem]->y, PosZ);
vertexs[index].normal = XMFLOAT3(mHeightMap[mem]->nx, mHeightMap[mem]->ny, mHeightMap[mem]->nz);
indices[index] = index;
++index;
//左三角形
//左下点
PosX = (float)i;
PosZ = (float)j;
mem = PosZ*mTerrainWidth + PosX;
vertexs[index].pos = XMFLOAT3(PosX, mHeightMap[mem]->y, PosZ);
vertexs[index].normal = XMFLOAT3(mHeightMap[mem]->nx, mHeightMap[mem]->ny, mHeightMap[mem]->nz);
indices[index] = index;
++index;
//左上点
PosX = (float)i;
PosZ = (float)(j + 1);
mem = PosZ*mTerrainWidth + PosX;
vertexs[index].pos = XMFLOAT3(PosX, mHeightMap[mem]->y, PosZ);
vertexs[index].normal = XMFLOAT3(mHeightMap[mem]->nx, mHeightMap[mem]->ny, mHeightMap[mem]->nz);
indices[index] = index;
++index;
//右上点
PosX = (float)(i + 1);
PosZ = (float)(j + 1);
mem= PosZ*mTerrainWidth+ PosX;
vertexs[index].pos = XMFLOAT3(PosX, mHeightMap[mem]->y, PosZ);
vertexs[index].normal = XMFLOAT3(mHeightMap[mem]->nx, mHeightMap[mem]->ny, mHeightMap[mem]->nz);
indices[index] = index;
++index;
}
}
//第一,填充(顶点)缓存形容结构体和子资源数据结构体,并创建顶点缓存
D3D11_BUFFER_DESC vertexBufferDesc;
vertexBufferDesc.Usage = D3D11_USAGE_DEFAULT; //静态缓存
vertexBufferDesc.ByteWidth = sizeof(Vertex) * mVertexCount;
vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vertexBufferDesc.CPUAccessFlags = 0;
vertexBufferDesc.MiscFlags = 0;
vertexBufferDesc.StructureByteStride = 0;
D3D11_SUBRESOURCE_DATA vertexData;
vertexData.pSysMem = vertexs;
vertexData.SysMemPitch = 0;
vertexData.SysMemSlicePitch = 0;
HR(d3dDevice->CreateBuffer(&vertexBufferDesc, &vertexData, &md3dVertexBuffer));
//第二,填充(索引)缓存形容结构体和子资源数据结构体,并创建索引缓存
D3D11_BUFFER_DESC indexBufferDesc;
indexBufferDesc.Usage = D3D11_USAGE_DEFAULT; //静态缓存
indexBufferDesc.ByteWidth = sizeof(unsigned long) * mIndexCount;
indexBufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER;
indexBufferDesc.CPUAccessFlags = 0;
indexBufferDesc.MiscFlags = 0;
indexBufferDesc.StructureByteStride = 0;
D3D11_SUBRESOURCE_DATA indexData;
indexData.pSysMem = indices;
indexData.SysMemPitch = 0;
indexData.SysMemSlicePitch = 0;
HR(d3dDevice->CreateBuffer(&indexBufferDesc, &indexData, &md3dIndexBuffer));
//释放顶点数组和索引数组(这时数据已经载入缓存,不需要这些数组了)
delete[]vertexs;
vertexs = NULL;
delete[]indices;
indices = NULL;
return true;
}
用程序运行得到:
此时看到的地形就是典型的桂林山水了,然而我们并不想得到那么陡峭的山,我们想得到平缓坡度不高的小山丘,我们怎么办呢?先分析下山为什么这么陡峭吧?
我们上面说过加载的顶点的Y值为0-255,但是我们整个地形中相邻的每个顶点的XZ平面上的距离仅仅是1个单位长度,而加载的Y值可能达到255,不难想到加载的地形这么陡峭了,所以要想得到平缓坡度小的山,我们在将高度图的数据加载进顶点缓存前得除以一个数N,让Y高度值缩小N倍,我取一个数10.0,代码如下面所示:
void TerrainClass::NormalizeHeightMap()
{
for (auto it = mHeightMap.begin(); it != mHeightMap.end(); ++it)
{
(*it)->y /= 10.0f;
}
}
这样程序运行得到:
这样我们就得到了地形就不是桂林山水,而是小山丘了。
第二,项目的C++代码的改进。
项目原来的C++源码有些偏C语言,某些语法的运用不太合理,我作出下面的修改:
(1)数组指针用C++的STL容器vector代替,并且vector存储为指针而非对象,具体原因见博客用vector保存对象时保存指针的优点, 以及reserve的使用
(2)不再使用using namespace,访问相应空间的变量,函数等等的时候尽量用std::的形式,如std::string,因为随着项目代码的增加,自己写的变量名容易跟某些命名空间的变量重名,会造成很大麻烦,我在写SoftRaster就吃到了苦头。
这里推荐一本最近看的C++书籍,<effective C++>,对C++的运用讲得非常好,和<C++ primer>并称C++两大神书。
源代码的链接如下: