《视觉开发专题》之 OpenGL 概述

2,321 阅读9分钟

这是 视觉专题 的第一篇,将分为:

  1. 什么是 OpenGL
  2. OpenGL 语法说明
  3. OpenGL 渲染管线
  4. OpenGL 程序&渲染流程分析

这几个小模块来一一介绍,阅读完本篇内容你将收获:

  1. OpenGL 是什么
  2. OpenGL 渲染管线的工作流程

友情提示:该篇文字较多,比较适合对 OpenGL 知之甚少的同学阅读,已经有相关经验的大佬可以溜了😝,闲言少叙,直入正题。

注: 该专题如未特殊说明,默认使用核心模式,OpenGL 的版本在3.3以上即可。


一:什么是 OpenGL

  一般它被认为是一个API (Application Programming Interface, 应用程序编程接口),包含了一系列可以操作图形、图像的函数(通过直接访问图形硬件设备的特性来实现)。事实上,OpenGL 本身并不是一个API,它仅仅是一个由 Khronos 组织制定并维护的规范(Specification)。

  OpenGL 规范严格规定了每个函数该如何执行,以及它们的输出值。至于内部具体每个函数是如何实现(Implement)的,将由 OpenGL 库的开发者自行决定(这里开发者通常是显卡的生产商)。

  因为 OpenGL 规范并没有规定实现的细节,具体的 OpenGL 库允许使用不同的实现,只要其功能和结果与规范相匹配即可。所以,当你使用 Apple 系统的时候,OpenGL 库是由 Apple 自身维护的。在 Linux 下,有显卡生产商提供的 OpenGL 库,也有一些爱好者改编的版本。这也意味着任何时候 OpenGL 库表现的行为与规范规定的不一致时,基本都是库的开发者留下的bug。

  OpenGL 是使用客户端 - 服务端的形式实现的,我们编写的应用程序可以看做客户端,而计算机图形硬件厂商所提供的 OpenGL 实现可以看做服务端。我们编写的 OpenGL 命令,最终会被转换为相关的协议提交给服务端,然后被执行并产生图像内容。


二:OpenGL 语法

  OpenGL 库中所有的函数都会以字符“gl”作为前缀,然后是个或多个大写字母开头的词组,以此来命名一个完成的函数(如 glBinVertexArray())。除此之外你还会看到“glfw”开头的函数,它们来自第三方库 GLFW,这是一个抽象化窗口管理和其他系统任务的开发库。类似的,还有“gl3w”开头的函数,它们来自三方库 GL3W。后续会进一步展开讲这两个库的内容。

  与函数命名约定类似,OpenGL 库中定义的常量采用“GL_”开头,通过#define来完成常量的定义。为了方便在不同的操作系统之间移植 OpenGL 程序,OpenGL 还为函数定义了不同的数据类型,如GLfloat,所以最好统一使用 OpenGL 定义的数据类型,这样就不需要关心系统兼容性问题了。

  由于 OpenGL 是一个 C 语言形式的库,因此它不能使用函数的重载来处理不同类型的数据,它通过函数名称的细微变化来实现同一类功能函数集的管理。举个例子:glUniform2f()glUniform3fv(),前者的后缀2f表示这个函数需要两个GLfloat 类型的参数(以此类推,目前一共定义了24种不同的glUniform*()函数),后者的后缀多出的一个v是 vector 的缩写,即表示它需要传入一个包含三个 GLfloat 类型元素的一维数组作为参数。

所有可以作为后缀的字母,以及它们所对应的数据类型:


三:OpenGL 渲染管线

  早期的(3.3版本以前) OpenGL 使用立即渲染模式(Immediate mode,即固定渲染管线):OpenGL的大多数功能都被库隐藏起来,开发者很少能控制 OpenGL 计算的过程。固定渲染管线较容易使用和理解,但是效率太低且不够灵活。

  当使用OpenGL的核心模式时,OpenGL 迫使我们使用现代的函数,现代函数具有更高的灵活性和效率性,也能让人更容易清楚 OpenGL 是如何运作的,更好的理解图形编程。但入门门槛也稍有增加。

  我们通常所说的渲染管线(rendering pipeline),它包含了两个部分:第一部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。下图是 OpenGL4.5 版本的管线示意图:

  OpenGL 首先接收用户提供的几何数据(顶点和几何图元),并且将它输入到一系列着色器阶段中进行处理,然后将处理后的数据送入光栅化单元(rasterizer)。光栅化单元负责对所有剪切区域内的图元生成片元数据,我们可以将一个片元视为一个“候选的像素”,然后对每个生成的片元都执行一个片元着色器,这一步会计算出这个片元的最终颜色。注意,即使在片元着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同,还会受深度混合的影响,这个后面会详细讲。

  我们可以通过控制我们需要的着色器来实现自己所需的功能,事实上,只有顶点着色器和片元着色器是必需的,细分和几何是可选的步骤。为了更好理解顶点着色器和片元着色器的分工和区别,可以总结为:顶点着色(包括细分和几何着色)决定了一个图元应该位于屏幕的什么位置,而片元着色使用这些信息来决定某个片元的颜色应该是什么


