D3D11地形渲染教程三之高度图(HeightMap)

本文介绍如何使用高度图(HeightMap)调整地形网格中的顶点高度,实现从平坦到复杂地形的变化。通过调整Y轴值比例,可以控制地形的陡峭程度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这节教程是关于加载高度图(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++两大神书。



源代码的链接如下:


评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值