本节书摘来自华章出版社《OpenGL编程指南》一书中的第3章,第3.1节,作者 Bill Licea-Kane ,更多章节内容可以访问云栖社区“华章计算机”公众号查看
3.1 OpenGL图元
OpenGL可以支持很多种不同的图元类型。不过它们最后都可以归结为三种类型中的一种,即点、线,或者三角形。线和三角形图元类型可以再组合为条带、循环体(线),或者扇面(三角形)。点、线和三角形也是大部分图形硬件设备所支持的基础图元类型。OpenGL还支持其他一些图元类型,包括作为细分器输入的Patch类型,以及作为几何着色器输入的邻接图元(adjacency primitive)。细分(以及细分着色器)的介绍可以参见第9章,而几何着色器的介绍可以参见第10章。在这两章中也会对Patch和邻接图元类型进行更深入的讲解。在这一节当中,我们只介绍点、线和三角形这三种图元类型。
3.1.1 点
点可以通过单一的顶点来表示。一个点也就是一个四维的齐次坐标值。因此,点实际上不存在面积,因此在OpenGL中它是通过显示屏幕(或者绘制缓存)上的一个四边形区域来模拟的。当渲染点图元的时候,OpenGL会通过一系列光栅化规则来判断点所覆盖的像素位置。在OpenGL中对点进行光栅化是非常直接的,如果某个采样值落入点在窗口坐标系中的四边形当中,那么就认为这个采样值被点所覆盖。四边形区域的边长等于点的大小,它是一个固定的状态(通过glPointSize()设置),也可以在顶点、细分和几何着色器中向内置变量gl_PointSize写入值来进行改变。只有开启了GL_PROGRAM_POINT_SIZE状态之后,我们才能在着色器中写入gl_PointSize,否则这个值将被忽略,系统依然会使用glPointSize()所设置的数值。
void glPointSize(GLfloat size);
设置固定的像素大小,如果没有开启GL_PROGRAM_POINT_SIZE,那么它将被用于设置点的大小。
默认的点大小为1.0。因此当我们渲染点的时候,每个顶点实际上都是屏幕上的一个像素(当然,被剪切的点除外)。如果点的大小增加(无论是通过glPointSize()还是向gl_PointSize写入一个大于1.0的值),那么每个点的顶点都会占据超过1个像素的值。例如,如果点的尺寸为1.2,并且顶点正好处于一个像素的中心,那么只有这个像素会受到光照的影响。但是如果顶点正好处于两个水平或者垂直的相邻像素中心之间,那么这两个像素都会受到光照的影响(即这两个像素都会被照亮)。如果顶点正好位于4个相邻像素的中点上,那么这4个像素都会被照亮,也就是说一个点会同时影响4个像素的值!
点精灵
如果使用OpenGL来渲染点,那么点的每个片元都会执行片元着色器。在本质上每个点都是屏幕上的方形区域,而每个像素都可以使用不同的颜色来着色。我们可以在片元着色器中通过解析纹理图来实现点的着色。OpenGL的片元着色器中提供了一个特殊的内置变量来辅助这一需求,它叫做gl_PointCoord,其中包含了当前片元在点区域内的坐标信息。gl_PointCoord只能在片元着色器中工作(将它包含在其他着色器中也没有什么意义),它的值只对于点的渲染有效。如果将gl_PointCoord作为输入纹理坐标使用,那么就可以使用位图和纹理替代简单的方块颜色。将结果进行alpha融混或者直接抛弃某些片元(使用discard关键字)之后,我们还可以创建不同形状的点精灵(point sprite)对象。
我们会在后面的内容即6.13节中给出有关点精灵的简短例子。
3.1.2 线、条带与循环线
OpenGL当中的线表示一条线段,而不是数学上的无限延伸的方向向量。独立的线可以通过一对顶点来表达,每个顶点表示线的一个端点。多段线也可以进行链接来表示一系列的线段,它们还可以是首尾闭合的。闭合的多段线叫做循环线(line loop),而开放的多段线(没有首尾闭合)叫做条带线(line strip)。与点类似的是,线从原理上来说也不存在面积,因此也需要使用特殊规则来判断线段的光栅化会影响哪些像素值。线段光栅化的规则也称作diamond exit规则。在OpenGL的标准说明书中给出了其详细的解释。但是,在这里还是需要对它进行重新讲解。假设每个像素在屏幕上的方形区域中都存在一个菱形,当对一条从点A到点B的线段进行光栅化,并且线段穿过了菱形的假想边时,这个像素应该受到其影响—除非菱形中包含的正好是点B(即线段的末端点位于菱形内)。不过,如果还绘制了另一条从点B到点C的线段,那么此时B点所在的像素只会更新一次。
diamond exit规则对于细线段是有效的,但是OpenGL也可以通过glLineWidth()函数来设置线段的宽度大小(相当于之前的glPointSize())。
void glLineWidth(GLfloat width);
设置线段的固定宽度。默认值为1.0。这里的width表示线段的宽度,它必须是一个大于0.0的值,否则会产生一个错误信息。
线段并不能通过类似gl_PointSize的着色器变量来设置,OpenGL中的线段绘制必须通过固定宽度的渲染状态来切换。如果线段宽度大于1,那么线段将被水平和垂直复制宽度大小的次数。如果线段为Y主序(即它主要是向垂直方向延伸的,而不是水平方向),那么复制过程是水平方向的。如果它是X主序,那么复制过程就是垂直方向进行的。
如果没有开启反走样的话,OpenGL标准对于线段端点的表示方法以及线宽的光栅化方法是相对自由的。如果开启了反走样的话,那么线段将被视为沿着线方向对齐的矩形块,其宽度等于当前设置的线宽。
3.1.3 三角形、条带与扇面
三角形是三个顶点的集合组成的。当我们分别渲染多个三角形的时候,每个三角形都与其他三角形完全独立。三角形的渲染是通过三个顶点到屏幕的投影以及三条边的链接来完成的。如果屏幕像素的采样值位于三条边的正侧半空间内的话,那么它受到了三角形的影响。如果两个三角形共享了一条边(即共享一对顶点),那么不可能有任何采样值同时位于这两个三角形之内。这一点非常重要的原因是,虽然OpenGL可以支持多种不同的光栅化算法,但是对于共享边上的像素值设置却有着严格的规定:
两个三角形的共享边上的像素值因为同时被两者所覆盖,因此不可能不受到光照计算的影响。
两个三角形的共享边上的像素值,不可能受到多于一个三角形的光照计算的影响。
这也就是说,OpenGL对于模型三角形共享边的光栅化过程不会产生任何裂缝,也不会产生重复的绘制。这一点对于三角形条带(triangle strip)或者扇面(triangle fan)的光栅化过程非常重要,此时前三个顶点将会构成第一个三角形,后继的顶点将与之前三角形的后两个顶点一起构成新的三角形。这一过程的图示如图3-1所示。
当渲染三角形扇面的时候,第一个顶点会作为一个共享点存在,它将作为每一个后继三角形的组成部分。而之后的每两个顶点都会与这个共享点一起组成新的三角形。三角形条带可以用于表达任何复杂程度的凸多边形形状。图3-2所示为三角形扇面的顶点布局。
这些图元类型都可以用于下一节介绍的绘制函数当中。它们通过OpenGL的枚举量来进行表达,并且作为渲染函数的输入参数。表3-1所示为t图元类型与OpenGL枚举量之间的对应关系。
将多边形渲染为点集、轮廓线或者实体
一个多边形有两个面:正面和背面,当不同的面朝向观察者时,它们的渲染结果可能是不一样的。因此在观察一个实体物体的剖面时,可以很明显地区分出它的内部和外部表面。默认情况下,正面和背面的绘制方法是一致的。如果要改变这一属性,或者仅仅使用轮廓线或者顶点来进行绘制的话,可以调用glPolygonMode()命令。
void glPolygonMode(GLenum face, GLenum mode);
控制多边形的正面与背面绘制模式。参数face必须是GL_FRONT_AND_BACK,而mode可以是GL_POINT、GL_LINE或者GL_FILL,它们分别设置多边形的绘制模式是点集、轮廓线还是填充模式。默认情况下,正面和背面的绘制都使用填充模式来完成。
多边形面的反转和裁减
从惯例上来说,多边形正面的顶点在屏幕上应该是逆时针方向排列的。因此我们可以构建任何“可能”的实体表面—从数学上来说,这类表面称作有向流形(orientable manifold),即方向一致的多边形—例如球体、环形体和茶壶等都是有向的;而克林瓶(Klein bottle)和莫比乌斯带(M鯾ius strip)不是。换句话说,这样的多边形可以是完全顺时针的,或者完全逆时针的。
假设我们一致地描述了一个有向的模型表面,但是它的外侧需要使用顺时针方向来进行描述。此时可以通过OpenGL的函数glFrontFace()来反转(reversing)背面,并重新设置多边形正面所对应的方向。
void glFrontFace(GLenum mode);
控制多边形正面的判断方式。默认模式为GL_CCW,即多边形投影到窗口坐标系之后,顶点按照逆时针排序的面作为正面。如果模式为GL_CW,那么采用顺时针方向的面将被认为是物体的正面。
顶点的方向(顺时针或者逆时针)也可以被称为顶点的趋势(winding)。
对于一个由不透明的且方向一致的多边形组成的、完全封闭的模型表面来说,它的所有背面多边形都是不可见的—它们永远会被正面多边形所遮挡。如果位于模型的外侧,那么可以开启裁减(culling)来直接抛弃OpenGL中的背面多边形。与之类似,如果位于模型之内,那么只有背面多边形是可见的。如果需要指示OpenGL自动抛弃正面或者背面的多边形,可以使用glCullFace()命令,同时通过glEnable()开启裁减。
void glCullFace(GLenum mode);
在转换到屏幕空间渲染之前,设置需要抛弃(裁减)哪一类多边形。mode可以是GL_FRONT、GL_BACK或者GL_FRONT_AND_BACK,分别表示正面、背面或者所有多边形。要使命令生效,我们还需要使用glEnable()和GL_CULL_FACE参数来开启裁减;之后也可以使用glDisable()和同样的参数来关闭它。
高级技巧
从更专业的角度来说,判断多边形的面是正面还是背面,需要依赖于这个多边形在窗口坐标系下的面积计算。而面积计算的一种方法是
其中xi和yi分别为多边形的n个顶点中第i个顶点的窗口坐标x和y,而i⊕1是公式(i + 1) mod n的缩写形式,其中mod表示取余数的操作。
假设我们设置为GL_CCW,那么a > 0的时候,顶点所对应的多边形就是位于正面的;否则它位于背面。如果设置为GL_CW且a < 0,那么对应的多边形位于正面;否则它位于背面。