VRML解析——使用OpenGL绘制wrl三维模型

vrml        这里,我们首先释出编译好的读取wrl文件的程序,如左图所示,是打开的一个CASIA-3D-Face数据库的三维模型文件,使用Opengl进行了绘制。这个程序还提供了一些快捷键,使我们可以方便的移动模型和视角,如ASDW等。

        这里提供了一个编译好的demo的下载链接,请注意,程序必须运行在64位的Windows上,你需要打开prototxt文件,修改你需要打开的wrl文件的目录,由于使用静态库进行编译,你不需要相关环境(Opengl protobuf glog等),但是你可能需要安装VC2013Runtime X64

        当然,你也可以使用此博客中的代码,编译其它版本的程序,下面的内容会默认你已经了解了本博客前两节的内容,对wrl文件的格式和本文读取wrl文件的方式有所了解。在本程序的icp类中包含了OpenGL的相关内容,参考了 LeheOpenGL  的代码设计,如果你对OpenGL还不太了解,推荐经典入门教程:Nehe的OpenGL教程。不多说了,开始吧。

 OpenGL基础篇

1.glViewport

glViewport(GLint x,GLint y,GLsizei width,GLsizei height)

        这个函数有四个参数,以像素为单位指定了视口大小,前两个参数为视口坐下角的位置(默认为(0,0)),后两个参数指定了视口的宽和高。glViewport可以被用来设置一个比打开窗口更小的绘制区域,比如3DMax中的多视口建模,可以打开多个绘图窗口,对于每个视口独立渲染。

        多视口程序不能在窗口resize/reshape时直接调用glViewport,而应该在此时记下窗口大小,然后在绘制场景时多次调用glViewport设置每个视口的位置和大小。对于每个视口,应分别调用glMatrixMode(GL_PROJECTION)和glMatrixMode(GL_MODELVIEW)以设置投影和建模矩阵。

2.gluPerspective

gluPerspective(GLdouble fovy,GLdouble aspect,GLdouble zNear,GLdouble zFar)

        有一篇博客讲的太好了,以下是博主个人的总结:

        把opengl的视角比作一个相机,第一个参数fovy是视场角,第二个参数是实际窗口的横纵比,后两个参数是视场范围,就是有一个最近能看到的平面zNear和一个最远能看到的平面zFar,正如人在看景物时太近或太远都会看不清一样(光学系统都有景深),在opengl中也是同样的道理。

3.gluLookAt

        前面讲到OpenGL正如一个相机,如果说glViewport是在调节相机拍摄相片的大小,gluPerspective是在调节相机的镜头,那么gluLookAt就是在放置相机了。

void gluLookAt(GLdouble eyex,GLdouble eyey,GLdouble eyez,GLdouble centerx,GLdouble centery,GLdouble centerz,GLdouble upx,GLdouble upy,GLdouble upz);

        这个API乍看上去有三组参数:第一组参数是在以坐标方式指定放置相机的位置,第二组参数是在指定相机对准的位置点,也就是简介给出了相机的朝向,而第三组参数则是在指定相机本身的姿态,以向量方式规定了相机的Up方向,如(0,1,0)就是正立观看,(1,0,0)就是把相机躺着放置地面上,(0,-1,0)就是把相机倒过来180度,图像就是倒立的了。当然也可以解算出相机的 pitch轴 yaw轴 roll轴 的方位角。

