OpenGL的主要作用就是将图形渲染到帧缓存中。为了实现这一要求,需要将复杂的物体分解成图元的形式(包括点、线、以及三角形),当它们分布密度足够高时,就可以表达为2D或3D物体形态。
OpenGL支持很多种不同的图元类型。不过最后它们都可以归结为三种类型中的一种,即点、线、或三角形。线和三角形图元类型可以再组合为条带、循环体(线),或者扇面(三角形)。点、线和三角形也是大部分图形硬件设备所支持的基础图元类型。
H1
点
点可以通过单一的顶点来表示。一个点也就是一个四维的齐次坐标。因此、点实际上不存在面积,在OpenGL中它是通过显示屏幕上的一个四边形区域来模拟的。当渲染图元的时候,OpenGL会通过一系列光栅化规则来判断点所覆盖的像素位置。四边形区域的边长等于点的大小,可以通过glPointSize或在片元着色器中向内置变量gl_PointSize写入值来进行改变,不过只有开启了GL_PROGRAM_POINT_SIZE状态之后,我们才能在着色器中写入gl_PointSize,否则这个值被忽略。
为了说明基本图元的绘制,现在设定了17顶点,它们的序号和在屏幕中的相对位置如下图所示:
17个顶点坐标数据是:
// 17个顶点
static const GLfloat vertex_positions[] =
{
-0.7f, 0.2f, 0.0f, 1.0f,
-0.6f, -0.2f, 0.0f, 1.0f,
-0.5f, 0.2f, 0.0f, 1.0f,
-0.4f, -0.2f, 0.0f, 1.0f,
-0.3f, 0.2f, 0.0f, 1.0f,
-0.2f, -0.2f, 0.0f, 1.0f,
-0.1f, 0.2f, 0.0f, 1.0f,
0.0f, -0.2f, 0.0f, 1.0f,
0.1f, 0.2f, 0.0f, 1.0f,
0.2f, -0.2f, 0.0f, 1.0f,
0.3f, 0.2f, 0.0f, 1.0f,
0.4f, -0.2f, 0.0f, 1.0f,
0.5f, 0.2f, 0.0f, 1.0f,
0.6f, -0.2f, 0.0f, 1.0f,
0.7f, 0.2f, 0.0f, 1.0f,
0.8f, -0.2f, 0.0f, 1.0f,
0.9f, 0.2f, 0.0f, 1.0f,
};
在OpenGL中绘制点使用:
setUniformValue(m_modelRenderColorLoc, RGBA_TO_VEC4(GL_COLOR_LIGHT_BLUE));
glPointSize(10.0f);
glDrawArrays(GL_POINTS, 0, 17);
上述代码将在屏幕中绘制17个点,不过点呈现的不是圆形而是四边形区域,也没有中间的数字标注。
H1
线
OpenGL当中的线表示一条线段,而不是数学意义上的无限延伸的方向向量。独立的线可以通过一对顶点来表达,每个顶点表示线的一个端点。
setUniformValue(m_modelRenderColorLoc, RGBA_TO_VEC4(GL_COLOR_RED));
glLineWidth(2.0f);
glDrawArrays(GL_LINES, 0, 17);
因每条线段有两个顶点,所以有8条线段,剩下一个顶点无法渲染成线段。
H1
条带线(Line stip)
多个顶点进行链接来表示一系列的线段,条带线首尾没有闭合。
setUniformValue(m_modelRenderColorLoc, RGBA_TO_VEC4(GL_COLOR_RED));
glLineWidth(2.0f);
glDrawArrays(GL_LINE_STRIP, 0, 17);
H1
循环线(Line loop)
与条带线不同的是,循环线首尾是闭合的。
setUniformValue(m_modelRenderColorLoc, RGBA_TO_VEC4(GL_COLOR_RED));
glLineWidth(2.0f);
glDrawArrays(GL_LINE_LOOP, 0, 17);
可以看到开始点和结束点之间有一条连线。首尾进行了闭合。
H1
三角形
三角形是三个顶点的结合组成的。当我们分别渲染多个三角形的时候,每个三角形都与其他三角形完全独立。
setUniformValue(m_modelRenderColorLoc, RGBA_TO_VEC4(GL_COLOR_LIGHT_BLUE));
glDrawArrays(GL_TRIANGLES, 0, 17);
因为共有17个顶点,因此共绘制了5个三角形,剩下两个顶点无法渲染成一个三角形。
H1
三角形条带(Triangle stip)
与独立的三角形不同的是, 前三个顶点将会构成第一个三角形,后继的顶点将与之前三角形的后两个顶点一起构成新的三角形。
setUniformValue(m_modelRenderColorLoc, RGBA_TO_VEC4(GL_COLOR_RED));
glLineWidth(2.0f);
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 17);
H1
三角形扇面(Triangle Fan)
当渲染三角形扇面的时候,第一个顶点会作为共享点存在,它将作为每一个后继三角形的组成部分。而之后的每两个顶点都会与这个共享点一起组成新的三角形。以上述的顶点数据为例,为了更形象的说明问题,对其中的顶点坐标做一些调整。将一个顶点移到原点下方,将其他顶点的y值全部变为正数。
static const GLfloat vertex_triangle_fan_positions[] =
{
0.0f, -0.7f, 0.0f, 1.0f,
-0.6f, 0.2f, 0.0f, 1.0f,
-0.5f, 0.2f, 0.0f, 1.0f,
-0.4f, 0.2f, 0.0f, 1.0f,
-0.3f, 0.2f, 0.0f, 1.0f,
-0.2f, 0.2f, 0.0f, 1.0f,
-0.1f, 0.2f, 0.0f, 1.0f,
0.0f, 0.2f, 0.0f, 1.0f,
0.1f, 0.2f, 0.0f, 1.0f,
0.2f, 0.2f, 0.0f, 1.0f,
0.3f, 0.2f, 0.0f, 1.0f,
0.4f, 0.2f, 0.0f, 1.0f,
0.5f, 0.2f, 0.0f, 1.0f,
0.6f, 0.2f, 0.0f, 1.0f,
0.7f, 0.2f, 0.0f, 1.0f,
0.8f, 0.2f, 0.0f, 1.0f,
0.9f, 0.2f, 0.0f, 1.0f,
};
setUniformValue(m_modelRenderColorLoc, RGBA_TO_VEC4(GL_COLOR_RED));
glLineWidth(2.0f);
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
glDrawArrays(GL_TRIANGLE_FAN, 0, 17);
绘制效果如下:
H1
四边形(GL_QUADS)
四边形是四个顶点的结合组成的。当我们分别渲染多个四边形的时候,每个四边形都与其他四边形完全独立。
setUniformValue(m_modelRenderColorLoc, RGBA_TO_VEC4(GL_COLOR_RED));
glLineWidth(2.0f);
glDrawArrays(GL_QUADS, 0, 17);
H1
四边形条带(GL_QUAD_STRIP)
与独立的四边形不同的是, 前四个顶点将会构成第一个四边形,后继的顶点将与之前四边形的后两个顶点一起构成新的四边形。
setUniformValue(m_modelRenderColorLoc, RGBA_TO_VEC4(GL_COLOR_RED));
glLineWidth(2.0f);
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
glDrawArrays(GL_QUAD_STRIP, 0, 17);
H1
多边形(POLYGON)
多边形就是把顶点按照顺序链接起来,且首尾集合的图形。我们使用三角形扇面的顶点作为例子。
// 多边形
glBindVertexArray(m_vao[1]);
glEnableVertexAttribArray(0);
setUniformValue(m_modelRenderColorLoc, RGBA_TO_VEC4(GL_COLOR_LIGHT_GREEN));
glPointSize(10.0f);
glDrawArrays(GL_POINTS, 0, 17);
setUniformValue(m_modelRenderColorLoc, RGBA_TO_VEC4(GL_COLOR_RED));
glLineWidth(2.0f);
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
glDrawArrays(GL_POLYGON, 0, 17);
H1
多边形渲染成点集、轮廓或者实体
一个多边形有两个面:正面和背面,当不同的面朝向观察者时,它们的渲染结果可能是不一样的。因此在观察一个物体的剖面时,可以很明显地区分出它的内部和外部表面。默认情况下,正面和背面的绘制方法是一致的,都是填充模式(GL_FILL)。如果要改变这一属性,或者进行使用轮廓线或者顶点来绘制的话,可以调用glPolygonMode命令。
glPolygonMode(GLenum face, GLenum mode)
比如下面的代码演示了正面使用填充模式,背面使用轮廓线模式。
glPolygonMode(GL_FRONT, GL_FILL);
glPolygonMode(GL_BACK, GL_LINE);
setUniformValue(m_modelRenderColorLoc, RGBA_TO_VEC4(GL_COLOR_LIGHT_BLUE));
glDrawArrays(GL_TRIANGLES, 0, 17);
我们看到正面的是蓝色填充的,而背面的只有轮廓线。你可能已经注意到,为什么同时绘制的三角形,其中有3个正面,2个背面呢?这是因为,从惯例上来讲,多边形正面的顶点是逆时针排列的,而背面是顺时针排列的。如下图所示:
如果我将上面OpenGL渲染的图元沿着Y轴顺时针旋转180°后,就可以看到它们的背面。
worldMatrix.setToIdentity();
worldMatrix.translate(0.0f, -0.5f, -2.0f);
worldMatrix.rotate(-180.0f, 0, 1, 0);
setUniformValue(m_worldMatrixLoc, worldMatrix);
setUniformValue(m_modelRenderColorLoc, RGBA_TO_VEC4(GL_COLOR_LIGHT_GREEN));
glPointSize(10.0f);
glDrawArrays(GL_POINTS, 0, 17);
setUniformValue(m_modelRenderColorLoc, RGBA_TO_VEC4(GL_COLOR_LIGHT_BLUE));
glDrawArrays(GL_TRIANGLES, 0, 17);
它们的背面使用的轮廓线或者填充模式,与未旋转时完全相反。
H1
多边形面的反转和裁剪
H2
多边形的反转
通过OpenGL的函数glFrontFace控制多边形正面的判断方式,默认模式为GL_CCW,即多边形投影到窗口坐标系之后,顶点按照逆时针排序作为正面。如果模式为GL_CW,那么采用顺时针方向的面被认为是物体的正面。
glPolygonMode(GL_FRONT, GL_FILL);
glPolygonMode(GL_BACK, GL_LINE);
glFrontFace(GL_CW);
setUniformValue(m_modelRenderColorLoc, RGBA_TO_VEC4(GL_COLOR_LIGHT_BLUE));
glDrawArrays(GL_TRIANGLES, 0, 17);
我们可以看到,使用 glFrontFace(GL_CW)反转后,原来的正面变为背面,而背面变为正面。
H2
如何判断多边形的正面或背面
从专业角度来说,判断多边形的面是正面还是背面,需要依赖于这个多边形在窗口坐标系下的面积计算。而面积计算的一种方式是
下图列出了两个三角形的共6个屏幕坐标,
依上述公式,我们可以计算得到
H2
多边形的裁剪
对于一个由不透明且方向一致的多边形组成的,完全封闭的模型表面来说,它的所有背面的多边形是不可见的--它们永远会被正面多边形所遮挡。如果位于模型的外侧,那么可以开启裁剪(culling)来直接抛弃OpenGL中的背面多边形。与之类似,那么位于模型内部,那么只有背面多边形是可见的。如果需要指示OpenGL自动抛弃正面或者背面多边形,可以使用glCullFace()命令,同时通过glEnable开启裁剪。
// 旋转180°后三角行的背面
worldMatrix.setToIdentity();
worldMatrix.translate(0.0f, -0.5f, -2.0f);
worldMatrix.rotate(-180.0f, 0, 1, 0);
setUniformValue(m_worldMatrixLoc, worldMatrix);
setUniformValue(m_modelRenderColorLoc, RGBA_TO_VEC4(GL_COLOR_LIGHT_GREEN));
glPointSize(10.0f);
glDrawArrays(GL_POINTS, 0, 17);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
setUniformValue(m_modelRenderColorLoc, RGBA_TO_VEC4(GL_COLOR_LIGHT_BLUE));
glDrawArrays(GL_TRIANGLES, 0, 17);
上面的代码表示在转换到屏幕空间渲染之前,设置了抛弃GL_BACK(背面)。因此你可以看到,在旋转180°之后,原来没有裁剪的话,那么背面显示的是轮廓线。当开启裁剪之后,背面的轮廓线也没有了,也就是说完全没有渲染,被裁剪了。
H1
图元的重启动
当需要处理较大的顶点数据时,我们可能会被迫执行大量的OpenGL绘制操作,并且每次绘制的内容总是与前一次图元的类型相同(例如GL_TRIANGLE_STIP)。
OpenGL支持在同一个渲染命令中进行图元重启的功能,此时需要指定一个特殊的值,叫做图元重启索引(primitive restart index),OpenGL内部会对它做特殊的处理。如果绘制调用过程中遇到了这个重启索引,那么就会从这个索引之后的顶点开始,重新开始相同图元类型的渲染。图元重启索引是通过glPrimitiveRestartIndex函数来完成的。
以上述的顶点数据为例,我们将8指定为图元重启索引。在三角形条带渲染的过程中,OpenGL会一直监控元素的数组中缓存中是否出现索引8,当这个值出现的时候,OpenGL不会创建一个顶点,而是结束当前三角形条带的绘制。下一个顶点(索引9)将成为一个新的三角形条带的第一个顶点,因此我们最终构建了两个三角形条带。
在使用图元重启时,我们需要创建GL_ELEMENT_ARRAY_BUFFER类型的缓存数组,这跟之前的GL_ARRAY_BUFFER不一样。GL_ELEMENT_ARRAY_BUFFER缓存的是顶点索引数据,而不是顶点数据本身。
// 顶点索引值
static const GLushort vertex_indices[] =
{
0, 1, 2, 3, 4, 5, 6, 7, // 第一组三角形条带
8, // 这是重启的索引
9, 10, 11, 12, 13, 14, 15, 16 // 第二组三角形条带
};
// 设置元素数组缓存
glGenBuffers(1, m_ebo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo[0]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(vertex_indices), vertex_indices, GL_STATIC_DRAW);
//....
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo[0]);
setUniformValue(m_modelRenderColorLoc, RGBA_TO_VEC4(GL_COLOR_RED));
glLineWidth(2.0f);
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
// 开启图元重启
glEnable(GL_PRIMITIVE_RESTART);
glPrimitiveRestartIndex(8);
glDrawElements(GL_TRIANGLE_STRIP, 17, GL_UNSIGNED_SHORT, NULL);
// 如果没有开启图元重启,需要执行两次
glDrawElements(GL_TRIANGLE_STRIP, 8, GL_UNSIGNED_SHORT, NULL);
glDrawElements(GL_TRIANGLE_STRIP, 8, GL_UNSIGNED_SHORT, (const GLvoid*)(9 * sizeof(GLushort)));