赵 刚
引言
在三维游戏等建立的虚拟世界中要求虚拟场景具有很高的逼真度,其中的三维地形逼真度是关键之一。然而三维地形的生成和绘制需要巨大的计算量,实景地形的生成还需要地形数据库的支持,在运算能力非常有限的PC机中实时生成逼真的实景三维地形一直是业界的一个难题。三维地形的生成方法经过了多年的探索,现已形成一系列优秀的算法,本文介绍的算法是一种入门的算法,学习该算法可为系统学习三维地形生成算法打下基础,该算法复杂度较低,运算快速,生成的地形可以满足小规模地形可视化的要求。
一、算法实现过程
Direct3D立即模式中,三维绘图使用DrawPrimitive方法,DrawPrimitive方法将三维模型分解为基本的点,线,和三角形面三种,本三维地形生成法中将地形全部分解成三角形,使用DrawPrimitive方法绘制一系列三角形,从而绘制出整个地形。DrawPrimitive 绘制三角形的方法有三种,第一种是绘制离散的三角形,每个三角形分别指定三个顶点,这种方法适合绘制零散的三角形,对于绘制成片的三角形效率较低。第二种是绘制三角形序列,第一个三角形指定三个顶点,其余的三角形只需要指定一个顶点,另外两个取前一个三角形的最后两个顶点,这样绘制的三角形全部连接在一块儿,适合于绘制成片的三角形。第三种是绘制三角形组成的扇形,以第一个顶点作为所有三角形个公共顶点,为成一个扇形,该方法只适合绘制扇形类的面。因此本地形生成方法采用第二种方法,但将地形中所有的三角形的顶点坐标按首尾相接方式排列起来以适合于第二种绘制三角形方法很难,幸运的是Direct3D立即模式提供了顶点索引的绘图方法,只要将待绘制顶点的编号传递给DrawPrimitive的姊妹函数DrawIndexedPrimitive,就可以完成和DrawPrimitive一样的功能。顶点在内存中采用数组的方式存储,因此顶点编号是顺序编号。因此只要提供一个使三角形的顶点按首尾相接的索引序列,就可以完成高效的绘图。
本三维地形生成方法生成地形的过程如下:
第一步:初始化一个正方形网络(如图1),网格数为64×64(规模太小,生成的地形不逼真,规模太大,PC机难以处理,因此经过多次试验,采用64×64格最为合适)每个正方形网格用两个三角形表示。网格的边长由可视区内的地面大小决定,比如网格边长为50米,可见的地面大小为50×64=3200米。(网格的边长小,生成的地面就小,网格的边长大,生成的地面就大,但地面的逼真度降低,试验表明采用50米比较合适)。
图1 正方形网格示意图(4×3)
顶点包含的信息有:
1. 位置坐标 x、y、z(x表示左右方向,y表示垂直方向,z表示纵深方向)
2. 法线坐标 nx、ny、nz(表示该点周围地面陡峭情况)
3. 颜色 diffuse(表示该点对光线的反射性质)
4. 纹理坐标 tu、tv(tu表示纹理横坐标,tv表示纹理纵坐标)
定义一个结构存储这些信息:
struct VERTEX
{
D3DVALUE x,y,z;
D3DVALUE nx,ny,nz;
D3DCOLOR diffuse;
D3DVALUE tu,tv;
};
在64×64的网格中,顶点的数量为(64+1)×(64+1)=4225,因此可声明一个长度为4225的数组存储顶点信息:VERTEX m_Vertex[4225];
顶点的编号规则为从左到右,从上到下顺序编号,这样第一行第一列的顶点为0号,第一行第二列的顶点为1号,第二行第一列的顶点为65号,依次类推……,因此64×64的正方形网的顶点可初始化如下:
for(j=0;j<65;j++)
{
for(i=0;i<65;i++)
{
m_Vertex[j*65+i].x=-m_Length*0.5f+m_Block*i;
m_Vertex[j*65+I].y=0.0f;
m_Vertex[j*65+i].z= m_Length*0.5f-m_Block*j;
m_Vertex[j*65+i].nx=0.0f;
m_Vertex[j*65+i].ny=1.0f;
m_Vertex[j*65+i].nz=0.0f;
m_Vertex[j*65+I].diffuse=0xffffffff;
}
}
其中 m_Length 表示可见地面长度(3200米)m_Block 表示网格长度(50米)
按这种方式初始化完的正方形网是一张平面(坐标y均为0),它表示的地面将是一片平地,不含高程数据,高程数据的加入将在稍后介绍。
为了使用DrawPrimitive第二种绘制三角形的方式绘图,必须制作一顶点索引序列,使顶点按三角形首尾相接的顺序排列。该顶点索引序列可如下赋值:
WORD m_Index[64*64*6];
for(j=0;j<64;j++)
{
for(i=0;i<64;i++)
{
m_Index[j*64*6+i*6+0]=(WORD)(j*65+i);
m_Index[j*64*6+i*6+1]=(WORD)(j*65+i+65);
m_Index[j*64*6+i*6+2]=(WORD)(j*65+i+65+1);
m_Index[j*64*6+i*6+3]=(WORD)(j*65+i);
m_Index[j*64*6+i*6+4]=(WORD)(j*65+i+1);
m_Index[j*64*6+i*6+5]=(WORD)(j*65+i+65+1);
}
}
地形的高程数据存贮在一个512×512像素的256色BMP文件里,该图形文件按实际地形高程测绘,使用图形中的红色分量表示地面的海拔高度,红色分量从 0~255共有256级,这样地面的高度也只有256级,这样的精度对于三维地形仿真已经足够。本方法中,为进一步提高地形的真实度,还在地形中融入了水面的效果,只要将高程图的红色分量指定为0,将生成水面而非地面,要指定为陆地,红色分量范围为1~255。高程图中的绿色分量用来指定是否有树林,绿色分量越大,表示树林越密,同时这片土地将呈现为草地效果,但绿色分量不可滥用,因为绘制树木很费时间,树木过多将降低程序地实时性,一般将可见地树木数量限制在500棵以内。典型地高程图 如图2。
图2 典型高程图
图中为了直观,将红色成分为零的区域绘制成蓝色,可以明显的看到水域部分构成了一条河流和两个湖泊。
高程图的分辨率为512像素×512像素,地形网格的每一个顶点对应一个像素,而顶点间隔为50米,这样这幅高程图表示的地形范围为边长512×50= 25600米(25.6公里)的正方形区域(655.36平方公里)这对一般的三维场景已经够用,如果仍不够用,可增大高程图的分辨率,或采用多幅高程图拼接,但会消耗更多的内存。
要使地面逼真,地面还应该贴上一层纹理图,纹理图可以是典型地面的照片,或用图像处理软件制作而成。为了正确贴上纹理图,应该该顶点指定纹理坐标,因显示存储器是很有限的,所以纹理图不可能很大,相对于巨大的地面来说,纹理图显得非常不足,因此纹理图要重复使用,就好像贴地砖一样,贴在地面上,这样带来的问题是地面上的纹理呈现周期性,就好像真的是地砖铺的一样,而且纹理图的拼接处会出现难看的裂纹,要减少这些现象,首先纹理图不能太小(本方法中采用1024×1024像素的位图)其次,纹理图的边缘要做特殊处理,使纹理拼接的时候不出现裂缝(这种图叫做可拼接图,广泛用于网页的底纹)本方法中纹理图的边缘不用做特殊处理,也不会出现裂缝,因为本法在贴纹理的时候让拼接处的两个纹理图成镜像关系,因此边缘的图形一致,不会错位,但缺点是可以见到很多对称的花纹,用小纹理拼接地表的方法有待改进,典型的地面纹理图如图3。
图3 土质地面纹理图
为了兼顾纹理的细腻度和低周期性,本法让每个网格拥有的纹理大小为64×64像素,这样一张1024×1024像素的纹理图可覆盖16×16个网格(也就是800米×800米的面积)纹理坐标的算法如下:
long p,q,a,b;
float x,y;
x= m_CenterPos[0]*m_ReciBlock-32;
y=-m_CenterPos[2]*m_ReciBlock-32;
for(j=0;j<65;j++)
{
for(i=0;i<65;i++)
{
p=(((DWORD)x+150+m_TexWidth/2+i)*64)%m_TexWidth;
q=(((DWORD)y+150+m_TexHeight/2+j)*64)%m_TexHeight;
a=(((DWORD)x+150+m_TexWidth/2+i)*64)/m_TexWidth;
b=(((DWORD)y+150+m_TexHeight/2+j)*64)/m_TexHeight;
if(a%2) m_Vertex[j*65+i].tu=(float)p/m_TexWidth;
else m_Vertex[j*65+i].tu=(float)(m_TexWidth-p)/m_TexWidth;
if(b%2) m_Vertex[j*65+i].tv=(float)q/m_TexHeight;
else m_Vertex[j*65+i].tv=(float)(m_TexHeight-q)/m_TexHeight;
}
}
其中 m_CenterPos[0],m_CenterPos[2]是可见地面中心的水平坐标
m_TexWidth,m_TexHeight 是纹理的宽度和高度
m_ReciBlock 是网格边长的倒数(1/50)
变量 m_CenterPos[0],m_CenterPos[2]用于确定绘制地面的中心坐标,在该坐标周围的指定距离内的地面将被绘制,当三维场景中的观察点移动时,地面中心坐标也要做相应的移动,否则观察点移动一定位置后,将会看到地面的边界,甚至看不到地面。一般来说,三维场景中可见区域是一个锥形,观察点位于锥顶,锥底垂直于观察方向,并向观察方向延伸。对于这样可视区,使地面的中心坐标和观察点的水平坐标一致是不可取的,因为在观察点后面有和观察点前面同样大小的一块地面被绘制了,但是却看不到,白白浪费时间,因此应将地面的中心坐标置于观察点正前方一定的距离处,对于边长为3200米的地面,这个距离取1200米是比较合适的。因此程序要负责把观测点正前方,距离观察点1200米的那个点的坐标算出来,用这个坐标给m_CenterPos[0], m_CenterPos[2]赋值,如图4。这样仍有一半左右的地面位于可视区之外,采用其他方法避免非可视区内地面的绘制。
图4 地面中心在可视区内的选择
到现在为止地面还是一张平面,还没有将高程数据写到顶点坐标里。下面将介绍如何将高程数据写入顶点坐标里。高程数据顺序存贮在512×512像素的图像文件里。我们认为图像的中心为坐标原点,往上和往右是坐标增长的方向,这样可以算得,图像的左上端点像素(即第一个像素)存储着坐标为(-256× 50,256×50)即(-12800,12800)地面的高程数据,第二个像素存贮着坐标为(-12750,12800)地面的高程数据一次类推,因此可以根据地面的水平坐标去高程图像中寻址,获取高程数据,高程数据加入网格中后如图5所示。
图5 高程数据加入地形网格中
为了提高绘制速度,本法对地形网格中每一个网格都进行一次可见性判断,如果网格位于可视区内,就绘制该网格,否则不绘制。本地形中共有64×64= 4096个网格,要进行4096次判断,但判断可见性的函数本身运行较慢,过多的判断反而适得其反,因此,本法中对网格可见性的判断以四个为一组,共判断 1024次,四个中只要有一个位于可视区内就认为四个都可见,否则不可见。这样虽然绘制效率有所降低,但却大大节省了可见性判断时间。试验证明以四个一组的分法总体运行速度最高。
本法又声明了另外一个顶点索引数组 WORD m_IndexTemp[4225],对于可见性判断成功的顶点,与其相关的顶点索引将存入m_IndexTemp,否则不存。这样 m_IndexTemp里只含有在可视区内的顶点索引,使DrawPrimitive绘制最少的三角形,最大程度提高速度。
对于高程图中红色分量为零的区域,通过以下方法将地表描述为水面:
1. 地表的颜色为浅蓝色,半透明。
2. 地表的纹理坐标周期性移动,使水面具有流动感。
处理如下:
for(j=0;j<65;j++)
{
for(i=0;i<65;i++)
{……
if(altitude==0) //水域地带
{
m_Vertex[j*65+i].diffuse=0x600030ff; //浅蓝色,半透明
if(Count/16%2) //处理水的流动感
{
p=(((DWORD)x+150+m_TexWidth/2+i)*64+Count%16)%m_TexWidth;
q=(((DWORD)y+150+m_TexHeight/2+j)*64+Count%16)%m_TexHeight;
a=(((DWORD)x+150+m_TexWidth/2+i)*64+Count%16)/m_TexWidth;
b=(((DWORD)y+150+m_TexHeight/2+j)*64+Count%16)/m_TexHeight;
}
else
{
p=(((DWORD)x+150+m_TexWidth/2+i)*64+16-Count%16)%m_TexWidth;
q=(((DWORD)y+150+m_TexHeight/2+j)*64+16-Count%16)%m_TexHeight;
a=(((DWORD)x+150+m_TexWidth/2+i)*64+16-Count%16)/m_TexWidth;
b=(((DWORD)y+150+m_TexHeight/2+j)*64+16-Count%16)/m_TexHeight;
}
}
if(a%2) m_Vertex[j*65+i].tu=(float)p/m_TexWidth;
else m_Vertex[j*65+i].tu=(float)(m_TexWidth-p)/m_TexWidth;
I f(b%2) m_Vertex[j*65+i].tv=(float)q/m_TexHeight;
else m_Vertex[j*65+i].tv=(float)(m_TexHeight-q)/m_TexHeight;
}
}
其中 Count是计数器,每0.1秒数值增加1。
对于高程图中绿色不为零的区域为树林,通过以下方法实现
1. 地面的颜色为暗绿色,表示草地
2. 随机产生树木坐标,在该树木坐标上用公共板技术显示树木。
实施如下:
if(m_ShowTree) //建立树木参数
{
long r,s;
if(green>0&&m_TreeNum {
r=abs((long)((m_Vertex[j*m_Row+i].tu
+m_Vertex[j*m_Row+i].tv*10)*10000)%1000);
for(s=0;s {
vect.x=m_Vertex[j*m_Row+i].x+m_RandPos[s+r][0];
vect.z=m_Vertex[j*m_Row+i].z+m_RandPos[s+r][2];
vect.y=m_Vertex[j*m_Row+i].y;
D3DMath_VectorMatrixMultiply(vect,vect,m_Matrix);
vect.y=GetHeight(vect.x*0.01f,vect.z*0.01f,FALSE)*100.0f;
m_TreePos[m_TreeNum][0]=vect.x;
m_TreePos[m_TreeNum][1]=vect.y;
m_TreePos[m_TreeNum][2]=vect.z;
m_TreePos[m_TreeNum][3]=(float)((long)(green+s+r)%5);
m_TreeNum++;
}
}
}
其中m_ShowTree 是逻辑量,为真时才绘制树木(可以不绘制树木以提高速度)
m_TreeMax 用来控制树木数量, 当m_TreeNum等于m_TreeMax时不再增加树木。
GetHight()是一个函数,用来计算地面高度。
最后绘制地形调用DrawIndexedPrimitive如下:
m_pd3dDevice->SetTexture(0,m_pTexture);
m_pd3dDevice->SetTransform(D3DTRANSFORMSTATE_WORLD,&m_Matrix);
m_pd3dDevice->SetRenderState(D3DRENDERSTATE_CULLMODE,D3DCULL_NONE);
m_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST,D3DFVF_VERTEX, m_Vertex, m_VertexNum,m_IndexTemp,m_IndexNum,NULL);
图6 最终效果示例
二、结束语
本文介绍的三维地形快速生成算法为典型的基于高度图的均匀网格地形生成算法,具有学习三维地形生成算法的入门指导作用。试验证明,在配置为PIII 667MHz,128MB内存,Matrix G400显示卡的计算机中以分辨率为1024×768运行,速度达到35帧/秒以上。该算法的程序代码已经使用在某大型工程中。