4.glBegin

        与我们眼中的世界不同,计算机的三维世界没有曲面 没有曲线,都是点云 直线段和三角形。glBegin就开始绘制以定点方式指定绘制图形了。由于OpenGL是一组状态机,所以着色器在处理图元时会参照之前的设置,下面给出glBegin的图元绘制方式和glBegin/glEnd间的着色器设置API。

        GL_POINTS:把每一个顶点作为一个点进行处理,顶点n即定义了点n,共绘制N个点
        GL_LINES:把每一个顶点作为一个独立的线段,顶点2n-1和2n之间共定义了n条线段,总共绘制N/2条线段
        GL_LINE_STRIP:绘制从第一个顶点到最后一个顶点依次相连的一组线段,第n和n+1个顶点定义了线段n,总共绘制n-1条线段
        GL_LINE_LOOP:绘制从第一个顶点到最后一个顶点依次相连的一组线段,然后最后一个顶点和第一个顶点相连,第n和n+1个顶点定义了线段n,总共绘制n条线段
        GL_TRIANGLES:把每个顶点作为一个独立的三角形,顶点3n-2、3n-1和3n定义了第n个三角形,总共绘制N/3个三角形
        GL_TRIANGLE_STRIP:绘制一组相连的三角形,对于奇数n,顶点n、n+1和n+2定义了第n个三角形;对于偶数n,顶点n+1、n和n+2定义了第n个三角形,总共绘制N-2个三角形
        GL_TRIANGLE_FAN:绘制一组相连的三角形,三角形是由第一个顶点及其后给定的顶点确定,顶点1、n+1和n+2定义了第n个三角形,总共绘制N-2个三角形
        GL_QUADS:绘制由四个顶点组成的一组单独的四边形。顶点4n-3、4n-2、4n-1和4n定义了第n个四边形。总共绘制N/4个四边形
        GL_QUAD_STRIP:绘制一组相连的四边形。每个四边形是由一对顶点及其后给定的一对顶点共同确定的。顶点2n-1、2n、2n+2和2n+1定义了第n个四边形,总共绘制N/2-1个四边形
        GL_POLYGON:绘制一个凸多边形。顶点1到n定义了这个多边形。
        glVertex*() 设置顶点坐标 
        glColor*() 设置当前颜色 
        glIndex*() 设置当前颜色表 
        glNormal*() 设置法向坐标 
        glEvalCoord*() 产生坐标 
        glCallList(),glCallLists() 执行显示列表 
        glTexCoord*() 设置纹理坐标 
        glEdgeFlag*() 控制边界绘制 
        glMaterial*() 设置材质

绘制篇

1.VRML格式

左图是我们处理的wrl文件的结点结构,可以在vrmlpad3.0中进行查看,文件指定了模型三维世界的各种信息。

