<Real Time 3D Terrain Engines Using C++ And DX9>
Chapter 6. Basic Terrain Geometry
终于一路读到了第六章,读过了这章以后,就应该可以会写简单的Terrain了吧,期待ing……
Height Maps as Terrain Input Data
地形,有高有低。如何能确定一个点的高低呢?最简单的方法是Height Map。
说白了,就是每个点都用一个值表示,这个值是输入进去的。这个值是一个高度,所以叫Height。一个点对应一个值,所以是Map。
这个Height Map通常是用Bitmap图片来输入的。怎么输入呢?用灰度图。白代表高,黑代表低。比如高度取值是[0,255],那么一个点对应图片上的一个像素,这个像素的灰度范围也是[0,255],简单又有效。
还有软件可以把真实的地形转成Bitmap的灰度图。
Procedural Height
如果Height Map不能满足需要的话,我们就应该尝试着用程序去生成高度。
最简单的方法是这样的:随机生成每一个点的height,然后再处理,直到地形看起来平滑为止。这个“看起来平滑”就是相邻的两个点的高度差在某一个阈值以内。
这种方法被Snook戏称为“猴子画法”。因为它看上去就如同让一只猴子去画,然后我们去修正。
还有很多简单而有效的方法可以可以做到直接生成平滑的地形。
Midpoint Displacement
中间点法是一个递归的方法。虽然也是随机算法,但是却可以生成平滑的地形(当roughness设置的恰当的时候)。
一个方的地形,四个角ABCD,他们的高度是随机生成的。有一个变量delta,初始是MaxHeight/2,在这里就是128了(因为地图高度范围是0-255)。
于是,在四个边的中点和正方形的中心这5个点的高度有如下计算公式:
Height = Base Value + [Offset]
Base Value是什么?就是相邻的两个角的高度平均值(中心是四个角的平均值)。
那Offset呢?Offset是随机的,范围就是[-delta,delta]。
然后,进入下一步。因为刚刚那5个点已经把大的方形分成了四个小的方形。那么接下来就递归去求每一个小的方形的内部的高度。计算方法同前(四个角也是已经确定了)。不过有一点值得注意的是,delta需要修正:
delta = delta * roughness
这个roughness是一个0-1之间的因子,就是粗糙因子。这个值约大,地形越粗糙。一般来说,取0.5就可以了。
Perlin Noise
Perlin噪声这个东西很久以前就听过了,这次居然在这里遇到了。这个东西是由Ken Perlin在1983年的时候发明的。由于Perlin噪声在图形学影响,Ken Perlin居然在1997年被发了一个奥斯卡奖,汗 -_-|||
对于Perlin噪声来说,可以适用于任何维度的空间,不过由于在这里Perlin噪声主要是用来生成纹理,所以就只讨论2D的了。
创建Perlin噪声的方法如下:
1)比如地形是一个n*n的Grid,那么每行就有n+1个vertex,一共有n+1列了。对于每一个vertex,我们在上面生成一个2D的随机向量。
这里有种很有效率的方法。首先,做个向量表,一共256个,均匀分布成一个圆周。然后随机从里面选一个就可以了。
2)然后,就把目标集中在每一个Grid的内部了。对于Grid内部的任意一个Pixel,四个Corner都有一个向量指向它,这样每个Corner上就有2个向量了。再对每一个Corner上的两个向量求点积,把这个值记作这个Corner的Height。(注意,并不是这个Corner所对应的Pixel的Height,这个Height仅仅是一个中间变量而已)
3)再把目标集中在左上角的那个指向Pixel的向量。那个可以说是这个Pixel相对于这个Grid的坐标了,记为x和y的话,那么现在需要计算一些量:
SX = 6 * x^5 - 15 * x^4 + 10 * x^3;
SY = 6 * y^5 - 15 * y^4 + 10 * y^3;
对于这个需要说明一下,最早Perlin的论文中提出的公式是 w = 3 * t^2 - 2 * t^3。这个公式运算起来要比现在所用的这个快,但是人工的痕迹很重。所以后来Perlin改进了这个公式,采用了现在的这个。如果想追求速度的话,可以换成快的这个。
4)成功就在不远处了。最后,只要做3个线性插值,就OK了(其中四个Corner的Height分别为h0 - h3):
avgX0 = h0 + (SX*(h2 - h0))
avgX1 = h1 + (SX*(h3 - h1))
result = avgX0 + (SY*(avgX1 - avgX0))
这个result当然就是最后的结果了,也就是这个点所对应的那个高度了。
具体的还是看书上的代码吧,一看即懂。另外,还有一种方法,就是用一个比率去缩放Texture,然后再把他们加起来,就可以得到新的Texture了,这样还可以降低人工的痕迹呢。
Processing Height Map Data
得把灰度图变成高度,才可以建立地形,也就是说,得先处理一下数据。
每个面都是三角形,每两个三角形组成一个Grid,再由Grid组成Terrain。
除了坐标外,还有一个法线的问题。对于每个面,很容易求得它的法线(任意两个边做叉积),那么vertex的法线呢?想让地形看起来平滑的话,就得把一个vertex周围的面的法线求一个平均值,这样,地形看起来就平滑了。
听说D3DX提供了一个函数把灰度图里面的法线都求出来,叫D3DXComputerNormalMap,要是真的的话那可太方便了。
Terrain Geometry Base Classes
主要就是两个类—— cTerrain 和 cTerrainSection。
cTerrain是由cTerrainSection拼起来的,这样做是为了便于管理。比如cTerrain是256x256的,而cTerrainSection是64x64的,那么一共就需要64个Section了。cTerrainSection继承于cSceneObjects的,所以可以放到render queue中,和其他物体一样可以进行空间管理。不过和cSceneObjects有区别的是,cTerrainSection并不储存IB和VB的信息,这些都是存在cTerrain里面的,cTerrainSection只是拿来用罢了。
Terrain Geometry Index Buffers
这里是用三角带(Triangle Strip)来作Index的。其中用到了一个小技巧,那就是退化三角形(degenerate triangle)。退化三角形是一种面积为0的三角形,它的三个顶点中有两个是同一个顶点。这个三角形是用来连接三角带的,这样就可以把行的三角带变成面的三角带了。
生成方法如下:比如要生成一个m*n的Grid,那么一共需要(m+1)*(n+1)个vertex。设一下:
xVerts = n + 1
yVerts = m + 1
然后需要计算一共有多少个三角带、每个要多少个索引和索引的总数:
Total_Strips = yVerts - 1
Total_Indexs_Per_Strip = xVerts * 2
Total_Indexes = (Total_Indexs_Per_Strip * Total_Strips) + (Total_Strips * 2) - 2
这里需要注意的是Total_Indexes,前面的Total_Indexs_Per_Strip * Total_Strips好理解,后面的那个其实应该是(Total_Strips - 1) * 2,这个就是退化三角形了,一行有2个退化三角形来连接。
初始化几个变量:
index,是一个数组,用来做index用的。
Start_Vert = 0
Line_Step,就是每行的Vertex的个数
然后:
for(j = 0; j < Total_Strips; ++j)
{
Vert = Start_Vert;
for(k = 0; k < xVerts; ++k)
{
*(index++) = Vert;
*(index++) = Vert + Line_Step;
}
Start_Vert += Line_Step;
// 加入退化三角形
if(j + 1 < Total_Strips)
{
*(index++) = (Vert - 1) + Line_Step;
*(index++) = Start_Vert;
}
}
我没有写书中的xStep和yStep,书上说学ROAM的时候才会用到,先不管它……
后面的几节就都是相关的Code了。
这章是实现地形的基础,而且还学到了Perlin Noise,挺开心。