OpenGL的镶嵌

 

镶嵌(tessellation)是将凹边形分割或者是凸边形相交边组成的多边形。因为OpenGL只接受凸多边形的渲染,这些非凸多边形必须在绘制前进行镶嵌。

上图分别为凹四边形、中间有洞及自交的多边形。

下载:tessellation.zip,stencilTess.zip

简介

镶嵌的基本过程是将非凸多边形的所有顶点发送至镶嵌器中而不是直接发送至OpenGL渲染管线。然后通过镶嵌器(tessellator)镶嵌多边形。最后,当镶嵌完成,镶嵌器会通过用户定义的回调函例程(callback routine)用OpenGL命令渲染镶嵌多边形。

OpenGL提供大量例程将凹多边形处理为凸多边形:

GLUtessellator* gluNewTess()
void gluDeleteTess(GLUtessellator *tess)

gluNewTess() 创建镶嵌器对象(tesselator object),gluDeleteTess()删除镶嵌器对象。如果创建失败将返回NULL指针。

void gluTessBeginPolygon(GLUtessellator *tess, void *userData)
void gluTessEndPolygon(GLUtessellator *tess)

除了用glBegin()和glEnd()程序块描述多边形的顶点,还可以使用tessellator-specific block, gluTessBeginPolygon()gluTessEndPolygon()。但必须在这个块中描述非凸多边形。

void gluTessBeginContour(GLUtessellator *tess)
void gluTessEndContour(GLUtessellator *tess)

一个多边形可能有超过一个封闭的轮廓(closed contour)(闭环(closed loop)),例如一个多边形有个洞以及2个轮廓,分别为内轮廓和外轮廓。每个轮廓都必须包含在块gluTessBeginContour() 和 gluTessEndContour()中。它是嵌套在gluTessBeginPolygon() 和 gluTessEndPolygon()块中。

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

gluTessVertex()指定轮廓顶点。镶嵌器利用顶点坐标完成镶嵌。所有顶点都大致在相同的平面上。第二个参数是需要镶嵌的顶点坐标,第三个参数用来渲染的数据。它不仅包括顶点坐标,还包括颜色、法线和纹理坐标。

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

在镶嵌步骤中,镶嵌器在准备绘制镶嵌多边形时将会调用一系列的回调函数。必须提供适当的回调函数(callback function),包含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);

示例

有三个镶嵌的例子:4个顶点的简单凹边形、含洞多边形、自交多边形(星形图)

下载: tessellation.zip

简单凹边形

tessellate1()描述了这个轮廓的镶嵌程序。在执行镶嵌时,OpenGL镶嵌器是最有效的基本类型;GL_TRIANGLE, GL_TRIANGLE_FAN, GL_TRIANGLE_STRIP 和 GL_LINE_LOOP。这个例子中使用GL_TRIANGLE_FAN镶嵌器三角化。

你可以在执行镶嵌时,记录回调函数中的OpenGL命令。下面的代码是由镶嵌器产生并记录在回调函数中。查看源码中各个回调函数是如何记录的。

glBegin(GL_TRIANGLE_FAN);
    glVertex3dv(v3);
    glVertex3dv(v0);
    glVertex3dv(v1);
    glVertex3dv(v2);
glEnd();

注意这个多边形时逆时针旋转的,也就是说多边形的平面法向量是指向平面外的。如果是顺时针,则法向量是相反方向,你看到的是反面而不是正面。你可以通过使用gluTessNormal()指定平面法线。

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

如果指定法线向量为(0,0,1),即使是顺时针旋转,你看到的仍然是正面(假设相机观察的是默认方向,-Z)。默认的法线的值是(0,0,0),这意味着镶嵌器将会计算给定坐标和旋绕方向的法线。

含洞多边形

第二个例子是有2个内外都为逆时针的回路。它被定义在tessellate2()中。镶嵌器产生相当于OpenGL命令:1个GL_TRIANGLE_STRIP,1个GL_TRIANGLES

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是怎么知道中间的区域是洞(未填充)。答案是下面的缠绕规律和绕数。 tessellator分配绕数的区域由多个轮廓分割。在这种情况下,有2个独立的区域:内部区域绕数是2,外部区域绕数是1。根据给定的默认绕数规则,GLU_TESS_WINDING_ODD,标有奇数的会被填充,偶数的不填充。

如果内部顺时针,则内部区域绕数为0,外部绕数仍为1。因此,内部区域是个洞(不填充),因为内部区域绕数是0不为奇数。

自交多边形

最后一个例子是星形图,自交多边形,定义在tessellator3()中。注意镶嵌器增加了两两边线相交的5个顶点,分别是v5,v6,v7,v8和v9。当镶嵌算法检测到相交时,然后提供GLU_TESS_COMBINE回调函数创建新的顶点数据,以便我们以后绘制它。

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

当两条线相交时,回调函数创建新的顶点数据。它包括四个参数:

newVert[3] :类型为GLdouble的x,y,z坐标数组。它存储的是镶嵌器创建的相交顶点的位置信息。第二个参数是4个相邻顶点数组指针,即相交两条线。第三个参数是4个相邻的4个权重值的数组。权重是用来计算交点的颜色、法线和纹理坐标插补。最后一个参数是指向输出顶点数据的指针。输出数据可能不仅包括顶点坐标,还包括颜色、法线和纹理坐标。镶嵌器将会根据绘制的顶点,把输出数据传送到GLU_TESS_VERTEX 回调例程中。

下面是镶嵌器阐述的OpenGL命令;2个三角扇形和1个三角形。

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个独立的区域,绕数如上图所示。

根据GLU_TESS_WINDING_ODD 规则,因为中间区域的绕数为偶数,该区域将不会被填充。我们需要一个不同的旋绕规则,因此我们需要同时填充奇数和偶数区域。这个例子的旋绕规则应该是GLU_TESS_WINDING_NONZERO,允许填充所有非零区域。(GLU_TESS_WINDING_POSITIVE也可以。)

镶嵌器提供gluTessProperty() 来改变旋绕规则和其它属性。例如GLU_TESS_BOUNDARY_ONLYGLU_TESS_TOLERANCE

void gluTessProperty(GLUtesselator *tess, GLenum property, GLdouble *value)

// 示例
gluTessProperty(tess, GLU_TESS_BOUNDARY_ONLY, GL_TRUE);
gluTessProperty(tess, GLU_TESS_WINDING_RULE, GLU_TESS_WINDING_NONZERO);

旋绕规则和绕数

假设多个轮廓(contours)相互重叠或者嵌套,将平面分为多个区域。旋绕规则确定这些区域是在内部还是外部。所以,内部的需要填充,外部的不需要填充。

多个轮廓将封闭区域划分开来,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 Tessellation:http://www.songho.ca/opengl/gltessellation.html#windingrules

posted @ 2016-02-24 17:15  弦断  阅读(2511)  评论(1编辑  收藏  举报