node

        VRML语言的格式再前两篇博客中已经介绍过了,这里只介绍编程上的处理。在处理VRML语言时,每个顶点都按顺序由一个索引值,比如最开头的点index=0,第二个点index=1……,而计算机的世界是由三角形构成的,于是在openGL绘制图形时,就会使用这些点的索引,从点云数据中找到三个点作为三角形的顶点,查看下面的wrl文件你可能会更清楚一些:

		geometry IndexedFaceSet {
			coord
			Coordinate {
				point [  -143.705 -200.667 -1644,
					 -143.129 -201.525 -1637.29,
					 -141.77 -200.39 -1641.39,
					 -141.235 -201.197 -1634.83,
					 -140.022 -200.046 -1640.07,
					 -139.323 -200.949 -1632.37,
					 -138.056 -199.846 -1637.31,
					 -137.466 -200.803 -1630.51,
					 -136.18 -199.616 -1635.17,
					 -135.876 -200.499 -1630.57,
					 -134.209 -199.487 -1632.65,
					 -134.138 -200.424 -1629.57,
					 -132.396 -199.304 -1631.02,
......
			colorPerVertex TRUE
			color
			Color { color [  0.447059 0.364706 0.239216,
			 0.466667 0.380392 0.223529,
			 0.454902 0.388235 0.239216,
			 0.415686 0.298039 0.188235,
			 0.470588 0.407843 0.215686,
			 0.376471 0.223529 0.133333,
			 0.454902 0.364706 0.223529,
			 0.443137 0.396078 0.211765,
			 0.454902 0.301961 0.2,
			 0.486275 0.423529 0.247059,
			 0.427451 0.301961 0.168627,
			 0.498039 0.423529 0.25098,
			 0.466667 0.4 0.235294,
			 0.533333 0.427451 0.247059,
			 0.505882 0.419608 0.258824,
			 0.521569 0.423529 0.254902,
			 0.545098 0.431373 0.25098,
			 0.529412 0.435294 0.258824,
......
coordIndex [0, 1, 2, -1,
					1, 3, 2, -1,
					2, 3, 4, -1,
					3, 5, 4, -1,
					4, 5, 6, -1,
					5, 7, 6, -1,
					6, 7, 8, -1,
					7, 9, 8, -1,
					8, 9, 10, -1,
					9, 11, 10, -1,
					10, 11, 12, -1,
					11, 13, 12, -1,
					12, 13, 14, -1,
					13, 15, 14, -1,
					14, 15, 16, -1,
					15, 17, 16, -1,
					16, 17, 18, -1,
					17, 19, 18, -1,
					18, 19, 20, -1,
					19, 21, 20, -1,
					20, 21, 22, -1,
					21, 23, 22, -1,
					22, 23, 24, -1,
					23, 25, 24, -1,
					24, 25, 26, -1,
					25, 27, 26, -1,
					26, 27, 28, -1,
					27, 29, 28, -1,
					28, 29, 30, -1,
......

在索引中-1代表当前绘制面的结束,-1之前的各个点构成一个多边形,之所以本文件中每个多边形都是三角形,这也是为了方便OpenGL直接使用三角形图元的方式进行绘制。

2.数据转储

在前一篇博客中,已经使用wrl类将wrl文件中需要的数据读入内存中了,下面我们正式开始介绍绘制三维图形的icp类。

为了方便使用索引,我们将各个顶点的颜色 法向量 位置 等信息转储到一个特殊的结构体中:

	typedef struct{
		GLdouble point[3];
		GLdouble color[3];
		GLdouble vecte[3];
	} vertext;
	int		SetVertex(std::vector<cv::Scalar> &,
			std::vector<cv::Scalar> &, std::vector<cv::Scalar> &);
	int		SetIndex(std::vector<std::vector<unsigned int>> &);

	std::vector<vertext> shape;
	std::vector<std::vector<unsigned int>> index;

// ......
int	COpenGLControl::SetVertex(std::vector<cv::Scalar> &point,
	std::vector<cv::Scalar> &color, std::vector<cv::Scalar> &vecte){
	//CHECK_EQ(point.size(), color.size()) << "point count is not same";
	//CHECK_EQ(point.size(), vecte.size()) << "point count is not same";
	shape.clear();
	cv::Scalar cs;
	vertext vec;
	for (int i = 0; i < point.size(); i++){
		cs = point[i];
		vec.point[0] = cs.val[0];
		vec.point[1] = cs.val[1];
		vec.point[2] = cs.val[2];
		cs = color[i];
		vec.color[0] = cs.val[0];
		vec.color[1] = cs.val[1];
		vec.color[2] = cs.val[2];
		cs = vecte[i];
		vec.vecte[0] = cs.val[0];
		vec.vecte[1] = cs.val[1];
		vec.vecte[2] = cs.val[2];
		shape.push_back(vec);
	}
	//CHECK_EQ(point.size(), shape.size()) << "point count is not same";
	//LOG(INFO) << "openGL received point count: " << shape.size();
	return (int)shape.size();
}

int	COpenGLControl::SetIndex(std::vector<std::vector<unsigned int>> & surface){
	index.clear();
	index = surface;
	//LOG(INFO) << "openGL received index count: " << index.size();
	return 0;
}

3.Opengl绘制

在opengl中,需要预先初始化光照 视角 位置等信息,我们注意到wrl文件中给出了viewpoint,于是我们的初始化过程如下:

		glShadeModel(GL_SMOOTH);
		glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
		glClearDepth(1.0f);										// 设置深度缓存
		glEnable(GL_DEPTH_TEST);								// 启用深度测试
		glDepthFunc(GL_LEQUAL);									// 所作深度测试的类型
		glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);		// 告诉系统进行透视修正
		glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient);
		glLightfv(GL_LIGHT1, GL_DIFFUSE, LightDiffuse);
		glLightfv(GL_LIGHT1, GL_POSITION, LightPosition);
		glEnable(GL_LIGHT1);
		glViewport(0, 0, cx, cy);
		glMatrixMode(GL_PROJECTION);
		glLoadIdentity();
		gluPerspective(25.0f, (float)cx / (float)cy, 1.0f, 2000.0f);
		glMatrixMode(GL_MODELVIEW);
		glLoadIdentity();
		gluLookAt(m_eyeX, m_eyeY, m_eyeZ,
			m_centerX, m_centerY, m_centerZ,
			m_upX, m_upY, m_upZ);
		glutPostRedisplay();

        好了,这样我们几乎配置好opengl了,直接使用三角图元方式进行绘制即可,于是我们不加解释的给出下面的绘图函数,其中一些涉及opengl的问题已经在这篇博客的第一节进行了解释。

		glBegin(GL_TRIANGLES); {
			for (int i = 0; i < index.size(); i++){
				for (int j = 0; j < 3; j++){
					unsigned int idx = index[i][j];
					glColor3dv(shape[idx].color);
					glNormal3dv(shape[idx].vecte);
					glVertex3dv(shape[idx].point);
				}
			}
		}

        连同上一节博客,本文就实现了一个VRML解释器。

        See You Next Chapter!

 

发表评论