OpenGL系列教程之十一:OpenGL网格化

网格化是将凹多边形或有边相交的多边形划分成凸多边形。由于openGL渲染时只接受凸多边形,这些非凸多边形在渲染之前必须先被网格化。


OpenGL系列教程之十一:OpenGL网格化_第1张图片OpenGL系列教程之十一:OpenGL网格化_第2张图片

第一行中第一个图形是4条边的凹多边形,第二个图形中间有个洞,第三个图形有相交的边


下载: tessellation.zip, stencilTess.zip





概述


网格化基本的步骤是将所有非凸多边形的顶点坐标发送到网格器而不是直接发送到OpenGL渲染管线中,然后网格器将所有多边形网格化。最后,网格化工作完成以后,网格器使用用户定义的回调模式调用实际的OpenGL命令来渲染网格化后的多边形。


OpenGL提供了一系列的将凹多边形处理成凸多边形的模式。

GLUtessellator* gluNewTess()

void gluDeleteTess(GLUtessellator *tess)


gluNewTess()创建一个网格器对象,gluDeleteTess()删除指定的网格器对象。如果创建失败,将会返回NULL。

void gluTessBeginPolygon(GLUtessellator *tess, void *userData)

void gluTessEndPolygon(GLUtessellator *tess)

不像之前使用glBegin()和glEnd()块来描述多边形的顶点,你需要使用特殊的网格器块,gluTessBeginPolygon()和gluTessEndPolygon().你必须在这个块中描述非凸多边形。

void gluTessBeginContour(GLUtessellator *tess)

void gluTessEndContour(GLUtessellator *tess)

一个多边形可能有多个封闭的轮廓线(封闭的回路),例如,一个有洞的多边形有2条回路,里面一条外面一条。每一个回路必须被gluTessBeginContour()和gluTessEndContour()包含。这是在gluTessBeginPolygon()和gluTessEndPolygon()中内嵌的块。


void gluTessVertex(GLUtessellator *tess, GLdouble cords[3], void *vertexData)


gluTessVertex()指定了回路的顶点。网格器使用这些顶点坐标执行网格化。所有的顶点需要位于同一个面中。第二个参数是网格化需要的顶点坐标,第三个参数是实际用来渲染的坐标,它可能不仅是顶点坐标,也可能是颜色坐标,法向量坐标,纹理坐标。

void gluTessCallback(GLUtessellator *tess, GLUenum type, void (*fn)())

在网格化的过程中,当OpenGL准备渲染网格化后的多边形形时网格器会调用一系列的回调模式。你必须指定适当的回调函数,这些回调函数包括实际渲染多边形的OpenGL命令,如glBegin(),glEnd(),glVertex*()等。


下面一小段代码显示了网格化的使用方法:

// 创建网格器
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);





例子:

OpenGL系列教程之十一:OpenGL网格化_第3张图片

上图中有3个网格器的例子,左边是一个简单的有4个顶点的凹多边形,中间是一个有洞的多边形,右边是一个有相交边的多边形(星形)。


下载源文件和可执行文件:tessellation.zip





一个简单的凹多边形的例子

OpenGL系列教程之十一:OpenGL网格化_第4张图片

凹多边形的网格化


这个轮廓的网格化模式定义在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();

注意多边形的环绕方式是逆时针的(CCW),这样多边形的表面法向量是指离多边形的。如果环绕方式是顺时钟的(CW),表面法向量则是指向多边形的,这样你看到的是多边形的背面而不是前面。你可以使用gluTessNormal()来明确地指明表面法向量。

void gluTessNormal(GLUtessellator *tess, GLdouble x, GLdouble y, GLdouble z)

如果你用(0,0,1)指定了法向量,那么你将一直看到多边形的前面即使环绕方式是顺时针的(我假设照相机是指向默认的方向,即-Z轴)。默认的法向量的值是(0,0,0),这意味着网格器会更加给定的顶点和环绕方式计算法向量。





有洞的多边形


OpenGL系列教程之十一:OpenGL网格化_第5张图片

第二个例子有两个逆时针的回路,里面一个外面一个。它被定义在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();

你可能会疑惑OpenGL网格器是怎么知道中间的区域是洞的(不需要填充)。答案是环绕的规则和环绕的数字。网格器给被回路划分的多个区域分配了环绕的数字。在本例中,有2个单独的区域:里面区域被分配的环绕数字是2,外面区域是1。使用默认的环绕规则,GLU_TESS_WINDING_ODD,被奇数标记的区域会被填充,被偶数标记的区域则不会被填充。


如果内部的轮廓是顺时针方向的,那么现在内部区域的环绕数字是0,外面的是1。因此,内部的区域依然是一个洞(不填充)因为内部区域的环绕数字不是偶数,而是0。环绕的规则在下面会讲到。




有边相交的多边形

OpenGL系列教程之十一:OpenGL网格化_第6张图片

最后一个例子是一个星型,它有边相交并被定义在tessellator3()。注意网格器添加了5个额外的顶点并插入了2条边,v5,v6,v7,v8和v9。当网格器算法检测到插入边时。GLU_TESS_COMBINE回调函数必须被提供来创建新的顶点,我们稍后会绘制它。

void combineCB(GLdouble newVert[3], GLdouble *neighbourVert[4],
               GLfloat neighborWeight[4], void **outData);

当两条边插入时回调函数用来创建新的顶点。它需要4个参数:newVert[3]是x,y,z坐标的一个数组,这是网格器检测到的新插入的顶点的坐标。第二个参数指向4个邻接顶点的坐标的指针。第3个参数是4个邻接顶点的权重因素。这个权重值会被用来计算插入点的颜色,法向量,或者纹理坐标。最后一个参数是指向输出顶点数据的指针。输出数据可能不止包含顶点坐标,也包含颜色,法向量,纹理坐标。玩个器将会将输出数据传送到GLU_TESS_VERTE回调模式中来绘制插入的顶点数据。


下面的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();

在这个多边形中考虑环绕规则和环绕数字。多边形被分成了6个单独的区域,环绕的数字如图所示:

OpenGL系列教程之十一:OpenGL网格化_第7张图片

使用GLU_TESS_WINDING_ODD规则,中间区域不会被填充因为它的环绕数字是偶数。我们需要另外一个环绕规则,这样我们可以填充环绕数字为奇数和偶数的区域。这种情况下则应该使用GLU_TESS_WINDING_NODZERO环绕规则,它会填充环绕数字非0的区域。(GLU_TESS_WINDING_POSITIVE同时也工作)


网格器提供gluTessProerty()函数改变环绕规则和其他属性,例如GLU_TESS_BOUNDARY_ONLY,glu_TESS_TOLERANCE。更多环绕规则的内容查看下面。




环绕规则和环绕数字


假设多个轮廓线,相互之间重叠或嵌套,将平面划分成了多个区域。环绕规则决定了哪些区域是里面还是外面。这样里面会被填充,外面不会被填充。

OpenGL系列教程之十一:OpenGL网格化_第8张图片

对每个被多条轮廓线封闭的区域,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别设置,那么什么都不会绘制(所有的区域都是外面)。

OpenGL系列教程之十一:OpenGL网格化_第9张图片

你可能感兴趣的:(OpenGL,openGL系列教程)