在本书的大部分内容中,重点是底层计算机图形学的基础知识,而不是与算法可能实现的 api 或硬件相关的任何细节。 本章采用了稍微不同的路线,并将使用图形硬件的细节与与硬件编程相关的一些实际问题混合在一起。本章旨在成为图形硬件的入门指南,并可作为研究图形硬件的一组每周实验的基础。
图形硬件描述了使用专门的基于光栅化(在某些情况下,基于光线追踪器)的硬件架构快速将 3D 对象渲染为计算机屏幕上的像素所必需的硬件组件。 使用术语“图形硬件”是为了引出执行一系列图形计算所需的物理组件的概念。换句话说,硬件是当前显示卡上的芯片组、晶体管、总线、处理器和计算核心的集合。 正如你将在本章学到的,并最终亲身体验到的那样,当前的图形硬件非常擅长处理 3D 对象的描述,并将这些表示转换为填充显示器的彩色像素。
在过去的十年里,图形硬件确实发生了非常迅速的变化。较新的图形硬件提供了更多的并行处理能力,以及对专门渲染的更好支持。快速发展的原因之一是电子游戏产业及其经济发展势头。本质上,这意味着每个新的显卡提供更好的性能和处理能力。因此,电子游戏在视觉上显得更加逼真。
图形硬件上的处理器(通常称为 gpu 或图形处理单元)是高度并行的,并提供数千个并发执行线程。硬件是为吞吐量而设计的,它允许在更短的时间内处理更多的像素和顶点。所有这些并行性都有利于图形算法,但其他工作也受益于并行硬件。 除了视频游戏,gpu 还用于加速物理计算,开发实时光线追踪代码,解决流体模拟的 Navier-Stokes 相关方程,并开发更快的代码来理解气候。 一些 api 和 sdk 已经开发出来,可以提供更直接的通用计算,比如 OpenCL 和 NVIDIA 的 CUDA。硬件加速光线追踪 api 也可以加速光线与物体的相交。类似地,用于编程视频游戏的图形组件的标准 api,如 OpenGL 和 DirectX,也允许利用图形硬件的并行能力。 随着新硬件的开发以支持更复杂的计算,这些 api 中的许多都发生了变化。
图形硬件是可编程的。作为开发人员,您可以控制与处理几何图形、顶点和最终成为像素的片元相关的许多计算。最近的硬件变化以及正在进行的 api 更新,如 OpenGL 或 DirectX,都支持完全可编程的管道。这些变化为开发人员提供了创造性的许可,以利用 gpu 上可用的计算。
在此之前,固定函数光栅化管道将计算强制到特定样式的顶点转换,照明和片元处理。管道的固定功能确保了基本的着色、照明和纹理可以非常快速地发生。
无论是可编程接口,还是固定函数计算,光栅化流水线的基本计算都是相似的。在点阵化流水线中,顶点经过观看变换矩阵和投影变换矩阵的变换,从局部空间变换到全局空间,最终转化为屏幕坐标。与几何顶点相关联的屏幕坐标集被栅格化为片元。管道的最后阶段将片元处理成像素,并可以应用每个片段的纹理查找,照明和任何必要的混合。
一般来说,流水线适合并行执行,GPU 内核可以同时处理顶点和片元。
在使用图形硬件时,可以方便地将 CPU 和 GPU 区分为单独的计算实体。 在这种情况下,术语“主机”指的是 CPU,包括它可用的线程和内存。术语“设备”指的是 GPU 或图形处理单元,以及与之相关的线程和内存。 这是有意义的,因为大多数图形硬件是由外部硬件组成的,通过 PCI 总线连接到机器上。硬件也可以作为一个单独的芯片组焊接到机器上。 从这个意义上说,图形硬件代表一个专门的协处理器,因为 CPU(及其核心)和 GPU 及其核心都可以编程。所有使用图形硬件的程序必须首先在 CPU 和 GPU 内存之间建立映射。这是一个相当低级的细节,但它是必需的,以便驻留在操作系统中的图形硬件驱动程序可以在硬件、操作系统和窗口系统软件之间进行接口。 回想一下,因为主机(CPU)和设备(GPU)是分开的,所以数据必须在两个系统之间进行通信。更正式地说,操作系统、硬件驱动程序、硬件和窗口系统之间的这种映射称为图形上下文。上下文通常通过对窗口系统的 API 调用来建立。 关于建立上下文的细节超出了本章的范围,但是许多窗口系统开发库都有方法来查询图形硬件的各种功能,并根据这些需求建立图形上下文。 因为设置上下文依赖于窗口系统,这也意味着这样的代码不太可能是跨平台的代码。然而,在实践中,或者至少在开始时,不太可能需要这种低级上下文设置代码,因为存在许多高级 api 来帮助人们开发可移植的交互式应用程序。
许多用于开发交互式应用程序的框架都支持查询输入设备,如键盘或鼠标。一些框架提供对网络、音频系统和其他高级系统资源的访问。在这方面,许多 api 都是开发图像甚至游戏应用程序的首选方式。
跨平台硬件加速通常是通过 OpenGL API 实现的。OpenGL 是一个开放的行业标准图形 API,支持多种类型图形硬件的硬件加速。OpenGL 代表了图形硬件编程最常见的 api 之一,以及 DirectX 等 api。虽然 OpenGL 可以在许多操作系统和硬件架构上使用,但 DirectX 是特定于基于微软的系统的。
当你用 OpenGL API 编程时,你至少是在为两个处理器编写代码:CPU 和 GPU。 OpenGL 是用 c 风格的 API 实现的,所有函数的前缀都是“gl”,以表明它们包含在 OpenGL 中。OpenGL 函数调用改变图形硬件的状态,并可用于声明和定义几何,加载顶点和片段着色器,并确定数据通过硬件时计算将如何发生。
本章介绍的 OpenGL 变体是 OpenGL 3.3 Core Profile 版本。虽然不是最新的 OpenGL 版本,但 3.3 版本的 OpenGL 符合 OpenGL 编程的未来方向。这些版本的重点是提高效率,同时将管道的编程完全交给开发人员。在早期版本的 OpenGL 中存在的许多函数调用在这些较新的 api 中不存在。例如,即时渲染模式。 即时模式渲染用于根据每帧的需要将数据从 CPU 内存发送到图形卡内存,并且通常非常低效,特别是对于较大的模型和复杂的场景。 当前的 API 侧重于在需要数据之前将数据存储在图形卡上,并在渲染时实例化它。 另一个例子是,OpenGL 的矩阵栈也被弃用了,开发者只能使用第三方矩阵库(比如 GLM)或者他们自己的类来创建必要的矩阵来进行查看、投影和转换。 因此,OpenGL 的着色器语言(GLSL)也承担了更大的角色,在着色器中执行必要的矩阵变换以及照明和着色。因为执行每个顶点转换和照明的固定功能管道不再存在,程序员必须自己开发所有着色器。
三个概念将有助于理解当代图形硬件编程。 首先是数据缓冲区的概念,它非常简单,是设备上的线性内存分配,可以存储 gpu 将要操作的各种数据。 第二个是图形卡保持计算状态的想法,该状态决定了场景数据和着色器如何与计算数据相关联。此外,状态可以从主机传输到设备,甚至可以在设备内部的着色器之间传输。 着色器代表了与每个顶点或每个片元处理相关的 GPU 上的计算发生的机制。 本章将重点关注顶点和片元着色器,但是专门的几何和计算着色器也存在于当前版本的 OpenGL 中。着色器在现代图形硬件的功能中扮演着非常重要的角色。
缓冲区是在图形硬件上存储数据的主要结构。它们表示图形硬件的内部内存,这些内存与几何、纹理和图像平面数据相关。 栅格化管道与硬件加速栅格化相关的计算读取和写入 GPU 上的各种缓冲区。从编程的角度来看,应用程序必须初始化应用程序所需的 GPU 上的缓冲区。这相当于主机到设备的复制操作。在执行的各个阶段结束时,也可以执行设备到主机传输副本,以便将数据从 GPU 拉到 CPU 内存。 此外,OpenGL 的 API 中确实存在允许将设备内存映射到主机内存的机制,以便应用程序可以直接写入显卡上的缓冲区。
在图形管道中,最终的像素颜色集可以链接到显示,或者它们可以作为 PNG 图像写入磁盘。与这些像素相关联的数据通常是颜色值的二维数组。数据本质上是二维的,但它在 GPU 上有效地表示为内存的一维线性阵列。这个数组实现了显示缓冲区,它最终被映射到窗口。渲染图像需要通过图形 API 将更改传递给图形硬件上的显示缓冲区。在光栅化流水线的末端,片元处理和混合阶段将数据写入输出显示缓冲存储器。同时,窗口系统读取显示缓冲区的内容,在监视器的窗口上产生光栅图像。
所以不需要非得使用 Qt 带的 OpenGL 库,只要有能力改变显示缓冲区,你用什么库都行。
大多数应用程序更喜欢双缓冲的显示状态。这意味着有两个缓冲区与图形窗口相关联:前缓冲区和后缓冲区。双缓冲系统的目的是应用程序可以将更改传递给后缓冲(因此,将更改写入该缓冲区),而前缓冲内存用于驱动窗口上的像素颜色。
在显示循环结束时,通过指针交换交换缓冲区。前缓冲区指针指向后缓冲区,后缓冲区指针随后被赋值给前一个前缓冲区。这样,窗口系统将用最新的缓冲区刷新窗口的内容。 如果缓冲区指针交换与窗口系统对整个显示的刷新同步,则呈现将看起来是无缝的。否则,用户可能会在实际显示中观察到几何图形的撕裂,因为场景几何图形的变化和片元的处理(并因此写入显示缓冲区)比屏幕刷新更快。
当显示器被视为内存缓冲区时,显示器上最简单的操作之一本质上是内存设置(或复制)操作,该操作将内存归零或清除到默认状态。对于图形程序,这可能意味着将窗口背景清除为特定的颜色。要在 OpenGL 应用程序中清除背景颜色(为黑色),可以使用以下代码:
此操作仅清除颜色缓冲区。除了颜色缓冲区,图形硬件还使用深度缓冲区来表示片元相对于相机的距离。清除深度缓冲区是必要的,以确保 zbuffer 算法的操作。清除深度缓冲区可以通过将两个位字段值放在一起来实现,如下所示:
通过对显示器颜色和深度缓冲区的缓冲区清除操作的说明,介绍了图形硬件状态的思想。glclearcolor 函数设置默认的颜色值,当调用 glClear 时,这些值被写入颜色缓冲区内的所有像素。clear 调用初始化显示缓冲区的颜色组件,还可以重置深度缓冲区的值。如果在应用程序中不改变透明颜色,则只需要设置一次透明颜色,并且通常在 OpenGL 程序的初始化中完成。每次调用 glClear 时,它都会使用之前设置的 clear 颜色状态。
还要注意,z-buffer 算法状态可以根据需要启用和禁用。z-buffer 算法在 OpenGL 中也被称为深度测试。通过启用它,在将任何片段颜色写入颜色缓冲区之前,将片段的深度值与当前存储在深度缓冲区中的深度值进行比较。有时,深度测试是不必要的,并且可能会降低应用程序的速度。禁用深度测试将阻止 z 缓冲区计算并改变可执行文件的行为。
OpenGL 中的状态概念模仿了面向对象类中静态变量的使用。 根据需要,程序员启用、禁用和/或设置驻留在显卡上的 OpenGL 变量的状态。这些状态会影响硬件上的任何后续计算。一般来说,高效的 OpenGL 程序会尽量减少状态变化,启用需要的状态,而禁用渲染不需要的状态。
[!note] 123 这部分建议看 Github 上的 LearnOpenGL 开源项目。 主页 - LearnOpenGL CN (learnopengl-cn.github.io)