网格化是将凹多边形或有边相交的多边形划分成凸多边形。由于openGL渲染时只接受凸多边形,这些非凸多边形在渲染之前必须先被网格化。
第一行中第一个图形是4条边的凹多边形,第二个图形中间有个洞,第三个图形有相交的边
下载: tessellation.zip, stencilTess.zip
概述
网格化基本的步骤是将所有非凸多边形的顶点坐标发送到网格器而不是直接发送到OpenGL渲染管线中,然后网格器将所有多边形网格化。最后,网格化工作完成以后,网格器使用用户定义的回调模式调用实际的OpenGL命令来渲染网格化后的多边形。
OpenGL提供了一系列的将凹多边形处理成凸多边形的模式。
GLUtessellator* gluNewTess()
void gluDeleteTess(GLUtessellator *tess)
void gluTessBeginPolygon(GLUtessellator *tess, void *userData)
void gluTessEndPolygon(GLUtessellator *tess)
void gluTessBeginContour(GLUtessellator *tess)
void gluTessEndContour(GLUtessellator *tess)
一个多边形可能有多个封闭的轮廓线(封闭的回路),例如,一个有洞的多边形有2条回路,里面一条外面一条。每一个回路必须被gluTessBeginContour()和gluTessEndContour()包含。这是在gluTessBeginPolygon()和gluTessEndPolygon()中内嵌的块。
void gluTessVertex(GLUtessellator *tess, GLdouble cords[3], void *vertexData)
void gluTessCallback(GLUtessellator *tess, GLUenum type, void (*fn)())
下面一小段代码显示了网格化的使用方法:
// 创建网格器
GLUtesselator *tess = gluNewTess();
// 注册回调函数
gluTessCallback(tess, GLU_TESS_BEGIN, beginCB);
gluTessCallback(tess, GLU_TESS_END, endCB);
gluTessCallback(tess, GLU_TESS_VERTEX, vertexCB);
gluTessCallback(tess, GLU_TESS_COMBINE, combineCB);
gluTessCallback(tess, GLU_TESS_ERROR, errorCB);
// 描述非凸多边形的顶点
gluTessBeginPolygon(tess, user_data);
// 第一条回路
gluTessBeginContour(tess);
gluTessVertex(tess, coords[0], vertex_data);
...
gluTessEndContour(tess);
// 第二条回路
gluTessBeginContour(tess);
gluTessVertex(tess, coords[5], vertex_data);
...
gluTessEndContour(tess);
...
gluTessEndPolygon(tess);
// 处理完成后删除网格器
gluDeleteTess(tess);
下载源文件和可执行文件:tessellation.zip
一个简单的凹多边形的例子
凹多边形的网格化
这个轮廓的网格化模式定义在tessellate1()函数中。OpenGL网格器定义和多种基本的图元类型来高效地执行网格化:GL_TRIANGLE, GL_TRIANGLE_FAN, GL_TRIANGLE_STRIP and GL_LINE_LOOP。本例中,网格器是使用GL_TRIANGLE_FAN将多边形划分成三角形。
你可以将实际的OpenGL记录在回调函数中,这些函数在网格化的过程中会被执行。下面的代码是网格器生成的并记录在回调函数中。在源文件中查看回调函数式如何记录的。
glBegin(GL_TRIANGLE_FAN);
glVertex3dv(v3);
glVertex3dv(v0);
glVertex3dv(v1);
glVertex3dv(v2);
glEnd();
void gluTessNormal(GLUtessellator *tess, GLdouble x, GLdouble y, GLdouble z)
如果你用(0,0,1)指定了法向量,那么你将一直看到多边形的前面即使环绕方式是顺时针的(我假设照相机是指向默认的方向,即-Z轴)。默认的法向量的值是(0,0,0),这意味着网格器会更加给定的顶点和环绕方式计算法向量。
有洞的多边形
第二个例子有两个逆时针的回路,里面一个外面一个。它被定义在tessellate2()中。网格器一个三角形一个三角形地生成实际的OpenGL命令。
glBegin(GL_TRIANGLE_STRIP);
glVertex3dv(v1);
glVertex3dv(v5);
glVertex3dv(v0);
glVertex3dv(v4);
glVertex3dv(v3);
glVertex3dv(v7);
glVertex3dv(v2);
glVertex3dv(v6);
glVertex3dv(v5);
glEnd();
glBegin(GL_TRIANGLES);
glVertex3dv(v5);
glVertex3dv(v1);
glVertex3dv(v2);
glEnd();
如果内部的轮廓是顺时针方向的,那么现在内部区域的环绕数字是0,外面的是1。因此,内部的区域依然是一个洞(不填充)因为内部区域的环绕数字不是偶数,而是0。环绕的规则在下面会讲到。
有边相交的多边形
最后一个例子是一个星型,它有边相交并被定义在tessellator3()。注意网格器添加了5个额外的顶点并插入了2条边,v5,v6,v7,v8和v9。当网格器算法检测到插入边时。GLU_TESS_COMBINE回调函数必须被提供来创建新的顶点,我们稍后会绘制它。
void combineCB(GLdouble newVert[3], GLdouble *neighbourVert[4],
GLfloat neighborWeight[4], void **outData);
下面的OpenGL命令是使用网格器生成的:
glBegin(GL_TRIANGLE_FAN);
glVertex3dv(v9);
glVertex3dv(v0);
glVertex3dv(v5);
glVertex3dv(v7);
glVertex3dv(v8);
glVertex3dv(v2);
glEnd();
glBegin(GL_TRIANGLE_FAN);
glVertex3dv(v6);
glVertex3dv(v1);
glVertex3dv(v7);
glVertex3dv(v5);
glVertex3dv(v3);
glEnd();
glBegin(GL_TRIANGLES);
glVertex3dv(v4);
glVertex3dv(v8);
glVertex3dv(v7);
glEnd();
使用GLU_TESS_WINDING_ODD规则,中间区域不会被填充因为它的环绕数字是偶数。我们需要另外一个环绕规则,这样我们可以填充环绕数字为奇数和偶数的区域。这种情况下则应该使用GLU_TESS_WINDING_NODZERO环绕规则,它会填充环绕数字非0的区域。(GLU_TESS_WINDING_POSITIVE同时也工作)
网格器提供gluTessProerty()函数改变环绕规则和其他属性,例如GLU_TESS_BOUNDARY_ONLY,glu_TESS_TOLERANCE。更多环绕规则的内容查看下面。
环绕规则和环绕数字
假设多个轮廓线,相互之间重叠或嵌套,将平面划分成了多个区域。环绕规则决定了哪些区域是里面还是外面。这样里面会被填充,外面不会被填充。
对每个被多条轮廓线封闭的区域,OpenGL网格器给这个区域分配了一个环绕数字。从一个区域内一点向各个方向发射一条射线。从0开始计数,如果这条射线与逆时针的轮廓线相交则加1,与顺时针的轮廓线相交则减1。计数完所有相交线后,最后环绕的数字表示那个区域。
如果环绕的规则是GLU_TESS_WINDING_ODD,环绕数字是奇数的区域是里面并被填充,环绕数字是偶数的区域是外面并不被填充。因此区域1和-1会被填充,区域0是一个洞。
可能的环绕规则如下:
GLU_TESS_WINDING_ODD: 填充奇数,默认的设置
GLU_TESS_WINDING_NONZERO: 填充非零区域
GLU_TESS_WINDING_POSITIVE: 填充正数区域
GLU_TESS_WINDING_NEGATIVE: 填充负数区域
GLU_TESS_WINDING_ABS_GEQ_TWO: 填充绝对值大于或等于2的区域
下面的图像显示了不同环绕规则下定义的不同的里面。如果GLU_TESS_WINDING_ABS_GEQ_TWO别设置,那么什么都不会绘制(所有的区域都是外面)。