四:OpenGL 程序&渲染流程分析

无论 OpenGL 的程序写的有多么庞大与复杂,它的基本结构通常都是类似的:

  1. 初始化物体渲染所对应的状态。
  2. 设置需要渲染的物体。

在上代码之前,咱们需要对必要的图形学名词有基本的理解。

  • 渲染:计算机从模型到最终的图像创建的过程。OpenGL 只是计算机渲染系统的其中一种,基于光栅化的系统。
  • OpenGL 状态机:可以看做一个上下文(context),在调用任何 OpenGL 的指令之前,都需要先创建并进入这样的上下文中,它可以记录自己当前的状态,并能接收新的输入(调用 OpenGL 函数),当关闭了上下文,就不再接收输入。
  • 着色器 (Shader):为图形渲染管线中的某个特定部分,将输入转化为输出的程序。在 OpenGL 中,会涉及到六种不同的着色阶段,其中最常用的包括顶点着色器以及片元着色器,前者用于处理顶点数据,后者用于处理光栅化后的片元数据。着色器运行在 GPU 上,在 OpenGL 使用它之前,必须经过编译并链接为一个着色器程序对象(Shader Program Object)。

一个简单顶点着色器的源代码示例:

void main(){
    gl_Position = ftransform();
}

一个简单片元着色器的源代码示例:

void main() {
    gl_FragColor = vec4(1.0,0.5,0.2,1.0);
}

它们都需要用 GLSL (OpenGL Shading Language,着色器语言) 。

  • 标准化设备坐标(Normalized Device Coordinates, NDC):一旦你的顶点坐标已经在顶点着色器中处理过,它们就应该是标准化设备坐标了,标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪,不会显示在你的屏幕上。

  • 像素:显示器上最小的可见单元。

  • 帧缓存:保存着所有计算机生成的图像的像素点,它是由图形硬件设备管理的一块独立内存区域,可以直接映射到最终的显示设备上。

  • 顶点缓冲对象:Vertex Buffer Object,VBO. 管理着在GPU内存(通常被称为显存)中,一块储存着大量顶点数据的内存。因为从CPU把数据发送到显卡相对较慢,所以只要可能我们都要尝试尽量一次性发送尽可能多的数据。

  • 顶点数组对象:Vertex Array Object,VAO. 可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个 VAO 中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的 VAO 就行了。这使在不同顶点数据和属性配置之间切换变得非常简单。

    当你打算绘制多个物体时,你首先要生成/配置所有的VAO(和必须的VBO及属性指针),然后储存它们供后面使用。当我们打算绘制物体的时候就拿出相应的VAO,绑定它,绘制完物体后,再解绑VAO。

  • 索引缓冲对象:Element Buffer Object,EBO或Index Buffer. Object,IBO. 专门储存顶点绘制的索引,OpenGL调用这些顶点的索引来决定该绘制哪个顶点。由于三角形是绘制的基本图形,对于一些复杂图形会存在很多三角形(共用边)的顶点重合问题,该索引就是为了解决此问题,避免顶点数据重复造成的资源浪费。

我结合学习资源写了一份简单的 OpenGL 示例程序 给你作参考,附有比较详细的渲染流程分析,需要的同学自取,这里就不占篇幅粘代码了。用 Xcode 打开程序直接 Run, 就可以看到该效果:

这份 Demo 使用了可编程渲染管线,自定义了简单的着色器,用两个着色器程序、两个VAO和VBO,画两个颜色不一样的三角形。初学者不需要搞清楚每一步的原理,先对 OpenGL 的语法和基本结构有个初步的了解就行。

总结

  枯燥的概念和晦涩的专业术语很容易让初学者望而生畏,所以此篇我只选了一些必要的概念,做了简要的介绍。建议结合文中提供的源码,把上面涉及到的步骤一一拆解,进而加深对相关概念以及 OpenGL 工作流程的理解。

  近来因工作和身体原因,文章更新滞后了两周有余,现已恢复正常。

  下一篇文章将使用固定管线来完成一些动画效果,顺带介绍涉及到的 OpenGL 知识,这样更便于理解。