19.1 高度图
地形渲染的实现思路是:以一个平面网格为基础(图19.1上图),通过调整网格顶点的高度(即,y坐标)模拟地形起伏,使网格表现出从高山到河谷的平滑过渡(图19.1中图)。当渲染网格时,我们可以使用一幅精致的纹理来表现沙滩、草地、石壁、雪山等自然地貌(图19.1下图)。
学习目标
- 学习如何为一个地形生成高度信息,形成高山与河谷之间的平滑过渡。
- 了解如何为地形添加纹理。
- 使用硬件曲面细分绘制一个带有连续LOD(level of detail)的地形。
- 了解如何在地形表面上放置摄像机或其他物体。
我们使用高度图(heightmap)描述地形中的山川起伏。高度图是一个矩阵,它的每个元素指定了地形网格中的一个特定顶点的高度。也就是,每个网格顶点都有一个与之对应的高度图元素,第ij个高度图元素提供了第ij个顶点的高度。在图像处理软件中,高度图一般显示为灰阶图像(grayscale map),其中,黑色表示最低海拔高度,白色表示最高海拔高度,不同亮度的灰色表示介于中间的海拔高度。图19.2展示了几个高度图的例子,以及使用这些高度图生成的地形。
当我们在磁盘上存储高度图时,通常只会为高度图中的每个元素分配一个字节的存储空间,也就是说,高度的取值范围是[0,255]。该范围足以表达地形中的高度变化。不过,在实际应用中,我们可能需要对该范围进行扩展,使它与我们的3D空间大小匹配。例如,当我们将3D空间的测量单位设为英寸时,一个字节根本无法表示小于0英寸或大于255英寸的高度值。基于上述原因,我们将使用浮点数存储那些载入到应用程序中的高度元素。这样,我们就可以有效地扩展[0,255]区间,把它缩放到任何大小都不成问题;而且,这样还可以对高度图进行过滤,生成整数之间的高度值。
注意:我们在6.11节中使用数学函数创建过一个“地形”,它是一个以代码来生成地形的例子。不过,它的用处不大,因为函数根本无法精确描述你实际想要的地形。高度图具有很好的灵活性,它可以由美术师来编辑和绘制。
19.1.1 创建高度图
高度图可以由代码或图像处理软件(比如Adobe Photoshop)生成。我们可以在Photoshop中使用滤镜生成各种不同的杂乱图案,作为高度图的底图,然后使用各种绘图工具做一些手工修改,从而得到一幅不错的高度图。而且,模糊滤镜可以有效地缓解高度图中的粗糙边缘。
你也可以使用某些工具软件来创建高度图,比如Terragen(http://www.planetside.co.uk/terragen/)可以通过程序生成高度图,它还提供了一个修改高度图的工具,也可以输出软件生成的高度图,然后导入到另一个图像处理程序(例如Photoshop)中进行手工修改。Bryce(http://www.daz3d.com/i.x/software/bryce/)也自带很多生成高度图的程序算法和高度图编辑器。Dark Tree(http://www.darksim.com/)是一个强大的纹理设置工具,也可用于生成高度灰度图。
在完成高度图的绘制之后,你需要把它保存为一个8位RAW文件。RAW文件只是把图像的二进制数据直接保存成文件,不添加任何编码信息。这样我们可以很容易地把图像载入到程序中。如果你使用的软件在保存RAW文件询问是否加入一个文件头,那么请选择“不加入文件头”。图19.3展示了Terragen的导出对话框。
注意,请在导出方式中选择8位RAW格式。
注意:高度图不一定存储为RAW格式;你可以根据需要选择任何一种图像格式。RAW格式只是我们可以使用的图像格式中的一种。我们使用RAW格式,主要是因为很多图像处理软件都支持这种格式,而且它用起来非常简单,我们可以很容易地把RAW文件载入到演示程序中。本书中的演示程序使用8位RAW文件(即,高度图中的每个元素均为8位整数)。
注意:如果256个高度级对于你的需要来说过于粗糙,那么可以考虑使用16位高度图,通过16位整数来存储高度值。Terragen支持16位RAW高度图。
19.1.2 载入RAW文件
由于RAW文件是一个连续的字节块(其中的每个字节是一个高度图元素),所以我们可以很容易地使用std::ifstream::read 方法把它读入内存中来。下面是具体的实现代码:
void Terrain::LoadHeightmap() { // 存储高度数据的数组 std::vector<unsigned char> in( mInfo.HeightmapWidth * mInfo.HeightmapHeight ); // 打开文件. std::ifstream inFile; inFile.open(mInfo.HeightMapFilename.c_str(), std::ios_base::binary); if(inFile) { // 读取RAW字节. inFile.read((char*)&in[0], (std::streamsize)in.size()); // Done with file. inFile.close(); } // 将数组中的数据复制到一个浮点数组并进行缩放 mHeightmap.resize(mInfo.HeightmapHeight * mInfo.HeightmapWidth, 0); for(UINT i = 0; i < mInfo.HeightmapHeight * mInfo.HeightmapWidth; ++i) { mHeightmap[i] = (in[i] / 255.0f)*mInfo.HeightScale; } }
mInfo是Terrain类的一个成员变量,它是一个InitInfo结构体实例。该结构体描述了地形的各种属性:
struct InitInfo { std::wstring HeightMapFilename; // 高度图文件名称 // 地形纹理的文件名称 std::wstring LayerMapFilename0; std::wstring LayerMapFilename1; std::wstring LayerMapFilename2; std::wstring LayerMapFilename3; std::wstring LayerMapFilename4; std::wstring BlendMapFilename; float HeightScale; UINT HeightmapWidth; UINT HeightmapHeight; float CellSpacing; };
注意:读者可以回顾5.11节,查阅有关网格构造的更多信息。
19.1.3 平滑处理
8位高度图的一个缺点是它只能表示256个离散的高度级。因此,我们无法模拟图19.5a所示的高度值;只能得到图19.5b的高度值。这样创建出来的地形会比我们实际想要的地形“粗糙”。而且,当高度值舍入之后,我们将无法恢复原始的高度值。不过,我们可以对图19.5b进行平滑处理,使它接近于图19.5a。
那么,我们所要做的就是通过读取RAW文件,把高度图载入到内存中。然后把字节数组复制给一个浮点数组,提高数值存储的精确度。接着,在浮点高度图上应用一个滤镜,对高度图进行平滑处理,降低邻接元素之间的高度差。该滤镜算法非常简单,它通过计算一个像素及其8个邻接像素的平均值来生成该像素的新颜色(参见图19.6):
\[{\bar h_{i,j}} = \frac{{{h_{i - 1,j - 1}} + {h_{i - 1,j}} + {h_{i - 1,j + 1}} + {h_{i,j - 1}} + {h_{i,j}} + {h_{i,j + 1}} + {h_{i + 1,j - 1}} + {h_{i + 1,j}} + {h_{i + 1,j + 1}}}}{9}\]
注意,在高度图边缘上的像素没有8个邻接像素,我们在处理这些像素时只能按照邻接像素的实际数量来计算平均值。
下面的函数用于计算高度图中的第ij个像素的平均值:
bool Terrain::InBounds(int i, int j) { // 如果ij是有效的索引则返回true; 否则返回false. return i >= 0 && i < (int)mInfo.HeightmapHeight && j >= 0 && j < (int)mInfo.HeightmapWidth; } float Terrain::Average(int i, int j) { // Function computes the average height of the ij element. // It averages itself with its eight neighbor pixels. Note // that if a pixel is missing neighbor, we just don't include it // in the average--that is, edge pixels don't have a neighbor pixel. // // ---------- // | 1| 2| 3| // ---------- // |4 |ij| 6| // ---------- // | 7| 8| 9| // ---------- float avg = 0.0f; float num = 0.0f; // Use int to allow negatives. If we use UINT, @ i=0, m=i-1=UINT_MAX // and no iterations of the outer for loop occur. for(int m = i-1; m <= i+1; ++m) { for(int n = j-1; n <= j+1; ++n) { if( InBounds(m,n) ) { avg += mHeightmap[m*mInfo.HeightmapWidth + n]; num += 1.0f; } } } return avg / num; }
当像素在高度图上时,函数inBounds返回true;反之返回false。这样,当我们对一个边缘像素的邻接像素进行采样时,如果某个邻接像素不在高度图范围内,那么inBounds会返回false,我们不把它计入平均值——因为它不存在。
要对整个高度图进行平滑,我们只需要为每个高度图像素计算平均值:
void Terrain::Smooth() { std::vector<float> dest( mHeightmap.size() ); for(UINT i = 0; i < mInfo.HeightmapHeight; ++i) { for(UINT j = 0; j < mInfo.HeightmapWidth; ++j) { dest[i*mInfo.HeightmapWidth+j] = Average(i,j); } } // 将过滤后的值替换旧值. mHeightmap = dest; }
19.1.4 高度图Shader资源视图
我们在下一节会看到,要支持曲面细分和位移映射,需要在shader程序中采样高度图。所以,需要创建一个高度图的shader资源视图。创建资源视图现在我们已经很熟悉了,唯一的一个技巧就是为了节省内存,我们使用了16位浮点数代替了32位。可以使用XNA库XMConvertFloatToHalf函数将32位浮点数转换为16位浮点数。
void Terrain::BuildHeightmapSRV(ID3D11Device* device) { D3D11_TEXTURE2D_DESC texDesc; texDesc.Width = mInfo.HeightmapWidth; texDesc.Height = mInfo.HeightmapHeight; texDesc.MipLevels = 1; texDesc.ArraySize = 1; texDesc.Format = DXGI_FORMAT_R16_FLOAT; texDesc.SampleDesc.Count = 1; texDesc.SampleDesc.Quality = 0; texDesc.Usage = D3D11_USAGE_DEFAULT; texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; texDesc.CPUAccessFlags = 0; texDesc.MiscFlags = 0; // HALF定义在xnamath.h中,用来存储16位浮点数. std::vector<HALF> hmap(mHeightmap.size()); std::transform(mHeightmap.begin(), mHeightmap.end(), hmap.begin(), XMConvertFloatToHalf); D3D11_SUBRESOURCE_DATA data; data.pSysMem = &hmap[0]; data.SysMemPitch = mInfo.HeightmapWidth*sizeof(HALF); data.SysMemSlicePitch = 0; ID3D11Texture2D* hmapTex = 0; HR(device->CreateTexture2D(&texDesc, &data, &hmapTex)); D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc; srvDesc.Format = texDesc.Format; srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; srvDesc.Texture2D.MostDetailedMip = 0; srvDesc.Texture2D.MipLevels = -1; HR(device->CreateShaderResourceView(hmapTex, &srvDesc, &mHeightMapSRV)); // SRV saves reference. ReleaseCOM(hmapTex); }文件下载(已下载 1057 次)
发布时间:2014/8/25 下午7:43:46 阅读次数:6629