又有好久没更新博客了,今天周末抽出一点时间写一些OpenGL用来绘制GIS中矢量和影像的文章。
OpenGL的介绍我在这里就没有必要介绍了,那OpenGL和QT的结合在这里就有必要先介绍一下,也就是怎么使用QT下的OpenGL框架。要想使用QT下的OpenGL框架,就必须要子类化QGLWidget,然后实现
void initializeGL(); //初始化窗口
void paintGL(); //画窗口
void resizeGL( int width, int height ); //重置窗口
这三个函数就可以了,第一个函数是初始化OpenGL的函数,函数如下:
void GeoGLWidget::initializeGL()
{
//initglew();
glewInit();
glClearColor(0.0, 0.0, 0.0, 0.0);
//glClearColor(1.0, 1.0, 1.0, 1.0);
glShadeModel(GL_FLAT);
glEnable(GL_LINE_SMOOTH);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glHint (GL_LINE_SMOOTH_HINT, GL_DONT_CARE);
//启用顶点数组
glEnableClientState(GL_VERTEX_ARRAY);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_LINE_SMOOTH);
const GLubyte* OpenGLVersion= glGetString(GL_VERSION);//返回当前OpenGL实现的版本号
const GLubyte* name =glGetString(GL_VENDOR);
const GLubyte* pszRender= glGetString(GL_RENDERER);
const GLubyte* pszGluVersion= gluGetString(GLU_VERSION);
gluGetString(GLU_EXTENSIONS);
const GLubyte *glslVersion=
glGetString(GL_SHADING_LANGUAGE_VERSION );
GLint nMaxStack = 0;
glGetIntegerv(GL_MAX_MODELVIEW_STACK_DEPTH,&nMaxStack);
printf("%d",nMaxStack);
if ( ! GLEW_ARB_vertex_program )
{
fprintf(stderr, "ARB_vertex_programis missing!\n");
}
//const GLubyte*glslVersion =glGetString(GL_SHADING_LANGUAGE_VERSION);
printf("%s\n",glslVersion);
//获得OpenGL扩展信息
const GLubyte *extensions= glGetString(GL_EXTENSIONS);
GLint nExtensions;
glGetIntegerv(GL_NUM_EXTENSIONS, &nExtensions);
}
第二个函数就是来实现绘图操作的函数
void GeoGLWidget::paintGL()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glColor3f(0.0, 1.0, 0.0);
glLoadIdentity();
glEnable(GL_POINT_SMOOTH) ;
glLineWidth(2.0f);
glFlush();
}
第三个参数就是窗口大小变化的重绘函数,基本的函数实现如下:
void GeoGLWidget::resizeGL( int width, int height )
{
if ( height == 0 )
{
height= 1;
}
int nWidth = width< height ? width: height;
glViewport(0, 0, /*0.5**/(GLint)width, /*0.5**/(GLint)height);
glMatrixMode(GL_PROJECTION );
glLoadIdentity();
GeoEnvelopegeoEnv;
m_poLayer->GetEnvelope(&geoEnv);
if (width <= height)
{
glOrtho(geoEnv.minX,geoEnv.maxX,geoEnv.minY
,geoEnv.maxY*((GLfloat)height/(GLfloat)width),1.0f,-1.0f);
}
else
{
GLdoublefScale = ((GLfloat)width/(GLfloat)height);
GLdouble fRight= geoEnv.maxX+(geoEnv.GetWidth()/width);
glOrtho(geoEnv.minX,
fRight,geoEnv.minY,geoEnv.maxY,1.0f,-1.0f);
}
}
由于GIS和影像主要是在二维平面上绘图,所以我这边使用正射投影,正射投影函数如下:
GLAPI void GLAPIENTRY glOrtho(GLdouble left, GLdouble right,GLdouble bottom,GLdouble top,GLdouble zNear,GLdouble zFar);
left是指窗口左边的坐标,right为窗口右边的坐标,同理bottom, top就代表上边界和下边界的坐标。zNea和zFar分辨代表近裁剪面和远裁剪面到坐标原点的距离。
这样一个基本的框架就搭建完成了,下一步是如何读取影像和矢量的数据并绘制出来。
GIS数据读取采用自己实现的简化版GIS引擎来读取,其实这里引擎部分实现的是符合OpenGIS简单要素访问协议规范的GIS内核,数据读取引擎只不过是将文件中的数据读出来转换为引擎中的数据结构,下一步可以实现简单的插件架构,GIS内核封装为底层核心API,各种数据读取封装为数据驱动插件API,然后在底层核心API和插件API的基础上构建应用程序。
以shapefile为例,通过下面的类实现读取
class GeoShapeLayer : public GeoVectorLayer
GeoShapeLayer是shapefile文件对应的驱动类,GeoVectorLayer是矢量数据图层的一个抽象类,本身只提供接口,具体的功能实现在各种数据格式的图层中,这样就保证了系统的可扩展性和稳定性。
对于影像数据的读取,我这边采用GDAL来读取所支持的类型,对于其他数据格式GDAL不支持的,也可以自己封装数据格式的原生API来实现读取。
class GeoGdalImageLayer : publicGeoImageLayer
GeoGdalImageLayer类就是GDAL支持的数据格式驱动,同样GeoImageLayer是一个抽象类。
下面,就以一个三波段8位的影像数据作为例子实现数据读取。
int GeoGdalImageLayer::ReadData(int nBandCount,int *pBandIndex,int nXstart,int nYstart,
int nWidth,int nHeight,int nBufXSize,int nBufYSize,void* poData)
{
assert(nBandCount > 0);
assert(pBandIndex != NULL);
assert(poData != NULL);
if (m_poDataset != NULL)
{
int nDataSize = GDALGetDataTypeSize(GDT_Byte)/8;
m_poDataset->RasterIO(GF_Read,nXstart,nYstart,nWidth,nHeight,poData,nBufXSize,nBufYSize,GDT_Byte,nBandCount,pBandIndex,nBandCount*nDataSize,
nBufXSize*nBandCount*nDataSize,1*nDataSize);
return1;
}
return 0;
}
这样数据的排列格式就是BIP格式,如果只有三个波段,我们也可以理解为RGBRGBRGB排列格式,这样便于OpenGL的绘制。具体的RasterIO函数详解可以参考GDAL的官方文档。好了,数据读取就到这里了。接下来该到绘制部分了吧。
这里为了说明问题,我找的矢量数据和影像数据有重叠的部分,都是福州市区的。这里我先读取矢量数据,先确定了OpenGL的正射投影的范围。这个确定了之后,接下来该确定影像的读取范围,是读取整个范围还是一部分,这个需要简单的计算,根据矢量的MBR,下面的代码是确定影像读取范围的行列号。
int bandList[3]= {1,2,3};
long nLeft = 0;
long nTop = 0;
long nRight = 0;
long nBottom = 0;
m_pImageLayer->WorldToPixel(geoEnv.minX,geoEnv.maxY,nLeft,nTop);
m_pImageLayer->WorldToPixel(geoEnv.maxX,geoEnv.minY,nRight,nBottom);
if (nLeft < 0)
{
nLeft= 0;
}
if (nRight >= m_pImageLayer->GetWidth());
{
nRight= m_pImageLayer->GetWidth()-1;
}
if (nTop < 0)
{
nTop= 0;
}
if (nBottom >= m_pImageLayer->GetHeight())
{
nBottom= m_pImageLayer->GetHeight()-1;
}
int nReadWidth = nRight-nLeft+1;
int nReadHeight = nBottom-nTop+1;
影像的像素范围计算出来后,然后需要将像素范围转换为屏幕像素范围,这主要是为了确定绘图的区域。
//然后计算像素范围对应的地理范围
double winMinx = 0;
double winMaxx = 0;
double winminy = 0;
double winmaxy = 0;
m_pImageLayer->PixelToWorld(nLeft,nTop,winMinx,winmaxy);
m_pImageLayer->PixelToWorld(nRight,nBottom,winMaxx,winminy);
PixelToWorld实现影像像素坐标转为地理坐标。这个影像对应的范围确定之后,然后需要转换为屏幕上对应的像素宽度和高度,这样绘制的位置才能正确无误。
//计算地理区域对应的屏幕像素区域,这就是绘制影像的窗口范围
GLdouble dbLeft = 0;
GLdouble dbRight = 0;
GLdouble dbTop = 0;
GLdouble dbBottom = 0;
WorldToScreen(winMinx,winmaxy,1.0,&dbLeft,&dbTop);
WorldToScreen(winMaxx,winminy,1.0,&dbRight,&dbBottom);
m_nDrawLeft= winMinx;
m_nDrawTop= winmaxy;
int nDrawWidth = fabs(dbRight-dbLeft)+1;
int nDrawHeight = fabs(dbBottom-dbTop)+1;
m_nDrawWidth= nDrawWidth;
m_nDrawHeight= nDrawHeight;
上面确定了影像绘制的起始坐标和宽度,就可以用glRasterPos3d和glDrawPixels来绘制影像了。
注意,OpenGL是从底向上扫描图像,而我们的遥感影像一般是从左上角的像素开始,为了保证影像看上去不是被翻转了的,可以再绘制前先用glPixelZoom函数设置下,具体的代码片段如下:
glPixelStorei(GL_UNPACK_ALIGNMENT,1);
glRasterPos3d(m_nDrawLeft,m_nDrawTop,0);
glPixelZoom(1.0,-1.0); //从上到下绘制
if (m_poData != NULL)
{
glDrawPixels(m_nDrawWidth,m_nDrawHeight,GL_RGB,GL_UNSIGNED_BYTE,m_poData);
}
上面的代码片段中WorldToScreen是将世界坐标转换为屏幕像素坐标的过程。这个转换过程主要用到GLU库中的gluProject函数,其声明如下:
int APIENTRY gluProject (
GLdouble objx,
GLdouble objy,
GLdouble objz,
const GLdouble modelMatrix[16],
const GLdouble projMatrix[16],
const GLint viewport[4],
GLdouble *winx,
GLdouble *winy,
GLdouble *winz);
objx,objy,objz代表物体的三维坐标,modelMatrix[16]代表模型视图矩阵、projMatrix[16]代表投影矩阵,viewport[4]为定义的视口变换,最后三个参数就是OpenGL的窗口坐标,注意他的左下角是原点,这是和窗口坐标的不同点,废话少说,上代码吧:
void GeoGLWidget::WorldToScreen(GLdoubleobjx, GLdoubleobjy, GLdoubleobjz, GLdouble*winx, GLdouble*winy)
{
//获得当前的模型变换矩阵
double dbModelMatrixs[16];
glGetDoublev(GL_MODELVIEW_MATRIX,dbModelMatrixs);
//获得投影变换的矩阵
double dbProjectionMartixs[16];
glGetDoublev(GL_PROJECTION_MATRIX,dbProjectionMartixs);
//获得视口坐标
GLint viewport[4];
glGetIntegerv(GL_VIEWPORT,viewport);
//获得opengl的视口坐标
GLdouble winX, winY, winZ;
gluProject(objx,objy,objz,dbModelMatrixs,dbProjectionMartixs,viewport,&winX,&winY,&winZ);
//求得opengl窗口坐标
*winx = (GLdouble)winX;
*winy = (GLdouble)viewport[3]- (GLdouble)winY;
}
不出意外,影像能够绘制在窗口中。
好了,既然影像已经显示出来了,最后要将矢量叠加上进行显示。
矢量的显示最原始的做法是使用glVertex系列函数,然而这种方法对于数据量比较大的话多次调用该函数会导致效率降低,为了提高效率,本文使用缓冲区对象存储顶点数组。
下面是绘制折线的代码片段
//渲染折线
glColor3f(0.0, 1.0, 1.0);
GeoCoordinate*poPoints = newGeoCoordinate[poRing->GetNumPoint()];
poRing->GetPoints(poPoints);
std::vector<double>vecVertexs;
int n = poRing->GetNumPoint();
GLuint*pIndex = newGLuint[n];
for(int j = 0; j < n; j ++)
{
vecVertexs.push_back(poPoints[j].x);
vecVertexs.push_back(poPoints[j].y);
pIndex[j] = j;
}
delete[]poPoints;
GLuint *pBuffer= new GLuint[1];
//glGenBuffersARB(1,pBuffer);
glGenBuffers(1,pBuffer);
glBindBuffer(GL_ARRAY_BUFFER,pBuffer[0]); //绑定对象、
glBufferData(GL_ARRAY_BUFFER,sizeof(double)*n*2,&vecVertexs[0],GL_STATIC_DRAW);//分配空间
glVertexPointer(2,GL_DOUBLE,0,BUFFER_SET(0)); //指定顶点
glDrawElements(GL_LINE_STRIP,poRing->GetNumPoint(),GL_UNSIGNED_INT,pIndex);
glFlush();
这样,我们就将影像和矢量叠加上了。如下图所示:
从上面的图可以看出,影像的范围比矢量小,我将投影范围设置为影像的范围,如下图所示:
其实这只是最简单的一个demo,在OpenGL的学习和钻研上还需要深入下去。