前言
随着地理信息系统产业的发展,三维产品也在生活中处处吸引着我们的眼球。作为数字城市的核心内容,城市模型的构建成为了目前研究的热点。OpenGL是独立于操作系统和硬件环境的三维图形库,其为实现逼真的三维绘制效果和建立交互的三维场景提供了高效率的函数库,在交互式三维图形建模能力和编程方面具有无可比拟的优越性。在VC++中配置OpenGL开发环境,利用OpenGL,VC++构建了地形的三维可视化模型,通过对三维地形进行纹理贴图,光照设置使得绘制结果更具真实感。结果表明,利用利用 VC++和OpenGL进行三维真实感地形生成是切实可行的,其实现的功能全面而且效率高。
三维地形可视化可以直观、真实地表达地形的三维信息及综合特征,在国民经济各个领域中有着广泛的应用价值和广阔的应用前景。基于上述理论及成果,以VC++为平台,利用OpenGL库函数编写三维地形可视化过程。首先读取DEM数据,构建三角面片,计算其法向量;以BMP格式图片作为纹理,通过纹理坐标与三角面片顶点坐标的匹配实现纹理贴图;最后为场景添加光照、投影等,使场景更具真实感。最终实现了一个可以旋转、缩放的三维地形展示。
一、技术路线
本文主要分为两个部分,三维地形生成与纹理贴图和环境设置。前者是研究的主要内容。先生成并读取DEM数据,运用栅格的位置坐标和高程值生成三维点,基于三维点构建三角面片;接着利用法向量生成函数计算得到各三角面片的法向量,为添加光照服务;最后读取RGB图像作为纹理,利用glTexCoor2f()与glVertex3f()函数为三角面片顶点指定纹理坐标,将纹理贴到三角面片上。同时运用OpenGL添加光照,设置视点等,使地形的三维效果更具真实感。技术路线如图1。
图1 技术路线
二、程序实现
1.建立光照模型
(1)打开光源
glEnable(GL_LIGHTING);//打开总开关
glEnable(GL_LIGHT0);//打开第0个光源开关
(2)设置光源属性
光源属性主要包括Position、Ambient、Diffuse、Specular,本文只考虑了前三者。
GLfloat light0Ambient[]={0.2f, 0.2f,0.2f,1.0f};
GLfloat light0Diffuse[]={1.0f,1.0f,1.0f,1.0f};
GLfloat light0position[]={1.0f,1.0f,1.0f,0.0f };
GL_AMBIENT表示该光源所发出的光,经过非常多次的反射后,最终遗留在整个光照环境中的强度(颜色)。GL_DIFFUSE 表示该光源所发出的光,照射到粗糙表面时经过漫反射,所得到的光的强度(颜色)。每个属性由四个值表示,分别代表了颜色的R,G,B,A值。GL_POSITION属性。表示光源所在的位置。由四个值(X,Y,Z,W)表示。如果第四个值 W为零,则表示该光源位于无限远处,前三个值表示了它所在的方向。这种光源称为方向性光源,通常,太阳可以近似的被认为是方向性光源。本文设置的即为接近太阳的光源。
glClearColor(0.0,0.0,0.0,1.0);
glClear(GL_COLOR_BUFFER_BIT); //清空深度缓存
glClear函数来自OpenGL,其中它是通过glClear使用红,绿,蓝以及AFA值来清除颜色缓冲区的,并且都被归一化在(0,1)之间的值,其实就是清空当前的所有颜色。
(3)设置材质参数
glColorMaterial(GL_FRONT_AND_BACK,GL_DIFFUSE);
glEnable(GL_COLOR_MATERIAL);
glShadeModel(GL_FLAT);
其中,GL_FRONT_AND_BACK代表材质的面,GL_DIFFUSE代表漫反射反射系数,包括四个参数,前三个参数RGB对应于该色光的反射百分比(反射系数)。如R=1.0,G=0.5,B=0.0,则反射全部红光,一半绿光,不反射蓝光。
2.读取DEM数据
(1)在Arcmap中将DEM数据转换成ASCII码存储形式,即将每个栅格的高程用文本的形式记录下来,或者可以自动生成一个指定大小n*n的二维数组,存放指定的高程信息。
(2)通过文件操作将栅格大小和每个栅格的高程读入,使用数组记录下高程,即为Z轴坐标,通过栅格大小计算出每个栅格距离中心点的X轴方向距离和Y轴方向距离,即为X轴坐标和Y轴坐标。将X、Y、Z存入动态数组DTMX、DTMY和DTMZ。具体的程序实现为:
void ddCalDTMXYZ(void)
{
int n,m;
float dz;
dz=(maxDtmZ+minDtmZ)/2.0f;
for(n=0;n<dtmDY;n++)
for(m=0;m<dtmDX;m++)
{ DTMX[n*dtmDX+m]=(float)(m-(dtmDX-1)/2.0f)*CellSize;//中心化
DTMY[n*dtmDX+m]=(float)(n-(dtmDY-1)/2.0f)*CellSize;
DTMZ[n*dtmDX+m]-=dz;
}
}
3.构建三角面片并计算法向量
在上一步骤中,实现了将DEM栅格数据的位置坐标和高程值(DTMX、DTMY、DTMZ)存入数组,接下来根据这些点的坐标建立不规则三角网,并计算所有三角面片的法向量。
(1)绘制三角形格网
对于三角形,OpenGL一般有三种绘制一系列三角形的方式,分别是GL_TRIANGLES、GL_TRIANGLE_STRIP和GL_TRIANGLE_FAN,本文采用第二种方式创建三角形格网。其规律是构建当前三角形的顶点的连接顺序依赖于要和前面已经出现过的2个顶点组成三角形的当前顶点的序号的奇偶性(如果从0开始):如果当前顶点是奇数:组成三角形的顶点排列顺序:T = [n-1 n-2 n]。如果当前顶点是偶数:组成三角形的顶点排列顺序:T = [n-2 n-1 n]。依据以上原理,通过编程实现三角网的绘制以及纹理坐标的计算。
(2)设置法向量
glNormal*()用来设置当前的法向量,这个法向量将被应用到紧接下来的glVertex*()所定义的顶点上。但是通常各个顶点的法向是各不相同的,所以我们通常在定义每个顶点之前都为它确定一次法向量。
在一个平面内,有两条相交的线段,假设其中一条为矢量W,另一条为矢量V,平面法向为N,则平面法向就等于两个矢量的叉积(遵循右手定则),即N=W×V。例如:一个三角形平面三个顶点分别为P0、P1、P2,相应两个向量为W、V,本文定义ddGetNormal()函数来计算三角面片的法向量,计算方式如下列代码所示:
void ddGetNormal(GLfloat p0[3],GLfloat p1[3],GLfloat p2[3],GLfloat *ddnv)// p0,p1,p2 是三角形的三个顶点
{
GLfloat wx,wy,wz,vx,vy,vz,nr,nx,ny,nz;
wx=p0[0]-p1[0]; wy=p0[1]-p1[1]; wz=p0[2]-p1[2];
vx=p2[0]-p1[0]; vy=p2[1]-p1[1]; vz=p2[2]-p1[2];
nx=wz*vy-wy*vz;
ny=wx*vz-wz*vx;
nz=wy*vx-wx*vy;
nr=(float)sqrt(nx*nx+ny*ny+nz*nz); // 向量单位化
ddnv[0]=nx/nr; ddnv[1]=ny/nr; ddnv[2]=nz/nr;
}
4.指定纹理坐标与贴图
(1)读取RGB图像
FILE* pFile=fopen("bnu.bmp","rb");// 读取文件中图象的宽度和高度
fseek(pFile,0x0012,SEEK_SET);
fread(&width,4,1,pFile);
fread(&height,4,1,pFile);
fseek(pFile,BMP_Header_Length, SEEK_SET)//计算每行像素所占字节数,并根据此数据计算总像素字节数
if( fread(pixels, total_bytes, 1, pFile) <= 0 )//读取像素
OpenGL在以前的很多版本中,限制纹理的大小必须是2的整数次方,即纹理的宽度和高度只能是16,32,64,128,256等值,直到最近的新版本才取消了这个限制。而且,一些OpenGL实现并没有支持到如此高的OpenGL版本。因此,在使用纹理时要特别注意其大小。尽量使用大小为2的整次方的纹理。当这个要求无法满足时,使用gluScaleImage函数把图像缩放至所指定的大小。本实验用的是512*512大小的bmp格式的图片,把像素值赋值给pixels指针。
(2)启用纹理与载入纹理
具体载入程序如下:
void ddInitTexture(void)
{
glPixelStorei(GL_UNPACK_ALIGNMENT, 1); //设置像素存储格式
glTexImage2D(GL_TEXTURE_2D, 0, 3, ImageWidth,ImageHeight, 0, GL_RGB, GL_UNSIGNED_BYTE,pixels);//&Image[0][0][0]
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE,GL_DECAL);
}
(3)指定纹理坐标
纹理一般有两种定义法,它们分别是连续法以及离散法。其中,连续法是指纹理可以用一个合适的二元函数来表示,纹理空间就是这个二元函数的定义域。相反的,离散法则是用一个特定的二维数组来定义纹理函数,每个二维数组的元素就表示对应的纹理空间中行、列间隔都固定的网格点的纹理值。另外,两个网格点之间的点的纹理值就通过两个格点间的值进行插值运算的得出。最后再根据纹理与物体两个不同空间的坐标变换来把纹理正确贴到对应物体的表面。在这里,也就是利用纹理贴图技术将纹理影像经过坐标变换贴到由数字地面高程模型 DEM 所构建成的三维模型上,这个过程是产生真实三维感的重要环节。
当我们绘制一个三角形时,只需要指定三个顶点的颜色。三角形中其它各点的颜色不需要我们指定,这些点的颜色是OpenGL自己通过计算得到的。 在我们学习OpneGL 光照时,法线向量、材质的指定,都是只需要在顶点处指定一下就可以了,其它地方的法线向量和材质都是OpenGL 自己通过计算去获得。
纹理的使用方法也与此类似。只要指定每一个顶点在纹理图象中所对应的像素位置,OpenGL 就会自动计算顶点以外的其它点在纹理图象中所对应的像素位置。
以二维纹理为例,规定纹理最左下角的坐标为(0, 0),最右上角的坐标为(1, 1),于是纹理中的每一个像素的位置都可以用两个浮点数来表示(三维纹理会用三个浮点数表示,一维纹理则只用一个即可)。使用glTexCoord*系列函数来指定纹理坐标。这些函数的用法与使用glVertex*系列函数来指定顶点坐标十分相似。例如:glTexCoord2f(0.0f,0.0f);指定使用(0, 0)纹理坐标。
每个顶点使用不同的纹理,下面这样形式的代码是比较常见的。
glBegin(/*...*/);
glTexCoord2f(/*...*/);glVertex3f(/*...*/);
glTexCoord2f(/*...*/);glVertex3f(/*...*/);
/*...*/
glEnd();
当我们用一个坐标表示顶点在三维空间的位置时,可以使用glRotate*等函数来对坐标进行转换。只要使用glMatrixMode(GL_TEXTURE),就可以把三维物体转变为二维图象,将三维空间切换到二维纹理矩阵。为了指定当前操作的是何种矩阵,我们使用了函数glMatrixMode。依据以上原理,编程实现三角网的绘制和纹理坐标的计算如下:
for(n=0;n<dtmDY-1;n++)
{
glBegin (GL_TRIANGLE_STRIP); //创建连续的三角网
for(m=0;m<dtmDX;m++)
{
i0=n*dtmDX+m;
i2=(n+1)*dtmDX+m;
if(m<dtmDX-1) glNormal3fv(Nvs[n][2*m]);
glTexCoord2f((float)m/dtmDX,(float)(n+1)/dtmDY);
glVertex3f(DTMX[i2],DTMY[i2],DTMZ[i2]);
if(m<dtmDX-1) glNormal3fv(Nvs[n][2*m+1]);
glTexCoord2f((float)m/dtmDX, (float)n/dtmDY);
glVertex3f(DTMX[i0],DTMY[i0],DTMZ[i0]);
}
glEnd();
}
glEndList();
}
5.设置视角
投影变换就是定义一个可视空间,可视空间以外的物体不会被绘制到屏幕上。如果需要操作投影矩阵,需要以GL_PROJECTION 为参数调用glMatrixMode函数。gluPerspective的实现是通过将当前矩阵与通过这个函数指定的参数而建立的矩阵相乘来实现的,而在OpenGL中,矩阵的相乘都是连乘的,也就是说,调用gluPerspective这个函数会与其他的变化矩阵的函数效果相叠加从而影响原矩阵,因此,在调用这个函数之前,通常需要先调用glLoadidentity来把当前矩阵单位化,从而使各种变换效果不会叠加,比如旋转就只旋转,透视就只透视,通过调用glLoadidentity就不会既旋转有透视了。
透视投影所产生的结果类似于照片,有近大远小的效果,比如在火车头内向前照一个铁轨的照片,两条铁轨似乎在远处相交了。 使用gluPerspective函数可以将当前的可视空间设置为透视投影空间。其参数的意义如下。
void gluPerspective(
GLdouble fovy, //角度,指定视景体的视野的角度,以度数为单位,y轴的上下方向
GLdouble aspect,//在观察着的角度中物体宽度和高度的比例
GLdouble zNear,// 指定观察者到视景体的最近的裁剪面的距离(必须为正数)
GLdouble zFar //指定观察者到视景体的最远的裁剪面的距离(必须为正数)
)
三、实验结果
最终实现基于VC++的3D地形绘制与纹理贴图,如下图:
图2 Google earth 北京地区卫星影像模拟三维地形
总结
VisualC++是是一个功能非常强大的编程开发环境,在VC 环境下进行OpenGL的编程,调用的是DLL动态链接库,因此速度也不会受到影响。本文利用建模技术建了三维地形模型,在场景中使用光照、纹理映射技术使之具有了较强的真实感,最终实现了三维地形的可视化。
后续本文会把代码cpp与工程文件列出,供大家学习。