HeightMap
=========
focus on 3d Terrain一书介绍了两种自动生成HeightMap的方法。作者Trent参考了编程精粹中提供的算法,并做了应用。
Trent的实现:
CTerrain类维护着一个结构体SHEIGHI_DATA
struct SHEIGHT_DATA
{
unsigned char* m_ucpData;//保存高度值
int m_iSize; // (must be a power of 2) 其实并不是必需的!m_iSize表示构成地形宽或高的顶点数
};
高度程的2种实现:
bool CTERRAIN::MakeTerrainFault( int iSize, int iIterations, int iMinDelta, int iMaxDelta, float fFilter )
bool CTERRAIN::MakeTerrainPlasma( int iSize, float fRoughness )
算法通用处理方法:
第一步,为m_ucpData申请内存:new char[m_iSize*m_iSize],用来保存高度值。
为fTempBuffer申请内存 :new float [m_iSize*m_iSize],引入是为了提高精度。
第二步,采用相应的算法生成高度值,通过过滤处理,使得到地形效果符合自然地形走势。
最后,把处理后的高度值要映射到区间[0-255] (可指定),然后保存到m_ucpData中。类CTerrain实例在得到高程图数据后,就可以
进行渲染工作了。
------------目标:理解算法过程--------------------------
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
第一部分:Fault Formation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
算法思想:
for( iCurrentIteration=0; iCurrentIteration<iIterations; iCurrentIteration++ )
{
首先,fTempBuffer[m_iSize*m_iSize]初始化为0。对应的渲染效果图,就是一个格子状的矩形。
接着,构成矩形的顶点中,随机取2个不同的顶点。(注意到:它们都在XZ平面上)
然后,连接这两个顶点坐标并做延长,为这条延长线其中一侧的顶点赋于高度值iHeight(迭代得到),另一侧则维持原值。
iHeight= iMaxDelta - ( ( iMaxDelta-iMinDelta )*iCurrentIteration )/iIterations;
其中iMaxDelta,iMinDelta为指定的最大高度极值。显然,一开始取到的是最大值。
最后,对整个数组中高度值做衰减处理,目的为了各顶点高度值过渡更平滑。
}
===
补充说明:
1.迭代过程,简单地说,就是对地形画一条随机线(由两个随机得到的不同顶点连线所得),然后对线一侧(原程序设定的是线的右侧)赋于高度值。 如果不做衰减处理的话,这时地形渲染后有强烈明暗效果,一边是黑色位置低,一边是白色,表示位置高. 迭代多次后,那么每个区域高度值过渡相当平和很多。
2.衰减处理
fTempBuffer[m_iSize*m_iSize]渲染后就是一个平面格子,构成格子的顶点的高度值决定了地形的平缓程度。
公式:后续顶点高度值(更新后)=因子*前一个顶点高度值+(1-因子)*后续顶点高度值
对迭代后得到的高度值做衰减处理过程:从左到右,从右到左,从上到下,从下到上。
一个例子:
地形fTempBuffer[4X4]
如果将下面顶点(字母符号)连接起来,就是渲染后的地形图(9个格子)。
m n o p
i j k l
e f g h
a b c d
其中fTempBuffer[0]表示的是最初顶点a所对应的高度值,fTempBuffer[0*4+3]对应的是顶点d.
按衰减处理过程:
->表示"衰减公式的影响"
从左到右:
a->b->c->d
e->f->g->h
i->j->k->l
m->n->o->p
从右到左:
d->c->b->a
h->g->f->e
l->k->j->i
p->o->n->m
从上到下:略
从下到上:略
3.如何确定落在分割线的一侧。
原程序说明:
随机生成的顶点A(x1,z1),B(x2,z2) ,其连线就是分割线。其它的待判定的点记C(x,z)
向量AB:(Dx1=x2-x1,Dz1=z2-z1)
向量AC:(Dx2=x-x1, Dz2=z-z1)
判定条件是:if( Dx2*Dz1-Dx1*Dz2)>0) ftempBuffer[x+z*m_iSize]=迭代得到高度值。
如何理解? (Dx2*Dz1-Dx1*Dz2)>0.
这个问题可转化为原生问题:如何判定顶点落在AB连线的哪一侧。
向量运算时,做点积是最直观的。画几条向量,可以知道,只要把向量AC和 {垂直AB的向量}做点积,就可以达到要求了。
求出垂直AB的向量(回想高中时代学过的复数知识):
向量AB逆时针转90度,
(Dx1,Dz1)*(cos90,sin90)=(Dx1*cos90-Dz1*sin90,Dx1*sin90+Dz1*cos90)
=(-Dz1,Dx1)
把(Dx2*Dz1-Dx1*Dz2)拆成点积形式(Dx2,Dz2).(Dz1,-Dx1)
比较可知:(Dz1,-Dx1)是向量AB顺时针转90得到的。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
第二部分 midpoint displacement
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
这种地形分形方式有好多称谓:Midpoint displacement ,plasma fractal ,diamond-square 算法。
顶点数据个数:m_iSize*m_iSize,地形实际大小为(m_iSize-1)*(m_iSize-1),其中m_iSize必须为2的n次幂。这个限制是针对顶点个数来说的!!!
注意:
屏幕坐标系下作者做的图示,和我们通常所用笛卡尔坐标系下所作的图示,其实是没有区别的,因为最终写到缓冲区的数据都是一致的。
例子:3x3的地形,(3x3 指得是顶点实际个数比)
ABC
DEF
GHI
比较
GHI
DEF
ABC
在保存数据都是采用行序,ABCDEFGHI依次存储.
算法思想:求正方形中点,求菱形中点,反复。
fTempBuffer[0]= 0.0f;
while( iRectSize>0 )
{
/*Diamond step
a.....b
. .
. e .
. .
c.....d
e = (a+b+c+d)/4 + random
In the code below:
a = (i,j)
b = (ni,j)
c = (i,nj)
d = (ni,nj)
e = (mi,mj) */
for( i=0; i<m_iSize; i+=iRectSize )
{
for( j=0; j<m_iSize; j+=iRectSize )
{
ni= ( i+iRectSize )%m_iSize;
nj= ( j+iRectSize )%m_iSize;
mi= ( i+iRectSize/2 );
mj= ( j+iRectSize/2 );
fTempBuffer[mi+mj*m_iSize]= ( float )( ( fTempBuffer[i+j*m_iSize] + fTempBuffer[ni+j*m_iSize] +
fTempBuffer[i+nj*m_iSize] + fTempBuffer[ni+nj*m_iSize] )/4 + RangedRandom( - fHeight/2, fHeight/2 ) );
}
}
/*Square step -
.......
. .
. .
. d .
. .
. .
......a..g..b
. . .
. . .
. e h f .
. . .
. . .
......c......
g = (d+f+a+b)/4 + random
h = (a+c+e+f)/4 + random
In the code below:
a = (i,j)
b = (ni,j)
c = (i,nj)
d = (mi,pmj)
ef = (mi,mj)
g = (mi,j)
h = (i,mj)*/
for( i=0; i<m_iSize; i+=iRectSize )
{
for( j=0; j<m_iSize; j+=iRectSize )
{
ni= (i+iRectSize)%m_iSize;
nj= (j+iRectSize)%m_iSize;
mi= (i+iRectSize/2);
mj= (j+iRectSize/2);
pmi= (i-iRectSize/2+m_iSize)%m_iSize;
pmj= (j-iRectSize/2+m_iSize)%m_iSize;
//Calculate the square value for the top side of the rectangle
fTempBuffer[mi+j*m_iSize]= ( float )( ( fTempBuffer[i+j*m_iSize]+fTempBuffer[ni+j*m_iSize] + fTempBuffer[mi+pmj*m_iSize]+fTempBuffer[mi+mj*m_iSize] )/4+
RangedRandom( -fHeight/2, fHeight/2 ) );
//Calculate the square value for the left side of the rectangle
fTempBuffer[i+mj*m_iSize]= ( float )( ( fTempBuffer[i+j*m_iSize]+fTempBuffer[i+nj*m_iSize] +fTempBuffer[pmi+mj*m_iSize]+fTempBuffer[mi+mj*m_iSize] )/4+
RangedRandom( -fHeight/2, fHeight/2 ) );
}
}
//reduce the rectangle size by two to prepare for the next
//displacement stage
iRectSize/= 2;
//reduce the height by the height reducer
fHeight*= fHeightReducer;
}
~~~~~~~
说明:
运行过程:以m_iSize=4为例。
=============
iRectSize=4时
=============
0XXXY
XXXXY
XX1XY
XXXXY
YYYYY
这一步,各跨度下构成的方格,为其中心赋高度值,Y是虚拟点,我们并不需要为它分配内存,所以申请内存
只要new float[4X4]即可,而不是new float[5X5]。这是通过取余%来实现的。
------------
0X2XY
XXXXY
3X1XY
XXXXY
YYYYY
这一步,构造菱形的中心,部分边缘处的点在对周围4个点采样时(这4个点构成菱形),个别点是不可见的,同样要
作取余%处理。
=============
iRectSize=2时
=============
0X2XY
X4X6Y
3X1XY
X5X7Y
YYYYY
------
0a2eY
b4f6Y
3c1gY
d5h7Y
YYYYY
------------
=============
iRectSize=1时
=============
作用对应于 Fault算法中的衰减处理。
这里的过程在diamand步是由左往右,由上往下,分别对顶点周围的4个点进行采样,
square步
同一顶点采样2次,接着由左往右,由上往下。
_________________________________________________________________________________________
~Fin~